From 6167618f5cee726119bd1f51dd7e30319fbbc32d Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Thu, 6 Aug 2026 19:29:21 +0530 Subject: [PATCH 01/49] fix: asset stack stacked over asset details (#30598) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../widgets/asset_viewer/asset_stack.widget.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart index 06aed2f09c..77832c2685 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart @@ -22,6 +22,11 @@ class AssetStackRow extends ConsumerWidget { return const SizedBox.shrink(); } + final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); + if (showingDetails) { + return const SizedBox.shrink(); + } + final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); final double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); From a9a99cffbfc758bd7ad70596bb191a583fb6cef2 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:10:32 +0530 Subject: [PATCH 02/49] chore: cleanup translation dependency on i18n (#30596) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/mise.toml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/mobile/mise.toml b/mobile/mise.toml index ee312178a4..7caf6dfbd5 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -58,13 +58,7 @@ depends = ["//:open-api-dart"] [tasks."codegen:translation"] alias = "translation" description = "Generate translations from i18n JSONs" -run = [ - { task = "//:i18n:format-fix" }, - { tasks = [ - "i18n:loader", - "i18n:keys", - ] }, -] +depends = ["//:i18n:format-fix", "i18n:loader", "i18n:keys"] [tasks."codegen:app-icon"] description = "Generate app icons" @@ -127,6 +121,7 @@ run = [ "dart format lib/generated/codegen_loader.g.dart", ] depends = ["//:open-api-dart"] +wait_for = ["//:i18n:format-fix"] [tasks."i18n:keys"] description = "Generate i18n keys" @@ -135,6 +130,7 @@ run = [ "dart run bin/generate_keys.dart", "dart format lib/generated/translations.g.dart", ] +wait_for = ["//:i18n:format-fix"] [tasks."analyze:dart"] description = "Run Dart analysis" From 5ad1e4e0f7ecefdb6f43ba74a01dc92b9ea6b7cb Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:36:20 +0200 Subject: [PATCH 03/49] fix: medium tests dependencies (#30612) --- server/src/services/base.service.ts | 7 ++- server/src/types.ts | 12 ++-- server/test/medium.factory.ts | 59 ++++++++++--------- .../workflow/workflow-core-plugin.spec.ts | 2 +- 4 files changed, 45 insertions(+), 35 deletions(-) diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 2195c66a22..6ed8ce925a 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -117,7 +117,7 @@ export const BASE_SERVICE_DEPENDENCIES = [ ViewRepository, WebsocketRepository, WorkflowRepository, -]; +] as const; @Injectable() export class BaseService { @@ -191,7 +191,7 @@ export class BaseService { ); } - static create(Service: ClassConstructor, ctx: BaseService) { + static create>(Service: T, ctx: BaseService) { const service = new Service( LoggingRepository.create(), ctx.accessRepository, @@ -242,6 +242,7 @@ export class BaseService { ctx.trashRepository, ctx.userRepository, ctx.versionRepository, + ctx.videoStreamRepository, ctx.viewRepository, ctx.websocketRepository, ctx.workflowRepository, @@ -249,7 +250,7 @@ export class BaseService { service.logger.setContext(BaseService.name); - return service as T; + return service as InstanceType; } get worker() { diff --git a/server/src/types.ts b/server/src/types.ts index dca3119c76..27995e841f 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -32,6 +32,7 @@ import { UserMetadataKey, WorkflowType, } from 'src/enum'; +import { Mocked } from 'vitest'; export type DeepPartial = T extends Date ? T @@ -647,7 +648,10 @@ export type JSONSchemaProperty = { required?: string[]; }; -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -export interface ClassConstructor extends Function { - new (...args: any[]): T; -} +export type ClassConstructor = T extends new (...args: infer R) => infer L + ? new (...args: R) => L + : new (...args: any[]) => unknown; + +export type ClassConstructorsToInstances[]> = { + [K in keyof T]: InstanceType | Mocked>; +}; diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index b335a0fba5..f0f14757b2 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -76,7 +76,7 @@ import { UserTable } from 'src/schema/tables/user.table'; import { BASE_SERVICE_DEPENDENCIES, BaseService } from 'src/services/base.service'; import { MetadataService } from 'src/services/metadata.service'; import { SyncService } from 'src/services/sync.service'; -import { ClassConstructor, UploadFile } from 'src/types'; +import { ClassConstructor, ClassConstructorsToInstances, UploadFile } from 'src/types'; import { getConfig, updateConfig } from 'src/utils/config'; import { mockEnvData } from 'test/repositories/config.repository.mock'; import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock'; @@ -88,29 +88,34 @@ import { Mocked } from 'vitest'; export const testAssetsDir = resolve(__dirname, '../../e2e/test-assets'); type MediumTestOptions = { - mock: ClassConstructor[]; - real: ClassConstructor[]; + mock: Array<(typeof BASE_SERVICE_DEPENDENCIES)[number]>; + real: Array<(typeof BASE_SERVICE_DEPENDENCIES)[number]>; database: Kysely; }; -export const newMediumService = (Service: ClassConstructor, options: MediumTestOptions) => { +type BaseServiceDeps = typeof BASE_SERVICE_DEPENDENCIES; + +export const newMediumService = >( + Service: S, + options: MediumTestOptions, +) => { const ctx = new MediumTestContext(Service, options); return { sut: ctx.sut, ctx }; }; -export class MediumTestContext { +export class MediumTestContext = ClassConstructor> { private repoCache: Record = {}; - private sutDeps: any[]; + private sutDeps: ClassConstructorsToInstances; - sut: S; + sut: InstanceType; database: Kysely; constructor( - Service: ClassConstructor, + Service: S, private options: MediumTestOptions, ) { this.sutDeps = this.makeDeps(options); - this.sut = new Service(...this.sutDeps); + this.sut = new Service(...this.sutDeps) as InstanceType; this.database = options.database; } @@ -128,7 +133,7 @@ export class MediumTestContext { throw new Error(`Real repository ${dep.name} is not a valid dependency`); } } - return (deps as ClassConstructor[]).map((dep) => { + return deps.map((dep) => { if (options.real.includes(dep)) { return this.get(dep); } @@ -136,10 +141,10 @@ export class MediumTestContext { if (options.mock.includes(dep)) { return newMockRepository(dep); } - }); + }) as unknown as ClassConstructorsToInstances; } - get(key: ClassConstructor): T { + get(key: T): InstanceType { if (!Object.hasOwn(this.repoCache, key.name)) { const real = newRealRepository(key, this.options.database); this.repoCache[key.name] = real; @@ -148,8 +153,8 @@ export class MediumTestContext { return this.repoCache[key.name]; } - getMock>(key: ClassConstructor): R { - const index = BASE_SERVICE_DEPENDENCIES.indexOf(key as any); + getMock>>(key: T): R { + const index = BASE_SERVICE_DEPENDENCIES.indexOf(key); if (index === -1 || !this.options.mock.includes(key)) { throw new Error(`getMock called with a key that is not a mock: ${key.name}`); } @@ -328,7 +333,7 @@ export class MediumTestContext { } } -export class SyncTestContext extends MediumTestContext { +export class SyncTestContext extends MediumTestContext { constructor(database: Kysely) { super(SyncService, { database, @@ -379,7 +384,7 @@ const mockStats = { birthtimeMs: 0, }; -export class ExifTestContext extends MediumTestContext { +export class ExifTestContext extends MediumTestContext { constructor(database: Kysely) { super(MetadataService, { database, @@ -431,7 +436,7 @@ export class ExifTestContext extends MediumTestContext { } } -const newRealRepository = (key: ClassConstructor, db: Kysely): T => { +const newRealRepository = (key: T, db: Kysely): InstanceType => { switch (key) { case AccessRepository: case AlbumRepository: @@ -457,41 +462,41 @@ const newRealRepository = (key: ClassConstructor, db: Kysely): T => { case UserRepository: case VersionHistoryRepository: case WorkflowRepository: { - return new key(db); + return new key(db) as InstanceType; } case ConfigRepository: case CryptoRepository: { - return new key(); + return new key() as InstanceType; } case DatabaseRepository: { - return new key(db, LoggingRepository.create(), new ConfigRepository()); + return new key(db, LoggingRepository.create(), new ConfigRepository()) as InstanceType; } case EmailRepository: { - return new key(LoggingRepository.create()); + return new key(LoggingRepository.create()) as InstanceType; } case MediaRepository: case MetadataRepository: { - return new key(LoggingRepository.create()); + return new key(LoggingRepository.create()) as InstanceType; } case PluginRepository: { - return new key(db, LoggingRepository.create()); + return new key(db, LoggingRepository.create()) as InstanceType; } case StorageRepository: { - return new key(LoggingRepository.create()); + return new key(LoggingRepository.create()) as InstanceType; } case TagRepository: { - return new key(db, LoggingRepository.create()); + return new key(db, LoggingRepository.create()) as InstanceType; } - case LoggingRepository as unknown as ClassConstructor: { - return new key() as unknown as T; + case LoggingRepository: { + return new key(undefined, undefined) as InstanceType; } default: { diff --git a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts index 8bd65af73f..dfb4868c0d 100644 --- a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts +++ b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts @@ -24,7 +24,7 @@ import { getKyselyDB } from 'test/utils'; let isInitialized = false; -class WorkflowTestContext extends MediumTestContext { +class WorkflowTestContext extends MediumTestContext { constructor(database: Kysely) { super(WorkflowExecutionService, { database, From be5cc30f5270ee886b4a713258f35abb77e21df8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:28:24 -0400 Subject: [PATCH 04/49] chore(deps): update prom/prometheus docker digest to 508729e (#30535) 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 013ff266fa..bb29fd99af 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -85,7 +85,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:3c42b892cf723fa54d2f262c37a0e1f80aa8c8ddb1da7b9b0df9455a35a7f893 + image: prom/prometheus@sha256:508729e0e2d18e11fd742a5a5ca70e557b940a93948c3c95fd0123a6fd538b69 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus From 51c2f21497800225e0622d515d0ef463d7af69cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:32:26 -0400 Subject: [PATCH 05/49] chore(deps): update grafana/grafana:12.4.6-ubuntu docker digest to 124ddf0 (#30534) 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 bb29fd99af..779065e327 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -97,7 +97,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:12.4.6-ubuntu@sha256:35c75489ff2e2e69c53977a180de491643d51fdb32108f9335317527978daed7 + image: grafana/grafana:12.4.6-ubuntu@sha256:124ddf024f95e61b562a02112b47bc64de75973370cf440b1356d31cd8178844 volumes: - grafana-data:/var/lib/grafana From 1c5770601dc5e9a19714040bc2cbb0d263afc055 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 8 Aug 2026 10:05:30 +0530 Subject: [PATCH 06/49] chore: add asset debug action to all bottom sheets (#30611) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../widgets/bottom_sheet/archive_bottom_sheet.widget.dart | 2 ++ .../widgets/bottom_sheet/favorite_bottom_sheet.widget.dart | 2 ++ .../widgets/bottom_sheet/local_album_bottom_sheet.widget.dart | 2 ++ .../widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart | 2 ++ .../bottom_sheet/partner_detail_bottom_sheet.widget.dart | 2 ++ .../widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart | 2 ++ .../widgets/bottom_sheet/trash_bottom_sheet.widget.dart | 2 ++ 7 files changed, 14 insertions(+) diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart index 3e5edaa149..837397254c 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; +import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/download.action.dart'; import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; @@ -73,6 +74,7 @@ class _ArchiveBottomSheetState extends ConsumerState { maxChildSize: 0.85, shouldCloseOnMinExtent: false, actions: const [ + .new(action: AssetDebugAction(source: .timeline)), .new(action: ShareAction(source: .timeline)), .new(action: ShareLinkAction(source: .timeline)), .new(action: ArchiveAction(source: .timeline)), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index 7540da4fff..3322035320 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; +import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/download.action.dart'; import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; @@ -75,6 +76,7 @@ class FavoriteBottomSheet extends ConsumerWidget { maxChildSize: 0.7, shouldCloseOnMinExtent: false, actions: const [ + .new(action: AssetDebugAction(source: .timeline)), .new(action: ShareAction(source: .timeline)), .new(action: ShareLinkAction(source: .timeline)), .new(action: FavoriteAction(source: .timeline)), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart index 478b37212e..94e708015c 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/share.action.dart'; import 'package:immich_mobile/presentation/actions/upload.action.dart'; @@ -66,6 +67,7 @@ class _LocalAlbumBottomSheetState extends ConsumerState { maxChildSize: 0.85, shouldCloseOnMinExtent: false, actions: const [ + .new(action: AssetDebugAction(source: .timeline)), .new(action: ShareAction(source: .timeline)), .new(action: DeleteAction(source: .timeline)), .new(action: CleanupLocalAction(source: .timeline)), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart index 3064ced2a4..32730b8ac4 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/download.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; @@ -16,6 +17,7 @@ class LockedFolderBottomSheet extends StatelessWidget { maxChildSize: 0.4, shouldCloseOnMinExtent: false, actions: [ + .new(action: AssetDebugAction(source: .timeline)), .new(action: ShareAction(source: .timeline)), .new(action: DownloadAction(source: .timeline)), .new(action: DeleteAction(source: .timeline)), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart index 8a20f26c73..c764669cf6 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/download.action.dart'; import 'package:immich_mobile/presentation/actions/share.action.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; @@ -65,6 +66,7 @@ class _PartnerDetailBottomSheetState extends ConsumerState[ + .new(action: AssetDebugAction(source: .timeline)), .new(action: ShareAction(source: .timeline)), .new(action: DownloadAction(source: .timeline)), ], diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index 1e51a26811..cdae658b45 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; +import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/download.action.dart'; import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; @@ -84,6 +85,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState maxChildSize: 0.85, shouldCloseOnMinExtent: false, actions: [ + const .new(action: AssetDebugAction(source: .timeline)), const .new(action: ShareAction(source: .timeline)), const .new(action: ShareLinkAction(source: .timeline)), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart index b496503dad..32e5b01a99 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; @@ -19,6 +20,7 @@ class TrashBottomBar extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + .new(action: AssetDebugAction(source: .timeline)), .new(action: DeleteAction(source: .timeline)), .new(action: RestoreAction(source: .timeline)), ], From 8d086d61c4877e04a31716eea50d484d745dd186 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Sat, 8 Aug 2026 17:54:48 +0200 Subject: [PATCH 07/49] chore: add good-first-issue llm rule to contributing.md (#30645) --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f2c9e62a0..5972872528 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,8 @@ We ask you not to open PRs generated with an LLM. We find that code generated li If you use an LLM to translate a PR description or title, that is fine. Please make sure however that you stick to the PR template and that the text is written concisely. +LLMs must not be used to fix GitHub issues labelled good first issue. These issues are generally not urgent, and are intended to be learning opportunities for new contributors to get familiar with the codebase. Whether you are a newcomer or not, fully automating the process of fixing this issue squanders the learning opportunity and doesn’t add much value to the project. Using LLMs to fix issues labelled as “good first issue” is forbidden, and those PRs will be closed automatically. + Misrepresenting LLM use, contribution farming (automated low-effort PRs), or repeatedly hitting auto-close rules may be grounds for a block at maintainer discretion. ## Feature freezes From 79e61c47c7c1bbea263b5996c5d70574bf241fae Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Sat, 8 Aug 2026 12:07:30 -0400 Subject: [PATCH 08/49] refactor(ml): remove insightface dependency (#30631) * remove insightface dependency * update tests --- machine-learning/conftest.py | 22 +- machine-learning/immich_ml/models/base.py | 2 +- .../models/facial_recognition/_ops.py | 74 ++ .../models/facial_recognition/detection.py | 42 +- .../models/facial_recognition/recognition.py | 36 +- .../immich_ml/models/ocr/detection.py | 3 +- .../immich_ml/models/ocr/recognition.py | 3 +- .../immich_ml/models/transforms.py | 53 +- machine-learning/immich_ml/schemas.py | 7 +- .../immich_ml/sessions/ann/__init__.py | 6 +- .../immich_ml/sessions/rknn/__init__.py | 2 +- machine-learning/pyproject.toml | 2 +- machine-learning/test_main.py | 358 ++++++---- machine-learning/uv.lock | 674 ++---------------- 14 files changed, 427 insertions(+), 857 deletions(-) create mode 100644 machine-learning/immich_ml/models/facial_recognition/_ops.py diff --git a/machine-learning/conftest.py b/machine-learning/conftest.py index 57741f3807..a5acc059d1 100644 --- a/machine-learning/conftest.py +++ b/machine-learning/conftest.py @@ -1,11 +1,10 @@ import json -from typing import Any, Iterator +from types import SimpleNamespace +from typing import Any, Callable, Iterator from unittest import mock -import numpy as np import pytest from fastapi.testclient import TestClient -from numpy.typing import NDArray from PIL import Image from immich_ml.config import log @@ -17,11 +16,6 @@ def pil_image() -> Image.Image: return Image.new("RGB", (600, 800)) -@pytest.fixture -def cv_image(pil_image: Image.Image) -> NDArray[np.float32]: - return np.asarray(pil_image)[:, :, ::-1] # PIL uses RGB while cv2 uses BGR - - @pytest.fixture def mock_get_model() -> Iterator[mock.Mock]: with mock.patch("immich_ml.models.cache.from_model_type", autospec=True) as mocked: @@ -129,6 +123,18 @@ def ort_session() -> Iterator[mock.Mock]: yield mocked +@pytest.fixture(scope="function") +def stub_session() -> Callable[..., mock.Mock]: + def _make(shape: tuple[Any, ...], outputs: Any = None, name: str = "input.1") -> mock.Mock: + session = mock.Mock() + session.get_inputs.return_value = [SimpleNamespace(name=name, shape=shape)] + if outputs is not None: + session.run.return_value = outputs + return session + + return _make + + @pytest.fixture(scope="function") def ann_session() -> Iterator[mock.Mock]: with mock.patch("immich_ml.sessions.ann.Ann") as mocked: diff --git a/machine-learning/immich_ml/models/base.py b/machine-learning/immich_ml/models/base.py index 00790ce744..5c40b21f32 100644 --- a/machine-learning/immich_ml/models/base.py +++ b/machine-learning/immich_ml/models/base.py @@ -57,7 +57,7 @@ class InferenceModel(ABC): self.load() if model_kwargs: self.configure(**model_kwargs) - return self._predict(*inputs) + return self._predict(*inputs, **model_kwargs) @abstractmethod def _predict(self, *inputs: Any, **model_kwargs: Any) -> Any: ... diff --git a/machine-learning/immich_ml/models/facial_recognition/_ops.py b/machine-learning/immich_ml/models/facial_recognition/_ops.py new file mode 100644 index 0000000000..6bdf5f5c3c --- /dev/null +++ b/machine-learning/immich_ml/models/facial_recognition/_ops.py @@ -0,0 +1,74 @@ +""" +Host-side geometry for the fused face models. + +The decode, NMS and Umeyama similarity are ports of insightface (Apache-2.0, https://github.com/deepinsight/insightface) +""" + +from functools import lru_cache + +import cv2 +import numpy as np +from numpy.typing import NDArray + +from immich_ml.models.transforms import ensure_dims + +DET_SIZE = 640 +ALIGNED_SIZE = 112 + +# the FPN levels for which the fused detector emits a (scores, boxes, kps) triple, +# and the anchors each feature-map cell carries, laid out anchor-major +DET_STRIDES = (8, 16, 32) +ANCHORS_PER_CELL = 2 + +# canonical ArcFace 5-point template for a 112x112 crop +ARCFACE_DST = np.array( + [[38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041]], + dtype=np.float32, +) + + +@lru_cache(maxsize=len(DET_STRIDES)) +def _anchor_centers(size: int, stride: int) -> NDArray[np.float32]: + ys, xs = np.mgrid[: size // stride, : size // stride] + centers = np.stack([xs, ys], axis=-1, dtype=np.float32).reshape(-1, 2) * stride + return np.repeat(centers, ANCHORS_PER_CELL, axis=0) + + +def decode_scrfd( + heads: list[NDArray[np.float32]], size: int = DET_SIZE +) -> tuple[NDArray[np.float32], NDArray[np.float32], NDArray[np.float32]]: + heads = [ensure_dims(head, 4) for head in heads] + scores, boxes, kps = [], [], [] + for level, stride in enumerate(DET_STRIDES): + centers = _anchor_centers(size, stride) + distance = heads[level + len(DET_STRIDES)][0] * stride + offsets = heads[level + 2 * len(DET_STRIDES)][0] * stride + scores.append(heads[level][0].squeeze(-1)) + boxes.append(np.concatenate([centers[None] - distance[:, :, :2], centers[None] + distance[:, :, 2:]], axis=2)) + kps.append(np.tile(centers, offsets.shape[2] // 2) + offsets) + + return np.concatenate(scores, axis=1), np.concatenate(boxes, axis=1), np.concatenate(kps, axis=1) + + +def nms(boxes: NDArray[np.float32], scores: NDArray[np.float32], threshold: float = 0.4) -> NDArray[np.intp]: + wh = np.column_stack([boxes[:, 0], boxes[:, 1], boxes[:, 2] - boxes[:, 0], boxes[:, 3] - boxes[:, 1]]) + keep = cv2.dnn.NMSBoxes(wh.tolist(), scores.tolist(), 0.0, threshold) # NMSBoxes treats the inputs as Sequences + return np.asarray(keep, dtype=np.intp).reshape(-1) + + +def umeyama(src: NDArray[np.float32], dst: NDArray[np.float32]) -> NDArray[np.float32]: + src_mean, dst_mean = src.mean(0), dst.mean(0) + src_c, dst_c = src - src_mean, dst - dst_mean + cov = dst_c.T @ src_c / len(src) + u, s, vt = np.linalg.svd(cov) + d = np.sign(np.linalg.det(u @ vt)) + diag = np.diag([1.0, d]) + rotation = u @ diag @ vt + scale = np.trace(np.diag(s) @ diag) / (src_c**2).sum() * len(src) + translation = dst_mean - scale * rotation @ src_mean + return np.hstack([scale * rotation, translation[:, None]], dtype=np.float32) + + +def align_face(image: NDArray[np.uint8], kps: NDArray[np.float32]) -> NDArray[np.float32]: + matrix = umeyama(kps, ARCFACE_DST) + return cv2.warpAffine(image, matrix, (ALIGNED_SIZE, ALIGNED_SIZE)).astype(np.float32) diff --git a/machine-learning/immich_ml/models/facial_recognition/detection.py b/machine-learning/immich_ml/models/facial_recognition/detection.py index 26c9208e48..a113755201 100644 --- a/machine-learning/immich_ml/models/facial_recognition/detection.py +++ b/machine-learning/immich_ml/models/facial_recognition/detection.py @@ -1,41 +1,31 @@ -from typing import Any - import numpy as np -from insightface.model_zoo import RetinaFace from numpy.typing import NDArray from immich_ml.models.base import InferenceModel -from immich_ml.models.transforms import decode_cv2 -from immich_ml.schemas import FaceDetectionOutput, ModelSession, ModelTask, ModelType +from immich_ml.models.transforms import decode_pil, letterbox, normalize +from immich_ml.schemas import FaceDetectionOutput, ModelTask, ModelType + +from ._ops import DET_SIZE, decode_scrfd, nms class FaceDetector(InferenceModel): depends = [] identity = (ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION) - def __init__(self, model_name: str, min_score: float = 0.7, **model_kwargs: Any) -> None: - self.min_score = model_kwargs.pop("minScore", min_score) - super().__init__(model_name, **model_kwargs) + def _predict(self, inputs: NDArray[np.uint8] | bytes, minScore: float) -> FaceDetectionOutput: + canvas, scale = letterbox(decode_pil(inputs), DET_SIZE) + blob = normalize(canvas.astype(np.float32), mean=127.5, std=128).transpose(2, 0, 1)[None] - def _load(self) -> ModelSession: - session = self._make_session(self.model_path) - self.model = RetinaFace(session=session) - self.model.prepare(ctx_id=0, det_thresh=self.min_score, input_size=(640, 640)) + input_name = self.session.get_inputs()[0].name + heads = self.session.run(None, {input_name: blob}) + scores, boxes, kps = decode_scrfd(heads, DET_SIZE) - return session + candidates = scores >= minScore + scores, boxes, kps = scores[candidates], boxes[candidates] / scale, kps[candidates] / scale + keep = nms(boxes, scores) - def _predict(self, inputs: NDArray[np.uint8] | bytes) -> FaceDetectionOutput: - inputs = decode_cv2(inputs) - - bboxes, landmarks = self._detect(inputs) return { - "boxes": bboxes[:, :4].round(), - "scores": bboxes[:, 4], - "landmarks": landmarks, + "boxes": boxes[keep].round(), + "scores": scores[keep], + "landmarks": kps[keep].reshape(-1, 5, 2), } - - def _detect(self, inputs: NDArray[np.uint8] | bytes) -> tuple[NDArray[np.float32], NDArray[np.float32]]: - return self.model.detect(inputs) # type: ignore - - def configure(self, **kwargs: Any) -> None: - self.model.det_thresh = kwargs.pop("minScore", self.model.det_thresh) diff --git a/machine-learning/immich_ml/models/facial_recognition/recognition.py b/machine-learning/immich_ml/models/facial_recognition/recognition.py index c144bd3eaa..2b0575ec38 100644 --- a/machine-learning/immich_ml/models/facial_recognition/recognition.py +++ b/machine-learning/immich_ml/models/facial_recognition/recognition.py @@ -4,15 +4,13 @@ from typing import Any import numpy as np import onnx import onnxruntime as ort -from insightface.model_zoo import ArcFaceONNX -from insightface.utils.face_align import norm_crop from numpy.typing import NDArray from onnx.tools.update_model_dims import update_inputs_outputs_dims from PIL import Image from immich_ml.config import log, settings from immich_ml.models.base import InferenceModel -from immich_ml.models.transforms import decode_cv2, serialize_np_array +from immich_ml.models.transforms import decode_pil, normalize, serialize_np_array from immich_ml.schemas import ( FaceDetectionOutput, FacialRecognitionOutput, @@ -22,6 +20,8 @@ from immich_ml.schemas import ( ModelType, ) +from ._ops import align_face + class FaceRecognizer(InferenceModel): depends = [(ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION)] @@ -37,10 +37,6 @@ class FaceRecognizer(InferenceModel): if (not self.batch_size or self.batch_size > 1) and str(session.get_inputs()[0].shape[0]) != "batch": self._add_batch_axis(self.model_path) session = self._make_session(self.model_path) - self.model = ArcFaceONNX( - self.model_path_for_format(ModelFormat.ONNX).as_posix(), - session=session, - ) return session def _predict( @@ -48,20 +44,21 @@ class FaceRecognizer(InferenceModel): ) -> FacialRecognitionOutput: if faces["boxes"].shape[0] == 0: return [] - inputs = decode_cv2(inputs) - cropped_faces = self._crop(inputs, faces) - embeddings = self._predict_batch(cropped_faces) + image = np.asarray(decode_pil(inputs), dtype=np.uint8) + crops = np.stack([align_face(image, kps).transpose(2, 0, 1) for kps in faces["landmarks"]]) + embeddings = self._predict_batch(normalize(crops, mean=127.5, std=127.5)) return self.postprocess(faces, embeddings) - def _predict_batch(self, cropped_faces: list[NDArray[np.uint8]]) -> NDArray[np.float32]: - if not self.batch_size or len(cropped_faces) <= self.batch_size: - embeddings: NDArray[np.float32] = self.model.get_feat(cropped_faces) - return embeddings + def _predict_batch(self, crops: NDArray[np.float32]) -> NDArray[np.float32]: + input_name = self.session.get_inputs()[0].name + if not self.batch_size or crops.shape[0] <= self.batch_size: + return self.session.run(None, {input_name: crops})[0] - batch_embeddings: list[NDArray[np.float32]] = [] - for i in range(0, len(cropped_faces), self.batch_size): - batch_embeddings.append(self.model.get_feat(cropped_faces[i : i + self.batch_size])) - return np.concatenate(batch_embeddings, axis=0) + batches = [ + self.session.run(None, {input_name: crops[i : i + self.batch_size]})[0] + for i in range(0, crops.shape[0], self.batch_size) + ] + return np.concatenate(batches, axis=0) def postprocess(self, faces: FaceDetectionOutput, embeddings: NDArray[np.float32]) -> FacialRecognitionOutput: return [ @@ -73,9 +70,6 @@ class FaceRecognizer(InferenceModel): for (x1, y1, x2, y2), embedding, score in zip(faces["boxes"], embeddings, faces["scores"]) ] - def _crop(self, image: NDArray[np.uint8], faces: FaceDetectionOutput) -> list[NDArray[np.uint8]]: - return [norm_crop(image, landmark) for landmark in faces["landmarks"]] - def _add_batch_axis(self, model_path: Path) -> None: log.debug(f"Adding batch axis to model {model_path}") proto = onnx.load(model_path) diff --git a/machine-learning/immich_ml/models/ocr/detection.py b/machine-learning/immich_ml/models/ocr/detection.py index 0a2cb8ad91..48dd87854b 100644 --- a/machine-learning/immich_ml/models/ocr/detection.py +++ b/machine-learning/immich_ml/models/ocr/detection.py @@ -63,7 +63,8 @@ class TextDetector(InferenceModel): return OrtSession(self.model_path) # partly adapted from RapidOCR - def _predict(self, inputs: Image.Image) -> TextDetectionOutput: + # TODO: take the options as parameters instead of applying them through `configure` + def _predict(self, inputs: Image.Image, **kwargs: Any) -> TextDetectionOutput: w, h = inputs.size if w < 32 or h < 32: return self._empty diff --git a/machine-learning/immich_ml/models/ocr/recognition.py b/machine-learning/immich_ml/models/ocr/recognition.py index 94f40c9285..ea7ff565d1 100644 --- a/machine-learning/immich_ml/models/ocr/recognition.py +++ b/machine-learning/immich_ml/models/ocr/recognition.py @@ -69,7 +69,8 @@ class TextRecognizer(InferenceModel): ) return session - def _predict(self, img: Image.Image, texts: TextDetectionOutput) -> TextRecognitionOutput: + # TODO: take the options as parameters instead of applying them through `configure` + def _predict(self, img: Image.Image, texts: TextDetectionOutput, **kwargs: Any) -> TextRecognitionOutput: boxes, box_scores = texts["boxes"], texts["scores"] if boxes.shape[0] == 0: return self._empty diff --git a/machine-learning/immich_ml/models/transforms.py b/machine-learning/immich_ml/models/transforms.py index 3f9d93bed3..ac220a3812 100644 --- a/machine-learning/immich_ml/models/transforms.py +++ b/machine-learning/immich_ml/models/transforms.py @@ -1,6 +1,5 @@ import string from io import BytesIO -from typing import IO import cv2 import numpy as np @@ -12,11 +11,10 @@ _PIL_RESAMPLING_METHODS = {resampling.name.lower(): resampling for resampling in _PUNCTUATION_TRANS = str.maketrans("", "", string.punctuation) -def resize_pil(img: Image.Image, size: int) -> Image.Image: +def resize_pil(img: Image.Image, size: int, resample: Image.Resampling = Image.Resampling.BICUBIC) -> Image.Image: if img.width < img.height: - return img.resize((size, int((img.height / img.width) * size)), resample=Image.Resampling.BICUBIC) - else: - return img.resize((int((img.width / img.height) * size), size), resample=Image.Resampling.BICUBIC) + return img.resize((size, int((img.height / img.width) * size)), resample=resample) + return img.resize((int((img.width / img.height) * size), size), resample=resample) # https://stackoverflow.com/a/60883103 @@ -36,7 +34,9 @@ def to_numpy(img: Image.Image) -> NDArray[np.float32]: def normalize( img: NDArray[np.float32], mean: float | NDArray[np.float32], std: float | NDArray[np.float32] ) -> NDArray[np.float32]: - return (img - mean) / std + img *= 1.0 / std + img -= mean / std + return img def get_pil_resampling(resample: str) -> Image.Resampling: @@ -47,26 +47,21 @@ def pil_to_cv2(image: Image.Image) -> NDArray[np.uint8]: return cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) # type: ignore -def decode_pil(image_bytes: bytes | IO[bytes] | Image.Image) -> Image.Image: - if isinstance(image_bytes, Image.Image): - return image_bytes - image: Image.Image = Image.open(BytesIO(image_bytes) if isinstance(image_bytes, bytes) else image_bytes) +def decode_pil(image_bytes: bytes | Image.Image | NDArray[np.uint8]) -> Image.Image: + image: Image.Image + match image_bytes: + case Image.Image(): + image = image_bytes + case np.ndarray(): + image = Image.fromarray(image_bytes) + case bytes(): + image = Image.open(BytesIO(image_bytes)) image.load() if not image.mode == "RGB": image = image.convert("RGB") return image -def decode_cv2(image_bytes: NDArray[np.uint8] | bytes | Image.Image) -> NDArray[np.uint8]: - match image_bytes: - case bytes() | memoryview() | bytearray(): - return pil_to_cv2(decode_pil(image_bytes)) # pillow is much faster than cv2 - case Image.Image(): - return pil_to_cv2(image_bytes) - case _: - return image_bytes - - def clean_text(text: str, canonicalize: bool = False) -> str: text = " ".join(text.split()) if canonicalize: @@ -78,3 +73,21 @@ def clean_text(text: str, canonicalize: bool = False) -> str: # TODO: use this in a less invasive way def serialize_np_array(arr: NDArray[np.float32]) -> str: return orjson.dumps(arr, option=orjson.OPT_SERIALIZE_NUMPY).decode() + + +def letterbox(image: NDArray[np.uint8] | Image.Image, size: int) -> tuple[NDArray[np.uint8], float]: + if isinstance(image, Image.Image): + image = np.asarray(image) + height, width = image.shape[:2] + if height > width: + new_height, new_width = size, int(size * width / height) + else: + new_width, new_height = size, int(size * height / width) + + canvas = np.zeros((size, size, 3), dtype=np.uint8) + cv2.resize(image, (new_width, new_height), dst=canvas[:new_height, :new_width]) + return canvas, new_height / height + + +def ensure_dims(array: NDArray[np.float32], ndim: int) -> NDArray[np.float32]: + return array if array.ndim >= ndim else np.expand_dims(array, axis=tuple(range(ndim - array.ndim))) diff --git a/machine-learning/immich_ml/schemas.py b/machine-learning/immich_ml/schemas.py index d5304cf763..d5695f5eb3 100644 --- a/machine-learning/immich_ml/schemas.py +++ b/machine-learning/immich_ml/schemas.py @@ -14,10 +14,7 @@ class ORJSONResponse(JSONResponse): class StrEnum(str, Enum): - value: str - - def __str__(self) -> str: - return self.value + __str__ = str.__str__ class BoundingBox(TypedDict): @@ -63,7 +60,7 @@ ModelIdentity = tuple[ModelType, ModelTask] class SessionNode(Protocol): @property - def name(self) -> str | None: ... + def name(self) -> str: ... @property def shape(self) -> tuple[int, ...]: ... diff --git a/machine-learning/immich_ml/sessions/ann/__init__.py b/machine-learning/immich_ml/sessions/ann/__init__.py index 6f36f675f6..d3279f9a17 100644 --- a/machine-learning/immich_ml/sessions/ann/__init__.py +++ b/machine-learning/immich_ml/sessions/ann/__init__.py @@ -37,11 +37,11 @@ class AnnSession: def get_inputs(self) -> list[SessionNode]: shapes = self.ann.input_shapes[self.model] - return [AnnNode(None, s) for s in shapes] + return [AnnNode(f"input.{i + 1}", s) for i, s in enumerate(shapes)] def get_outputs(self) -> list[SessionNode]: shapes = self.ann.output_shapes[self.model] - return [AnnNode(None, s) for s in shapes] + return [AnnNode(f"output.{i + 1}", s) for i, s in enumerate(shapes)] def run( self, @@ -54,5 +54,5 @@ class AnnSession: class AnnNode(NamedTuple): - name: str | None + name: str shape: tuple[int, ...] diff --git a/machine-learning/immich_ml/sessions/rknn/__init__.py b/machine-learning/immich_ml/sessions/rknn/__init__.py index e388e4febc..90f2e63ed3 100644 --- a/machine-learning/immich_ml/sessions/rknn/__init__.py +++ b/machine-learning/immich_ml/sessions/rknn/__init__.py @@ -69,7 +69,7 @@ class RknnSession: class RknnNode(NamedTuple): - name: str | None + name: str shape: tuple[int, ...] diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index af4d8edf44..e95ef7afef 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -10,8 +10,8 @@ dependencies = [ "fastapi>=0.95.2,<1.0", "gunicorn>=21.1.0", "huggingface-hub>=1.0,<2.0", - "insightface>=0.7.3,<2.0", "numpy>=2.4.0,<3.0", + "onnx>=1.22.0", "opencv-python-headless>=4.7.0.72,<5.0", "orjson>=3.9.5", "pillow>=12.2,<13", diff --git a/machine-learning/test_main.py b/machine-learning/test_main.py index be574c6397..567c6ab176 100644 --- a/machine-learning/test_main.py +++ b/machine-learning/test_main.py @@ -7,7 +7,6 @@ from types import SimpleNamespace from typing import Any, Callable from unittest import mock -import cv2 import numpy as np import onnxruntime as ort import orjson @@ -516,7 +515,7 @@ class TestAnnSession: inputs = session.get_inputs() assert len(inputs) == 1 - assert inputs[0].name is None + assert inputs[0].name == "input.1" assert inputs[0].shape == (1, 3, 224, 224) def test_get_outputs(self, ann_session: mock.Mock) -> None: @@ -527,7 +526,7 @@ class TestAnnSession: outputs = session.get_outputs() assert len(outputs) == 1 - assert outputs[0].name is None + assert outputs[0].name == "output.1" assert outputs[0].shape == (1, 3, 224, 224) def test_run(self, ann_session: mock.Mock, mocker: MockerFixture) -> None: @@ -781,89 +780,189 @@ class TestCLIP: assert np.allclose(tokens["attention_mask"], np.array([mock_attention_mask], dtype=np.int32), atol=0) +def make_scrfd_heads(detections: list[tuple[int, int, float]]) -> list[np.ndarray]: + """Build the 9 head tensors a SCRFD keypoint model emits at 640x640. + + `detections` is a list of (cell_x, cell_y, score) placed on the stride-8 level. + Distances and keypoint offsets are fixed so the decoded geometry is known by + hand rather than derived from the code under test: + box = [cx - 1*8, cy - 2*8, cx + 3*8, cy + 4*8] + kps = [cx, cy] + [0, 1, 2, ... 9] * 8, reshaped to 5 points + """ + heads: list[np.ndarray] = [] + counts = [(640 // stride) ** 2 * 2 for stride in (8, 16, 32)] + for channels in (1, 4, 10): + for n in counts: + heads.append(np.zeros((n, channels), dtype=np.float32)) + for cell_x, cell_y, score in detections: + i = 2 * (cell_y * 80 + cell_x) # anchor-major, 2 anchors per cell + heads[0][i] = score + heads[3][i] = [1, 2, 3, 4] + heads[6][i] = np.arange(10) + return heads + + +def expected_box(cell_x: int, cell_y: int) -> list[float]: + cx, cy = cell_x * 8, cell_y * 8 + return [cx - 8, cy - 16, cx + 24, cy + 32] + + +def expected_landmarks(cell_x: int, cell_y: int) -> np.ndarray: + cx, cy = cell_x * 8, cell_y * 8 + return (np.tile([cx, cy], 5) + np.arange(10) * 8).reshape(5, 2).astype(np.float32) + + +@pytest.fixture +def batch_axis(mocker: MockerFixture) -> SimpleNamespace: + """Patches for the `_add_batch_axis` path: the onnx module it rewrites the model + through, and the download it would otherwise trigger on load.""" + mocker.patch("immich_ml.models.base.InferenceModel.download") + return SimpleNamespace( + onnx=mocker.patch("immich_ml.models.facial_recognition.recognition.onnx", autospec=True), + update_dims=mocker.patch( + "immich_ml.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True + ), + ) + + class TestFaceRecognition: - def test_set_min_score(self, snapshot_download: mock.Mock, ort_session: mock.Mock, path: mock.Mock) -> None: - path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx" - - face_detector = FaceDetector("buffalo_s", min_score=0.5, cache_dir="test_cache") - face_detector.load() - - assert face_detector.min_score == 0.5 - assert face_detector.model.det_thresh == 0.5 - - def test_detection(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None: + def test_detection(self, stub_session: Callable[..., mock.Mock], mocker: MockerFixture) -> None: mocker.patch.object(FaceDetector, "load") - face_detector = FaceDetector("buffalo_s", min_score=0.0, cache_dir="test_cache") + face_detector = FaceDetector("buffalo_s", cache_dir="test_cache") - det_model = mock.Mock() - num_faces = 2 - bbox = np.random.rand(num_faces, 4).astype(np.float32) - scores = np.array([[0.67]] * num_faces).astype(np.float32) - kpss = np.random.rand(num_faces, 5, 2).astype(np.float32) - det_model.detect.return_value = (np.concatenate([bbox, scores], axis=-1), kpss) - face_detector.model = det_model + session = stub_session((1, 3, 640, 640), outputs=make_scrfd_heads([(10, 10, 0.9), (50, 50, 0.8)])) + face_detector.session = session - faces = face_detector.predict(cv_image) + faces = face_detector.predict(Image.new("RGB", (640, 640)), minScore=0.7) assert isinstance(faces, dict) - assert isinstance(faces.get("boxes", None), np.ndarray) - assert isinstance(faces.get("landmarks", None), np.ndarray) - assert isinstance(faces.get("scores", None), np.ndarray) - assert np.equal(faces["boxes"], bbox.round()).all() - assert np.equal(faces["landmarks"], kpss).all() - assert np.equal(faces["scores"], scores).all() - det_model.detect.assert_called_once() + assert set(faces) == {"boxes", "scores", "landmarks"} + # NMS returns highest score first + assert faces["boxes"].tolist() == [expected_box(10, 10), expected_box(50, 50)] + assert np.allclose(faces["scores"], [0.9, 0.8]) + assert faces["landmarks"].shape == (2, 5, 2) + assert np.allclose(faces["landmarks"][0], expected_landmarks(10, 10)) + assert np.allclose(faces["landmarks"][1], expected_landmarks(50, 50)) - def test_recognition(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None: + def test_detection_applies_min_score_per_request( + self, stub_session: Callable[..., mock.Mock], mocker: MockerFixture + ) -> None: + mocker.patch.object(FaceDetector, "load") + face_detector = FaceDetector("buffalo_s", cache_dir="test_cache") + + session = stub_session((1, 3, 640, 640), outputs=make_scrfd_heads([(10, 10, 0.9), (50, 50, 0.5)])) + face_detector.session = session + + # the threshold is a request parameter, so the same loaded model must honour both + assert face_detector.predict(Image.new("RGB", (640, 640)), minScore=0.7)["boxes"].shape[0] == 1 + assert face_detector.predict(Image.new("RGB", (640, 640)), minScore=0.4)["boxes"].shape[0] == 2 + + def test_detection_scales_boxes_back_to_the_original_image( + self, stub_session: Callable[..., mock.Mock], mocker: MockerFixture + ) -> None: + mocker.patch.object(FaceDetector, "load") + face_detector = FaceDetector("buffalo_s", cache_dir="test_cache") + + session = stub_session((1, 3, 640, 640), outputs=make_scrfd_heads([(10, 10, 0.9)])) + face_detector.session = session + + # a 320x320 image is letterboxed up to 640, so coordinates come back halved + faces = face_detector.predict(Image.new("RGB", (320, 320)), minScore=0.7) + + assert faces["boxes"].tolist() == [[v / 2 for v in expected_box(10, 10)]] + assert np.allclose(faces["landmarks"][0], expected_landmarks(10, 10) / 2) + + def test_recognition(self, stub_session: Callable[..., mock.Mock], mocker: MockerFixture) -> None: mocker.patch.object(FaceRecognizer, "load") mocker.patch( "immich_ml.models.facial_recognition.recognition.ort.get_available_providers", return_value=["CPUExecutionProvider"], ) - face_recognizer = FaceRecognizer("buffalo_s", min_score=0.0, cache_dir="test_cache") + face_recognizer = FaceRecognizer("buffalo_s", cache_dir="test_cache") + # a uniform grey image whose crops land wholly inside it, so every sampled + # pixel is 128 and the normalised value the session receives is exact + image = Image.new("RGB", (600, 800), (128, 128, 128)) num_faces = 2 + arcface_dst = np.array( + [[38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041]], + dtype=np.float32, + ) + kpss = np.stack([arcface_dst * 2 + [200, 300], arcface_dst * 2 + [220, 320]]).astype(np.float32) bbox = np.random.rand(num_faces, 4).astype(np.float32) scores = np.array([0.67] * num_faces).astype(np.float32) - kpss = np.random.rand(num_faces, 5, 2).astype(np.float32) - faces = {"boxes": bbox, "landmarks": kpss, "scores": scores} + embeddings = np.random.rand(num_faces, 512).astype(np.float32) - rec_model = mock.Mock() - embedding = np.random.rand(num_faces, 512).astype(np.float32) - rec_model.get_feat.return_value = embedding - face_recognizer.model = rec_model + session = stub_session(("batch", 3, 112, 112), outputs=[embeddings]) + face_recognizer.session = session - faces = face_recognizer.predict(cv_image, faces) + faces = face_recognizer.predict(image, {"boxes": bbox, "landmarks": kpss, "scores": scores}) assert isinstance(faces, list) assert len(faces) == num_faces for face in faces: - assert isinstance(face.get("boundingBox"), dict) assert set(face["boundingBox"]) == {"x1", "y1", "x2", "y2"} assert all(isinstance(val, np.float32) for val in face["boundingBox"].values()) - embedding_str = face.get("embedding") - assert isinstance(embedding_str, str) - embedding = orjson.loads(embedding_str) + embedding = orjson.loads(face["embedding"]) assert isinstance(embedding, list) assert len(embedding) == 512 assert isinstance(face.get("score", None), np.float32) - rec_model.get_feat.assert_called_once() - call_args = rec_model.get_feat.call_args_list[0].args - assert len(call_args) == 1 - assert isinstance(call_args[0], list) - assert isinstance(call_args[0][0], np.ndarray) - assert call_args[0][0].shape == (112, 112, 3) + session.run.assert_called_once() + crops = session.run.call_args.args[1]["input.1"] + assert crops.shape == (num_faces, 3, 112, 112) + assert crops.dtype == np.float32 + # mean/std 127.5, not raw 0-255. atol is loose enough for the float32 cancellation + # in normalize's scale-then-subtract, but still rejects a wrong mean or std + assert np.allclose(crops, (128 - 127.5) / 127.5, atol=1e-6) + + def test_recognition_returns_early_without_faces(self, pil_image: Image.Image, mocker: MockerFixture) -> None: + mocker.patch.object(FaceRecognizer, "load") + mocker.patch( + "immich_ml.models.facial_recognition.recognition.ort.get_available_providers", + return_value=["CPUExecutionProvider"], + ) + face_recognizer = FaceRecognizer("buffalo_s", cache_dir="test_cache") + session = mock.Mock() + face_recognizer.session = session + + empty = { + "boxes": np.empty((0, 4), dtype=np.float32), + "landmarks": np.empty((0, 5, 2), dtype=np.float32), + "scores": np.empty(0, dtype=np.float32), + } + + assert face_recognizer.predict(pil_image, empty) == [] + session.run.assert_not_called() + + def test_recognition_batches_when_batch_size_is_set( + self, pil_image: Image.Image, stub_session: Callable[..., mock.Mock], mocker: MockerFixture + ) -> None: + mocker.patch.object(FaceRecognizer, "load") + mocker.patch( + "immich_ml.models.facial_recognition.recognition.ort.get_available_providers", + return_value=["CPUExecutionProvider"], + ) + face_recognizer = FaceRecognizer("buffalo_s", cache_dir="test_cache") + face_recognizer.batch_size = 2 + + num_faces = 5 + session = stub_session((1, 3, 112, 112)) + session.run.side_effect = lambda _, feed: [np.zeros((feed["input.1"].shape[0], 512), dtype=np.float32)] + face_recognizer.session = session + + faces = { + "boxes": np.random.rand(num_faces, 4).astype(np.float32), + "landmarks": (np.random.rand(num_faces, 5, 2) * 100).astype(np.float32), + "scores": np.array([0.67] * num_faces, dtype=np.float32), + } + assert len(face_recognizer.predict(pil_image, faces)) == num_faces + assert session.run.call_count == 3 # 2 + 2 + 1 + assert [c.args[1]["input.1"].shape[0] for c in session.run.call_args_list] == [2, 2, 1] def test_recognition_adds_batch_axis_for_ort( - self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture + self, batch_axis: SimpleNamespace, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture ) -> None: - onnx = mocker.patch("immich_ml.models.facial_recognition.recognition.onnx", autospec=True) - update_dims = mocker.patch( - "immich_ml.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True - ) - mocker.patch("immich_ml.models.base.InferenceModel.download") - mocker.patch("immich_ml.models.facial_recognition.recognition.ArcFaceONNX") mocker.patch( "immich_ml.models.facial_recognition.recognition.ort.get_available_providers", return_value=["CPUExecutionProvider"], @@ -884,24 +983,20 @@ class TestFaceRecognition: output_dims.type.tensor_type.shape.dim = [SimpleNamespace(dim_value=size) for size in [1, 800]] proto.graph.output = [output_dims] - onnx.load.return_value = proto + batch_axis.onnx.load.return_value = proto face_recognizer = FaceRecognizer("buffalo_s", cache_dir=path) face_recognizer.load() assert face_recognizer.batch_size is None - update_dims.assert_called_once_with(proto, {"input.1": ["batch", 3, 224, 224]}, {"output.1": ["batch", 800]}) - onnx.save.assert_called_once_with(update_dims.return_value, face_recognizer.model_path) + batch_axis.update_dims.assert_called_once_with( + proto, {"input.1": ["batch", 3, 224, 224]}, {"output.1": ["batch", 800]} + ) + batch_axis.onnx.save.assert_called_once_with(batch_axis.update_dims.return_value, face_recognizer.model_path) def test_recognition_does_not_add_batch_axis_if_exists( - self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture + self, batch_axis: SimpleNamespace, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture ) -> None: - onnx = mocker.patch("immich_ml.models.facial_recognition.recognition.onnx", autospec=True) - update_dims = mocker.patch( - "immich_ml.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True - ) - mocker.patch("immich_ml.models.base.InferenceModel.download") - mocker.patch("immich_ml.models.facial_recognition.recognition.ArcFaceONNX") mocker.patch( "immich_ml.models.facial_recognition.recognition.ort.get_available_providers", return_value=["CPUExecutionProvider"], @@ -916,88 +1011,53 @@ class TestFaceRecognition: face_recognizer = FaceRecognizer("buffalo_s", cache_dir=path) face_recognizer.load() + # batching is available here, so the axis is only skipped because it already exists assert face_recognizer.batch_size is None - update_dims.assert_not_called() - onnx.load.assert_not_called() - onnx.save.assert_not_called() + batch_axis.update_dims.assert_not_called() + batch_axis.onnx.load.assert_not_called() + batch_axis.onnx.save.assert_not_called() - def test_recognition_does_not_add_batch_axis_for_armnn( - self, ann_session: mock.Mock, path: mock.Mock, mocker: MockerFixture + @pytest.mark.parametrize( + ("session_fixture", "suffix", "model_kwargs", "available_providers"), + [ + pytest.param("ann_session", ".armnn", {"model_format": ModelFormat.ARMNN}, None, id="armnn"), + pytest.param( + "ort_session", ".onnx", {}, ["OpenVINOExecutionProvider", "CPUExecutionProvider"], id="openvino" + ), + pytest.param( + "ort_session", ".onnx", {}, ["MIGraphXExecutionProvider", "CPUExecutionProvider"], id="migraphx" + ), + ], + ) + def test_recognition_does_not_add_batch_axis_when_batching_is_unsupported( + self, + request: pytest.FixtureRequest, + batch_axis: SimpleNamespace, + path: mock.Mock, + ort_pybind: mock.Mock, + mocker: MockerFixture, + session_fixture: str, + suffix: str, + model_kwargs: dict[str, Any], + available_providers: list[str] | None, ) -> None: - onnx = mocker.patch("immich_ml.models.facial_recognition.recognition.onnx", autospec=True) - update_dims = mocker.patch( - "immich_ml.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True - ) - mocker.patch("immich_ml.models.base.InferenceModel.download") - mocker.patch("immich_ml.models.facial_recognition.recognition.ArcFaceONNX") - path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".armnn" - - inputs = [SimpleNamespace(name="input.1", shape=("batch", 3, 224, 224))] - outputs = [SimpleNamespace(name="output.1", shape=("batch", 800))] - ann_session.return_value.get_inputs.return_value = inputs - ann_session.return_value.get_outputs.return_value = outputs - - face_recognizer = FaceRecognizer("buffalo_s", model_format=ModelFormat.ARMNN, cache_dir=path) - face_recognizer.load() - - assert face_recognizer.batch_size == 1 - update_dims.assert_not_called() - onnx.load.assert_not_called() - onnx.save.assert_not_called() - - def test_recognition_does_not_add_batch_axis_for_openvino( - self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture - ) -> None: - onnx = mocker.patch("immich_ml.models.facial_recognition.recognition.onnx", autospec=True) - update_dims = mocker.patch( - "immich_ml.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True - ) - mocker.patch("immich_ml.models.base.InferenceModel.download") - mocker.patch("immich_ml.models.facial_recognition.recognition.ArcFaceONNX") - path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx" - - inputs = [SimpleNamespace(name="input.1", shape=("batch", 3, 224, 224))] - outputs = [SimpleNamespace(name="output.1", shape=("batch", 800))] - ort_session.return_value.get_inputs.return_value = inputs - ort_session.return_value.get_outputs.return_value = outputs - - face_recognizer = FaceRecognizer( - "buffalo_s", model_format=ModelFormat.ARMNN, cache_dir=path, providers=["OpenVINOExecutionProvider"] - ) - face_recognizer.load() - - assert face_recognizer.batch_size == 1 - update_dims.assert_not_called() - onnx.load.assert_not_called() - onnx.save.assert_not_called() - - def test_recognition_does_not_add_batch_axis_for_migraphx( - self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture - ) -> None: - onnx = mocker.patch("immich_ml.models.facial_recognition.recognition.onnx", autospec=True) - update_dims = mocker.patch( - "immich_ml.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True - ) - mocker.patch("immich_ml.models.base.InferenceModel.download") - mocker.patch("immich_ml.models.facial_recognition.recognition.ArcFaceONNX") + session = request.getfixturevalue(session_fixture) + ort_pybind.get_available_openvino_device_ids.return_value = ["CPU"] mocker.patch( "immich_ml.models.facial_recognition.recognition.ort.get_available_providers", - return_value=["MIGraphXExecutionProvider", "CPUExecutionProvider"], + return_value=available_providers, ) - path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx" + path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = suffix + session.return_value.get_inputs.return_value = [SimpleNamespace(name="input.1", shape=(1, 3, 224, 224))] + session.return_value.get_outputs.return_value = [SimpleNamespace(name="output.1", shape=(1, 800))] - inputs = [SimpleNamespace(name="input.1", shape=(1, 3, 224, 224))] - outputs = [SimpleNamespace(name="output.1", shape=(1, 800))] - ort_session.return_value.get_inputs.return_value = inputs - ort_session.return_value.get_outputs.return_value = outputs - - face_recognizer = FaceRecognizer("buffalo_s", cache_dir=path) + face_recognizer = FaceRecognizer("buffalo_s", cache_dir=path, **model_kwargs) face_recognizer.load() assert face_recognizer.batch_size == 1 - update_dims.assert_not_called() - onnx.load.assert_not_called() - onnx.save.assert_not_called() + batch_axis.update_dims.assert_not_called() + batch_axis.onnx.load.assert_not_called() + batch_axis.onnx.save.assert_not_called() def test_set_custom_max_batch_size(self, mocker: MockerFixture) -> None: mocker.patch.object(settings, "max_batch_size", MaxBatchSize(facial_recognition=2)) @@ -1045,10 +1105,10 @@ class TestOcr: rapid_recognizer.assert_called_once_with( OcrOptions( - session=ort_session.return_value, - rec_batch_num=6, - rec_img_shape=(3, 48, 320), - model_root_dir=text_recognizer.cache_dir, + session=ort_session.return_value, + rec_batch_num=6, + rec_img_shape=(3, 48, 320), + model_root_dir=text_recognizer.cache_dir, ) ) @@ -1063,10 +1123,10 @@ class TestOcr: rapid_recognizer.assert_called_once_with( OcrOptions( - session=ort_session.return_value, - rec_batch_num=4, - rec_img_shape=(3, 48, 320), - model_root_dir=text_recognizer.cache_dir, + session=ort_session.return_value, + rec_batch_num=4, + rec_img_shape=(3, 48, 320), + model_root_dir=text_recognizer.cache_dir, ) ) @@ -1083,10 +1143,10 @@ class TestOcr: rapid_recognizer.assert_called_once_with( OcrOptions( - session=ort_session.return_value, - rec_batch_num=6, - rec_img_shape=(3, 48, 320), - model_root_dir=text_recognizer.cache_dir, + session=ort_session.return_value, + rec_batch_num=6, + rec_img_shape=(3, 48, 320), + model_root_dir=text_recognizer.cache_dir, ) ) diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index 8a04d34711..fa92147445 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -25,23 +25,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/37/d7/15d67e05b235d1ed8c3ce61688fe4d84130e72af1657acadfaac3479f4cf/aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d", size = 28199, upload-time = "2024-09-25T13:20:22.688Z" }, ] -[[package]] -name = "albumentations" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "opencv-python-headless" }, - { name = "pyyaml" }, - { name = "qudida" }, - { name = "scikit-image" }, - { name = "scipy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/14/d6/8dd5b690d28a332a0b2c3179a345808b5d4c7ad5ddc079b7e116098dff35/albumentations-1.3.1.tar.gz", hash = "sha256:a6a38388fe546c568071e8c82f414498e86c9ed03c08b58e7a88b31cf7a244c6", size = 176371, upload-time = "2023-06-10T07:44:32.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/f6/c486cedb4f75147232f32ec4c97026714cfef7c7e247a1f0427bc5489f66/albumentations-1.3.1-py3-none-any.whl", hash = "sha256:6b641d13733181d9ecdc29550e6ad580d1bfa9d25e2213a66940062f25e291bd", size = 125706, upload-time = "2023-06-10T07:44:30.373Z" }, -] - [[package]] name = "annotated-doc" version = "0.0.4" @@ -326,88 +309,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/28/d28211d29bcc3620b1fece85a65ce5bb22f18670a03cd28ea4b75ede270c/configargparse-1.7.1-py3-none-any.whl", hash = "sha256:8b586a31f9d873abd1ca527ffbe58863c99f36d896e2829779803125e83be4b6", size = 25607, upload-time = "2025-05-23T14:26:15.923Z" }, ] -[[package]] -name = "contourpy" -version = "1.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, - { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, - { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, - { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, - { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, - { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, - { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, - { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, - { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, - { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, - { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, - { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, - { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, - { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, - { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, - { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, - { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, - { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, - { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, - { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, - { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, - { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, - { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, - { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, - { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, - { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, - { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, - { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, - { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, - { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, - { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, - { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, - { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, - { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, - { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, - { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, - { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, - { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, - { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, - { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, - { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, - { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, - { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, - { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, - { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, - { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, - { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, - { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, - { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, - { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, - { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, - { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, - { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, - { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, - { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, -] - [[package]] name = "coverage" version = "7.13.4" @@ -512,46 +413,6 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] -[[package]] -name = "cycler" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, -] - -[[package]] -name = "cython" -version = "3.0.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/09/ffb61f29b8e3d207c444032b21328327d753e274ea081bc74e009827cc81/Cython-3.0.8.tar.gz", hash = "sha256:8333423d8fd5765e7cceea3a9985dd1e0a5dfeb2734629e1a2ed2d6233d39de6", size = 2744096, upload-time = "2024-01-10T11:01:02.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/a7/f4a0bc9a80e23b380daa2ebb4879bf434aaa0b3b91f7ad8a7f9762b4bd1b/Cython-3.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aae26f9663e50caf9657148403d9874eea41770ecdd6caf381d177c2b1bb82ba", size = 3113615, upload-time = "2024-01-10T11:34:05.899Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e9/e9295df74246c165b91253a473bfa179debf739c9bee961cbb3ae56c2b79/Cython-3.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:547eb3cdb2f8c6f48e6865d5a741d9dd051c25b3ce076fbca571727977b28ac3", size = 3436320, upload-time = "2024-01-10T11:02:08.689Z" }, - { url = "https://files.pythonhosted.org/packages/26/2c/6a887c957aa53e44f928119dea628a5dfacc8e875424034f5fecac9daba4/Cython-3.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a567d4b9ba70b26db89d75b243529de9e649a2f56384287533cf91512705bee", size = 3591755, upload-time = "2024-01-10T11:02:11.773Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b8/f9c97bae6281da50b3ecb1f7fef0f7f7851eae084609b364717a2b366bf1/Cython-3.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51d1426263b0e82fb22bda8ea60dc77a428581cc19e97741011b938445d383f1", size = 3636099, upload-time = "2024-01-10T11:02:15.191Z" }, - { url = "https://files.pythonhosted.org/packages/17/ae/cd055c2c081c67a6fcad1d8d17d82bd6395b14c6741e3a938f40318c8bc5/Cython-3.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c26daaeccda072459b48d211415fd1e5507c06bcd976fa0d5b8b9f1063467d7b", size = 3458119, upload-time = "2024-01-10T11:02:19.103Z" }, - { url = "https://files.pythonhosted.org/packages/72/ab/ac6f5548d6194f4bb2fc8c6c996aa7369f0fa1403e4d4de787d9e9309b27/Cython-3.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:289ce7838208211cd166e975865fd73b0649bf118170b6cebaedfbdaf4a37795", size = 3614418, upload-time = "2024-01-10T11:02:22.732Z" }, - { url = "https://files.pythonhosted.org/packages/70/e2/3e3e448b7a94887bec3235bcb71957b6681dc42b4536459f8f54d46fa936/Cython-3.0.8-cp311-cp311-win32.whl", hash = "sha256:c8aa05f5e17f8042a3be052c24f2edc013fb8af874b0bf76907d16c51b4e7871", size = 2572819, upload-time = "2024-01-10T11:02:25.976Z" }, - { url = "https://files.pythonhosted.org/packages/85/7d/58635941dfbb5b4e197adb88080b9cbfb230dc3b75683698a530a1989bdb/Cython-3.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:000dc9e135d0eec6ecb2b40a5b02d0868a2f8d2e027a41b0fe16a908a9e6de02", size = 2784167, upload-time = "2024-01-10T11:02:28.808Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8e/28f8c6109990eef7317ab7e43644092b49a88a39f9373dcd19318946df09/Cython-3.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:90d3fe31db55685d8cb97d43b0ec39ef614fcf660f83c77ed06aa670cb0e164f", size = 3135638, upload-time = "2024-01-10T11:34:22.889Z" }, - { url = "https://files.pythonhosted.org/packages/83/1f/4720cb682b8ed1ab9749dea35351a66dd29b6a022628cce038415660c384/Cython-3.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e24791ddae2324e88e3c902a765595c738f19ae34ee66bfb1a6dac54b1833419", size = 3340052, upload-time = "2024-01-10T11:02:32.471Z" }, - { url = "https://files.pythonhosted.org/packages/8a/47/ec3fceb9e8f7d6fa130216b8740038e1df7c8e5f215bba363fcf1272a6c1/Cython-3.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f020fa1c0552052e0660790b8153b79e3fc9a15dbd8f1d0b841fe5d204a6ae6", size = 3510079, upload-time = "2024-01-10T11:02:35.312Z" }, - { url = "https://files.pythonhosted.org/packages/71/31/b458127851e248effb909e2791b55870914863cde7c60b94db5ee65d7867/Cython-3.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18bfa387d7a7f77d7b2526af69a65dbd0b731b8d941aaff5becff8e21f6d7717", size = 3573972, upload-time = "2024-01-10T11:02:39.044Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d5/ca6513844d0634abd05ba12304053a454bb70441a9520afa9897d4300156/Cython-3.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fe81b339cffd87c0069c6049b4d33e28bdd1874625ee515785bf42c9fdff3658", size = 3356158, upload-time = "2024-01-10T11:02:42.125Z" }, - { url = "https://files.pythonhosted.org/packages/33/59/98a87b6264f4ad45c820db13c4ec657567476efde020c49443cc842a86af/Cython-3.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:80fd94c076e1e1b1ee40a309be03080b75f413e8997cddcf401a118879863388", size = 3522312, upload-time = "2024-01-10T11:02:45.056Z" }, - { url = "https://files.pythonhosted.org/packages/2b/cb/132115d07a0b9d4f075e0741db70a5416b424dcd875b2bb0dd805e818222/Cython-3.0.8-cp312-cp312-win32.whl", hash = "sha256:85077915a93e359a9b920280d214dc0cf8a62773e1f3d7d30fab8ea4daed670c", size = 2602579, upload-time = "2024-01-10T11:02:48.368Z" }, - { url = "https://files.pythonhosted.org/packages/b4/69/cb4620287cd9ef461103e122c0a2ae7f7ecf183e02510676fb5a15c95b05/Cython-3.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:0cb2dcc565c7851f75d496f724a384a790fab12d1b82461b663e66605bec429a", size = 2791268, upload-time = "2024-01-10T11:02:51.483Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7f/f584f5d15323feb897d42ef0e9d910649e2150d7a30cf7e7a8cc1d236e6f/Cython-3.0.8-py2.py3-none-any.whl", hash = "sha256:171b27051253d3f9108e9759e504ba59ff06e7f7ba944457f94deaf9c21bf0b6", size = 1168213, upload-time = "2024-01-10T11:00:56.857Z" }, -] - -[[package]] -name = "easydict" -version = "1.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d2/deb3296d08097fedd622d423c0ec8b68b78c1704b3f1545326f6ce05c75c/easydict-1.11.tar.gz", hash = "sha256:dcb1d2ed28eb300c8e46cd371340373abc62f7c14d6dea74fdfc6f1069061c78", size = 6644, upload-time = "2023-10-23T23:01:37.686Z" } - [[package]] name = "fastapi" version = "0.136.3" @@ -627,55 +488,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/12/d5c79ee252793ffe845d58a913197bfa02ae9a0b5c9bc3dc4b58d477b9e7/flatbuffers-23.5.26-py2.py3-none-any.whl", hash = "sha256:c0ff356da363087b915fde4b8b45bdda73432fc17cddb3c8157472eab1422ad1", size = 26744, upload-time = "2023-05-26T17:35:14.269Z" }, ] -[[package]] -name = "fonttools" -version = "4.61.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, - { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, - { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, - { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, - { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, - { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, - { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, - { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, - { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, - { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, - { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, - { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, - { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, - { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, - { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, - { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, - { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, - { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, - { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, - { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, - { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, - { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, - { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, - { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, - { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, - { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, - { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, - { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, - { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, - { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, -] - [[package]] name = "fsspec" version = "2023.12.2" @@ -959,19 +771,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] -[[package]] -name = "imageio" -version = "2.33.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "pillow" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/25/38/f4c568318c656352d211eec6954460dc3af0b7583a6682308f8a66e4c19b/imageio-2.33.1.tar.gz", hash = "sha256:78722d40b137bd98f5ec7312119f8aea9ad2049f76f434748eb306b6937cc1ce", size = 387374, upload-time = "2023-12-11T02:26:44.715Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/69/3aaa69cb0748e33e644fda114c9abd3186ce369edd4fca11107e9f39c6a7/imageio-2.33.1-py3-none-any.whl", hash = "sha256:c5094c48ccf6b2e6da8b4061cd95e1209380afafcbeae4a4e280938cce227e1d", size = 313345, upload-time = "2023-12-11T02:26:42.724Z" }, -] - [[package]] name = "immich-ml" version = "3.1.0" @@ -981,8 +780,8 @@ dependencies = [ { name = "fastapi" }, { name = "gunicorn" }, { name = "huggingface-hub" }, - { name = "insightface" }, { name = "numpy" }, + { name = "onnx" }, { name = "opencv-python-headless" }, { name = "orjson" }, { name = "pillow" }, @@ -1062,8 +861,8 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.95.2,<1.0" }, { name = "gunicorn", specifier = ">=21.1.0" }, { name = "huggingface-hub", specifier = ">=1.0,<2.0" }, - { name = "insightface", specifier = ">=0.7.3,<2.0" }, { name = "numpy", specifier = ">=2.4.0,<3.0" }, + { name = "onnx", specifier = ">=1.22.0" }, { name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.23.2,<2" }, { name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.23.2,<2" }, { name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.23.2,<2" }, @@ -1133,27 +932,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, ] -[[package]] -name = "insightface" -version = "0.7.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "albumentations" }, - { name = "cython" }, - { name = "easydict" }, - { name = "matplotlib" }, - { name = "numpy" }, - { name = "onnx" }, - { name = "pillow" }, - { name = "prettytable" }, - { name = "requests" }, - { name = "scikit-image" }, - { name = "scikit-learn" }, - { name = "scipy" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/8d/0f4af90999ca96cf8cb846eb5ae27c5ef5b390f9c090dd19e4fa76364c13/insightface-0.7.3.tar.gz", hash = "sha256:f191f719612ebb37018f41936814500544cd0f86e6fcd676c023f354c668ddf7", size = 439490, upload-time = "2023-04-02T08:01:54.541Z" } - [[package]] name = "itsdangerous" version = "2.1.2" @@ -1175,65 +953,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271, upload-time = "2024-05-05T23:41:59.928Z" }, ] -[[package]] -name = "joblib" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/0f/d3b33b9f106dddef461f6df1872b7881321b247f3d255b87f61a7636f7fe/joblib-1.3.2.tar.gz", hash = "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1", size = 1987720, upload-time = "2023-08-09T09:23:40.503Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/40/d551139c85db202f1f384ba8bcf96aca2f329440a844f924c8a0040b6d02/joblib-1.3.2-py3-none-any.whl", hash = "sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9", size = 302207, upload-time = "2023-08-09T09:23:34.583Z" }, -] - -[[package]] -name = "kiwisolver" -version = "1.4.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/2d/226779e405724344fc678fcc025b812587617ea1a48b9442628b688e85ea/kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec", size = 97552, upload-time = "2023-08-24T09:30:39.861Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/eb/9e099ad7c47c279995d2d20474e1821100a5f10f847739bd65b1c1f02442/kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5", size = 127403, upload-time = "2023-08-24T09:28:39.3Z" }, - { url = "https://files.pythonhosted.org/packages/a6/94/695922e71288855fc7cace3bdb52edda9d7e50edba77abb0c9d7abb51e96/kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90", size = 68156, upload-time = "2023-08-24T09:28:40.301Z" }, - { url = "https://files.pythonhosted.org/packages/4a/fe/23d7fa78f7c66086d196406beb1fb2eaf629dd7adc01c3453033303d17fa/kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797", size = 66166, upload-time = "2023-08-24T09:28:41.235Z" }, - { url = "https://files.pythonhosted.org/packages/f1/68/f472bf16c9141bb1bea5c0b8c66c68fc1ccb048efdbd8f0872b92125724e/kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9", size = 1334300, upload-time = "2023-08-24T09:28:42.409Z" }, - { url = "https://files.pythonhosted.org/packages/8d/26/b4569d1f29751fca22ee915b4ebfef5974f4ef239b3335fc072882bd62d9/kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437", size = 1426579, upload-time = "2023-08-24T09:28:43.677Z" }, - { url = "https://files.pythonhosted.org/packages/f3/a3/804fc7c8bf233806ec0321c9da35971578620f2ab4fafe67d76100b3ce52/kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9", size = 1541360, upload-time = "2023-08-24T09:28:45.939Z" }, - { url = "https://files.pythonhosted.org/packages/07/ef/286e1d26524854f6fbd6540e8364d67a8857d61038ac743e11edc42fe217/kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da", size = 1470091, upload-time = "2023-08-24T09:28:47.959Z" }, - { url = "https://files.pythonhosted.org/packages/17/ba/17a706b232308e65f57deeccae503c268292e6a091313f6ce833a23093ea/kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e", size = 1426259, upload-time = "2023-08-24T09:28:49.224Z" }, - { url = "https://files.pythonhosted.org/packages/d0/f3/a0925611c9d6c2f37c5935a39203cadec6883aa914e013b46c84c4c2e641/kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8", size = 1847516, upload-time = "2023-08-24T09:28:50.979Z" }, - { url = "https://files.pythonhosted.org/packages/da/85/82d59bb8f7c4c9bb2785138b72462cb1b161668f8230c58bbb28c0403cd5/kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d", size = 1946228, upload-time = "2023-08-24T09:28:52.812Z" }, - { url = "https://files.pythonhosted.org/packages/34/3c/6a37f444c0233993881e5db3a6a1775925d4d9d2f2609bb325bb1348ed94/kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0", size = 1901716, upload-time = "2023-08-24T09:28:54.115Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7e/180425790efc00adfd47db14e1e341cb4826516982334129012b971121a6/kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f", size = 1852871, upload-time = "2023-08-24T09:28:55.433Z" }, - { url = "https://files.pythonhosted.org/packages/1b/9a/13c68b2edb1fa74321e60893a9a5829788e135138e68060cf44e2d92d2c3/kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f", size = 1870265, upload-time = "2023-08-24T09:28:56.855Z" }, - { url = "https://files.pythonhosted.org/packages/9f/0a/fa56a0fdee5da2b4c79899c0f6bd1aefb29d9438c2d66430e78793571c6b/kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac", size = 46649, upload-time = "2023-08-24T09:28:58.021Z" }, - { url = "https://files.pythonhosted.org/packages/1e/37/d3c2d4ba2719059a0f12730947bbe1ad5ee8bff89e8c35319dcb2c9ddb4c/kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355", size = 56116, upload-time = "2023-08-24T09:28:58.994Z" }, - { url = "https://files.pythonhosted.org/packages/f3/7a/debbce859be1a2711eb8437818107137192007b88d17b5cfdb556f457b42/kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a", size = 125484, upload-time = "2023-08-24T09:28:59.975Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e0/bf8df75ba93b9e035cc6757dd5dcaf63084fdc1c846ae134e818bd7e0f03/kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192", size = 67332, upload-time = "2023-08-24T09:29:01.733Z" }, - { url = "https://files.pythonhosted.org/packages/26/61/58bb691f6880588be3a4801d199bd776032ece07203faf3e4a8b377f7d9b/kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45", size = 64987, upload-time = "2023-08-24T09:29:02.789Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a3/96ac5413068b237c006f54dd8d70114e8756d70e3da7613c5aef20627e22/kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7", size = 1370613, upload-time = "2023-08-24T09:29:03.912Z" }, - { url = "https://files.pythonhosted.org/packages/4d/12/f48539e6e17068b59c7f12f4d6214b973431b8e3ac83af525cafd27cebec/kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db", size = 1463183, upload-time = "2023-08-24T09:29:05.244Z" }, - { url = "https://files.pythonhosted.org/packages/f3/70/26c99be8eb034cc8e3f62e0760af1fbdc97a842a7cbc252f7978507d41c2/kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff", size = 1581248, upload-time = "2023-08-24T09:29:06.531Z" }, - { url = "https://files.pythonhosted.org/packages/17/f6/f75f20e543639b09b2de7fc864274a5a9b96cda167a6210a1d9d19306b9d/kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228", size = 1508815, upload-time = "2023-08-24T09:29:07.867Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d5/bc0f22ac108743062ab703f8d6d71c9c7b077b8839fa358700bfb81770b8/kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16", size = 1466042, upload-time = "2023-08-24T09:29:09.403Z" }, - { url = "https://files.pythonhosted.org/packages/75/18/98142500f21d6838bcab49ec919414a1f0c6d049d21ddadf139124db6a70/kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9", size = 1885159, upload-time = "2023-08-24T09:29:10.66Z" }, - { url = "https://files.pythonhosted.org/packages/21/49/a241eff9e0ee013368c1d17957f9d345b0957493c3a43d82ebb558c90b0a/kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162", size = 1981694, upload-time = "2023-08-24T09:29:12.469Z" }, - { url = "https://files.pythonhosted.org/packages/90/90/9490c3de4788123041b1d600d64434f1eed809a2ce9f688075a22166b289/kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4", size = 1941579, upload-time = "2023-08-24T09:29:13.743Z" }, - { url = "https://files.pythonhosted.org/packages/b7/bb/a0cc488ef2aa92d7d304318c8549d3ec8dfe6dd3c2c67a44e3922b77bc4f/kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3", size = 1888168, upload-time = "2023-08-24T09:29:15.097Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e9/9c0de8e45fef3d63f85eed3b1757f9aa511065942866331ef8b99421f433/kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a", size = 1908464, upload-time = "2023-08-24T09:29:16.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/60/4f0fd50b08f5be536ea0cef518ac7255d9dab43ca40f3b93b60e3ddf80dd/kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20", size = 46473, upload-time = "2023-08-24T09:29:17.956Z" }, - { url = "https://files.pythonhosted.org/packages/63/50/2746566bdf4a6a842d117367d05c90cfb87ac04e9e2845aa1fa21f071362/kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9", size = 56004, upload-time = "2023-08-24T09:29:19.329Z" }, -] - -[[package]] -name = "lazy-loader" -version = "0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, -] - [[package]] name = "librt" version = "0.11.0" @@ -1374,70 +1093,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/44/dbaf65876e258facd65f586dde158387ab89963e7f2235551afc9c2e24c2/MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", size = 16979, upload-time = "2023-09-07T16:00:57.77Z" }, ] -[[package]] -name = "matplotlib" -version = "3.10.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "contourpy" }, - { name = "cycler" }, - { name = "fonttools" }, - { name = "kiwisolver" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pillow" }, - { name = "pyparsing" }, - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/91/f2939bb60b7ebf12478b030e0d7f340247390f402b3b189616aad790c366/matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076", size = 34804044, upload-time = "2025-07-31T18:09:33.805Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/c7/1f2db90a1d43710478bb1e9b57b162852f79234d28e4f48a28cc415aa583/matplotlib-3.10.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dcfc39c452c6a9f9028d3e44d2d721484f665304857188124b505b2c95e1eecf", size = 8239216, upload-time = "2025-07-31T18:07:51.947Z" }, - { url = "https://files.pythonhosted.org/packages/82/6d/ca6844c77a4f89b1c9e4d481c412e1d1dbabf2aae2cbc5aa2da4a1d6683e/matplotlib-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:903352681b59f3efbf4546985142a9686ea1d616bb054b09a537a06e4b892ccf", size = 8102130, upload-time = "2025-07-31T18:07:53.65Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1e/5e187a30cc673a3e384f3723e5f3c416033c1d8d5da414f82e4e731128ea/matplotlib-3.10.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:080c3676a56b8ee1c762bcf8fca3fe709daa1ee23e6ef06ad9f3fc17332f2d2a", size = 8666471, upload-time = "2025-07-31T18:07:55.304Z" }, - { url = "https://files.pythonhosted.org/packages/03/c0/95540d584d7d645324db99a845ac194e915ef75011a0d5e19e1b5cee7e69/matplotlib-3.10.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b4984d5064a35b6f66d2c11d668565f4389b1119cc64db7a4c1725bc11adffc", size = 9500518, upload-time = "2025-07-31T18:07:57.199Z" }, - { url = "https://files.pythonhosted.org/packages/ba/2e/e019352099ea58b4169adb9c6e1a2ad0c568c6377c2b677ee1f06de2adc7/matplotlib-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3967424121d3a46705c9fa9bdb0931de3228f13f73d7bb03c999c88343a89d89", size = 9552372, upload-time = "2025-07-31T18:07:59.41Z" }, - { url = "https://files.pythonhosted.org/packages/b7/81/3200b792a5e8b354f31f4101ad7834743ad07b6d620259f2059317b25e4d/matplotlib-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:33775bbeb75528555a15ac29396940128ef5613cf9a2d31fb1bfd18b3c0c0903", size = 8100634, upload-time = "2025-07-31T18:08:01.801Z" }, - { url = "https://files.pythonhosted.org/packages/52/46/a944f6f0c1f5476a0adfa501969d229ce5ae60cf9a663be0e70361381f89/matplotlib-3.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:c61333a8e5e6240e73769d5826b9a31d8b22df76c0778f8480baf1b4b01c9420", size = 7978880, upload-time = "2025-07-31T18:08:03.407Z" }, - { url = "https://files.pythonhosted.org/packages/66/1e/c6f6bcd882d589410b475ca1fc22e34e34c82adff519caf18f3e6dd9d682/matplotlib-3.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2", size = 8253056, upload-time = "2025-07-31T18:08:05.385Z" }, - { url = "https://files.pythonhosted.org/packages/53/e6/d6f7d1b59413f233793dda14419776f5f443bcccb2dfc84b09f09fe05dbe/matplotlib-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389", size = 8110131, upload-time = "2025-07-31T18:08:07.293Z" }, - { url = "https://files.pythonhosted.org/packages/66/2b/bed8a45e74957549197a2ac2e1259671cd80b55ed9e1fe2b5c94d88a9202/matplotlib-3.10.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea", size = 8669603, upload-time = "2025-07-31T18:08:09.064Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a7/315e9435b10d057f5e52dfc603cd353167ae28bb1a4e033d41540c0067a4/matplotlib-3.10.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468", size = 9508127, upload-time = "2025-07-31T18:08:10.845Z" }, - { url = "https://files.pythonhosted.org/packages/7f/d9/edcbb1f02ca99165365d2768d517898c22c6040187e2ae2ce7294437c413/matplotlib-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369", size = 9566926, upload-time = "2025-07-31T18:08:13.186Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d9/6dd924ad5616c97b7308e6320cf392c466237a82a2040381163b7500510a/matplotlib-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b", size = 8107599, upload-time = "2025-07-31T18:08:15.116Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f3/522dc319a50f7b0279fbe74f86f7a3506ce414bc23172098e8d2bdf21894/matplotlib-3.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2", size = 7978173, upload-time = "2025-07-31T18:08:21.518Z" }, - { url = "https://files.pythonhosted.org/packages/8d/05/4f3c1f396075f108515e45cb8d334aff011a922350e502a7472e24c52d77/matplotlib-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:354204db3f7d5caaa10e5de74549ef6a05a4550fdd1c8f831ab9bca81efd39ed", size = 8253586, upload-time = "2025-07-31T18:08:23.107Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2c/e084415775aac7016c3719fe7006cdb462582c6c99ac142f27303c56e243/matplotlib-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b072aac0c3ad563a2b3318124756cb6112157017f7431626600ecbe890df57a1", size = 8110715, upload-time = "2025-07-31T18:08:24.675Z" }, - { url = "https://files.pythonhosted.org/packages/52/1b/233e3094b749df16e3e6cd5a44849fd33852e692ad009cf7de00cf58ddf6/matplotlib-3.10.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d52fd5b684d541b5a51fb276b2b97b010c75bee9aa392f96b4a07aeb491e33c7", size = 8669397, upload-time = "2025-07-31T18:08:26.778Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ec/03f9e003a798f907d9f772eed9b7c6a9775d5bd00648b643ebfb88e25414/matplotlib-3.10.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7a09ae2f4676276f5a65bd9f2bd91b4f9fbaedf49f40267ce3f9b448de501f", size = 9508646, upload-time = "2025-07-31T18:08:28.848Z" }, - { url = "https://files.pythonhosted.org/packages/91/e7/c051a7a386680c28487bca27d23b02d84f63e3d2a9b4d2fc478e6a42e37e/matplotlib-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ba6c3c9c067b83481d647af88b4e441d532acdb5ef22178a14935b0b881188f4", size = 9567424, upload-time = "2025-07-31T18:08:30.726Z" }, - { url = "https://files.pythonhosted.org/packages/36/c2/24302e93ff431b8f4173ee1dd88976c8d80483cadbc5d3d777cef47b3a1c/matplotlib-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:07442d2692c9bd1cceaa4afb4bbe5b57b98a7599de4dabfcca92d3eea70f9ebe", size = 8107809, upload-time = "2025-07-31T18:08:33.928Z" }, - { url = "https://files.pythonhosted.org/packages/0b/33/423ec6a668d375dad825197557ed8fbdb74d62b432c1ed8235465945475f/matplotlib-3.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:48fe6d47380b68a37ccfcc94f009530e84d41f71f5dae7eda7c4a5a84aa0a674", size = 7978078, upload-time = "2025-07-31T18:08:36.764Z" }, - { url = "https://files.pythonhosted.org/packages/51/17/521fc16ec766455c7bb52cc046550cf7652f6765ca8650ff120aa2d197b6/matplotlib-3.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b80eb8621331449fc519541a7461987f10afa4f9cfd91afcd2276ebe19bd56c", size = 8295590, upload-time = "2025-07-31T18:08:38.521Z" }, - { url = "https://files.pythonhosted.org/packages/f8/12/23c28b2c21114c63999bae129fce7fd34515641c517ae48ce7b7dcd33458/matplotlib-3.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47a388908e469d6ca2a6015858fa924e0e8a2345a37125948d8e93a91c47933e", size = 8158518, upload-time = "2025-07-31T18:08:40.195Z" }, - { url = "https://files.pythonhosted.org/packages/81/f8/aae4eb25e8e7190759f3cb91cbeaa344128159ac92bb6b409e24f8711f78/matplotlib-3.10.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b6b49167d208358983ce26e43aa4196073b4702858670f2eb111f9a10652b4b", size = 8691815, upload-time = "2025-07-31T18:08:42.238Z" }, - { url = "https://files.pythonhosted.org/packages/d0/ba/450c39ebdd486bd33a359fc17365ade46c6a96bf637bbb0df7824de2886c/matplotlib-3.10.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a8da0453a7fd8e3da114234ba70c5ba9ef0e98f190309ddfde0f089accd46ea", size = 9522814, upload-time = "2025-07-31T18:08:44.914Z" }, - { url = "https://files.pythonhosted.org/packages/89/11/9c66f6a990e27bb9aa023f7988d2d5809cb98aa39c09cbf20fba75a542ef/matplotlib-3.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52c6573dfcb7726a9907b482cd5b92e6b5499b284ffacb04ffbfe06b3e568124", size = 9573917, upload-time = "2025-07-31T18:08:47.038Z" }, - { url = "https://files.pythonhosted.org/packages/b3/69/8b49394de92569419e5e05e82e83df9b749a0ff550d07631ea96ed2eb35a/matplotlib-3.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:a23193db2e9d64ece69cac0c8231849db7dd77ce59c7b89948cf9d0ce655a3ce", size = 8181034, upload-time = "2025-07-31T18:08:48.943Z" }, - { url = "https://files.pythonhosted.org/packages/47/23/82dc435bb98a2fc5c20dffcac8f0b083935ac28286413ed8835df40d0baa/matplotlib-3.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:56da3b102cf6da2776fef3e71cd96fcf22103a13594a18ac9a9b31314e0be154", size = 8023337, upload-time = "2025-07-31T18:08:50.791Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e0/26b6cfde31f5383503ee45dcb7e691d45dadf0b3f54639332b59316a97f8/matplotlib-3.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:96ef8f5a3696f20f55597ffa91c28e2e73088df25c555f8d4754931515512715", size = 8253591, upload-time = "2025-07-31T18:08:53.254Z" }, - { url = "https://files.pythonhosted.org/packages/c1/89/98488c7ef7ea20ea659af7499628c240a608b337af4be2066d644cfd0a0f/matplotlib-3.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:77fab633e94b9da60512d4fa0213daeb76d5a7b05156840c4fd0399b4b818837", size = 8112566, upload-time = "2025-07-31T18:08:55.116Z" }, - { url = "https://files.pythonhosted.org/packages/52/67/42294dfedc82aea55e1a767daf3263aacfb5a125f44ba189e685bab41b6f/matplotlib-3.10.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27f52634315e96b1debbfdc5c416592edcd9c4221bc2f520fd39c33db5d9f202", size = 9513281, upload-time = "2025-07-31T18:08:56.885Z" }, - { url = "https://files.pythonhosted.org/packages/e7/68/f258239e0cf34c2cbc816781c7ab6fca768452e6bf1119aedd2bd4a882a3/matplotlib-3.10.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:525f6e28c485c769d1f07935b660c864de41c37fd716bfa64158ea646f7084bb", size = 9780873, upload-time = "2025-07-31T18:08:59.241Z" }, - { url = "https://files.pythonhosted.org/packages/89/64/f4881554006bd12e4558bd66778bdd15d47b00a1f6c6e8b50f6208eda4b3/matplotlib-3.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1f5f3ec4c191253c5f2b7c07096a142c6a1c024d9f738247bfc8e3f9643fc975", size = 9568954, upload-time = "2025-07-31T18:09:01.244Z" }, - { url = "https://files.pythonhosted.org/packages/06/f8/42779d39c3f757e1f012f2dda3319a89fb602bd2ef98ce8faf0281f4febd/matplotlib-3.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:707f9c292c4cd4716f19ab8a1f93f26598222cd931e0cd98fbbb1c5994bf7667", size = 8237465, upload-time = "2025-07-31T18:09:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/cf/f8/153fd06b5160f0cd27c8b9dd797fcc9fb56ac6a0ebf3c1f765b6b68d3c8a/matplotlib-3.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:21a95b9bf408178d372814de7baacd61c712a62cae560b5e6f35d791776f6516", size = 8108898, upload-time = "2025-07-31T18:09:05.231Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ee/c4b082a382a225fe0d2a73f1f57cf6f6f132308805b493a54c8641006238/matplotlib-3.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a6b310f95e1102a8c7c817ef17b60ee5d1851b8c71b63d9286b66b177963039e", size = 8295636, upload-time = "2025-07-31T18:09:07.306Z" }, - { url = "https://files.pythonhosted.org/packages/30/73/2195fa2099718b21a20da82dfc753bf2af58d596b51aefe93e359dd5915a/matplotlib-3.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94986a242747a0605cb3ff1cb98691c736f28a59f8ffe5175acaeb7397c49a5a", size = 8158575, upload-time = "2025-07-31T18:09:09.083Z" }, - { url = "https://files.pythonhosted.org/packages/f6/e9/a08cdb34618a91fa08f75e6738541da5cacde7c307cea18ff10f0d03fcff/matplotlib-3.10.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ff10ea43288f0c8bab608a305dc6c918cc729d429c31dcbbecde3b9f4d5b569", size = 9522815, upload-time = "2025-07-31T18:09:11.191Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/34d8b7e0d1bb6d06ef45db01dfa560d5a67b1c40c0b998ce9ccde934bb09/matplotlib-3.10.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6adb644c9d040ffb0d3434e440490a66cf73dbfa118a6f79cd7568431f7a012", size = 9783514, upload-time = "2025-07-31T18:09:13.307Z" }, - { url = "https://files.pythonhosted.org/packages/12/09/d330d1e55dcca2e11b4d304cc5227f52e2512e46828d6249b88e0694176e/matplotlib-3.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4fa40a8f98428f789a9dcacd625f59b7bc4e3ef6c8c7c80187a7a709475cf592", size = 9573932, upload-time = "2025-07-31T18:09:15.335Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3b/f70258ac729aa004aca673800a53a2b0a26d49ca1df2eaa03289a1c40f81/matplotlib-3.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:95672a5d628b44207aab91ec20bf59c26da99de12b88f7e0b1fb0a84a86ff959", size = 8322003, upload-time = "2025-07-31T18:09:17.416Z" }, - { url = "https://files.pythonhosted.org/packages/5b/60/3601f8ce6d76a7c81c7f25a0e15fde0d6b66226dd187aa6d2838e6374161/matplotlib-3.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:2efaf97d72629e74252e0b5e3c46813e9eeaa94e011ecf8084a971a31a97f40b", size = 8153849, upload-time = "2025-07-31T18:09:19.673Z" }, - { url = "https://files.pythonhosted.org/packages/dc/d6/e921be4e1a5f7aca5194e1f016cb67ec294548e530013251f630713e456d/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:160e125da27a749481eaddc0627962990f6029811dbeae23881833a011a0907f", size = 8233224, upload-time = "2025-07-31T18:09:27.512Z" }, - { url = "https://files.pythonhosted.org/packages/ec/74/a2b9b04824b9c349c8f1b2d21d5af43fa7010039427f2b133a034cb09e59/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac3d50760394d78a3c9be6b28318fe22b494c4fcf6407e8fd4794b538251899b", size = 8098539, upload-time = "2025-07-31T18:09:29.629Z" }, - { url = "https://files.pythonhosted.org/packages/fc/66/cd29ebc7f6c0d2a15d216fb572573e8fc38bd5d6dec3bd9d7d904c0949f7/matplotlib-3.10.5-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c49465bf689c4d59d174d0c7795fb42a21d4244d11d70e52b8011987367ac61", size = 8672192, upload-time = "2025-07-31T18:09:31.407Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -1449,39 +1104,43 @@ wheels = [ [[package]] name = "ml-dtypes" -version = "0.5.3" +version = "0.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/a7/aad060393123cfb383956dca68402aff3db1e1caffd5764887ed5153f41b/ml_dtypes-0.5.3.tar.gz", hash = "sha256:95ce33057ba4d05df50b1f3cfefab22e351868a843b3b15a46c65836283670c9", size = 692316, upload-time = "2025-07-29T18:39:19.454Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/f1/720cb1409b5d0c05cff9040c0e9fba73fa4c67897d33babf905d5d46a070/ml_dtypes-0.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4a177b882667c69422402df6ed5c3428ce07ac2c1f844d8a1314944651439458", size = 667412, upload-time = "2025-07-29T18:38:25.275Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d5/05861ede5d299f6599f86e6bc1291714e2116d96df003cfe23cc54bcc568/ml_dtypes-0.5.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9849ce7267444c0a717c80c6900997de4f36e2815ce34ac560a3edb2d9a64cd2", size = 4964606, upload-time = "2025-07-29T18:38:27.045Z" }, - { url = "https://files.pythonhosted.org/packages/db/dc/72992b68de367741bfab8df3b3fe7c29f982b7279d341aa5bf3e7ef737ea/ml_dtypes-0.5.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3f5ae0309d9f888fd825c2e9d0241102fadaca81d888f26f845bc8c13c1e4ee", size = 4938435, upload-time = "2025-07-29T18:38:29.193Z" }, - { url = "https://files.pythonhosted.org/packages/81/1c/d27a930bca31fb07d975a2d7eaf3404f9388114463b9f15032813c98f893/ml_dtypes-0.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:58e39349d820b5702bb6f94ea0cb2dc8ec62ee81c0267d9622067d8333596a46", size = 206334, upload-time = "2025-07-29T18:38:30.687Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d8/6922499effa616012cb8dc445280f66d100a7ff39b35c864cfca019b3f89/ml_dtypes-0.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:66c2756ae6cfd7f5224e355c893cfd617fa2f747b8bbd8996152cbdebad9a184", size = 157584, upload-time = "2025-07-29T18:38:32.187Z" }, - { url = "https://files.pythonhosted.org/packages/0d/eb/bc07c88a6ab002b4635e44585d80fa0b350603f11a2097c9d1bfacc03357/ml_dtypes-0.5.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:156418abeeda48ea4797db6776db3c5bdab9ac7be197c1233771e0880c304057", size = 663864, upload-time = "2025-07-29T18:38:33.777Z" }, - { url = "https://files.pythonhosted.org/packages/cf/89/11af9b0f21b99e6386b6581ab40fb38d03225f9de5f55cf52097047e2826/ml_dtypes-0.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1db60c154989af253f6c4a34e8a540c2c9dce4d770784d426945e09908fbb177", size = 4951313, upload-time = "2025-07-29T18:38:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a9/b98b86426c24900b0c754aad006dce2863df7ce0bb2bcc2c02f9cc7e8489/ml_dtypes-0.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b255acada256d1fa8c35ed07b5f6d18bc21d1556f842fbc2d5718aea2cd9e55", size = 4928805, upload-time = "2025-07-29T18:38:38.29Z" }, - { url = "https://files.pythonhosted.org/packages/50/c1/85e6be4fc09c6175f36fb05a45917837f30af9a5146a5151cb3a3f0f9e09/ml_dtypes-0.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:da65e5fd3eea434ccb8984c3624bc234ddcc0d9f4c81864af611aaebcc08a50e", size = 208182, upload-time = "2025-07-29T18:38:39.72Z" }, - { url = "https://files.pythonhosted.org/packages/9e/17/cf5326d6867be057f232d0610de1458f70a8ce7b6290e4b4a277ea62b4cd/ml_dtypes-0.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:8bb9cd1ce63096567f5f42851f5843b5a0ea11511e50039a7649619abfb4ba6d", size = 161560, upload-time = "2025-07-29T18:38:41.072Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/1bcc98a66de7b2455dfb292f271452cac9edc4e870796e0d87033524d790/ml_dtypes-0.5.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5103856a225465371fe119f2fef737402b705b810bd95ad5f348e6e1a6ae21af", size = 663781, upload-time = "2025-07-29T18:38:42.984Z" }, - { url = "https://files.pythonhosted.org/packages/fd/2c/bd2a79ba7c759ee192b5601b675b180a3fd6ccf48ffa27fe1782d280f1a7/ml_dtypes-0.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cae435a68861660af81fa3c5af16b70ca11a17275c5b662d9c6f58294e0f113", size = 4956217, upload-time = "2025-07-29T18:38:44.65Z" }, - { url = "https://files.pythonhosted.org/packages/14/f3/091ba84e5395d7fe5b30c081a44dec881cd84b408db1763ee50768b2ab63/ml_dtypes-0.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6936283b56d74fbec431ca57ce58a90a908fdbd14d4e2d22eea6d72bb208a7b7", size = 4933109, upload-time = "2025-07-29T18:38:46.405Z" }, - { url = "https://files.pythonhosted.org/packages/bc/24/054036dbe32c43295382c90a1363241684c4d6aaa1ecc3df26bd0c8d5053/ml_dtypes-0.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:d0f730a17cf4f343b2c7ad50cee3bd19e969e793d2be6ed911f43086460096e4", size = 208187, upload-time = "2025-07-29T18:38:48.24Z" }, - { url = "https://files.pythonhosted.org/packages/a6/3d/7dc3ec6794a4a9004c765e0c341e32355840b698f73fd2daff46f128afc1/ml_dtypes-0.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:2db74788fc01914a3c7f7da0763427280adfc9cd377e9604b6b64eb8097284bd", size = 161559, upload-time = "2025-07-29T18:38:50.493Z" }, - { url = "https://files.pythonhosted.org/packages/12/91/e6c7a0d67a152b9330445f9f0cf8ae6eee9b83f990b8c57fe74631e42a90/ml_dtypes-0.5.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:93c36a08a6d158db44f2eb9ce3258e53f24a9a4a695325a689494f0fdbc71770", size = 689321, upload-time = "2025-07-29T18:38:52.03Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6c/b7b94b84a104a5be1883305b87d4c6bd6ae781504474b4cca067cb2340ec/ml_dtypes-0.5.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e44a3761f64bc009d71ddb6d6c71008ba21b53ab6ee588dadab65e2fa79eafc", size = 5274495, upload-time = "2025-07-29T18:38:53.797Z" }, - { url = "https://files.pythonhosted.org/packages/5b/38/6266604dffb43378055394ea110570cf261a49876fc48f548dfe876f34cc/ml_dtypes-0.5.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdf40d2aaabd3913dec11840f0d0ebb1b93134f99af6a0a4fd88ffe924928ab4", size = 5285422, upload-time = "2025-07-29T18:38:56.603Z" }, - { url = "https://files.pythonhosted.org/packages/7c/88/8612ff177d043a474b9408f0382605d881eeb4125ba89d4d4b3286573a83/ml_dtypes-0.5.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:aec640bd94c4c85c0d11e2733bd13cbb10438fb004852996ec0efbc6cacdaf70", size = 661182, upload-time = "2025-07-29T18:38:58.414Z" }, - { url = "https://files.pythonhosted.org/packages/6f/2b/0569a5e88b29240d373e835107c94ae9256fb2191d3156b43b2601859eff/ml_dtypes-0.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bda32ce212baa724e03c68771e5c69f39e584ea426bfe1a701cb01508ffc7035", size = 4956187, upload-time = "2025-07-29T18:39:00.611Z" }, - { url = "https://files.pythonhosted.org/packages/51/66/273c2a06ae44562b104b61e6b14444da00061fd87652506579d7eb2c40b1/ml_dtypes-0.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c205cac07d24a29840c163d6469f61069ce4b065518519216297fc2f261f8db9", size = 4930911, upload-time = "2025-07-29T18:39:02.405Z" }, - { url = "https://files.pythonhosted.org/packages/93/ab/606be3e87dc0821bd360c8c1ee46108025c31a4f96942b63907bb441b87d/ml_dtypes-0.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:cd7c0bb22d4ff86d65ad61b5dd246812e8993fbc95b558553624c33e8b6903ea", size = 216664, upload-time = "2025-07-29T18:39:03.927Z" }, - { url = "https://files.pythonhosted.org/packages/30/a2/e900690ca47d01dffffd66375c5de8c4f8ced0f1ef809ccd3b25b3e6b8fa/ml_dtypes-0.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:9d55ea7f7baf2aed61bf1872116cefc9d0c3693b45cae3916897ee27ef4b835e", size = 160203, upload-time = "2025-07-29T18:39:05.671Z" }, - { url = "https://files.pythonhosted.org/packages/53/21/783dfb51f40d2660afeb9bccf3612b99f6a803d980d2a09132b0f9d216ab/ml_dtypes-0.5.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:e12e29764a0e66a7a31e9b8bf1de5cc0423ea72979f45909acd4292de834ccd3", size = 689324, upload-time = "2025-07-29T18:39:07.567Z" }, - { url = "https://files.pythonhosted.org/packages/09/f7/a82d249c711abf411ac027b7163f285487f5e615c3e0716c61033ce996ab/ml_dtypes-0.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19f6c3a4f635c2fc9e2aa7d91416bd7a3d649b48350c51f7f715a09370a90d93", size = 5275917, upload-time = "2025-07-29T18:39:09.339Z" }, - { url = "https://files.pythonhosted.org/packages/7f/3c/541c4b30815ab90ebfbb51df15d0b4254f2f9f1e2b4907ab229300d5e6f2/ml_dtypes-0.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ab039ffb40f3dc0aeeeba84fd6c3452781b5e15bef72e2d10bcb33e4bbffc39", size = 5285284, upload-time = "2025-07-29T18:39:11.532Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" }, + { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" }, + { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, + { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, ] [[package]] @@ -1606,15 +1265,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" }, ] -[[package]] -name = "networkx" -version = "3.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c4/80/a84676339aaae2f1cfdf9f418701dd634aef9cc76f708ef55c36ff39c3ca/networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6", size = 2073928, upload-time = "2023-10-28T08:41:39.364Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/f0/8fbc882ca80cf077f1b246c0e3c3465f7f415439bdea6b899f6b19f61f70/networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2", size = 1647772, upload-time = "2023-10-28T08:41:36.945Z" }, -] - [[package]] name = "numpy" version = "2.4.6" @@ -1709,7 +1359,7 @@ wheels = [ [[package]] name = "onnx" -version = "1.19.1" +version = "1.22.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ml-dtypes" }, @@ -1717,31 +1367,26 @@ dependencies = [ { name = "protobuf" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/2f/c619eb65769357e9b6de9212c9a821ab39cd484448e5d6b3fb5fb0a64c6d/onnx-1.19.1.tar.gz", hash = "sha256:737524d6eb3907d3499ea459c6f01c5a96278bb3a0f2ff8ae04786fb5d7f1ed5", size = 12033525, upload-time = "2025-10-10T04:01:34.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/19/8ea73a64b368b75fe339771a20a02bc61ea1f551484c9e3d9d0bfbd0450f/onnx-1.22.0.tar.gz", hash = "sha256:ef40c0aaf0b643857ea9306fc7eddce17eaf9fb0407e4801f1fc5758443a38e0", size = 12024721, upload-time = "2026-06-15T12:50:05.354Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/07/0019c72924909e4f64b9199770630ab7b8d7914b912b03230e68f5eda7ae/onnx-1.19.1-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:17aaf5832126de0a5197a5864e4f09a764dd7681d3035135547959b4b6b77a09", size = 18320936, upload-time = "2025-10-10T04:00:04.235Z" }, - { url = "https://files.pythonhosted.org/packages/af/2f/5c47acf740dc35f0decc640844260fbbdc0efa0565657c93fd7ff30f13f3/onnx-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01b292a4d0b197c45d8184545bbc8ae1df83466341b604187c1b05902cb9c920", size = 18044269, upload-time = "2025-10-10T04:00:07.449Z" }, - { url = "https://files.pythonhosted.org/packages/d5/61/6c457ee8c3a62a3cad0a4bfa4c5436bb3ac4df90c3551d40bee1224b5b51/onnx-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1839af08ab4a909e4af936b8149c27f8c64b96138981024e251906e0539d8bf9", size = 18218092, upload-time = "2025-10-10T04:00:11.135Z" }, - { url = "https://files.pythonhosted.org/packages/54/d5/ab832e1369505e67926a70e9a102061f89ad01f91aa296c4b1277cb81b25/onnx-1.19.1-cp311-cp311-win32.whl", hash = "sha256:0bdbb676e3722bd32f9227c465d552689f49086f986a696419d865cb4e70b989", size = 16344809, upload-time = "2025-10-10T04:00:14.634Z" }, - { url = "https://files.pythonhosted.org/packages/8b/b5/6eb4611d24b85002f878ba8476b4cecbe6f9784c0236a3c5eff85236cc0a/onnx-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:1346853df5c1e3ebedb2e794cf2a51e0f33759affd655524864ccbcddad7035b", size = 16464319, upload-time = "2025-10-10T04:00:18.235Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ff/f0e1f06420c70e20d497fec7c94a864d069943b6312bedd4224c0ab946f8/onnx-1.19.1-cp311-cp311-win_arm64.whl", hash = "sha256:2d69c280c0e665b7f923f499243b9bb84fe97970b7a4668afa0032045de602c8", size = 16437503, upload-time = "2025-10-10T04:00:21.247Z" }, - { url = "https://files.pythonhosted.org/packages/50/07/f6c5b2cffef8c29e739616d1415aea22f7b7ef1f19c17f02b7cff71f5498/onnx-1.19.1-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:3612193a89ddbce5c4e86150869b9258780a82fb8c4ca197723a4460178a6ce9", size = 18327840, upload-time = "2025-10-10T04:00:24.259Z" }, - { url = "https://files.pythonhosted.org/packages/93/20/0568ebd52730287ae80cac8ac893a7301c793ea1630984e2519ee92b02a9/onnx-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6c2fd2f744e7a3880ad0c262efa2edf6d965d0bd02b8f327ec516ad4cb0f2f15", size = 18042539, upload-time = "2025-10-10T04:00:27.693Z" }, - { url = "https://files.pythonhosted.org/packages/14/fd/cd7a0fd10a04f8cc5ae436b63e0022e236fe51b9dbb8ee6317fd48568c72/onnx-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:485d3674d50d789e0ee72fa6f6e174ab81cb14c772d594f992141bd744729d8a", size = 18218271, upload-time = "2025-10-10T04:00:30.495Z" }, - { url = "https://files.pythonhosted.org/packages/65/68/cc8b8c05469fe08384b446304ad7e6256131ca0463bf6962366eebec98c0/onnx-1.19.1-cp312-cp312-win32.whl", hash = "sha256:638bc56ff1a5718f7441e887aeb4e450f37a81c6eac482040381b140bd9ba601", size = 16345111, upload-time = "2025-10-10T04:00:34.982Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5e/d1cb16693598a512c2cf9ffe0841d8d8fd2c83ae8e889efd554f5aa427cf/onnx-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:bc7e2e4e163e679721e547958b5a7db875bf822cad371b7c1304aa4401a7c7a4", size = 16465621, upload-time = "2025-10-10T04:00:39.107Z" }, - { url = "https://files.pythonhosted.org/packages/90/32/da116cc61fdef334782aa7f87a1738431dd1af1a5d1a44bd95d6d51ad260/onnx-1.19.1-cp312-cp312-win_arm64.whl", hash = "sha256:17c215b1c0f20fe93b4cbe62668247c1d2294b9bc7f6be0ca9ced28e980c07b7", size = 16437505, upload-time = "2025-10-10T04:00:42.255Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b8/ab1fdfe2e8502f4dc4289fc893db35816bd20d080d8370f86e74dda5f598/onnx-1.19.1-cp313-cp313-macosx_12_0_universal2.whl", hash = "sha256:4e5f938c68c4dffd3e19e4fd76eb98d298174eb5ebc09319cdd0ec5fe50050dc", size = 18327815, upload-time = "2025-10-10T04:00:45.682Z" }, - { url = "https://files.pythonhosted.org/packages/04/40/eb875745a4b92aea10e5e32aa2830f409c4d7b6f7b48ca1c4eaad96636c5/onnx-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:86e20a5984b017feeef2dbf4ceff1c7c161ab9423254968dd77d3696c38691d0", size = 18041464, upload-time = "2025-10-10T04:00:48.557Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/8586135f40dbe4989cec4d413164bc8fc5c73d37c566f33f5ea3a7f2b6f6/onnx-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d9c467f0f29993c12f330736af87972f30adb8329b515f39d63a0db929cb2c", size = 18218244, upload-time = "2025-10-10T04:00:51.891Z" }, - { url = "https://files.pythonhosted.org/packages/51/b5/4201254b8683129db5da3fb55aa1f7e56d0a8d45c66ce875dec21ca1ff25/onnx-1.19.1-cp313-cp313-win32.whl", hash = "sha256:65eee353a51b4e4ca3e797784661e5376e2b209f17557e04921eac9166a8752e", size = 16345330, upload-time = "2025-10-10T04:00:54.858Z" }, - { url = "https://files.pythonhosted.org/packages/69/67/c6d239afbcdbeb6805432969b908b5c9f700c96d332b34e3f99518d76caf/onnx-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:c3bc87e38b53554b1fc9ef7b275c81c6f5c93c90a91935bb0aa8d4d498a6d48e", size = 16465567, upload-time = "2025-10-10T04:00:57.893Z" }, - { url = "https://files.pythonhosted.org/packages/99/fe/89f1e40f5bc54595ff0dcf5391ce19e578b528973ccc74dd99800196d30d/onnx-1.19.1-cp313-cp313-win_arm64.whl", hash = "sha256:e41496f400afb980ec643d80d5164753a88a85234fa5c06afdeebc8b7d1ec252", size = 16437562, upload-time = "2025-10-10T04:01:00.703Z" }, - { url = "https://files.pythonhosted.org/packages/86/43/b186ccbc8fe7e93643a6a6d40bbf2bb6ce4fb9469bbd3453c77e270c50ad/onnx-1.19.1-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:5f6274abf0fd74e80e78ecbb44bd44509409634525c89a9b38276c8af47dc0a2", size = 18355703, upload-time = "2025-10-10T04:01:03.735Z" }, - { url = "https://files.pythonhosted.org/packages/60/f1/22ee4d8b8f9fa4cb1d1b9579da3b4b5187ddab33846ec5ac744af02c0e2b/onnx-1.19.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07dcd4d83584eb4bf8f21ac04c82643712e5e93ac2a0ed10121ec123cb127e1e", size = 18047830, upload-time = "2025-10-10T04:01:06.552Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a4/8f3d51e3a095d42cdf2039a590cff06d024f2a10efbd0b1a2a6b3825f019/onnx-1.19.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1975860c3e720db25d37f1619976582828264bdcc64fa7511c321ac4fc01add3", size = 18221126, upload-time = "2025-10-10T04:01:09.77Z" }, - { url = "https://files.pythonhosted.org/packages/4f/0d/f9d6c2237083f1aac14b37f0b03b0d81f1147a8e2af0c3828165e0a6a67b/onnx-1.19.1-cp313-cp313t-win_amd64.whl", hash = "sha256:9807d0e181f6070ee3a6276166acdc571575d1bd522fc7e89dba16fd6e7ffed9", size = 16465560, upload-time = "2025-10-10T04:01:13.212Z" }, - { url = "https://files.pythonhosted.org/packages/36/70/8418a58faa7d606d6a92cab69ae8d361b3b3969bf7e7e9a65a86d5d1b674/onnx-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6ee83e6929d75005482d9f304c502ac7c9b8d6db153aa6b484dae74d0f28570", size = 18042812, upload-time = "2025-10-10T04:01:15.919Z" }, + { url = "https://files.pythonhosted.org/packages/0c/55/30825c02c92a0380ce84c3feeeec95d329fa77548ba58cb10ad4bbfd83c6/onnx-1.22.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:2d8f229a553fa440fe623ed7b36fca5e7762da3af871c3f8f8ce451df73e2914", size = 20167891, upload-time = "2026-06-15T12:49:14.212Z" }, + { url = "https://files.pythonhosted.org/packages/4b/24/cd4ab52ecaf41c3fbed674772ccbfe39041cb257b8471a47a37e48bff3f8/onnx-1.22.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1a89a7cb9ba13d78f009bdec448ec82a98972589734f157022a2bff7a5973a6", size = 18892720, upload-time = "2026-06-15T12:49:16.904Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a0/c9d9d56ceadb1c0a90a7cbec5a0510520ab6538938944fa84548e4b5b054/onnx-1.22.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d0a2bdb15eb2b3cb65c438f3423d9620d14fdce32f92380e6bb1b2e09568ef5", size = 19110720, upload-time = "2026-06-15T12:49:19.812Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/e43e5a68d9cadde55df75310027f87127333a77e5ddcea14c73e96a10cac/onnx-1.22.0-cp311-cp311-win32.whl", hash = "sha256:239958534464612fbcb6ed23d5228aaa925b39b8773f58726809ffdccb4edd1c", size = 17083746, upload-time = "2026-06-15T12:49:22.935Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/cc0a9f2cf4522e42829d089927b4b75924d32f50dca237482e7b741df003/onnx-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:8561a2c00041c07e08db0c228593b5b4694100398685f348532af7dbb84189da", size = 17215684, upload-time = "2026-06-15T12:49:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/c9/99/0f049f9eaa06c8383060c5f0a338e3a6caac8822e6e326c9162f05abf95a/onnx-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:8907b9b9389893bc0dc6314cc00ee1e3a69844e48d689eacc6a0340411a7da58", size = 17210398, upload-time = "2026-06-15T12:49:29.091Z" }, + { url = "https://files.pythonhosted.org/packages/ee/6a/481561f1093834376ed493e4ca42a73e5be0d50031f2969c86593bdc7c96/onnx-1.22.0-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:596fbf0490947533c1c1045ba860851dc9fb77471023dac9a71ba5b42ceab103", size = 20167081, upload-time = "2026-06-15T12:49:32.078Z" }, + { url = "https://files.pythonhosted.org/packages/84/55/b34fc2aa30aa54b4a775402d24c4082242c720283a274fe976ac8eb94480/onnx-1.22.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae5a563f281cd9d2845622cecf6c092a57e4ee1b138f66fdbbdd4200567a5e16", size = 18889249, upload-time = "2026-06-15T12:49:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/09/a6/bd32357e6cc1ecb473afd78193d7231724f284435d2db25696ecfaaa1503/onnx-1.22.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:955e02e1f6d385b53d52f9cd7b9cdf5caf417c300bcfe3c64c6d542be763845b", size = 19106514, upload-time = "2026-06-15T12:49:37.424Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9d/3af461ac6c714b8b369cb71499659932f4f12cfb066250b62f7567c3d530/onnx-1.22.0-cp312-abi3-pyemscripten_2025_0_wasm32.whl", hash = "sha256:82e9f27fc1223cb06d68a56bed6f9d3caf3d0dad1b61bce45006d529b15bd94c", size = 16966387, upload-time = "2026-06-15T12:49:40.918Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f0/68195b5e5a53e333faf2660f5352ee43738d0e42fc5216cc6b1871a9fbfb/onnx-1.22.0-cp312-abi3-win32.whl", hash = "sha256:cc8b66b312f8f03a53e268afb67180a2d97dd12cc79e2b61361c6c0073448016", size = 17081568, upload-time = "2026-06-15T12:49:43.398Z" }, + { url = "https://files.pythonhosted.org/packages/13/a8/734725bb703c5fabb687f79c79e51249475212b3eb37771ac4a4ac9b487f/onnx-1.22.0-cp312-abi3-win_amd64.whl", hash = "sha256:72ccebab3bac07215c204ce8848d42e78eaaa666badbf72d25cd359b9f269e3a", size = 17213290, upload-time = "2026-06-15T12:49:45.933Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/8ce48d8ae26a8761ad4e5dc771961b155c5c3c7c8540ec7f2f2d71b69af0/onnx-1.22.0-cp312-abi3-win_arm64.whl", hash = "sha256:f3c120dcdb70ad738f3c061b32798f408ea299eb69f84dd69ab4a6bf3c2ec01f", size = 17207030, upload-time = "2026-06-15T12:49:48.635Z" }, + { url = "https://files.pythonhosted.org/packages/f3/13/47323b97846387848efb1044ded11bb94b83526f3d1fbdb37c6480d4520f/onnx-1.22.0-cp314-cp314t-macosx_12_0_universal2.whl", hash = "sha256:19e45e4af88e3fe3261458d4b8cc461957ae2782a358a3560503569bf3b23b72", size = 20176465, upload-time = "2026-06-15T12:49:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/13/0c/d3b8a7e7eee123938586c608bb9894b5723f2342b9450c0eec59fbec7099/onnx-1.22.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c21a0e59fd967a95b358e4a6e756d1f1eec2d304a83480f329f66e30d2bf0223", size = 18894028, upload-time = "2026-06-15T12:49:54.451Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8a/da2a97ab46fe6e0cd9beb3ac14603a22f5be492f9ca347faf8233a07bb33/onnx-1.22.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2632406b8f523ef2e2873c363f90b20a3d88c0fbcfac757d3addffccf8f452c2", size = 19110420, upload-time = "2026-06-15T12:49:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a3/ce984063017518307ebfaa545782fc400e593dc2d7fdf4f23ce4be1ed197/onnx-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a3a39fc4643867aecb33417fdddb11e308ee79d2d4a584b9d50cc7aec2091b13", size = 17237547, upload-time = "2026-06-15T12:50:00.382Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/257a880384a1dd502d543b0067945074d63cd17d0840e958355bc8197da8/onnx-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:8e268cdc0547e3949799ffd4a44451dc2b9080b57d0824a2db680b6ec65506f0", size = 17231391, upload-time = "2026-06-15T12:50:03.047Z" }, ] [[package]] @@ -2058,18 +1703,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, ] -[[package]] -name = "prettytable" -version = "3.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/c0/5e9c4d2a643a00a6f67578ef35485173de273a4567279e4f0c200c01386b/prettytable-3.9.0.tar.gz", hash = "sha256:f4ed94803c23073a90620b201965e5dc0bccf1760b7a7eaf3158cab8aaffdf34", size = 47874, upload-time = "2023-09-11T14:04:14.548Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/81/316b6a55a0d1f327d04cc7b0ba9d04058cb62de6c3a4d4b0df280cbe3b0b/prettytable-3.9.0-py3-none-any.whl", hash = "sha256:a71292ab7769a5de274b146b276ce938786f56c31cf7cea88b6f3775d82fe8c8", size = 27772, upload-time = "2023-09-11T14:03:45.582Z" }, -] - [[package]] name = "protobuf" version = "6.33.2" @@ -2274,15 +1907,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", size = 1179756, upload-time = "2023-11-21T20:43:49.423Z" }, ] -[[package]] -name = "pyparsing" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/fe/65c989f70bd630b589adfbbcd6ed238af22319e90f059946c26b4835e44b/pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db", size = 884814, upload-time = "2023-07-30T15:07:02.617Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/92/8486ede85fcc088f1b3dba4ce92dd29d126fd96b0008ea213167940a2475/pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb", size = 103139, upload-time = "2023-07-30T15:06:59.829Z" }, -] - [[package]] name = "pytest" version = "9.0.3" @@ -2338,18 +1962,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] -[[package]] -name = "python-dateutil" -version = "2.8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4c/c4/13b4776ea2d76c115c1d1b84579f3764ee6d57204f6be27119f13a61d0a9/python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", size = 357324, upload-time = "2021-07-14T08:19:19.783Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/7a/87837f39d0296e723bb9b62bbb257d0355c7f6128853c78955f57342a56d/python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9", size = 247702, upload-time = "2021-07-14T08:19:18.161Z" }, -] - [[package]] name = "python-dotenv" version = "1.0.0" @@ -2511,21 +2123,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] -[[package]] -name = "qudida" -version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "opencv-python-headless" }, - { name = "scikit-learn" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3e/2d/bab8babd9dc9a9e4df6eb115540cee4322c1a74078fb6f3b3ebc452a22b3/qudida-0.0.4.tar.gz", hash = "sha256:db198e2887ab0c9aa0023e565afbff41dfb76b361f85fd5e13f780d75ba18cc8", size = 3100, upload-time = "2021-08-09T16:47:55.807Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/a1/a5f4bebaa31d109003909809d88aeb0d4b201463a9ea29308d9e4f9e7655/qudida-0.0.4-py3-none-any.whl", hash = "sha256:4519714c40cd0f2e6c51e1735edae8f8b19f4efe1f33be13e9d644ca5f736dd6", size = 3478, upload-time = "2021-08-09T16:47:54.637Z" }, -] - [[package]] name = "rapidocr" version = "3.8.1" @@ -2661,139 +2258,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" }, ] -[[package]] -name = "scikit-image" -version = "0.25.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "imageio" }, - { name = "lazy-loader" }, - { name = "networkx" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pillow" }, - { name = "scipy" }, - { name = "tifffile" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057, upload-time = "2025-02-18T18:04:30.395Z" }, - { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335, upload-time = "2025-02-18T18:04:33.449Z" }, - { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783, upload-time = "2025-02-18T18:04:36.594Z" }, - { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376, upload-time = "2025-02-18T18:04:39.856Z" }, - { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698, upload-time = "2025-02-18T18:04:42.868Z" }, - { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000, upload-time = "2025-02-18T18:04:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893, upload-time = "2025-02-18T18:04:51.049Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389, upload-time = "2025-02-18T18:04:54.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435, upload-time = "2025-02-18T18:04:57.586Z" }, - { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474, upload-time = "2025-02-18T18:05:01.166Z" }, - { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, - { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, - { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, - { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, - { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, -] - -[[package]] -name = "scikit-learn" -version = "1.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "joblib" }, - { name = "numpy" }, - { name = "scipy" }, - { name = "threadpoolctl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/84/5f4af978fff619706b8961accac84780a6d298d82a8873446f72edb4ead0/scikit_learn-1.7.1.tar.gz", hash = "sha256:24b3f1e976a4665aa74ee0fcaac2b8fccc6ae77c8e07ab25da3ba6d3292b9802", size = 7190445, upload-time = "2025-07-18T08:01:54.5Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/bd/a23177930abd81b96daffa30ef9c54ddbf544d3226b8788ce4c3ef1067b4/scikit_learn-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90c8494ea23e24c0fb371afc474618c1019dc152ce4a10e4607e62196113851b", size = 9334838, upload-time = "2025-07-18T08:01:11.239Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a1/d3a7628630a711e2ac0d1a482910da174b629f44e7dd8cfcd6924a4ef81a/scikit_learn-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:bb870c0daf3bf3be145ec51df8ac84720d9972170786601039f024bf6d61a518", size = 8651241, upload-time = "2025-07-18T08:01:13.234Z" }, - { url = "https://files.pythonhosted.org/packages/26/92/85ec172418f39474c1cd0221d611345d4f433fc4ee2fc68e01f524ccc4e4/scikit_learn-1.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40daccd1b5623f39e8943ab39735cadf0bdce80e67cdca2adcb5426e987320a8", size = 9718677, upload-time = "2025-07-18T08:01:15.649Z" }, - { url = "https://files.pythonhosted.org/packages/df/ce/abdb1dcbb1d2b66168ec43b23ee0cee356b4cc4100ddee3943934ebf1480/scikit_learn-1.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30d1f413cfc0aa5a99132a554f1d80517563c34a9d3e7c118fde2d273c6fe0f7", size = 9511189, upload-time = "2025-07-18T08:01:18.013Z" }, - { url = "https://files.pythonhosted.org/packages/b2/3b/47b5eaee01ef2b5a80ba3f7f6ecf79587cb458690857d4777bfd77371c6f/scikit_learn-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:c711d652829a1805a95d7fe96654604a8f16eab5a9e9ad87b3e60173415cb650", size = 8914794, upload-time = "2025-07-18T08:01:20.357Z" }, - { url = "https://files.pythonhosted.org/packages/cb/16/57f176585b35ed865f51b04117947fe20f130f78940c6477b6d66279c9c2/scikit_learn-1.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3cee419b49b5bbae8796ecd690f97aa412ef1674410c23fc3257c6b8b85b8087", size = 9260431, upload-time = "2025-07-18T08:01:22.77Z" }, - { url = "https://files.pythonhosted.org/packages/67/4e/899317092f5efcab0e9bc929e3391341cec8fb0e816c4789686770024580/scikit_learn-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2fd8b8d35817b0d9ebf0b576f7d5ffbbabdb55536b0655a8aaae629d7ffd2e1f", size = 8637191, upload-time = "2025-07-18T08:01:24.731Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1b/998312db6d361ded1dd56b457ada371a8d8d77ca2195a7d18fd8a1736f21/scikit_learn-1.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:588410fa19a96a69763202f1d6b7b91d5d7a5d73be36e189bc6396bfb355bd87", size = 9486346, upload-time = "2025-07-18T08:01:26.713Z" }, - { url = "https://files.pythonhosted.org/packages/ad/09/a2aa0b4e644e5c4ede7006748f24e72863ba2ae71897fecfd832afea01b4/scikit_learn-1.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3142f0abe1ad1d1c31a2ae987621e41f6b578144a911ff4ac94781a583adad7", size = 9290988, upload-time = "2025-07-18T08:01:28.938Z" }, - { url = "https://files.pythonhosted.org/packages/15/fa/c61a787e35f05f17fc10523f567677ec4eeee5f95aa4798dbbbcd9625617/scikit_learn-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3ddd9092c1bd469acab337d87930067c87eac6bd544f8d5027430983f1e1ae88", size = 8735568, upload-time = "2025-07-18T08:01:30.936Z" }, - { url = "https://files.pythonhosted.org/packages/52/f8/e0533303f318a0f37b88300d21f79b6ac067188d4824f1047a37214ab718/scikit_learn-1.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b7839687fa46d02e01035ad775982f2470be2668e13ddd151f0f55a5bf123bae", size = 9213143, upload-time = "2025-07-18T08:01:32.942Z" }, - { url = "https://files.pythonhosted.org/packages/71/f3/f1df377d1bdfc3e3e2adc9c119c238b182293e6740df4cbeac6de2cc3e23/scikit_learn-1.7.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a10f276639195a96c86aa572ee0698ad64ee939a7b042060b98bd1930c261d10", size = 8591977, upload-time = "2025-07-18T08:01:34.967Z" }, - { url = "https://files.pythonhosted.org/packages/99/72/c86a4cd867816350fe8dee13f30222340b9cd6b96173955819a5561810c5/scikit_learn-1.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:13679981fdaebc10cc4c13c43344416a86fcbc61449cb3e6517e1df9d12c8309", size = 9436142, upload-time = "2025-07-18T08:01:37.397Z" }, - { url = "https://files.pythonhosted.org/packages/e8/66/277967b29bd297538dc7a6ecfb1a7dce751beabd0d7f7a2233be7a4f7832/scikit_learn-1.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f1262883c6a63f067a980a8cdd2d2e7f2513dddcef6a9eaada6416a7a7cbe43", size = 9282996, upload-time = "2025-07-18T08:01:39.721Z" }, - { url = "https://files.pythonhosted.org/packages/e2/47/9291cfa1db1dae9880420d1e07dbc7e8dd4a7cdbc42eaba22512e6bde958/scikit_learn-1.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:ca6d31fb10e04d50bfd2b50d66744729dbb512d4efd0223b864e2fdbfc4cee11", size = 8707418, upload-time = "2025-07-18T08:01:42.124Z" }, - { url = "https://files.pythonhosted.org/packages/61/95/45726819beccdaa34d3362ea9b2ff9f2b5d3b8bf721bd632675870308ceb/scikit_learn-1.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:781674d096303cfe3d351ae6963ff7c958db61cde3421cd490e3a5a58f2a94ae", size = 9561466, upload-time = "2025-07-18T08:01:44.195Z" }, - { url = "https://files.pythonhosted.org/packages/ee/1c/6f4b3344805de783d20a51eb24d4c9ad4b11a7f75c1801e6ec6d777361fd/scikit_learn-1.7.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:10679f7f125fe7ecd5fad37dd1aa2daae7e3ad8df7f3eefa08901b8254b3e12c", size = 9040467, upload-time = "2025-07-18T08:01:46.671Z" }, - { url = "https://files.pythonhosted.org/packages/6f/80/abe18fe471af9f1d181904203d62697998b27d9b62124cd281d740ded2f9/scikit_learn-1.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f812729e38c8cb37f760dce71a9b83ccfb04f59b3dca7c6079dcdc60544fa9e", size = 9532052, upload-time = "2025-07-18T08:01:48.676Z" }, - { url = "https://files.pythonhosted.org/packages/14/82/b21aa1e0c4cee7e74864d3a5a721ab8fcae5ca55033cb6263dca297ed35b/scikit_learn-1.7.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88e1a20131cf741b84b89567e1717f27a2ced228e0f29103426102bc2e3b8ef7", size = 9361575, upload-time = "2025-07-18T08:01:50.639Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/f4777fcd5627dc6695fa6b92179d0edb7a3ac1b91bcd9a1c7f64fa7ade23/scikit_learn-1.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b1bd1d919210b6a10b7554b717c9000b5485aa95a1d0f177ae0d7ee8ec750da5", size = 9277310, upload-time = "2025-07-18T08:01:52.547Z" }, -] - -[[package]] -name = "scipy" -version = "1.16.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/4a/b927028464795439faec8eaf0b03b011005c487bb2d07409f28bf30879c4/scipy-1.16.1.tar.gz", hash = "sha256:44c76f9e8b6e8e488a586190ab38016e4ed2f8a038af7cd3defa903c0a2238b3", size = 30580861, upload-time = "2025-07-27T16:33:30.834Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/91/812adc6f74409b461e3a5fa97f4f74c769016919203138a3bf6fc24ba4c5/scipy-1.16.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:c033fa32bab91dc98ca59d0cf23bb876454e2bb02cbe592d5023138778f70030", size = 36552519, upload-time = "2025-07-27T16:26:29.658Z" }, - { url = "https://files.pythonhosted.org/packages/47/18/8e355edcf3b71418d9e9f9acd2708cc3a6c27e8f98fde0ac34b8a0b45407/scipy-1.16.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6e5c2f74e5df33479b5cd4e97a9104c511518fbd979aa9b8f6aec18b2e9ecae7", size = 28638010, upload-time = "2025-07-27T16:26:38.196Z" }, - { url = "https://files.pythonhosted.org/packages/d9/eb/e931853058607bdfbc11b86df19ae7a08686121c203483f62f1ecae5989c/scipy-1.16.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0a55ffe0ba0f59666e90951971a884d1ff6f4ec3275a48f472cfb64175570f77", size = 20909790, upload-time = "2025-07-27T16:26:43.93Z" }, - { url = "https://files.pythonhosted.org/packages/45/0c/be83a271d6e96750cd0be2e000f35ff18880a46f05ce8b5d3465dc0f7a2a/scipy-1.16.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f8a5d6cd147acecc2603fbd382fed6c46f474cccfcf69ea32582e033fb54dcfe", size = 23513352, upload-time = "2025-07-27T16:26:50.017Z" }, - { url = "https://files.pythonhosted.org/packages/7c/bf/fe6eb47e74f762f933cca962db7f2c7183acfdc4483bd1c3813cfe83e538/scipy-1.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb18899127278058bcc09e7b9966d41a5a43740b5bb8dcba401bd983f82e885b", size = 33534643, upload-time = "2025-07-27T16:26:57.503Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ba/63f402e74875486b87ec6506a4f93f6d8a0d94d10467280f3d9d7837ce3a/scipy-1.16.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adccd93a2fa937a27aae826d33e3bfa5edf9aa672376a4852d23a7cd67a2e5b7", size = 35376776, upload-time = "2025-07-27T16:27:06.639Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b4/04eb9d39ec26a1b939689102da23d505ea16cdae3dbb18ffc53d1f831044/scipy-1.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:18aca1646a29ee9a0625a1be5637fa798d4d81fdf426481f06d69af828f16958", size = 35698906, upload-time = "2025-07-27T16:27:14.943Z" }, - { url = "https://files.pythonhosted.org/packages/04/d6/bb5468da53321baeb001f6e4e0d9049eadd175a4a497709939128556e3ec/scipy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d85495cef541729a70cdddbbf3e6b903421bc1af3e8e3a9a72a06751f33b7c39", size = 38129275, upload-time = "2025-07-27T16:27:23.873Z" }, - { url = "https://files.pythonhosted.org/packages/c4/94/994369978509f227cba7dfb9e623254d0d5559506fe994aef4bea3ed469c/scipy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:226652fca853008119c03a8ce71ffe1b3f6d2844cc1686e8f9806edafae68596", size = 38644572, upload-time = "2025-07-27T16:27:32.637Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d9/ec4864f5896232133f51382b54a08de91a9d1af7a76dfa372894026dfee2/scipy-1.16.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81b433bbeaf35728dad619afc002db9b189e45eebe2cd676effe1fb93fef2b9c", size = 36575194, upload-time = "2025-07-27T16:27:41.321Z" }, - { url = "https://files.pythonhosted.org/packages/5c/6d/40e81ecfb688e9d25d34a847dca361982a6addf8e31f0957b1a54fbfa994/scipy-1.16.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:886cc81fdb4c6903a3bb0464047c25a6d1016fef77bb97949817d0c0d79f9e04", size = 28594590, upload-time = "2025-07-27T16:27:49.204Z" }, - { url = "https://files.pythonhosted.org/packages/0e/37/9f65178edfcc629377ce9a64fc09baebea18c80a9e57ae09a52edf84880b/scipy-1.16.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:15240c3aac087a522b4eaedb09f0ad061753c5eebf1ea430859e5bf8640d5919", size = 20866458, upload-time = "2025-07-27T16:27:54.98Z" }, - { url = "https://files.pythonhosted.org/packages/2c/7b/749a66766871ea4cb1d1ea10f27004db63023074c22abed51f22f09770e0/scipy-1.16.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f81a25805f3659b48126b5053d9e823d3215e4a63730b5e1671852a1705921", size = 23539318, upload-time = "2025-07-27T16:28:01.604Z" }, - { url = "https://files.pythonhosted.org/packages/c4/db/8d4afec60eb833a666434d4541a3151eedbf2494ea6d4d468cbe877f00cd/scipy-1.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6c62eea7f607f122069b9bad3f99489ddca1a5173bef8a0c75555d7488b6f725", size = 33292899, upload-time = "2025-07-27T16:28:09.147Z" }, - { url = "https://files.pythonhosted.org/packages/51/1e/79023ca3bbb13a015d7d2757ecca3b81293c663694c35d6541b4dca53e98/scipy-1.16.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f965bbf3235b01c776115ab18f092a95aa74c271a52577bcb0563e85738fd618", size = 35162637, upload-time = "2025-07-27T16:28:17.535Z" }, - { url = "https://files.pythonhosted.org/packages/b6/49/0648665f9c29fdaca4c679182eb972935b3b4f5ace41d323c32352f29816/scipy-1.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f006e323874ffd0b0b816d8c6a8e7f9a73d55ab3b8c3f72b752b226d0e3ac83d", size = 35490507, upload-time = "2025-07-27T16:28:25.705Z" }, - { url = "https://files.pythonhosted.org/packages/62/8f/66cbb9d6bbb18d8c658f774904f42a92078707a7c71e5347e8bf2f52bb89/scipy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8fd15fc5085ab4cca74cb91fe0a4263b1f32e4420761ddae531ad60934c2119", size = 37923998, upload-time = "2025-07-27T16:28:34.339Z" }, - { url = "https://files.pythonhosted.org/packages/14/c3/61f273ae550fbf1667675701112e380881905e28448c080b23b5a181df7c/scipy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7b8013c6c066609577d910d1a2a077021727af07b6fab0ee22c2f901f22352a", size = 38508060, upload-time = "2025-07-27T16:28:43.242Z" }, - { url = "https://files.pythonhosted.org/packages/93/0b/b5c99382b839854a71ca9482c684e3472badc62620287cbbdab499b75ce6/scipy-1.16.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5451606823a5e73dfa621a89948096c6528e2896e40b39248295d3a0138d594f", size = 36533717, upload-time = "2025-07-27T16:28:51.706Z" }, - { url = "https://files.pythonhosted.org/packages/eb/e5/69ab2771062c91e23e07c12e7d5033a6b9b80b0903ee709c3c36b3eb520c/scipy-1.16.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:89728678c5ca5abd610aee148c199ac1afb16e19844401ca97d43dc548a354eb", size = 28570009, upload-time = "2025-07-27T16:28:57.017Z" }, - { url = "https://files.pythonhosted.org/packages/f4/69/bd75dbfdd3cf524f4d753484d723594aed62cfaac510123e91a6686d520b/scipy-1.16.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e756d688cb03fd07de0fffad475649b03cb89bee696c98ce508b17c11a03f95c", size = 20841942, upload-time = "2025-07-27T16:29:01.152Z" }, - { url = "https://files.pythonhosted.org/packages/ea/74/add181c87663f178ba7d6144b370243a87af8476664d5435e57d599e6874/scipy-1.16.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5aa2687b9935da3ed89c5dbed5234576589dd28d0bf7cd237501ccfbdf1ad608", size = 23498507, upload-time = "2025-07-27T16:29:05.202Z" }, - { url = "https://files.pythonhosted.org/packages/1d/74/ece2e582a0d9550cee33e2e416cc96737dce423a994d12bbe59716f47ff1/scipy-1.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0851f6a1e537fe9399f35986897e395a1aa61c574b178c0d456be5b1a0f5ca1f", size = 33286040, upload-time = "2025-07-27T16:29:10.201Z" }, - { url = "https://files.pythonhosted.org/packages/e4/82/08e4076df538fb56caa1d489588d880ec7c52d8273a606bb54d660528f7c/scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fedc2cbd1baed37474b1924c331b97bdff611d762c196fac1a9b71e67b813b1b", size = 35176096, upload-time = "2025-07-27T16:29:17.091Z" }, - { url = "https://files.pythonhosted.org/packages/fa/79/cd710aab8c921375711a8321c6be696e705a120e3011a643efbbcdeeabcc/scipy-1.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2ef500e72f9623a6735769e4b93e9dcb158d40752cdbb077f305487e3e2d1f45", size = 35490328, upload-time = "2025-07-27T16:29:22.928Z" }, - { url = "https://files.pythonhosted.org/packages/71/73/e9cc3d35ee4526d784520d4494a3e1ca969b071fb5ae5910c036a375ceec/scipy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:978d8311674b05a8f7ff2ea6c6bce5d8b45a0cb09d4c5793e0318f448613ea65", size = 37939921, upload-time = "2025-07-27T16:29:29.108Z" }, - { url = "https://files.pythonhosted.org/packages/21/12/c0efd2941f01940119b5305c375ae5c0fcb7ec193f806bd8f158b73a1782/scipy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:81929ed0fa7a5713fcdd8b2e6f73697d3b4c4816d090dd34ff937c20fa90e8ab", size = 38479462, upload-time = "2025-07-27T16:30:24.078Z" }, - { url = "https://files.pythonhosted.org/packages/7a/19/c3d08b675260046a991040e1ea5d65f91f40c7df1045fffff412dcfc6765/scipy-1.16.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:bcc12db731858abda693cecdb3bdc9e6d4bd200213f49d224fe22df82687bdd6", size = 36938832, upload-time = "2025-07-27T16:29:35.057Z" }, - { url = "https://files.pythonhosted.org/packages/81/f2/ce53db652c033a414a5b34598dba6b95f3d38153a2417c5a3883da429029/scipy-1.16.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:744d977daa4becb9fc59135e75c069f8d301a87d64f88f1e602a9ecf51e77b27", size = 29093084, upload-time = "2025-07-27T16:29:40.201Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ae/7a10ff04a7dc15f9057d05b33737ade244e4bd195caa3f7cc04d77b9e214/scipy-1.16.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:dc54f76ac18073bcecffb98d93f03ed6b81a92ef91b5d3b135dcc81d55a724c7", size = 21365098, upload-time = "2025-07-27T16:29:44.295Z" }, - { url = "https://files.pythonhosted.org/packages/36/ac/029ff710959932ad3c2a98721b20b405f05f752f07344622fd61a47c5197/scipy-1.16.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:367d567ee9fc1e9e2047d31f39d9d6a7a04e0710c86e701e053f237d14a9b4f6", size = 23896858, upload-time = "2025-07-27T16:29:48.784Z" }, - { url = "https://files.pythonhosted.org/packages/71/13/d1ef77b6bd7898720e1f0b6b3743cb945f6c3cafa7718eaac8841035ab60/scipy-1.16.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4cf5785e44e19dcd32a0e4807555e1e9a9b8d475c6afff3d21c3c543a6aa84f4", size = 33438311, upload-time = "2025-07-27T16:29:54.164Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e0/e64a6821ffbb00b4c5b05169f1c1fddb4800e9307efe3db3788995a82a2c/scipy-1.16.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3d0b80fb26d3e13a794c71d4b837e2a589d839fd574a6bbb4ee1288c213ad4a3", size = 35279542, upload-time = "2025-07-27T16:30:00.249Z" }, - { url = "https://files.pythonhosted.org/packages/57/59/0dc3c8b43e118f1e4ee2b798dcc96ac21bb20014e5f1f7a8e85cc0653bdb/scipy-1.16.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8503517c44c18d1030d666cb70aaac1cc8913608816e06742498833b128488b7", size = 35667665, upload-time = "2025-07-27T16:30:05.916Z" }, - { url = "https://files.pythonhosted.org/packages/45/5f/844ee26e34e2f3f9f8febb9343748e72daeaec64fe0c70e9bf1ff84ec955/scipy-1.16.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:30cc4bb81c41831ecfd6dc450baf48ffd80ef5aed0f5cf3ea775740e80f16ecc", size = 38045210, upload-time = "2025-07-27T16:30:11.655Z" }, - { url = "https://files.pythonhosted.org/packages/8d/d7/210f2b45290f444f1de64bc7353aa598ece9f0e90c384b4a156f9b1a5063/scipy-1.16.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c24fa02f7ed23ae514460a22c57eca8f530dbfa50b1cfdbf4f37c05b5309cc39", size = 38593661, upload-time = "2025-07-27T16:30:17.825Z" }, - { url = "https://files.pythonhosted.org/packages/81/ea/84d481a5237ed223bd3d32d6e82d7a6a96e34756492666c260cef16011d1/scipy-1.16.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:796a5a9ad36fa3a782375db8f4241ab02a091308eb079746bc0f874c9b998318", size = 36525921, upload-time = "2025-07-27T16:30:30.081Z" }, - { url = "https://files.pythonhosted.org/packages/4e/9f/d9edbdeff9f3a664807ae3aea383e10afaa247e8e6255e6d2aa4515e8863/scipy-1.16.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:3ea0733a2ff73fd6fdc5fecca54ee9b459f4d74f00b99aced7d9a3adb43fb1cc", size = 28564152, upload-time = "2025-07-27T16:30:35.336Z" }, - { url = "https://files.pythonhosted.org/packages/3b/95/8125bcb1fe04bc267d103e76516243e8d5e11229e6b306bda1024a5423d1/scipy-1.16.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:85764fb15a2ad994e708258bb4ed8290d1305c62a4e1ef07c414356a24fcfbf8", size = 20836028, upload-time = "2025-07-27T16:30:39.421Z" }, - { url = "https://files.pythonhosted.org/packages/77/9c/bf92e215701fc70bbcd3d14d86337cf56a9b912a804b9c776a269524a9e9/scipy-1.16.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ca66d980469cb623b1759bdd6e9fd97d4e33a9fad5b33771ced24d0cb24df67e", size = 23489666, upload-time = "2025-07-27T16:30:43.663Z" }, - { url = "https://files.pythonhosted.org/packages/5e/00/5e941d397d9adac41b02839011594620d54d99488d1be5be755c00cde9ee/scipy-1.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7cc1ffcc230f568549fc56670bcf3df1884c30bd652c5da8138199c8c76dae0", size = 33358318, upload-time = "2025-07-27T16:30:48.982Z" }, - { url = "https://files.pythonhosted.org/packages/0e/87/8db3aa10dde6e3e8e7eb0133f24baa011377d543f5b19c71469cf2648026/scipy-1.16.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ddfb1e8d0b540cb4ee9c53fc3dea3186f97711248fb94b4142a1b27178d8b4b", size = 35185724, upload-time = "2025-07-27T16:30:54.26Z" }, - { url = "https://files.pythonhosted.org/packages/89/b4/6ab9ae443216807622bcff02690262d8184078ea467efee2f8c93288a3b1/scipy-1.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4dc0e7be79e95d8ba3435d193e0d8ce372f47f774cffd882f88ea4e1e1ddc731", size = 35554335, upload-time = "2025-07-27T16:30:59.765Z" }, - { url = "https://files.pythonhosted.org/packages/9c/9a/d0e9dc03c5269a1afb60661118296a32ed5d2c24298af61b676c11e05e56/scipy-1.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f23634f9e5adb51b2a77766dac217063e764337fbc816aa8ad9aaebcd4397fd3", size = 37960310, upload-time = "2025-07-27T16:31:06.151Z" }, - { url = "https://files.pythonhosted.org/packages/5e/00/c8f3130a50521a7977874817ca89e0599b1b4ee8e938bad8ae798a0e1f0d/scipy-1.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:57d75524cb1c5a374958a2eae3d84e1929bb971204cc9d52213fb8589183fc19", size = 39319239, upload-time = "2025-07-27T16:31:59.942Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f2/1ca3eda54c3a7e4c92f6acef7db7b3a057deb135540d23aa6343ef8ad333/scipy-1.16.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:d8da7c3dd67bcd93f15618938f43ed0995982eb38973023d46d4646c4283ad65", size = 36939460, upload-time = "2025-07-27T16:31:11.865Z" }, - { url = "https://files.pythonhosted.org/packages/80/30/98c2840b293a132400c0940bb9e140171dcb8189588619048f42b2ce7b4f/scipy-1.16.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:cc1d2f2fd48ba1e0620554fe5bc44d3e8f5d4185c8c109c7fbdf5af2792cfad2", size = 29093322, upload-time = "2025-07-27T16:31:17.045Z" }, - { url = "https://files.pythonhosted.org/packages/c1/e6/1e6e006e850622cf2a039b62d1a6ddc4497d4851e58b68008526f04a9a00/scipy-1.16.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:21a611ced9275cb861bacadbada0b8c0623bc00b05b09eb97f23b370fc2ae56d", size = 21365329, upload-time = "2025-07-27T16:31:21.188Z" }, - { url = "https://files.pythonhosted.org/packages/8e/02/72a5aa5b820589dda9a25e329ca752842bfbbaf635e36bc7065a9b42216e/scipy-1.16.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dfbb25dffc4c3dd9371d8ab456ca81beeaf6f9e1c2119f179392f0dc1ab7695", size = 23897544, upload-time = "2025-07-27T16:31:25.408Z" }, - { url = "https://files.pythonhosted.org/packages/2b/dc/7122d806a6f9eb8a33532982234bed91f90272e990f414f2830cfe656e0b/scipy-1.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0ebb7204f063fad87fc0a0e4ff4a2ff40b2a226e4ba1b7e34bf4b79bf97cd86", size = 33442112, upload-time = "2025-07-27T16:31:30.62Z" }, - { url = "https://files.pythonhosted.org/packages/24/39/e383af23564daa1021a5b3afbe0d8d6a68ec639b943661841f44ac92de85/scipy-1.16.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f1b9e5962656f2734c2b285a8745358ecb4e4efbadd00208c80a389227ec61ff", size = 35286594, upload-time = "2025-07-27T16:31:36.112Z" }, - { url = "https://files.pythonhosted.org/packages/95/47/1a0b0aff40c3056d955f38b0df5d178350c3d74734ec54f9c68d23910be5/scipy-1.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e1a106f8c023d57a2a903e771228bf5c5b27b5d692088f457acacd3b54511e4", size = 35665080, upload-time = "2025-07-27T16:31:42.025Z" }, - { url = "https://files.pythonhosted.org/packages/64/df/ce88803e9ed6e27fe9b9abefa157cf2c80e4fa527cf17ee14be41f790ad4/scipy-1.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:709559a1db68a9abc3b2c8672c4badf1614f3b440b3ab326d86a5c0491eafae3", size = 38050306, upload-time = "2025-07-27T16:31:48.109Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6c/a76329897a7cae4937d403e623aa6aaea616a0bb5b36588f0b9d1c9a3739/scipy-1.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c0c804d60492a0aad7f5b2bb1862f4548b990049e27e828391ff2bf6f7199998", size = 39427705, upload-time = "2025-07-27T16:31:53.96Z" }, -] - [[package]] name = "setuptools" version = "80.9.0" @@ -2910,27 +2374,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/05/e6600db80270777c4a64238a98d442f0fd07cc8915be2a1c16da7f2b9e74/sympy-1.12-py3-none-any.whl", hash = "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5", size = 5742435, upload-time = "2023-05-10T18:22:14.76Z" }, ] -[[package]] -name = "threadpoolctl" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/8a/c05f7831beb32aff70f808766224f11c650f7edfd49b27a8fc6666107006/threadpoolctl-3.2.0.tar.gz", hash = "sha256:c96a0ba3bdddeaca37dc4cc7344aafad41cdb8c313f74fdfe387a867bba93355", size = 36266, upload-time = "2023-07-13T14:53:53.299Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/12/fd4dea011af9d69e1cad05c75f3f7202cdcbeac9b712eea58ca779a72865/threadpoolctl-3.2.0-py3-none-any.whl", hash = "sha256:2b7818516e423bdaebb97c723f86a7c6b0a83d3f3b0970328d66f4d9104dc032", size = 15539, upload-time = "2023-07-13T14:53:39.336Z" }, -] - -[[package]] -name = "tifffile" -version = "2023.12.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/6c0eadea1ccfcda27e6cce400c366098b5b082138a073f4252fe399f4148/tifffile-2023.12.9.tar.gz", hash = "sha256:9dd1da91180a6453018a241ff219e1905f169384355cd89c9ef4034c1b46cdb8", size = 353467, upload-time = "2023-12-09T20:46:29.203Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/a4/569fc717831969cf48bced350bdaf070cdeab06918d179429899e144358d/tifffile-2023.12.9-py3-none-any.whl", hash = "sha256:9b066e4b1a900891ea42ffd33dab8ba34c537935618b9893ddef42d7d422692f", size = 223627, upload-time = "2023-12-09T20:46:26.569Z" }, -] - [[package]] name = "tokenizers" version = "0.23.1" @@ -3211,15 +2654,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/a5/7aba9435beb863c2490bae3173a45f42044ac7a48155d3dd42ab49cfae45/watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6", size = 268026, upload-time = "2023-10-13T13:05:38.591Z" }, ] -[[package]] -name = "wcwidth" -version = "0.2.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, -] - [[package]] name = "websocket-client" version = "1.8.0" From e9eafc316146fdc559021d985dd5856dff68bc5a Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:44:14 +0530 Subject: [PATCH 09/49] refactor: gallery permission notifier (#30477) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/constants/enums.dart | 11 ++ .../services/device_permission.service.dart | 47 +++++++ .../domain/services/local_sync.service.dart | 2 +- .../domain/services/sync_stream.service.dart | 2 +- .../gallery_permission.provider.dart | 111 +++------------- .../repositories/permission.repository.dart | 70 +++++++---- mobile/lib/services/network.service.dart | 2 +- mobile/test/repository.mocks.dart | 2 +- mobile/test/unit/mocks.dart | 22 ++++ .../gallery_permission_service_test.dart | 118 ++++++++++++++++++ 10 files changed, 260 insertions(+), 127 deletions(-) create mode 100644 mobile/lib/domain/services/device_permission.service.dart create mode 100644 mobile/test/unit/services/gallery_permission_service_test.dart diff --git a/mobile/lib/constants/enums.dart b/mobile/lib/constants/enums.dart index d59c48c045..d3f85ceb2e 100644 --- a/mobile/lib/constants/enums.dart +++ b/mobile/lib/constants/enums.dart @@ -24,3 +24,14 @@ enum SlideshowLook { contain, cover, blurredBackground } enum SlideshowDirection { forward, backward, shuffle } enum PartnerDirection { sharedBy, sharedWith } + +enum DevicePermission { photos, videos, storage, mediaLocation } + +enum DevicePermissionStatus { + denied, + granted, + limited, + permanentlyDenied; + + bool get hasAccess => this == granted || this == limited; +} diff --git a/mobile/lib/domain/services/device_permission.service.dart b/mobile/lib/domain/services/device_permission.service.dart new file mode 100644 index 0000000000..d6ce128b8a --- /dev/null +++ b/mobile/lib/domain/services/device_permission.service.dart @@ -0,0 +1,47 @@ +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/repositories/permission.repository.dart'; + +typedef _Handler = Future Function(DevicePermission); + +class DevicePermissionService { + const DevicePermissionService(this._permissionRepository); + + final DevicePermissionRepository _permissionRepository; + + Future galleryStatus() => _gallery(_permissionRepository.getStatus); + + Future requestGallery() => _gallery(_permissionRepository.request); + + Future _gallery(_Handler handler) async { + if (CurrentPlatform.isIOS) { + return handler(.photos); + } + + final sdkVersion = await _permissionRepository.getAndroidSdkVersion(); + const maxExternalStorageSdk = 32; // READ/WRITE_EXTERNAL_STORAGE - Android 12.1 + final status = sdkVersion <= maxExternalStorageSdk ? await handler(.storage) : await _photosAndVideos(handler); + + const minMediaLocationSdk = 29; // ACCESS_MEDIA_LOCATION - Android 10 + if (status.hasAccess && sdkVersion >= minMediaLocationSdk) { + final mediaLocation = await handler(.mediaLocation); + return mediaLocation.hasAccess ? status : mediaLocation; + } + + return status; + } + + Future _photosAndVideos(_Handler handler) async { + final photos = await handler(.photos); + if (!photos.hasAccess) { + return photos; + } + + final videos = await handler(.videos); + if (!videos.hasAccess) { + return videos; + } + + return photos == .granted && videos == .granted ? .granted : .limited; + } +} diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index b4ebc66f89..9e272ac65f 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -27,7 +27,7 @@ class LocalSyncService { final NativeSyncApi _nativeSyncApi; final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; final AssetMediaRepository _assetMediaRepository; - final IPermissionRepository _permissionRepository; + final DevicePermissionRepository _permissionRepository; final Completer? _cancellation; final Logger _log = Logger("DeviceSyncService"); diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 9b4d4bb275..0eb3ff2f99 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -36,7 +36,7 @@ class SyncStreamService { final DriftLocalAssetRepository _localAssetRepository; final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; final AssetMediaRepository _assetMediaRepository; - final IPermissionRepository _permissionRepository; + final DevicePermissionRepository _permissionRepository; final SyncMigrationRepository _syncMigrationRepository; final ApiService _api; final Completer? _cancellation; diff --git a/mobile/lib/providers/gallery_permission.provider.dart b/mobile/lib/providers/gallery_permission.provider.dart index 6d4703c834..6be504bd2e 100644 --- a/mobile/lib/providers/gallery_permission.provider.dart +++ b/mobile/lib/providers/gallery_permission.provider.dart @@ -1,109 +1,28 @@ import 'dart:async'; -import 'dart:io'; -import 'package:device_info_plus/device_info_plus.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:permission_handler/permission_handler.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/services/device_permission.service.dart'; +import 'package:immich_mobile/repositories/permission.repository.dart'; -class GalleryPermissionNotifier extends StateNotifier { - GalleryPermissionNotifier() - : super(PermissionStatus.denied) // Denied is the initial state - { - // Sets the initial state +final galleryPermissionServiceProvider = Provider((ref) { + return DevicePermissionService(ref.watch(permissionRepositoryProvider)); +}); + +class GalleryPermissionNotifier extends StateNotifier { + GalleryPermissionNotifier(this._service) : super(.denied) { unawaited(getGalleryPermissionStatus()); } - bool get hasPermission => state.isGranted || state.isLimited; + final DevicePermissionService _service; - /// Requests the gallery permission - Future requestGalleryPermission() async { - PermissionStatus result; - // Android 32 and below uses Permission.storage - if (Platform.isAndroid) { - final androidInfo = await DeviceInfoPlugin().androidInfo; - if (androidInfo.version.sdkInt <= 32) { - // Android 32 and below need storage - final permission = await Permission.storage.request(); - result = permission; - } else { - // Android 33 need photo & video - final photos = await Permission.photos.request(); - if (!photos.isGranted) { - // Don't ask twice for the same permission - state = photos; - return photos; - } - final videos = await Permission.videos.request(); + bool get hasPermission => state.hasAccess; - // Return the joint result of those two permissions - final PermissionStatus status; - if ((photos.isGranted && videos.isGranted) || (photos.isLimited && videos.isLimited)) { - status = PermissionStatus.granted; - } else if (photos.isDenied || videos.isDenied) { - status = PermissionStatus.denied; - } else if (photos.isPermanentlyDenied || videos.isPermanentlyDenied) { - status = PermissionStatus.permanentlyDenied; - } else { - status = PermissionStatus.denied; - } + Future requestGalleryPermission() async => state = await _service.requestGallery(); - result = status; - } - if (result == PermissionStatus.granted && androidInfo.version.sdkInt >= 29) { - result = await Permission.accessMediaLocation.request(); - } - } else { - // iOS can use photos - final photos = await Permission.photos.request(); - result = photos; - } - state = result; - return result; - } - - /// Checks the current state of the gallery permissions without - /// requesting them again - Future getGalleryPermissionStatus() async { - PermissionStatus result; - // Android 32 and below uses Permission.storage - if (Platform.isAndroid) { - final androidInfo = await DeviceInfoPlugin().androidInfo; - if (androidInfo.version.sdkInt <= 32) { - // Android 32 and below need storage - final permission = await Permission.storage.status; - result = permission; - } else { - // Android 33 needs photo & video - final photos = await Permission.photos.status; - final videos = await Permission.videos.status; - - // Return the joint result of those two permissions - final PermissionStatus status; - if ((photos.isGranted && videos.isGranted) || (photos.isLimited && videos.isLimited)) { - status = PermissionStatus.granted; - } else if (photos.isDenied || videos.isDenied) { - status = PermissionStatus.denied; - } else if (photos.isPermanentlyDenied || videos.isPermanentlyDenied) { - status = PermissionStatus.permanentlyDenied; - } else { - status = PermissionStatus.denied; - } - - result = status; - } - if (state == PermissionStatus.granted && androidInfo.version.sdkInt >= 29) { - result = await Permission.accessMediaLocation.status; - } - } else { - // iOS can use photos - final photos = await Permission.photos.status; - result = photos; - } - state = result; - return result; - } + Future getGalleryPermissionStatus() async => state = await _service.galleryStatus(); } -final galleryPermissionNotifier = StateNotifierProvider( - (ref) => GalleryPermissionNotifier(), +final galleryPermissionNotifier = StateNotifierProvider( + (ref) => GalleryPermissionNotifier(ref.watch(galleryPermissionServiceProvider)), ); diff --git a/mobile/lib/repositories/permission.repository.dart b/mobile/lib/repositories/permission.repository.dart index e4b6d99f25..932be4a290 100644 --- a/mobile/lib/repositories/permission.repository.dart +++ b/mobile/lib/repositories/permission.repository.dart @@ -1,67 +1,83 @@ +import 'package:device_info_plus/device_info_plus.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/platform/permission_api.g.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; -import 'package:permission_handler/permission_handler.dart'; +import 'package:permission_handler/permission_handler.dart' as ph; final permissionRepositoryProvider = Provider((ref) { - return PermissionRepository(ref.watch(permissionApiProvider)); + return DevicePermissionRepository(ref.watch(permissionApiProvider)); }); -class PermissionRepository implements IPermissionRepository { +class DevicePermissionRepository { final PermissionApi _permissionApi; - const PermissionRepository(this._permissionApi); + const DevicePermissionRepository(this._permissionApi); - @override - Future hasLocationWhenInUsePermission() { - return Permission.locationWhenInUse.isGranted; + Future getStatus(DevicePermission permission) async => + (await permission.handler.status).toDevicePermissionStatus(); + + Future request(DevicePermission permission) async => + (await permission.handler.request()).toDevicePermissionStatus(); + + // TODO(shenlong): Move this to it's own device info repo + Future getAndroidSdkVersion() async { + if (CurrentPlatform.isIOS) { + throw UnsupportedError("This method is available only on Android"); + } + + final androidInfo = await DeviceInfoPlugin().androidInfo; + return androidInfo.version.sdkInt; } - @override + Future hasLocationWhenInUsePermission() => ph.Permission.locationWhenInUse.isGranted; + Future requestLocationWhenInUsePermission() async { - final result = await Permission.locationWhenInUse.request(); + final result = await ph.Permission.locationWhenInUse.request(); return result.isGranted; } - @override Future hasLocationAlwaysPermission() { - return Permission.locationAlways.isGranted; + return ph.Permission.locationAlways.isGranted; } - @override Future requestLocationAlwaysPermission() async { - final result = await Permission.locationAlways.request(); + final result = await ph.Permission.locationAlways.request(); return result.isGranted; } - @override Future openSettings() { - return openAppSettings(); + return ph.openAppSettings(); } - @override Future hasManageMediaPermission() { return _permissionApi.hasManageMediaPermission(); } - @override Future requestManageMediaPermission() { return _permissionApi.requestManageMediaPermission(); } - @override Future manageMediaPermission() { return _permissionApi.manageMediaPermission(); } } -abstract interface class IPermissionRepository { - Future hasLocationWhenInUsePermission(); - Future requestLocationWhenInUsePermission(); - Future hasLocationAlwaysPermission(); - Future requestLocationAlwaysPermission(); - Future openSettings(); - Future hasManageMediaPermission(); - Future requestManageMediaPermission(); - Future manageMediaPermission(); +extension on DevicePermission { + ph.Permission get handler => switch (this) { + .photos => ph.Permission.photos, + .videos => ph.Permission.videos, + .storage => ph.Permission.storage, + .mediaLocation => ph.Permission.accessMediaLocation, + }; +} + +extension on ph.PermissionStatus { + DevicePermissionStatus toDevicePermissionStatus() => switch (this) { + .granted => .granted, + .limited => .limited, + .permanentlyDenied => .permanentlyDenied, + _ => .denied, + }; } diff --git a/mobile/lib/services/network.service.dart b/mobile/lib/services/network.service.dart index 8622400e7a..e31ecb617e 100644 --- a/mobile/lib/services/network.service.dart +++ b/mobile/lib/services/network.service.dart @@ -8,7 +8,7 @@ final networkServiceProvider = Provider((ref) { class NetworkService { final NetworkRepository _repository; - final IPermissionRepository _permissionRepository; + final DevicePermissionRepository _permissionRepository; const NetworkService(this._repository, this._permissionRepository); diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart index b56a8a098a..164e7fe062 100644 --- a/mobile/test/repository.mocks.dart +++ b/mobile/test/repository.mocks.dart @@ -12,7 +12,7 @@ class MockAssetApiRepository extends Mock implements AssetApiRepository {} class MockAssetMediaRepository extends Mock implements AssetMediaRepository {} -class MockPermissionRepository extends Mock implements IPermissionRepository {} +class MockPermissionRepository extends Mock implements DevicePermissionRepository {} class MockAuthApiRepository extends Mock implements AuthApiRepository {} diff --git a/mobile/test/unit/mocks.dart b/mobile/test/unit/mocks.dart index 1d43198263..6c41320a85 100644 --- a/mobile/test/unit/mocks.dart +++ b/mobile/test/unit/mocks.dart @@ -32,6 +32,7 @@ class RepositoryMocks { final trashedAsset = MockTrashedLocalAssetRepository(); final remoteAlbum = MockRemoteAlbumRepository(); final albumApi = MockDriftAlbumApiRepository(); + final permission = PermissionRepositoryStub(MockPermissionRepository()); final nativeApi = NativeSyncApiStub(MockNativeSyncApi()); final assetApi = AssetApiRepositoryStub(MockAssetApiRepository()); @@ -55,6 +56,7 @@ class RepositoryMocks { assetApi.reset(); assetMedia.reset(); download.reset(); + permission.reset(); _stubLocalAlbumRepository(); _stubLocalAssetRepository(); _stubRemoteAssetRepository(); @@ -63,6 +65,7 @@ class RepositoryMocks { _stubAssetApiRepository(); _stubAssetMediaRepository(); _stubDownloadRepository(); + _stubPermissionRepository(); } void _stubRemoteAssetRepository() { @@ -101,6 +104,12 @@ class RepositoryMocks { void _stubDownloadRepository() { when(download.downloadAllAssets).thenAnswer((_) async => const []); } + + void _stubPermissionRepository() { + when(permission.getStatus).thenAnswer((_) async => DevicePermissionStatus.denied); + when(permission.request).thenAnswer((_) async => DevicePermissionStatus.denied); + when(permission.getAndroidSdkVersion).thenAnswer((_) async => 34); + } } class ServiceMocks { @@ -220,6 +229,8 @@ void _registerFallbacks() { registerFallbackValue(ShareAssetType.original); registerFallbackValue(const UploadCallbacks()); registerFallbackValue(_FakeBuildContext()); + registerFallbackValue(DevicePermissionStatus.granted); + registerFallbackValue(DevicePermission.photos); } class _FakeBuildContext extends Fake implements BuildContext {} @@ -400,6 +411,17 @@ extension type const DownloadRepositoryStub(MockDownloadRepository repo) impleme () => repo.downloadAllAssets(any()); } +extension type const PermissionRepositoryStub(MockPermissionRepository repo) implements Stub { + Future Function() get getStatus => + () => repo.getStatus(any()); + + Future Function() get request => + () => repo.request(any()); + + Future Function() get getAndroidSdkVersion => + () => repo.getAndroidSdkVersion(); +} + extension type const TagServiceStub(MockTagService service) implements Stub { Future Function() get bulkTagAssets => () => service.bulkTagAssets(any(), any()); diff --git a/mobile/test/unit/services/gallery_permission_service_test.dart b/mobile/test/unit/services/gallery_permission_service_test.dart new file mode 100644 index 0000000000..9a399f9948 --- /dev/null +++ b/mobile/test/unit/services/gallery_permission_service_test.dart @@ -0,0 +1,118 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/services/device_permission.service.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../mocks.dart'; + +void main() { + late RepositoryMocks mocks; + late DevicePermissionService sut; + + void stubPermissions(Map statuses) { + DevicePermissionStatus answer(Invocation i) => statuses[i.positionalArguments.first] ?? .denied; + when(mocks.permission.getStatus).thenAnswer((i) async => answer(i)); + when(mocks.permission.request).thenAnswer((i) async => answer(i)); + } + + setUp(() { + debugDefaultTargetPlatformOverride = .android; + mocks = .new(); + sut = .new(mocks.permission.repo); + }); + + tearDown(() { + debugDefaultTargetPlatformOverride = null; + }); + + group('DevicePermissionService', () { + group('Gallery', () { + test('is denied when media location is missing but photos and videos are granted', () async { + when(mocks.permission.getAndroidSdkVersion).thenAnswer((_) async => 34); + stubPermissions({.photos: .granted, .videos: .granted, .mediaLocation: .denied}); + expect(await sut.galleryStatus(), DevicePermissionStatus.denied); + verify(() => mocks.permission.repo.getStatus(DevicePermission.mediaLocation)).called(1); + }); + + test('is granted when media location is granted', () async { + when(mocks.permission.getAndroidSdkVersion).thenAnswer((_) async => 34); + stubPermissions({.photos: .granted, .videos: .granted, .mediaLocation: .granted}); + expect(await sut.galleryStatus(), DevicePermissionStatus.granted); + verify(() => mocks.permission.repo.getStatus(DevicePermission.mediaLocation)).called(1); + }); + + test('checks media location on the legacy storage path', () async { + when(mocks.permission.getAndroidSdkVersion).thenAnswer((_) async => 30); + stubPermissions({.storage: .granted, .mediaLocation: .denied}); + expect(await sut.galleryStatus(), DevicePermissionStatus.denied); + verify(() => mocks.permission.repo.getStatus(DevicePermission.mediaLocation)).called(1); + }); + + test('ignores media location below SDK 29', () async { + when(mocks.permission.getAndroidSdkVersion).thenAnswer((_) async => 28); + stubPermissions({.storage: .granted, .mediaLocation: .denied}); + expect(await sut.galleryStatus(), DevicePermissionStatus.granted); + verifyNever(() => mocks.permission.repo.getStatus(.mediaLocation)); + }); + + test('ignores media location on iOS', () async { + debugDefaultTargetPlatformOverride = .iOS; + stubPermissions({.photos: .granted, .mediaLocation: .denied}); + expect(await sut.galleryStatus(), DevicePermissionStatus.granted); + verifyNever(() => mocks.permission.repo.getStatus(.mediaLocation)); + }); + + test('is limited when only one of photos and videos is limited', () async { + stubPermissions({.photos: .granted, .videos: .limited, .mediaLocation: .granted}); + expect(await sut.galleryStatus(), DevicePermissionStatus.limited); + + stubPermissions({.photos: .limited, .videos: .granted, .mediaLocation: .granted}); + expect(await sut.galleryStatus(), DevicePermissionStatus.limited); + + stubPermissions({.photos: .limited, .videos: .limited, .mediaLocation: .granted}); + expect(await sut.galleryStatus(), DevicePermissionStatus.limited); + }); + + test('is denied when only one of photos and videos is denied', () async { + stubPermissions({.photos: .granted, .videos: .denied, .mediaLocation: .granted}); + expect(await sut.galleryStatus(), DevicePermissionStatus.denied); + + stubPermissions({.photos: .denied, .videos: .granted, .mediaLocation: .granted}); + expect(await sut.galleryStatus(), DevicePermissionStatus.denied); + + stubPermissions({.photos: .limited, .videos: .denied, .mediaLocation: .granted}); + expect(await sut.galleryStatus(), DevicePermissionStatus.denied); + }); + + test('is permanently denied when only one of photos and videos is permanently denied', () async { + stubPermissions({.photos: .granted, .videos: .permanentlyDenied, .mediaLocation: .granted}); + expect(await sut.galleryStatus(), DevicePermissionStatus.permanentlyDenied); + + stubPermissions({.photos: .permanentlyDenied, .videos: .denied, .mediaLocation: .granted}); + expect(await sut.galleryStatus(), DevicePermissionStatus.permanentlyDenied); + + stubPermissions({.photos: .permanentlyDenied, .videos: .granted, .mediaLocation: .granted}); + expect(await sut.galleryStatus(), DevicePermissionStatus.permanentlyDenied); + }); + + test('checks media location when access is only partial', () async { + stubPermissions({.photos: .limited, .videos: .limited, .mediaLocation: .denied}); + expect(await sut.galleryStatus(), DevicePermissionStatus.denied); + verify(() => mocks.permission.repo.getStatus(DevicePermission.mediaLocation)).called(1); + }); + + test('stays limited when media location is granted, but photos or video is limited', () async { + stubPermissions({.photos: .granted, .videos: .limited, .mediaLocation: .granted}); + expect(await sut.galleryStatus(), DevicePermissionStatus.limited); + }); + }); + + test('does not ask for videos when photos was refused', () async { + stubPermissions({.photos: .permanentlyDenied}); + + expect(await sut.requestGallery(), DevicePermissionStatus.permanentlyDenied); + verifyNever(() => mocks.permission.repo.request(.videos)); + }); + }); +} From 161e15ee2f18aeebad855d26cdf06941056fd42b Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Sat, 8 Aug 2026 12:48:33 -0400 Subject: [PATCH 10/49] fix(deployment): Add huggingface cache directory to Dockerfile (#30357) * Add huggingface cache directory to Dockerfile Updated Dockerfile to include huggingface cache directory. * Update Dockerfile environment variables and comments Removed MPLCONFIGDIR environment variable and adjusted comments. --- machine-learning/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index afbd72e99e..c8191e307b 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -160,8 +160,8 @@ ENV IMMICH_SOURCE_REF=${BUILD_SOURCE_REF} ENV IMMICH_SOURCE_COMMIT=${BUILD_SOURCE_COMMIT} ENV IMMICH_SOURCE_URL=https://github.com/immich-app/immich/commit/${BUILD_SOURCE_COMMIT} -# store matplotlib config in /cache subfolder to enable rootless deployment with a single volume -ENV MPLCONFIGDIR=/cache/matplotlib +# store huggingface in /cache subfolder to enable rootless deployment with a single volume +ENV HF_HOME=/cache/hf-cache ENTRYPOINT ["tini", "--"] CMD ["python", "-m", "immich_ml"] From 2314330c78b00d7bc67e3b613a194c27aeeb8212 Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Sun, 9 Aug 2026 00:09:14 +0530 Subject: [PATCH 11/49] fix: translation (#30549) --- i18n/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/en.json b/i18n/en.json index 857265f004..536b7677ac 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2186,7 +2186,7 @@ "upload_day_count": "{date}: {count, plural, one {# upload} other {# uploads}}", "upload_details": "Upload Details", "upload_error_with_count": "Upload error for {count, plural, one {# asset} other {# assets}}", - "upload_errors": "Upload completed with {count, plural, one {# error} other {# errors}}, refresh the page to see new upload assets.", + "upload_errors": "Upload completed with {count, plural, one {# error} other {# errors}}.", "upload_finished": "Upload finished", "upload_progress": "Remaining {remaining, number} - Processed {processed, number}/{total, number}", "upload_skipped_duplicates": "Skipped {count, plural, one {# duplicate asset} other {# duplicate assets}}", From 35c2a90fcacd98f3135319152aef27ef88ddaf46 Mon Sep 17 00:00:00 2001 From: Santo Shakil Date: Sun, 9 Aug 2026 23:54:55 +0600 Subject: [PATCH 12/49] fix(mobile): video playback controls dead for backed up videos opened from search (#30587) * key video player state by asset id instead of hero tag * expose id on BaseAsset instead of a separate playerKey --- .../domain/models/asset/base_asset.model.dart | 1 + .../models/asset/local_asset.model.dart | 1 + .../models/asset/remote_asset.model.dart | 1 + .../pages/drift_slideshow.page.dart | 14 +-- .../asset_viewer/bottom_bar.widget.dart | 2 +- .../asset_viewer/video_viewer.widget.dart | 4 +- .../asset_viewer/asset_viewer.provider.dart | 6 +- .../test/domain/models/base_asset_test.dart | 27 ++++++ .../widgets/asset_viewer/bottom_bar_test.dart | 85 +++++++++++++++++++ 9 files changed, 128 insertions(+), 13 deletions(-) create mode 100644 mobile/test/presentation/widgets/asset_viewer/bottom_bar_test.dart diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart index d7d74daa25..ac89fe9a0a 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -77,6 +77,7 @@ sealed class BaseAsset { // Overridden in subclasses AssetState get storage; + String get id; String? get localId; String? get remoteId; String get heroTag; diff --git a/mobile/lib/domain/models/asset/local_asset.model.dart b/mobile/lib/domain/models/asset/local_asset.model.dart index 0ad4c8f39b..15039761a8 100644 --- a/mobile/lib/domain/models/asset/local_asset.model.dart +++ b/mobile/lib/domain/models/asset/local_asset.model.dart @@ -1,6 +1,7 @@ part of 'base_asset.model.dart'; class LocalAsset extends BaseAsset { + @override final String id; final String? remoteAssetId; final String? cloudId; diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart index 387a817eab..5707329ba3 100644 --- a/mobile/lib/domain/models/asset/remote_asset.model.dart +++ b/mobile/lib/domain/models/asset/remote_asset.model.dart @@ -4,6 +4,7 @@ enum AssetVisibility { timeline, hidden, archive, locked } // Model for an asset stored in the server class RemoteAsset extends BaseAsset { + @override final String id; final String? localAssetId; final String? thumbHash; diff --git a/mobile/lib/presentation/pages/drift_slideshow.page.dart b/mobile/lib/presentation/pages/drift_slideshow.page.dart index e6c78796d3..48b29b3499 100644 --- a/mobile/lib/presentation/pages/drift_slideshow.page.dart +++ b/mobile/lib/presentation/pages/drift_slideshow.page.dart @@ -93,8 +93,8 @@ class _DriftSlideshowPageState extends ConsumerState with Si if (asset.isImage) { _createTimer(); - } else if (ref.read(videoPlayerProvider(asset.heroTag)).status == VideoPlaybackStatus.paused) { - unawaited(ref.read(videoPlayerProvider(asset.heroTag).notifier).play()); + } else if (ref.read(videoPlayerProvider(asset.id)).status == VideoPlaybackStatus.paused) { + unawaited(ref.read(videoPlayerProvider(asset.id).notifier).play()); } else { unawaited(_nextPage()); } @@ -113,7 +113,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si final asset = widget.timeline.getAssetSafe(_index)!; if (!asset.isImage) { - unawaited(ref.read(videoPlayerProvider(asset.heroTag).notifier).pause()); + unawaited(ref.read(videoPlayerProvider(asset.id).notifier).pause()); } setState(() { @@ -300,7 +300,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si borderRadius: BorderRadius.zero, minHeight: 5, value: - ref.read(videoPlayerProvider(asset.heroTag).select((s) => s.position)).inMilliseconds / + ref.read(videoPlayerProvider(asset.id).select((s) => s.position)).inMilliseconds / asset.duration.inMilliseconds, ); } @@ -374,13 +374,13 @@ class _DriftSlideshowPageState extends ConsumerState with Si builder: (context, value, _) => buildPhotoView(scale * (1.0 + value * _kenBurnsZoom)), ); } else { - final status = ref.read(videoPlayerProvider(asset.heroTag).select((s) => s.status)); - final position = ref.read(videoPlayerProvider(asset.heroTag)).position; + final status = ref.read(videoPlayerProvider(asset.id).select((s) => s.status)); + final position = ref.read(videoPlayerProvider(asset.id)).position; if (status == VideoPlaybackStatus.completed && isCurrent && position.inMicroseconds > 0) { unawaited(_nextPage()); } else if (status == VideoPlaybackStatus.playing) { - unawaited(ref.read(videoPlayerProvider(asset.heroTag).notifier).setLoop(false)); + unawaited(ref.read(videoPlayerProvider(asset.id).notifier).setLoop(false)); } return PhotoView.customChild( diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index 4ff839881a..15b90e58ea 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -94,7 +94,7 @@ class ViewerBottomBar extends ConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ if (asset.isImage) OcrToggleButton(asset: asset), - if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag), + if (asset.isVideo) VideoControls(videoPlayerName: asset.id), if (!isReadonlyModeEnabled) ImmichColorOverride( color: Colors.white, diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart index 8b3d8a9978..9011888b54 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -47,7 +47,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg bool _isVideoReady = false; bool _shouldPlayOnForeground = true; - VideoPlayerNotifier get _notifier => ref.read(videoPlayerProvider(widget.asset.heroTag).notifier); + VideoPlayerNotifier get _notifier => ref.read(videoPlayerProvider(widget.asset.id).notifier); @override void initState() { @@ -304,7 +304,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg @override Widget build(BuildContext context) { final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - final status = ref.watch(videoPlayerProvider(widget.asset.heroTag).select((v) => v.status)); + final status = ref.watch(videoPlayerProvider(widget.asset.id).select((v) => v.status)); return IgnorePointer( child: Stack( diff --git a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart index fd2a6aebd9..ac929d2b6a 100644 --- a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart +++ b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart @@ -67,9 +67,9 @@ class AssetViewerStateNotifier extends Notifier { } state = state.copyWith(showingDetails: showing, showingControls: showing ? true : state.showingControls); - final heroTag = state.currentAsset?.heroTag; - if (heroTag != null) { - final notifier = ref.read(videoPlayerProvider(heroTag).notifier); + final id = state.currentAsset?.id; + if (id != null) { + final notifier = ref.read(videoPlayerProvider(id).notifier); showing ? notifier.hold() : notifier.release(); } } diff --git a/mobile/test/domain/models/base_asset_test.dart b/mobile/test/domain/models/base_asset_test.dart index 4c2eb3ca38..6a6c836aa7 100644 --- a/mobile/test/domain/models/base_asset_test.dart +++ b/mobile/test/domain/models/base_asset_test.dart @@ -43,4 +43,31 @@ void main() { expect(localOnly.refersToSameAsset(remoteOnly), isTrue); }); }); + + group('BaseAsset.id', () { + test('survives the DB copy filling localId', () { + final searchCopy = RemoteAssetFactory.create(id: 'asset-1'); + final mergedCopy = searchCopy.copyWith(localId: 'local-1'); + + expect(searchCopy.heroTag, isNot(mergedCopy.heroTag)); + expect(mergedCopy.id, searchCopy.id); + }); + + test('survives a local asset gaining a remote id', () { + final localOnly = LocalAssetFactory.create(id: 'local-1'); + final uploaded = localOnly.copyWith(remoteId: 'asset-1'); + + expect(localOnly.heroTag, isNot(uploaded.heroTag)); + expect(uploaded.id, localOnly.id); + }); + + test('differs between assets', () { + final a = RemoteAssetFactory.create(id: 'asset-1'); + final b = RemoteAssetFactory.create(id: 'asset-2'); + final local = LocalAssetFactory.create(id: 'local-1'); + + expect(a.id, isNot(b.id)); + expect(local.id, isNot(a.id)); + }); + }); } diff --git a/mobile/test/presentation/widgets/asset_viewer/bottom_bar_test.dart b/mobile/test/presentation/widgets/asset_viewer/bottom_bar_test.dart new file mode 100644 index 0000000000..f2463c090d --- /dev/null +++ b/mobile/test/presentation/widgets/asset_viewer/bottom_bar_test.dart @@ -0,0 +1,85 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/services/gcast.service.dart'; +import 'package:immich_mobile/utils/asset_filter.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:native_video_player/native_video_player.dart'; + +import '../../../service.mocks.dart'; +import '../../../unit/factories/remote_asset_factory.dart'; +import '../../../widget_tester_extensions.dart'; + +class MockNativeVideoPlayerController extends Mock implements NativeVideoPlayerController {} + +class MockTimelineService extends Mock implements TimelineService {} + +class TestReadOnlyModeNotifier extends ReadOnlyModeNotifier { + @override + bool build() => true; +} + +void main() { + testWidgets('player stays bound after local id arrives', (tester) async { + final searchCopy = RemoteAssetFactory.create(type: .video); + final mergedCopy = searchCopy.copyWith(localId: 'local-1', isFavorite: true); + final assetService = MockAssetService(); + final controller = MockNativeVideoPlayerController(); + final timeline = MockTimelineService(); + final updates = StreamController(sync: true); + when(() => assetService.watchAsset(searchCopy)).thenAnswer((_) => updates.stream); + when(controller.play).thenAnswer((_) async {}); + when(controller.pause).thenAnswer((_) async {}); + when(() => timeline.origin).thenReturn(.search); + + late WidgetRef ref; + + await tester.pumpConsumerWidget( + Consumer( + builder: (context, widgetRef, _) { + ref = widgetRef; + return const ViewerBottomBar(); + }, + ), + overrides: [ + assetServiceProvider.overrideWithValue(assetService), + assetsActionProvider(ActionSource.viewer).overrideWithValue(const AssetFilter({})), + gCastServiceProvider.overrideWithValue(MockGCastService()), + inLockedViewProvider.overrideWithValue(true), + ownedAssetsActionProvider(ActionSource.viewer).overrideWithValue(const AssetFilter({})), + readonlyModeProvider.overrideWith(TestReadOnlyModeNotifier.new), + timelineServiceProvider.overrideWithValue(timeline), + ], + ); + + final viewer = ref.read(assetViewerProvider.notifier); + viewer.setAsset(searchCopy); + await tester.pump(); + ref.read(videoPlayerProvider(searchCopy.id).notifier).attachController(controller); + updates.add(mergedCopy); + await tester.pump(); + await tester.tap(find.byType(IconButton)); + await tester.pump(); + + verify(controller.play).called(1); + viewer.setShowingDetails(true); + await tester.pump(); + verify(controller.pause).called(1); + + await tester.pumpWidget(const SizedBox.shrink()); + unawaited(updates.close()); + }); +} From c52bf9995bfde09d4b41d04f27ab53f470c1af02 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:28:15 +0530 Subject: [PATCH 13/49] chore: pump flutter to 3.44.9 (#30658) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/mise.lock | 18 +++++++++--------- mobile/mise.toml | 2 +- mobile/pubspec.lock | 2 +- mobile/pubspec.yaml | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/mobile/mise.lock b/mobile/mise.lock index 8f2f9572fb..cd3ac25848 100644 --- a/mobile/mise.lock +++ b/mobile/mise.lock @@ -1,30 +1,30 @@ # @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html [[tools."aqua:flutter/flutter"]] -version = "3.44.8" +version = "3.44.9" backend = "aqua:flutter/flutter" [tools."aqua:flutter/flutter"."platforms.linux-arm64"] -url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz" +url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.9-stable.tar.xz" [tools."aqua:flutter/flutter"."platforms.linux-arm64-musl"] -url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz" +url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.9-stable.tar.xz" [tools."aqua:flutter/flutter"."platforms.linux-x64"] -url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz" +url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.9-stable.tar.xz" [tools."aqua:flutter/flutter"."platforms.linux-x64-musl"] -url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz" +url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.9-stable.tar.xz" [tools."aqua:flutter/flutter"."platforms.macos-arm64"] -checksum = "blake3:79c780876c64f5a66c015845f544bdb20275f7f3bf819e6c831f23df3e96b8db" -url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.8-stable.zip" +checksum = "blake3:e5c14d6b5e917a8578327d2564ab78ece83cd685c74c29572ddaf7655f42205d" +url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.9-stable.zip" [tools."aqua:flutter/flutter"."platforms.macos-x64"] -url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.8-stable.zip" +url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.9-stable.zip" [tools."aqua:flutter/flutter"."platforms.windows-x64"] -url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.8-stable.zip" +url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.9-stable.zip" [[tools."github:CQLabs/homebrew-dcm"]] version = "1.37.0" diff --git a/mobile/mise.toml b/mobile/mise.toml index 7caf6dfbd5..08a26c0c3b 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -1,5 +1,5 @@ [tools] -"aqua:flutter/flutter" = "3.44.8" +"aqua:flutter/flutter" = "3.44.9" java = "21.0.2" [tools."github:CQLabs/homebrew-dcm"] diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index f02df0b622..ac19481079 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -2030,4 +2030,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.12.0 <4.0.0" - flutter: "3.44.8" + flutter: "3.44.9" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index c2848e6dd8..32e67de4f6 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -6,7 +6,7 @@ version: 3.1.0+3057 environment: sdk: '>=3.12.0 <4.0.0' - flutter: 3.44.8 + flutter: 3.44.9 dependencies: async: ^2.13.1 From 9862e50aab90f47d5c2d6daafef5c381a8fbc3c6 Mon Sep 17 00:00:00 2001 From: Santo Shakil Date: Mon, 10 Aug 2026 21:14:23 +0600 Subject: [PATCH 14/49] fix(mobile): decode remote thumbnails at displayed size (#29965) * fix(mobile): decode remote thumbnails at displayed size * use nullable decode size and reuse the tapped thumbnail in the viewer * clean up decode size naming and guards --- .../alextran/immich/images/LocalImagesImpl.kt | 22 +++- .../immich/images/RemoteImagesImpl.kt | 13 +- .../ios/Runner/Images/RemoteImagesImpl.swift | 52 ++++++-- .../infrastructure/loaders/image_request.dart | 27 +++- .../loaders/remote_image_request.dart | 27 +++- .../asset_viewer/asset_page.widget.dart | 13 +- .../widgets/asset_viewer/asset_preloader.dart | 12 +- .../asset_viewer/asset_viewer.page.dart | 19 +-- .../widgets/images/image_provider.dart | 15 ++- .../widgets/images/remote_image_provider.dart | 34 +++-- .../widgets/images/thumbnail.widget.dart | 13 +- .../widgets/images/thumbnail_tile.widget.dart | 6 +- .../widgets/timeline/constants.dart | 2 +- .../widgets/timeline/fixed/segment.model.dart | 46 ++++--- .../asset_viewer/asset_viewer.provider.dart | 14 ++- mobile/pigeon/remote_image_api.dart | 9 +- .../loaders/remote_image_request_test.dart | 117 ++++++++++++++++++ .../asset_viewer_provider_test.dart | 16 +++ 18 files changed, 386 insertions(+), 71 deletions(-) create mode 100644 mobile/test/infrastructure/loaders/remote_image_request_test.dart diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImagesImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImagesImpl.kt index 7b49d8ca67..af953612cf 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImagesImpl.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImagesImpl.kt @@ -33,12 +33,28 @@ data class Request( val callback: (Result?>) -> Unit ) +/** Set [exactSize] to decode at the smallest size that covers [target] without upscaling. */ @RequiresApi(Build.VERSION_CODES.Q) -inline fun ImageDecoder.Source.decodeBitmap(target: Size = Size(0, 0)): Bitmap { +inline fun ImageDecoder.Source.decodeBitmap(target: Size = Size(0, 0), exactSize: Boolean = false): Bitmap { return ImageDecoder.decodeBitmap(this) { decoder, info, _ -> if (target.width > 0 && target.height > 0) { - val sample = max(1, min(info.size.width / target.width, info.size.height / target.height)) - decoder.setTargetSampleSize(sample) + if (exactSize) { + val fillScale = max( + target.width.toDouble() / info.size.width, + target.height.toDouble() / info.size.height + ) + val scale = min(1.0, fillScale) + if (scale < 1) { + val width = ceil(info.size.width * scale).toInt() + val height = ceil(info.size.height * scale).toInt() + if (width > 0 && height > 0) { + decoder.setTargetSize(width, height) + } + } + } else { + val sample = max(1, min(info.size.width / target.width, info.size.height / target.height)) + decoder.setTargetSampleSize(sample) + } } decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE decoder.setTargetColorSpace(ColorSpace.get(ColorSpace.Named.SRGB)) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt index 1623c9cb09..cdc82c871c 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt @@ -5,6 +5,7 @@ import android.graphics.ImageDecoder import android.os.Build import android.os.CancellationSignal import android.os.OperationCanceledException +import android.util.Size import androidx.exifinterface.media.ExifInterface import app.alextran.immich.INITIAL_BUFFER_SIZE import app.alextran.immich.NativeBuffer @@ -79,6 +80,8 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi { url: String, requestId: Long, preferEncoded: Boolean, + width: Long?, + height: Long?, callback: (Result?>) -> Unit ) { val signal = CancellationSignal() @@ -102,13 +105,21 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi { if (!preferEncoded && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { decodeExecutor.execute { val res = if (signal.isCanceled) null else try { - val bitmap = ImageDecoder.createSource(NativeBuffer.wrap(buffer.pointer, buffer.offset)).decodeBitmap() // The embedded preview a raw decodes to has no orientation, so read the container's. val orientation = if (isRawMime(contentType)) { readRawOrientation(NativeBuffer.wrap(buffer.pointer, buffer.offset), buffer.offset) } else { ExifInterface.ORIENTATION_NORMAL } + val target = when (orientation) { + ExifInterface.ORIENTATION_TRANSPOSE, + ExifInterface.ORIENTATION_ROTATE_90, + ExifInterface.ORIENTATION_TRANSVERSE, + ExifInterface.ORIENTATION_ROTATE_270 -> Size(height?.toInt() ?: 0, width?.toInt() ?: 0) + else -> Size(width?.toInt() ?: 0, height?.toInt() ?: 0) + } + val source = ImageDecoder.createSource(NativeBuffer.wrap(buffer.pointer, buffer.offset)) + val bitmap = source.decodeBitmap(target, exactSize = true) if (orientation == ExifInterface.ORIENTATION_NORMAL || orientation == ExifInterface.ORIENTATION_UNDEFINED) { bitmap.toNativeBuffer() } else { diff --git a/mobile/ios/Runner/Images/RemoteImagesImpl.swift b/mobile/ios/Runner/Images/RemoteImagesImpl.swift index de1f6dec89..b4b70f0b30 100644 --- a/mobile/ios/Runner/Images/RemoteImagesImpl.swift +++ b/mobile/ios/Runner/Images/RemoteImagesImpl.swift @@ -1,5 +1,6 @@ import Accelerate import Flutter +import ImageIO import MobileCoreServices import Photos @@ -27,21 +28,21 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi { bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue), renderingIntent: .perceptual )! - private static let decodeOptions = [ + private static let decodeOptions: [NSString: Bool] = [ kCGImageSourceShouldCache: false, kCGImageSourceShouldCacheImmediately: true, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceCreateThumbnailFromImageAlways: true - ] as CFDictionary + ] - func requestImage(url: String, requestId: Int64, preferEncoded: Bool, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) { + func requestImage(url: String, requestId: Int64, preferEncoded: Bool, width: Int64?, height: Int64?, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) { var urlRequest = URLRequest(url: URL(string: url)!) urlRequest.cachePolicy = .returnCacheDataElseLoad let request = RemoteImageRequest(id: requestId, completion: completion) let task = URLSessionManager.shared.session.dataTask(with: urlRequest) { data, response, error in - Self.handleCompletion(request: request, encoded: preferEncoded, data: data, response: response, error: error) + Self.handleCompletion(request: request, encoded: preferEncoded, width: width, height: height, data: data, response: response, error: error) } request.task = task @@ -49,7 +50,7 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi { task.resume() } - private static func handleCompletion(request: RemoteImageRequest, encoded: Bool, data: Data?, response: URLResponse?, error: Error?) { + private static func handleCompletion(request: RemoteImageRequest, encoded: Bool, width: Int64?, height: Int64?, data: Data?, response: URLResponse?, error: Error?) { if request.isCancelled { return request.completion(ImageProcessing.cancelledResult) } @@ -87,8 +88,17 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi { return request.completion(ImageProcessing.cancelledResult) } - guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil), - let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions) else { + guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else { + registry.remove(requestId: request.id) + return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil))) + } + + var options: [NSString: Any] = decodeOptions + if let maxPixelSize = targetThumbnailRenderSize(imageSource: imageSource, width: width, height: height) { + options[kCGImageSourceThumbnailMaxPixelSize] = maxPixelSize + } + + guard let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else { registry.remove(requestId: request.id) return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil))) } @@ -120,6 +130,34 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi { } } + /// Returns the longest rendered edge needed to cover the requested size. + private static func targetThumbnailRenderSize(imageSource: CGImageSource, width: Int64?, height: Int64?) -> Int? { + guard let width, + let height, + width > 0, + height > 0, + let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any], + let pixelWidth = properties[kCGImagePropertyPixelWidth] as? NSNumber, + let pixelHeight = properties[kCGImagePropertyPixelHeight] as? NSNumber else { + return nil + } + + let orientation = (properties[kCGImagePropertyOrientation] as? NSNumber) + .flatMap { CGImagePropertyOrientation(rawValue: $0.uint32Value) } ?? .up + let swapsDimensions: Bool + switch orientation { + case .leftMirrored, .right, .rightMirrored, .left: + swapsDimensions = true + default: + swapsDimensions = false + } + let sourceWidth = swapsDimensions ? pixelHeight.doubleValue : pixelWidth.doubleValue + let sourceHeight = swapsDimensions ? pixelWidth.doubleValue : pixelHeight.doubleValue + let fillScale = max(Double(width) / sourceWidth, Double(height) / sourceHeight) + let scale = min(1, fillScale) + return scale < 1 ? Int(ceil(max(sourceWidth * scale, sourceHeight * scale))) : nil + } + func cancelRequest(requestId: Int64) { Self.registry.remove(requestId: requestId)?.cancel() } diff --git a/mobile/lib/infrastructure/loaders/image_request.dart b/mobile/lib/infrastructure/loaders/image_request.dart index 8b7cdc7621..6b2be218dc 100644 --- a/mobile/lib/infrastructure/loaders/image_request.dart +++ b/mobile/lib/infrastructure/loaders/image_request.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:ffi'; +import 'dart:math' as math; import 'dart:ui' as ui; import 'package:ffi/ffi.dart'; @@ -36,7 +37,11 @@ abstract class ImageRequest { void _onCancelled(); - Future<(ui.Codec, ui.ImageDescriptor)?> _codecFromEncodedPlatformImage(int address, int length) async { + Future<(ui.Codec, ui.ImageDescriptor)?> _codecFromEncodedPlatformImage( + int address, + int length, { + ui.Size? decodeSize, + }) async { final pointer = Pointer.fromAddress(address); if (_isCancelled) { malloc.free(pointer); @@ -62,7 +67,8 @@ abstract class ImageRequest { return null; } - final codec = await descriptor.instantiateCodec(); + final target = _targetSize(descriptor.width, descriptor.height, decodeSize); + final codec = await descriptor.instantiateCodec(targetWidth: target?.$1, targetHeight: target?.$2); if (_isCancelled) { descriptor.dispose(); codec.dispose(); @@ -72,8 +78,8 @@ abstract class ImageRequest { return (codec, descriptor); } - Future _fromEncodedPlatformImage(int address, int length) async { - final result = await _codecFromEncodedPlatformImage(address, length); + Future _fromEncodedPlatformImage(int address, int length, {ui.Size? decodeSize}) async { + final result = await _codecFromEncodedPlatformImage(address, length, decodeSize: decodeSize); if (result == null) { return null; } @@ -96,6 +102,19 @@ abstract class ImageRequest { return frame; } + (int, int)? _targetSize(int width, int height, ui.Size? decodeSize) { + if (width <= 0 || height <= 0 || decodeSize == null || decodeSize.width <= 0 || decodeSize.height <= 0) { + return null; + } + + final scale = math.max(decodeSize.width / width, decodeSize.height / height); + if (scale >= 1) { + return null; + } + + return ((width * scale).ceil(), (height * scale).ceil()); + } + Future _fromDecodedPlatformImage(int address, int width, int height, int rowBytes) async { final pointer = Pointer.fromAddress(address); if (_isCancelled) { diff --git a/mobile/lib/infrastructure/loaders/remote_image_request.dart b/mobile/lib/infrastructure/loaders/remote_image_request.dart index d6a25753fb..7b504697ac 100644 --- a/mobile/lib/infrastructure/loaders/remote_image_request.dart +++ b/mobile/lib/infrastructure/loaders/remote_image_request.dart @@ -3,7 +3,10 @@ part of 'image_request.dart'; class RemoteImageRequest extends ImageRequest { final String uri; - RemoteImageRequest({required this.uri}); + /// Physical size to decode, or null for the source size. + final ui.Size? decodeSize; + + RemoteImageRequest({required this.uri, this.decodeSize}); @override Future load(ImageDecoderCallback decode, {double scale = 1.0}) async { @@ -11,10 +14,20 @@ class RemoteImageRequest extends ImageRequest { return null; } - final info = await remoteImageApi.requestImage(uri, requestId: requestId, preferEncoded: false); + final info = await remoteImageApi.requestImage( + uri, + requestId: requestId, + preferEncoded: false, + width: decodeSize?.width.ceil(), + height: decodeSize?.height.ceil(), + ); // Android falls back to encoded data if native decoding fails, so check for both shapes of the response. final frame = switch (info) { - {'pointer': final int pointer, 'length': final int length} => await _fromEncodedPlatformImage(pointer, length), + {'pointer': final int pointer, 'length': final int length} => await _fromEncodedPlatformImage( + pointer, + length, + decodeSize: decodeSize, + ), { 'pointer': final int pointer, 'width': final int width, @@ -33,7 +46,13 @@ class RemoteImageRequest extends ImageRequest { return null; } - final info = await remoteImageApi.requestImage(uri, requestId: requestId, preferEncoded: true); + final info = await remoteImageApi.requestImage( + uri, + requestId: requestId, + preferEncoded: true, + width: null, + height: null, + ); if (info == null) { return null; } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart index c887f8e65a..7556505158 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -333,9 +333,15 @@ class _AssetPageState extends ConsumerState { required bool isCurrent, required bool isPlayingMotionVideo, required String? localFilePath, + required Size? remoteThumbnailSize, }) { final size = context.sizeData; - final imageProvider = getFullImageProvider(asset, size: size, localFilePath: localFilePath); + final imageProvider = getFullImageProvider( + asset, + size: size, + localFilePath: localFilePath, + remoteThumbnailSize: remoteThumbnailSize, + ); if (asset.isImage && !isPlayingMotionVideo) { return PhotoView( @@ -397,7 +403,9 @@ class _AssetPageState extends ConsumerState { @override Widget build(BuildContext context) { - final currentAsset = ref.watch(assetViewerProvider.select((s) => s.currentAsset)); + final (currentAsset, thumbnailSize) = ref.watch( + assetViewerProvider.select((s) => (s.currentAsset, s.thumbnailSize)), + ); _showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); final stackIndex = ref.watch(assetViewerProvider.select((s) => s.stackIndex)); final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider); @@ -454,6 +462,7 @@ class _AssetPageState extends ConsumerState { isCurrent: isCurrent, isPlayingMotionVideo: isPlayingMotionVideo, localFilePath: viewIntentFilePath, + remoteThumbnailSize: thumbnailSize, ), ), if (showingOcr && displayAsset.width != null && displayAsset.height != null) diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_preloader.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_preloader.dart index 4d1856b90d..8c153e9995 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_preloader.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_preloader.dart @@ -17,7 +17,8 @@ class AssetPreloader { AssetPreloader({required this.timelineService, required this.mounted}); - void preload(int index, Size size) { + /// Preloads adjacent images with the current thumbnail size. + void preload(int index, Size size, {Size? thumbnailSize}) { unawaited(timelineService.preloadAssets(index)); _timer?.cancel(); _timer = Timer(Durations.medium4, () async { @@ -33,13 +34,14 @@ class AssetPreloader { } _prevStream?.removeListener(_dummyListener); _nextStream?.removeListener(_dummyListener); - _prevStream = prev != null ? _resolveImage(prev, size) : null; - _nextStream = next != null ? _resolveImage(next, size) : null; + _prevStream = prev != null ? _resolveImage(prev, size, thumbnailSize) : null; + _nextStream = next != null ? _resolveImage(next, size, thumbnailSize) : null; }); } - ImageStream _resolveImage(BaseAsset asset, Size size) { - return getFullImageProvider(asset, size: size).resolve(ImageConfiguration.empty)..addListener(_dummyListener); + ImageStream _resolveImage(BaseAsset asset, Size size, Size? thumbnailSize) { + return getFullImageProvider(asset, size: size, remoteThumbnailSize: thumbnailSize).resolve(ImageConfiguration.empty) + ..addListener(_dummyListener); } void dispose() { diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index 23cc6119fb..731d8f3481 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -65,7 +65,8 @@ class AssetViewer extends ConsumerStatefulWidget { @override ConsumerState createState() => _AssetViewerState(); - static void setAsset(WidgetRef ref, BaseAsset asset) { + /// Sets the asset and thumbnail size before opening the viewer. + static void setAsset(WidgetRef ref, BaseAsset asset, {Size? thumbnailSize}) { ref.read(assetViewerProvider.notifier).reset(); // Hide controls by default for videos @@ -73,11 +74,7 @@ class AssetViewer extends ConsumerStatefulWidget { ref.read(assetViewerProvider.notifier).setControls(false); } - _setAsset(ref, asset); - } - - static void _setAsset(WidgetRef ref, BaseAsset asset) { - ref.read(assetViewerProvider.notifier).setAsset(asset); + ref.read(assetViewerProvider.notifier).setAsset(asset, thumbnailSize: thumbnailSize); } } @@ -163,7 +160,11 @@ class _AssetViewerState extends ConsumerState { } void _onAssetInit(Duration timeStamp) { - _preloader.preload(widget.initialIndex, context.sizeData); + _preloader.preload( + widget.initialIndex, + context.sizeData, + thumbnailSize: ref.read(assetViewerProvider).thumbnailSize, + ); _handleCasting(); } @@ -181,8 +182,8 @@ class _AssetViewerState extends ConsumerState { return; } - AssetViewer._setAsset(ref, asset); - _preloader.preload(index, context.sizeData); + ref.read(assetViewerProvider.notifier).setAsset(asset); + _preloader.preload(index, context.sizeData, thumbnailSize: ref.read(assetViewerProvider).thumbnailSize); _handleCasting(); _stackChildrenKeepAlive?.close(); _stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive(); diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index aaf3e0dbaf..bdedd29d45 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -155,6 +155,7 @@ ImageProvider getFullImageProvider( Size size = const Size(1080, 1920), bool edited = true, String? localFilePath, + Size? remoteThumbnailSize, }) { // Create new provider and cache it final ImageProvider provider; @@ -189,13 +190,21 @@ ImageProvider getFullImageProvider( assetType: asset.type, isAnimated: asset.isAnimatedImage, edited: edited, + thumbnailSize: remoteThumbnailSize, ); } return provider; } -ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution, bool edited = true}) { +ImageProvider? getThumbnailImageProvider( + BaseAsset asset, { + Size size = kThumbnailResolution, + + /// Physical size to decode for remote thumbnails, or null for the source size. + Size? remoteSize, + bool edited = true, +}) { if (_shouldUseLocalAsset(asset)) { final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; return LocalThumbProvider(id: id, size: size, assetType: asset.type, checksum: asset.checksum); @@ -203,7 +212,9 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId; final thumbhash = asset is RemoteAsset ? asset.thumbHash ?? "" : ""; - return assetId != null ? RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: thumbhash, edited: edited) : null; + return assetId != null + ? RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: thumbhash, edited: edited, decodeSize: remoteSize) + : null; } bool _shouldUseLocalAsset(BaseAsset asset) => diff --git a/mobile/lib/presentation/widgets/images/remote_image_provider.dart b/mobile/lib/presentation/widgets/images/remote_image_provider.dart index 9618ee72ad..b533fd8cf5 100644 --- a/mobile/lib/presentation/widgets/images/remote_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/remote_image_provider.dart @@ -14,10 +14,17 @@ class RemoteImageProvider extends CancellableImageProvider final String url; final bool edited; - RemoteImageProvider({required this.url, this.edited = true}); + /// Physical size to decode, or null for the source size. + final Size? decodeSize; - RemoteImageProvider.thumbnail({required String assetId, required String thumbhash, this.edited = true}) - : url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash, edited: edited); + RemoteImageProvider({required this.url, this.edited = true, this.decodeSize}); + + RemoteImageProvider.thumbnail({ + required String assetId, + required String thumbhash, + this.edited = true, + this.decodeSize, + }) : url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash, edited: edited); @override Future obtainKey(ImageConfiguration configuration) { @@ -37,7 +44,7 @@ class RemoteImageProvider extends CancellableImageProvider } Stream _codec(RemoteImageProvider key, ImageDecoderCallback decode) { - final request = this.request = RemoteImageRequest(uri: key.url); + final request = this.request = RemoteImageRequest(uri: key.url, decodeSize: key.decodeSize); return loadRequest(request, decode, isFinal: true); } @@ -47,13 +54,13 @@ class RemoteImageProvider extends CancellableImageProvider return true; } if (other is RemoteImageProvider) { - return url == other.url && edited == other.edited; + return url == other.url && edited == other.edited && decodeSize == other.decodeSize; } return false; } @override - int get hashCode => url.hashCode ^ edited.hashCode; + int get hashCode => url.hashCode ^ edited.hashCode ^ decodeSize.hashCode; } class RemoteFullImageProvider extends CancellableImageProvider @@ -64,12 +71,16 @@ class RemoteFullImageProvider extends CancellableImageProvider [ DiagnosticsProperty('Image provider', this), DiagnosticsProperty('Asset Id', key.assetId), @@ -96,7 +109,12 @@ class RemoteFullImageProvider extends CancellableImageProvider [ DiagnosticsProperty('Image provider', this), diff --git a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart index 90bb79cced..5bfeb25977 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart @@ -26,17 +26,22 @@ class Thumbnail extends StatefulWidget { required String remoteId, required String thumbhash, this.fit = BoxFit.cover, - Size size = kThumbnailResolution, + + /// Physical size to decode, or null for the source size. + Size? decodeSize, super.key, - }) : imageProvider = RemoteImageProvider.thumbnail(assetId: remoteId, thumbhash: thumbhash), + }) : imageProvider = RemoteImageProvider.thumbnail(assetId: remoteId, thumbhash: thumbhash, decodeSize: decodeSize), thumbhashProvider = null; Thumbnail.fromAsset({ required BaseAsset? asset, this.fit = BoxFit.cover, - /// The logical UI size of the thumbnail. This is only used to determine the ideal image resolution and does not affect the widget size. + /// Decode size for local thumbnails. This does not affect the widget size. Size size = kThumbnailResolution, + + /// Physical size to decode for remote thumbnails. + Size? remoteSize, super.key, }) : thumbhashProvider = switch (asset) { RemoteAsset() when asset.thumbHash != null && asset.localId == null => ThumbHashProvider( @@ -44,7 +49,7 @@ class Thumbnail extends StatefulWidget { ), _ => null, }, - imageProvider = asset == null ? null : getThumbnailImageProvider(asset, size: size); + imageProvider = asset == null ? null : getThumbnailImageProvider(asset, size: size, remoteSize: remoteSize); @override State createState() => _ThumbnailState(); diff --git a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart index 1d3dcb2cf0..953efc144d 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart @@ -16,6 +16,7 @@ class ThumbnailTile extends ConsumerStatefulWidget { const ThumbnailTile( this.asset, { this.size = kThumbnailResolution, + this.remoteSize, this.fit = BoxFit.cover, this.showStorageIndicator = false, this.lockSelection = false, @@ -26,6 +27,9 @@ class ThumbnailTile extends ConsumerStatefulWidget { final BaseAsset? asset; final Size size; + + /// Physical size to decode for remote thumbnails. + final Size? remoteSize; final BoxFit fit; final bool showStorageIndicator; final bool lockSelection; @@ -108,7 +112,7 @@ class _ThumbnailTileState extends ConsumerState { // but other solutions have failed thus far. key: ValueKey(isCurrentAsset), tag: '${asset?.heroTag}_$heroIndex', - child: Thumbnail.fromAsset(asset: asset, size: widget.size), + child: Thumbnail.fromAsset(asset: asset, size: widget.size, remoteSize: widget.remoteSize), // Placeholderbuilder used to hide indicators on first hero animation, since flightShuttleBuilder isn't called until both source and destination hero exist in widget tree. placeholderBuilder: (context, heroSize, child) { if (!_hideIndicators) { diff --git a/mobile/lib/presentation/widgets/timeline/constants.dart b/mobile/lib/presentation/widgets/timeline/constants.dart index 84892c79f7..86c44d4a7f 100644 --- a/mobile/lib/presentation/widgets/timeline/constants.dart +++ b/mobile/lib/presentation/widgets/timeline/constants.dart @@ -9,5 +9,5 @@ const double kScrubberThumbHeight = 48.0; const Duration kTimelineScrubberFadeInDuration = Duration(milliseconds: 300); const Duration kTimelineScrubberFadeOutDuration = Duration(milliseconds: 800); -const Size kThumbnailResolution = Size.square(320); // TODO: make the resolution vary based on actual tile size +const Size kThumbnailResolution = Size.square(320); const kThumbnailDiskCacheSize = 1024 << 20; // 1GiB diff --git a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart index b108acde3b..32bbffa95e 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart @@ -144,19 +144,6 @@ class _FixedSegmentRow extends ConsumerWidget { TimelineService timelineService, bool isDynamicLayout, ) { - final children = [ - for (int i = 0; i < assets.length; i++) - TimelineAssetIndexWrapper( - assetIndex: assetIndex + i, - segmentIndex: 0, // For simplicity, using 0 for now - child: _AssetTileWidget( - key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)), - asset: assets[i], - assetIndex: assetIndex + i, - ), - ), - ]; - final widths = List.filled(assets.length, tileHeight); if (isDynamicLayout) { @@ -186,6 +173,20 @@ class _FixedSegmentRow extends ConsumerWidget { } } + final children = [ + for (int i = 0; i < assets.length; i++) + TimelineAssetIndexWrapper( + assetIndex: assetIndex + i, + segmentIndex: 0, // For simplicity, using 0 for now + child: _AssetTileWidget( + key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)), + asset: assets[i], + assetIndex: assetIndex + i, + size: Size(widths[i], tileHeight), + ), + ), + ]; + return TimelineDragRegion( child: TimelineRow( height: tileHeight, @@ -201,10 +202,18 @@ class _FixedSegmentRow extends ConsumerWidget { class _AssetTileWidget extends ConsumerWidget { final BaseAsset asset; final int assetIndex; + final Size size; - const _AssetTileWidget({super.key, required this.asset, required this.assetIndex}); + const _AssetTileWidget({super.key, required this.asset, required this.assetIndex, required this.size}); - Future _handleOnTap(BuildContext ctx, WidgetRef ref, int assetIndex, BaseAsset asset, int? heroOffset) async { + Future _handleOnTap( + BuildContext ctx, + WidgetRef ref, + int assetIndex, + BaseAsset asset, + int? heroOffset, + Size remoteSize, + ) async { final multiSelectState = ref.read(multiSelectProvider); if (multiSelectState.forceEnable || multiSelectState.isEnabled) { @@ -216,7 +225,7 @@ class _AssetTileWidget extends ConsumerWidget { } ref.read(isPlayingMotionVideoProvider.notifier).playing = false; - AssetViewer.setAsset(ref, asset); + AssetViewer.setAsset(ref, asset, thumbnailSize: remoteSize); unawaited( ctx.pushRoute( AssetViewerRoute( @@ -256,6 +265,8 @@ class _AssetTileWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final remoteSize = size * MediaQuery.devicePixelRatioOf(context); + final heroOffset = TabsRouterScope.of(context)?.controller.activeIndex ?? 0; final lockSelection = _getLockSelectionStatus(ref); @@ -265,10 +276,11 @@ class _AssetTileWidget extends ConsumerWidget { return RepaintBoundary( child: GestureDetector( - onTap: () => lockSelection ? null : _handleOnTap(context, ref, assetIndex, asset, heroOffset), + onTap: () => lockSelection ? null : _handleOnTap(context, ref, assetIndex, asset, heroOffset, remoteSize), onLongPress: () => lockSelection || isReadonlyModeEnabled ? null : _handleOnLongPress(ref, asset), child: ThumbnailTile( asset, + remoteSize: remoteSize, lockSelection: lockSelection, showStorageIndicator: showStorageIndicator, showStackIndicator: showStackIndicator, diff --git a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart index ac929d2b6a..fd7fbebe36 100644 --- a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart +++ b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -18,6 +19,9 @@ abstract class AssetViewerState with _$AssetViewerState { @Default(false) bool isZoomed, @Default(false) bool showingOcr, BaseAsset? currentAsset, + + /// Physical thumbnail size retained while paging through the viewer. + Size? thumbnailSize, @Default(0) int stackIndex, }) = _AssetViewerState; } @@ -37,11 +41,17 @@ class AssetViewerStateNotifier extends Notifier { state = const AssetViewerState(); } - void setAsset(BaseAsset asset) { + void setAsset(BaseAsset asset, {Size? thumbnailSize}) { if (asset == state.currentAsset) { return; } - state = state.copyWith(currentAsset: asset, stackIndex: 0, showingOcr: false); + // Swiping to a neighbor passes no size; keep the tapped tile's so neighbors reuse it. + state = state.copyWith( + currentAsset: asset, + thumbnailSize: thumbnailSize ?? state.thumbnailSize, + stackIndex: 0, + showingOcr: false, + ); _watchCurrentAsset(asset); } diff --git a/mobile/pigeon/remote_image_api.dart b/mobile/pigeon/remote_image_api.dart index 7f0135acb8..277b7016fd 100644 --- a/mobile/pigeon/remote_image_api.dart +++ b/mobile/pigeon/remote_image_api.dart @@ -13,8 +13,15 @@ import 'package:pigeon/pigeon.dart'; ) @HostApi() abstract class RemoteImageApi { + /// Width and height are the physical decode size, or null for the source size. @async - Map? requestImage(String url, {required int requestId, required bool preferEncoded}); + Map? requestImage( + String url, { + required int requestId, + required bool preferEncoded, + int? width, + int? height, + }); void cancelRequest(int requestId); diff --git a/mobile/test/infrastructure/loaders/remote_image_request_test.dart b/mobile/test/infrastructure/loaders/remote_image_request_test.dart new file mode 100644 index 0000000000..97bdf3e2bf --- /dev/null +++ b/mobile/test/infrastructure/loaders/remote_image_request_test.dart @@ -0,0 +1,117 @@ +import 'dart:ffi'; +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:ffi/ffi.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/infrastructure/loaders/image_request.dart'; +import 'package:immich_mobile/platform/remote_image_api.g.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const channel = BasicMessageChannel( + 'dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage', + RemoteImageApi.pigeonChannelCodec, + ); + late List args; + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler(channel, ( + message, + ) async { + args = message! as List; + return [null]; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler(channel, null); + }); + + Future loadEncoded(String path, ui.Size decodeSize) async { + final bytes = await File(path).readAsBytes(); + final pointer = malloc(bytes.length)..asTypedList(bytes.length).setAll(0, bytes); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler(channel, ( + message, + ) async { + return [ + {'pointer': pointer.address, 'length': bytes.length}, + ]; + }); + final request = RemoteImageRequest(uri: 'https://example.test/fallback', decodeSize: decodeSize); + + return (await request.load((_, {getTargetSize}) => throw UnimplementedError()))!.image; + } + + test('passes the requested decode size to the platform', () async { + final request = RemoteImageRequest(uri: 'https://example.test/thumbnail', decodeSize: const ui.Size(319.1, 179.1)); + + await request.load((_, {getTargetSize}) => throw UnimplementedError()); + + expect(args[0], 'https://example.test/thumbnail'); + expect(args[2], isFalse); + expect(args[3], 320); + expect(args[4], 180); + }); + + test('leaves requests without a size unbounded', () async { + final request = RemoteImageRequest(uri: 'https://example.test/thumbnail'); + + await request.load((_, {getTargetSize}) => throw UnimplementedError()); + + expect(args[3], isNull); + expect(args[4], isNull); + }); + + test('leaves encoded animation requests unbounded', () async { + final request = RemoteImageRequest(uri: 'https://example.test/animation', decodeSize: const ui.Size(320, 180)); + + await request.loadCodec(); + + expect(args[2], isTrue); + expect(args[3], isNull); + expect(args[4], isNull); + }); + + test('keeps portrait cover quality in a wide tile', () async { + final image = await loadEncoded('assets/feature_message/ocr.webp', const ui.Size(963, 642)); + + expect(image.width, 963); + expect(image.height, 1453); + image.dispose(); + }); + + test('preserves cover quality for extreme aspect ratios', () async { + final image = await loadEncoded('assets/immich-logo-inline-light.png', const ui.Size.square(320)); + + expect(image.width, 1311); + expect(image.height, 320); + image.dispose(); + }); + + test('does not upscale encoded fallback', () async { + final image = await loadEncoded('assets/feature_message/ocr.webp', const ui.Size.square(2000)); + + expect(image.width, 1206); + expect(image.height, 1819); + image.dispose(); + }); + + test('uses the decode size in the provider cache key', () { + final small = RemoteImageProvider(url: 'https://example.test/thumbnail', decodeSize: const ui.Size.square(160)); + final large = RemoteImageProvider(url: 'https://example.test/thumbnail', decodeSize: const ui.Size.square(320)); + + expect(small, isNot(large)); + }); + + test('shares the cache key when no decode size is set', () { + final first = RemoteImageProvider(url: 'https://example.test/thumbnail'); + final second = RemoteImageProvider(url: 'https://example.test/thumbnail'); + + expect(first, second); + expect(first.hashCode, second.hashCode); + }); +} diff --git a/mobile/test/providers/asset_viewer/asset_viewer_provider_test.dart b/mobile/test/providers/asset_viewer/asset_viewer_provider_test.dart index 27033a5774..454923affa 100644 --- a/mobile/test/providers/asset_viewer/asset_viewer_provider_test.dart +++ b/mobile/test/providers/asset_viewer/asset_viewer_provider_test.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -107,5 +108,20 @@ void main() { await pumpEventQueue(); expect(container.read(assetViewerProvider).currentAsset, updatedTwo); }); + + test('keeps the tapped tile size while swiping', () { + final first = RemoteAssetFactory.create(); + final second = RemoteAssetFactory.create(); + final notifier = container.read(assetViewerProvider.notifier); + + notifier.setAsset(first, thumbnailSize: const Size.square(642)); + expect(container.read(assetViewerProvider).thumbnailSize, const Size.square(642)); + + notifier.setAsset(second); + expect(container.read(assetViewerProvider).thumbnailSize, const Size.square(642)); + + notifier.reset(); + expect(container.read(assetViewerProvider).thumbnailSize, isNull); + }); }); } From 0ff47f417855eb6710e18951c8e705b99698828b Mon Sep 17 00:00:00 2001 From: Santo Shakil Date: Mon, 10 Aug 2026 21:39:38 +0600 Subject: [PATCH 15/49] fix(mobile): don't let a frozen sync block syncing on resume (#29870) * fix(mobile): don't let a frozen sync block syncing on resume * share the task lists between the cancel paths * fix analyzer issues in resume integration test * isolate the resume e2e test and pin the worker pool setup --- .../failed_sync_resume_e2e_test.dart | 101 +++++++++++++ .../failed_sync_resume_test.dart | 133 ++++++++++++++++++ .../test_utils/fake_immich_server.dart | 25 +++- mobile/lib/domain/utils/background_sync.dart | 114 ++++++++++----- .../providers/app_life_cycle.provider.dart | 6 + 5 files changed, 336 insertions(+), 43 deletions(-) create mode 100644 mobile/integration_test/failed_sync_resume_e2e_test.dart create mode 100644 mobile/integration_test/failed_sync_resume_test.dart diff --git a/mobile/integration_test/failed_sync_resume_e2e_test.dart b/mobile/integration_test/failed_sync_resume_e2e_test.dart new file mode 100644 index 0000000000..255074df93 --- /dev/null +++ b/mobile/integration_test/failed_sync_resume_e2e_test.dart @@ -0,0 +1,101 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/services/background_worker.service.dart'; +import 'package:immich_mobile/domain/utils/background_sync.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/main.dart' as app; +import 'package:immich_mobile/platform/background_worker_api.g.dart'; +import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/bootstrap.dart'; +import 'package:immich_mobile/wm_executor.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'test_utils/fake_immich_server.dart'; + +// Issue #28082 end-to-end: a resume after a sync froze mid-flight starts a fresh sync. +// Kept in its own file on purpose: run after the failed_sync_resume tests in one process, +// the resume sync starves past its window - a cross-test interaction that survived pool, +// sqlite and bg-worker audits unnamed. One file per process is device-proven green. +void main() { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; + + late Drift drift; + late FakeImmichServer server; + + // main()'s formula with a higher floor: a wedged frozen worker plus a full local + // sync must not starve the fresh resume sync out of its 25s window on 4 cores. + final poolSize = max(Platform.numberOfProcessors - 1, 8); + + setUpAll(() async { + await app.initApp(); + (drift, _) = await Bootstrap.initDomain(); + // A background-worker schedule persisted by real app use on this device can + // launch a second engine mid-file (own isolate pool + full sync) and starve + // these tests on a small device. Unregister it for the whole run. + await BackgroundWorkerFgService(BackgroundWorkerFgHostApi()).disable(); + }); + + setUp(() async { + // A task completing while dispose tears the pool down re-warms it (_schedule + // on the cleared pool re-creates workers), and init on a warm pool is silently + // ignored - poolSize would never apply. Reset first, then verify it took. + await workerManagerPatch.dispose(); + await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: poolSize); + expect( + workerManagerPatch.pool.length, + poolSize, + reason: 'init was ignored: a straggler from the previous test re-warmed the pool', + ); + server = await FakeImmichServer.start(); + await ApiService().resolveAndSetEndpoint(server.endpoint); + await drift.delete(drift.userEntity).go(); + }); + + tearDown(() async { + // Close the server first so any held-open sync stream ends and its isolate unwinds, + // then drain the pool - otherwise dispose waits on the frozen read. + await server.close(); + await workerManagerPatch.dispose(); + }); + + testWidgets('a resume after a sync froze mid-flight starts a fresh sync', (tester) async { + final manager = BackgroundSyncManager(); + // Not disposed on purpose: driftOverride closes the drift on dispose, and that + // drift belongs to setUpAll. The container only holds the shared drift and a + // fire-and-forget resume; the frozen isolates + server are drained by tearDown. + final container = ProviderContainer( + overrides: [driftProvider.overrideWith(driftOverride(drift)), backgroundSyncProvider.overrideWithValue(manager)], + ); + + // A first sync opens /sync/stream and never finishes - the frozen state a + // suspended sync isolate is left in. Holding the stream open keeps + // _syncTask non-null, exactly as it is across an iOS process suspension. + unawaited(manager.syncRemote()); + await server + .streamOpenedNth(1) + .timeout(const Duration(seconds: 30), onTimeout: () => fail('first sync isolate never opened /sync/stream')); + + // The lifecycle then goes background -> foreground. handleAppResume runs the + // resume sync exactly once. On the buggy build it hangs on the stale task's + // future, so it is not awaited here. + final notifier = container.read(appStateProvider.notifier); + await notifier.handleAppPause(); + unawaited(notifier.handleAppResume()); + + await server + .streamOpenedNth(2) + .timeout( + const Duration(seconds: 25), + onTimeout: () => fail('resume did not start a fresh remote sync - the stale frozen sync blocked it (#28082)'), + ); + expect(server.streamOpenCount, greaterThanOrEqualTo(2)); + }); +} diff --git a/mobile/integration_test/failed_sync_resume_test.dart b/mobile/integration_test/failed_sync_resume_test.dart new file mode 100644 index 0000000000..c23319dffe --- /dev/null +++ b/mobile/integration_test/failed_sync_resume_test.dart @@ -0,0 +1,133 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/services/background_worker.service.dart'; +import 'package:immich_mobile/domain/utils/background_sync.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/main.dart' as app; +import 'package:immich_mobile/platform/background_worker_api.g.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/bootstrap.dart'; +import 'package:immich_mobile/wm_executor.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'test_utils/fake_immich_server.dart'; + +// Issue #28082: a remote sync in-flight when the app is backgrounded stays referenced +// but frozen across the suspension. On resume the app drops the stale task +// (cancelResumeSyncs) and starts a fresh sync. +// +// Device/emulator tests: real worker isolates + a real drift db + a loopback fake server +// (same pattern as background_sync_teardown_test). The mobile integration-test CI job is +// disabled in test.yml, so like Mert's teardown test this is a local/on-device guard. +// +// The end-to-end resume test lives in failed_sync_resume_e2e_test.dart - one process +// per file keeps it clear of the cross-test interaction described there. +void main() { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; + + late Drift drift; + late FakeImmichServer server; + + // main()'s formula with a higher floor: a wedged frozen worker plus a full local + // sync must not starve the fresh resume sync out of its 25s window on 4 cores. + final poolSize = max(Platform.numberOfProcessors - 1, 8); + + setUpAll(() async { + await app.initApp(); + (drift, _) = await Bootstrap.initDomain(); + // A background-worker schedule persisted by real app use on this device can + // launch a second engine mid-file (own isolate pool + full sync) and starve + // these tests on a small device. Unregister it for the whole run. + await BackgroundWorkerFgService(BackgroundWorkerFgHostApi()).disable(); + }); + + setUp(() async { + // A task completing while dispose tears the pool down re-warms it (_schedule + // on the cleared pool re-creates workers), and init on a warm pool is silently + // ignored - poolSize would never apply. Reset first, then verify it took. + await workerManagerPatch.dispose(); + await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: poolSize); + expect( + workerManagerPatch.pool.length, + poolSize, + reason: 'init was ignored: a straggler from the previous test re-warmed the pool', + ); + server = await FakeImmichServer.start(); + await ApiService().resolveAndSetEndpoint(server.endpoint); + await drift.delete(drift.userEntity).go(); + }); + + tearDown(() async { + // Close the server first so any held-open sync stream ends and its isolate unwinds, + // then drain the pool - otherwise dispose waits on the frozen read. + await server.close(); + await workerManagerPatch.dispose(); + }); + + // Self-contained (bare manager, no fire-and-forget resume), so it runs first: its + // frozen syncs are fully drained by tearDown. + testWidgets('a cancelled sync task does not clear the slot of the fresh task that superseded it', (tester) async { + final manager = BackgroundSyncManager(); + + // First sync opens /sync/stream and is held open - the frozen suspended state. + unawaited(manager.syncRemote()); + await server + .streamOpenedNth(1) + .timeout(const Duration(seconds: 30), onTimeout: () => fail('first sync isolate never opened /sync/stream')); + + // Resume drops the stale task then immediately starts a fresh one, exactly as + // _handleBetaTimelineResume does. cancelResumeSyncs cancels the first task; its + // completion chain then fires and must NOT null the fresh task's slot. + unawaited(manager.cancelResumeSyncs()); + unawaited(manager.syncRemote()); + await server + .streamOpenedNth(2) + .timeout(const Duration(seconds: 30), onTimeout: () => fail('fresh sync isolate never opened /sync/stream')); + + // A third sync stream only opens if the cancelled task cleared the fresh slot, + // letting the dedupe guard start a redundant sync. + var thirdOpened = false; + unawaited(server.streamOpenedNth(3).then((_) => thirdOpened = true)); + + // Let the cancelled task's completion chain settle, then ask to sync again. + await Future.delayed(const Duration(milliseconds: 200)); + unawaited(manager.syncRemote()); + await Future.delayed(const Duration(seconds: 3)); + + expect( + thirdOpened, + isFalse, + reason: 'the cancelled task cleared the fresh task slot, so a redundant third sync started (#28082 clobber)', + ); + expect(server.streamOpenCount, 2); + }); + + // The false-error-flash fix: a task cancelled by cancelResumeSyncs completes with a + // CanceledError, which is not a sync failure and must not reach onRemoteSyncError. + // Before the filter the stale task reported an error right after the fresh sync + // started, so the status UI showed a failure for the whole healthy run. + testWidgets('a cancelled sync does not report a false error to the status callbacks', (tester) async { + final errors = []; + final manager = BackgroundSyncManager(onRemoteSyncError: errors.add); + + // Hold the stream open so the task is genuinely in-flight when it is cancelled. + unawaited(manager.syncRemote()); + await server + .streamOpenedNth(1) + .timeout(const Duration(seconds: 30), onTimeout: () => fail('sync isolate never opened /sync/stream')); + + await manager.cancelResumeSyncs(); + // Let the cancelled task's completion chain run before checking the callbacks. + await Future.delayed(const Duration(milliseconds: 100)); + + expect( + errors, + isEmpty, + reason: 'a cancelled task is not a real error; its CanceledError must not fire onRemoteSyncError', + ); + }); +} diff --git a/mobile/integration_test/test_utils/fake_immich_server.dart b/mobile/integration_test/test_utils/fake_immich_server.dart index c434f83bc5..7c63098c52 100644 --- a/mobile/integration_test/test_utils/fake_immich_server.dart +++ b/mobile/integration_test/test_utils/fake_immich_server.dart @@ -10,6 +10,8 @@ class FakeImmichServer { final (int, int, int) version; final Completer _streamOpened = Completer(); + final List _streamOpens = []; + final Map> _openWaiters = {}; int ackRequests = 0; @@ -18,6 +20,17 @@ class FakeImmichServer { /// Resolves when the sync isolate opens `POST /sync/stream`. Future get streamOpened => _streamOpened.future; + /// How many `/sync/stream` requests have opened so far. + int get streamOpenCount => _streamOpens.length; + + /// Resolves when the [n]-th (1-indexed) `/sync/stream` opens. + Future streamOpenedNth(int n) { + if (_streamOpens.length >= n) { + return Future.value(_streamOpens[n - 1]); + } + return (_openWaiters[n] ??= Completer()).future; + } + static Future start({(int, int, int) version = (3, 0, 0)}) async { final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); final fake = FakeImmichServer._(server, version); @@ -58,13 +71,17 @@ class FakeImmichServer { request.response ..statusCode = HttpStatus.ok ..headers.contentType = ContentType('application', 'jsonlines+json') - ..contentLength = -1 // chunked: stays open to stream incrementally + ..contentLength = + -1 // chunked: stays open to stream incrementally ..bufferOutput = false; // Flush headers so the client's send() resolves and enters its read loop. await request.response.flush(); + final stream = SyncStream._(request.response); + _streamOpens.add(stream); if (!_streamOpened.isCompleted) { - _streamOpened.complete(SyncStream._(request.response)); + _streamOpened.complete(stream); } + _openWaiters.remove(_streamOpens.length)?.complete(stream); } Future _respondJson(HttpRequest request, Object body) async { @@ -83,8 +100,8 @@ class FakeImmichServer { } Future close() async { - if (_streamOpened.isCompleted) { - await (await _streamOpened.future).close(); + for (final stream in _streamOpens) { + await stream.close(); } await _server.close(force: true); } diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart index db0d93c92f..f1e0328165 100644 --- a/mobile/lib/domain/utils/background_sync.dart +++ b/mobile/lib/domain/utils/background_sync.dart @@ -50,16 +50,39 @@ class BackgroundSyncManager { this.onCloudIdSyncError, }); + // The tasks the app-resume path re-runs. One in-flight when the app was suspended + // stays referenced but frozen, so on resume the dedupe guards would hand back the + // stale task instead of syncing (#28082). Websocket and cloud-id are excluded - the + // resume path never restarts them. [_allTasks] builds on this so the lists can't drift. + List get _resumeSyncTasks => [_syncTask, _deviceAlbumSyncTask, _hashTask, _linkedAlbumSyncTask]; + + List get _allTasks => [_syncWebsocketTask, _cloudIdSyncTask, ..._resumeSyncTasks]; + Future cancel() async { _syncQueued = false; - final tasks = [ - _syncTask, - _syncWebsocketTask, - _cloudIdSyncTask, - _linkedAlbumSyncTask, - _deviceAlbumSyncTask, - _hashTask, - ]; + final tasks = _allTasks; + _syncTask = null; + _syncWebsocketTask = null; + _cloudIdSyncTask = null; + _linkedAlbumSyncTask = null; + _deviceAlbumSyncTask = null; + _hashTask = null; + await _cancelAll(tasks); + } + + Future cancelResumeSyncs() async { + _syncQueued = false; + final tasks = _resumeSyncTasks; + _syncTask = null; + _deviceAlbumSyncTask = null; + _hashTask = null; + _linkedAlbumSyncTask = null; + await _cancelAll(tasks); + } + + // Cancels every task in [tasks] and waits for them to unwind. Callers null out + // their own fields first, so the sync guards see a clean slate immediately. + Future _cancelAll(List tasks) async { final futures = [ for (final task in tasks) if (task != null) task.future, @@ -67,13 +90,6 @@ class BackgroundSyncManager { for (final task in tasks) { task?.cancel(); } - _syncTask = null; - _syncWebsocketTask = null; - _cloudIdSyncTask = null; - _linkedAlbumSyncTask = null; - _deviceAlbumSyncTask = null; - _hashTask = null; - try { await Future.wait(futures); } on CanceledError { @@ -84,14 +100,14 @@ class BackgroundSyncManager { // No need to cancel the task, as it can also be run when the user logs out Future syncLocal({bool full = false}) { if (_deviceAlbumSyncTask != null) { - return _deviceAlbumSyncTask!.future; + return _deviceAlbumSyncTask!.future.catchError((_) {}, test: (error) => error is CanceledError); } onLocalSyncStart?.call(); // We use a ternary operator to avoid [_deviceAlbumSyncTask] from being // captured by the closure passed to [runInIsolateGentle]. - _deviceAlbumSyncTask = full + final task = _deviceAlbumSyncTask = full ? runInIsolateGentle( computation: (ref) => ref.read(localSyncServiceProvider).sync(full: true), debugLabel: 'local-sync-full-true', @@ -101,37 +117,43 @@ class BackgroundSyncManager { debugLabel: 'local-sync-full-false', ); - return _deviceAlbumSyncTask! + return task .whenComplete(() { - _deviceAlbumSyncTask = null; + if (identical(_deviceAlbumSyncTask, task)) { + _deviceAlbumSyncTask = null; + } onLocalSyncComplete?.call(); }) .catchError((error) { - onLocalSyncError?.call(error.toString()); - _deviceAlbumSyncTask = null; + if (error is! CanceledError) { + onLocalSyncError?.call(error.toString()); + } }); } Future hashAssets() { if (_hashTask != null) { - return _hashTask!.future; + return _hashTask!.future.catchError((_) {}, test: (error) => error is CanceledError); } onHashingStart?.call(); - _hashTask = runInIsolateGentle( + final task = _hashTask = runInIsolateGentle( computation: (ref) => ref.read(hashServiceProvider).hashAssets(), debugLabel: 'hash-assets', ); - return _hashTask! + return task .whenComplete(() { onHashingComplete?.call(); - _hashTask = null; + if (identical(_hashTask, task)) { + _hashTask = null; + } }) .catchError((error) { - onHashingError?.call(error.toString()); - _hashTask = null; + if (error is! CanceledError) { + onHashingError?.call(error.toString()); + } }); } @@ -143,11 +165,11 @@ class BackgroundSyncManager { onRemoteSyncStart?.call(); - _syncTask = runInIsolateGentle( + final task = _syncTask = runInIsolateGentle( computation: (ref) => ref.read(syncStreamServiceProvider).sync(), debugLabel: 'remote-sync', ); - return _syncTask! + return task .then((result) { final success = result ?? false; onRemoteSyncComplete?.call(success); @@ -155,15 +177,21 @@ class BackgroundSyncManager { return success; }) .catchError((error) { - onRemoteSyncError?.call(error.toString()); + if (error is! CanceledError) { + onRemoteSyncError?.call(error.toString()); + } _syncQueued = false; return false; }) + // A task clears only its own slot: one that was cancelled and superseded by a + // fresh task (see cancelResumeSyncs) must not null the new task's slot. .whenComplete(() { - _syncTask = null; - if (_syncQueued) { - _syncQueued = false; - unawaited(syncRemote()); + if (identical(_syncTask, task)) { + _syncTask = null; + if (_syncQueued) { + _syncQueued = false; + unawaited(syncRemote()); + } } }); } @@ -210,13 +238,21 @@ class BackgroundSyncManager { Future syncLinkedAlbum() { if (_linkedAlbumSyncTask != null) { - return _linkedAlbumSyncTask!.future; + return _linkedAlbumSyncTask!.future.catchError((_) {}, test: (error) => error is CanceledError); } - _linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated, debugLabel: 'linked-album-sync'); - return _linkedAlbumSyncTask!.whenComplete(() { - _linkedAlbumSyncTask = null; - }); + final task = _linkedAlbumSyncTask = runInIsolateGentle( + computation: syncLinkedAlbumsIsolated, + debugLabel: 'linked-album-sync', + ); + return task + .whenComplete(() { + if (identical(_linkedAlbumSyncTask, task)) { + _linkedAlbumSyncTask = null; + } + }) + // a cancelled resume sync is not a failure; absorb it so the websocket callers don't get an uncaught error + .catchError((_) {}, test: (error) => error is CanceledError); } Future syncCloudIds() { diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 7dc475ab93..f369a37142 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -112,6 +112,12 @@ class AppLifeCycleNotifier extends StateNotifier { await Future.delayed(const Duration(milliseconds: 500)); final backgroundManager = _ref.read(backgroundSyncProvider); + + // Drop any sync that froze mid-flight while the app was suspended so resume + // starts fresh instead of awaiting the stale task (#28082). cancelResumeSyncs + // clears the task refs synchronously, so the syncs below see a clean slate. + unawaited(backgroundManager.cancelResumeSyncs()); + final isAlbumLinkedSyncEnable = _ref.read(appConfigProvider).backup.syncAlbums; try { From 235daff56125c5d7d5727f9b01f7a838dab0124e Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 10 Aug 2026 14:12:45 -0700 Subject: [PATCH 16/49] chore(mobile): more complicated Freezed pass (#30452) * chore(mobile): example freezed implementation on some models --- mobile/lib/constants/aspect_ratios.dart | 31 +-- mobile/lib/domain/models/log.model.dart | 61 ++---- mobile/lib/domain/models/map.model.dart | 21 +- mobile/lib/domain/models/memory.model.dart | 163 ++------------ mobile/lib/domain/models/person.model.dart | 97 +-------- .../domain/models/search_result.model.dart | 26 +-- mobile/lib/domain/models/tag.model.dart | 28 +-- mobile/lib/domain/models/user.model.dart | 86 ++------ .../domain/models/user_metadata.model.dart | 201 ++---------------- .../models/auth/auxilary_endpoint.model.dart | 77 ++----- .../models/auth/biometric_status.model.dart | 34 +-- .../lib/models/cast/cast_manager_state.dart | 91 ++------ .../models/download/download_state.model.dart | 108 ++-------- .../search/search_curated_content.model.dart | 74 ++----- .../models/search/search_filter.model.dart | 53 +---- .../actions/similar_photos.action.dart | 2 +- .../pages/search/drift_search.page.dart | 10 +- .../widgets/timeline/timeline.state.dart | 50 +---- .../timeline/timeline_drag_region.dart | 23 +- .../backup/drift_backup.provider.dart | 91 ++------ .../infrastructure/remote_album.provider.dart | 27 +-- .../search/search_filter.provider.dart | 37 +--- .../timeline/multiselect.provider.dart | 48 +---- .../upload_profile_image.provider.dart | 56 +---- .../services/background_upload.service.dart | 44 ++-- .../search/search_filter/camera_picker.dart | 4 +- 26 files changed, 272 insertions(+), 1271 deletions(-) diff --git a/mobile/lib/constants/aspect_ratios.dart b/mobile/lib/constants/aspect_ratios.dart index 9ad7b8c739..49165e0913 100644 --- a/mobile/lib/constants/aspect_ratios.dart +++ b/mobile/lib/constants/aspect_ratios.dart @@ -1,13 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; -class CropAspectRatio { - final int? numerator; - final int? denominator; +part 'aspect_ratios.freezed.dart'; - final String? customLabel; - final IconData? icon; +@freezed +abstract class CropAspectRatio with _$CropAspectRatio { + const CropAspectRatio._(); - const CropAspectRatio({this.numerator, this.denominator, this.customLabel, this.icon}); + const factory CropAspectRatio({int? numerator, int? denominator, String? customLabel, IconData? icon}) = + _CropAspectRatio; static const free = CropAspectRatio(customLabel: "Free", icon: Icons.crop_free); static const original = CropAspectRatio(customLabel: "Original", icon: Icons.crop_original); @@ -22,24 +23,6 @@ class CropAspectRatio { CropAspectRatio get flipped { return CropAspectRatio(numerator: denominator, denominator: numerator, customLabel: customLabel, icon: icon); } - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is CropAspectRatio && - other.numerator == numerator && - other.denominator == denominator && - other.customLabel == customLabel && - other.icon == icon; - } - - @override - int get hashCode { - return numerator.hashCode ^ denominator.hashCode ^ customLabel.hashCode ^ icon.hashCode; - } } const aspectRatioFree = CropAspectRatio.free; diff --git a/mobile/lib/domain/models/log.model.dart b/mobile/lib/domain/models/log.model.dart index bed1729f9d..72267bcb5c 100644 --- a/mobile/lib/domain/models/log.model.dart +++ b/mobile/lib/domain/models/log.model.dart @@ -1,51 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'log.model.freezed.dart'; + /// Log levels according to dart logging [Level] enum LogLevel { all, finest, finer, fine, config, info, warning, severe, shout, off } -class LogMessage { - final String message; - final LogLevel level; - final DateTime createdAt; - final String? logger; - final String? error; - final String? stack; - - const LogMessage({ - required this.message, - required this.level, - required this.createdAt, - this.logger, - this.error, - this.stack, - }); - - @override - bool operator ==(covariant LogMessage other) { - if (identical(this, other)) { - return true; - } - - return other.message == message && - other.level == level && - other.createdAt == createdAt && - other.logger == logger && - other.error == error && - other.stack == stack; - } - - @override - int get hashCode { - return message.hashCode ^ level.hashCode ^ createdAt.hashCode ^ logger.hashCode ^ error.hashCode ^ stack.hashCode; - } - - @override - String toString() { - return '''LogMessage: { -message: $message, -level: $level, -createdAt: $createdAt, -logger: ${logger ?? ''}, -error: ${error ?? ''}, -stack: ${stack ?? ''}, -}'''; - } +@freezed +abstract class LogMessage with _$LogMessage { + const factory LogMessage({ + required String message, + required LogLevel level, + required DateTime createdAt, + String? logger, + String? error, + String? stack, + }) = _LogMessage; } diff --git a/mobile/lib/domain/models/map.model.dart b/mobile/lib/domain/models/map.model.dart index b55f176bfd..52f6fe8afd 100644 --- a/mobile/lib/domain/models/map.model.dart +++ b/mobile/lib/domain/models/map.model.dart @@ -1,20 +1,9 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; -class Marker { - final LatLng location; - final String assetId; +part 'map.model.freezed.dart'; - const Marker({required this.location, required this.assetId}); - - @override - bool operator ==(covariant Marker other) { - if (identical(this, other)) { - return true; - } - - return other.location == location && other.assetId == assetId; - } - - @override - int get hashCode => location.hashCode ^ assetId.hashCode; +@freezed +abstract class Marker with _$Marker { + const factory Marker({required LatLng location, required String assetId}) = _Marker; } diff --git a/mobile/lib/domain/models/memory.model.dart b/mobile/lib/domain/models/memory.model.dart index e786ca18b1..a75a4a4f0f 100644 --- a/mobile/lib/domain/models/memory.model.dart +++ b/mobile/lib/domain/models/memory.model.dart @@ -1,22 +1,22 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; -import 'package:collection/collection.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +part 'memory.model.freezed.dart'; + +// TODO(agg23): Remove enum suffix enum MemoryTypeEnum { // do not change this order! onThisDay, } -class MemoryData { - final int year; +@Freezed(fromJson: false, toJson: false) +abstract class MemoryData with _$MemoryData { + const MemoryData._(); - const MemoryData({required this.year}); - - MemoryData copyWith({int? year}) { - return MemoryData(year: year ?? this.year); - } + const factory MemoryData({required int year}) = _MemoryData; Map toMap() { return {'year': year}; @@ -29,144 +29,25 @@ class MemoryData { String toJson() => json.encode(toMap()); factory MemoryData.fromJson(String source) => MemoryData.fromMap(json.decode(source) as Map); - - @override - String toString() => 'MemoryData(year: $year)'; - - @override - bool operator ==(covariant MemoryData other) { - if (identical(this, other)) { - return true; - } - - return other.year == year; - } - - @override - int get hashCode => year.hashCode; } // Model for a memory stored in the server -class DriftMemory { - final String id; - final DateTime createdAt; - final DateTime updatedAt; - final DateTime? deletedAt; - final String ownerId; - - // enum - final MemoryTypeEnum type; - final MemoryData data; - final bool isSaved; - final DateTime memoryAt; - final DateTime? seenAt; - final DateTime? showAt; - final DateTime? hideAt; - final List assets; - - const DriftMemory({ - required this.id, - required this.createdAt, - required this.updatedAt, - this.deletedAt, - required this.ownerId, - required this.type, - required this.data, - required this.isSaved, - required this.memoryAt, - this.seenAt, - this.showAt, - this.hideAt, - required this.assets, - }); - - DriftMemory copyWith({ - String? id, - DateTime? createdAt, - DateTime? updatedAt, +// TODO(agg23): DriftMemoryRepository currently mutates `assets` +@Freezed(makeCollectionsUnmodifiable: false) +abstract class DriftMemory with _$DriftMemory { + const factory DriftMemory({ + required String id, + required DateTime createdAt, + required DateTime updatedAt, DateTime? deletedAt, - String? ownerId, - MemoryTypeEnum? type, - MemoryData? data, - bool? isSaved, - DateTime? memoryAt, + required String ownerId, + required MemoryTypeEnum type, + required MemoryData data, + required bool isSaved, + required DateTime memoryAt, DateTime? seenAt, DateTime? showAt, DateTime? hideAt, - List? assets, - }) { - return DriftMemory( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - deletedAt: deletedAt ?? this.deletedAt, - ownerId: ownerId ?? this.ownerId, - type: type ?? this.type, - data: data ?? this.data, - isSaved: isSaved ?? this.isSaved, - memoryAt: memoryAt ?? this.memoryAt, - seenAt: seenAt ?? this.seenAt, - showAt: showAt ?? this.showAt, - hideAt: hideAt ?? this.hideAt, - assets: assets ?? this.assets, - ); - } - - @override - String toString() { - return '''Memory { - id: $id, - createdAt: $createdAt, - updatedAt: $updatedAt, - deletedAt: ${deletedAt ?? ""}, - ownerId: $ownerId, - type: $type, - data: $data, - isSaved: $isSaved, - memoryAt: $memoryAt, - seenAt: ${seenAt ?? ""}, - showAt: ${showAt ?? ""}, - hideAt: ${hideAt ?? ""}, - assets: $assets -}'''; - } - - @override - bool operator ==(covariant DriftMemory other) { - if (identical(this, other)) { - return true; - } - final listEquals = const DeepCollectionEquality().equals; - - return other.id == id && - other.createdAt == createdAt && - other.updatedAt == updatedAt && - other.deletedAt == deletedAt && - other.ownerId == ownerId && - other.type == type && - other.data == data && - other.isSaved == isSaved && - other.memoryAt == memoryAt && - other.seenAt == seenAt && - other.showAt == showAt && - other.hideAt == hideAt && - listEquals(other.assets, assets); - } - - @override - int get hashCode { - return id.hashCode ^ - createdAt.hashCode ^ - updatedAt.hashCode ^ - deletedAt.hashCode ^ - ownerId.hashCode ^ - type.hashCode ^ - data.hashCode ^ - isSaved.hashCode ^ - memoryAt.hashCode ^ - seenAt.hashCode ^ - showAt.hashCode ^ - hideAt.hashCode ^ - assets.hashCode; - } + required List assets, + }) = _DriftMemory; } diff --git a/mobile/lib/domain/models/person.model.dart b/mobile/lib/domain/models/person.model.dart index 38d740eeb4..56e6d3990d 100644 --- a/mobile/lib/domain/models/person.model.dart +++ b/mobile/lib/domain/models/person.model.dart @@ -1,100 +1,19 @@ -import 'dart:convert'; - import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'person.model.freezed.dart'; // TODO: Remove PersonDto once Isar is removed -class PersonDto { - const PersonDto({ - required this.id, - this.birthDate, - required this.isHidden, - required this.name, - required this.thumbnailPath, - this.updatedAt, - }); - - final String id; - final DateTime? birthDate; - final bool isHidden; - final String name; - final String thumbnailPath; - final DateTime? updatedAt; - - @override - String toString() { - return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)'; - } - - PersonDto copyWith({ - String? id, +@freezed +abstract class PersonDto with _$PersonDto { + const factory PersonDto({ + required String id, DateTime? birthDate, - bool? isHidden, - String? name, - String? thumbnailPath, + required bool isHidden, + required String name, + required String thumbnailPath, DateTime? updatedAt, - }) { - return PersonDto( - id: id ?? this.id, - birthDate: birthDate ?? this.birthDate, - isHidden: isHidden ?? this.isHidden, - name: name ?? this.name, - thumbnailPath: thumbnailPath ?? this.thumbnailPath, - updatedAt: updatedAt ?? this.updatedAt, - ); - } - - Map toMap() { - return { - 'id': id, - 'birthDate': birthDate?.millisecondsSinceEpoch, - 'isHidden': isHidden, - 'name': name, - 'thumbnailPath': thumbnailPath, - 'updatedAt': updatedAt?.millisecondsSinceEpoch, - }; - } - - factory PersonDto.fromMap(Map map) { - return PersonDto( - id: map['id'] as String, - birthDate: map['birthDate'] != null ? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int) : null, - isHidden: map['isHidden'] as bool, - name: map['name'] as String, - thumbnailPath: map['thumbnailPath'] as String, - updatedAt: map['updatedAt'] != null ? DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int) : null, - ); - } - - String toJson() => json.encode(toMap()); - - factory PersonDto.fromJson(String source) => PersonDto.fromMap(json.decode(source) as Map); - - @override - bool operator ==(covariant PersonDto other) { - if (identical(this, other)) { - return true; - } - - return other.id == id && - other.birthDate == birthDate && - other.isHidden == isHidden && - other.name == name && - other.thumbnailPath == thumbnailPath && - other.updatedAt == updatedAt; - } - - @override - int get hashCode { - return id.hashCode ^ - birthDate.hashCode ^ - isHidden.hashCode ^ - name.hashCode ^ - thumbnailPath.hashCode ^ - updatedAt.hashCode; - } + }) = _PersonDto; } // Model for a person stored in the server diff --git a/mobile/lib/domain/models/search_result.model.dart b/mobile/lib/domain/models/search_result.model.dart index 6a782e2f37..11409e64a2 100644 --- a/mobile/lib/domain/models/search_result.model.dart +++ b/mobile/lib/domain/models/search_result.model.dart @@ -1,25 +1,15 @@ -import 'package:collection/collection.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -class SearchResult { - final List assets; - final int? nextPage; +part 'search_result.model.freezed.dart'; - const SearchResult({required this.assets, this.nextPage}); +@Freezed(toStringOverride: false) +abstract class SearchResult with _$SearchResult { + const SearchResult._(); + const factory SearchResult({required List assets, int? nextPage}) = _SearchResult; + + // Explicitly don't log results, only attributes @override String toString() => 'SearchResult(assets: ${assets.length}, nextPage: $nextPage)'; - - @override - bool operator ==(covariant SearchResult other) { - if (identical(this, other)) { - return true; - } - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.assets, assets) && other.nextPage == nextPage; - } - - @override - int get hashCode => assets.hashCode ^ nextPage.hashCode; } diff --git a/mobile/lib/domain/models/tag.model.dart b/mobile/lib/domain/models/tag.model.dart index ba9aef02ee..d38931a285 100644 --- a/mobile/lib/domain/models/tag.model.dart +++ b/mobile/lib/domain/models/tag.model.dart @@ -1,29 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:openapi/api.dart'; -class Tag { - final String id; - final String value; +part 'tag.model.freezed.dart'; - const Tag({required this.id, required this.value}); - - @override - String toString() { - return 'Tag(id: $id, value: $value)'; - } - - @override - bool operator ==(covariant Tag other) { - if (identical(this, other)) { - return true; - } - - return other.id == id && other.value == value; - } - - @override - int get hashCode { - return id.hashCode ^ value.hashCode; - } +@freezed +abstract class Tag with _$Tag { + const factory Tag({required String id, required String value}) = _Tag; static Tag fromDto(TagResponseDto dto) { return Tag(id: dto.id, value: dto.value); diff --git a/mobile/lib/domain/models/user.model.dart b/mobile/lib/domain/models/user.model.dart index d1a7fc5546..96e07137c9 100644 --- a/mobile/lib/domain/models/user.model.dart +++ b/mobile/lib/domain/models/user.model.dart @@ -1,7 +1,10 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; import 'dart:ui'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'user.model.freezed.dart'; + enum AvatarColor { // do not change this order or reuse indices for other purposes, adding is OK primary("primary"), @@ -164,78 +167,15 @@ profileChangedAt: $profileChangedAt quotaUsageInBytes.hashCode; } -class PartnerUserDto { - final String id; - final String email; - final String name; - final bool inTimeline; - - final String? profileImagePath; - - const PartnerUserDto({ - required this.id, - required this.email, - required this.name, - required this.inTimeline, - this.profileImagePath, - }); - - PartnerUserDto copyWith({String? id, String? email, String? name, bool? inTimeline, String? profileImagePath}) { - return PartnerUserDto( - id: id ?? this.id, - email: email ?? this.email, - name: name ?? this.name, - inTimeline: inTimeline ?? this.inTimeline, - profileImagePath: profileImagePath ?? this.profileImagePath, - ); - } - - Map toMap() { - return { - 'id': id, - 'email': email, - 'name': name, - 'inTimeline': inTimeline, - 'profileImagePath': profileImagePath, - }; - } - - factory PartnerUserDto.fromMap(Map map) { - return PartnerUserDto( - id: map['id'] as String, - email: map['email'] as String, - name: map['name'] as String, - inTimeline: map['inTimeline'] as bool, - profileImagePath: map['profileImagePath'] != null ? map['profileImagePath'] as String : null, - ); - } - - String toJson() => json.encode(toMap()); - - factory PartnerUserDto.fromJson(String source) => PartnerUserDto.fromMap(json.decode(source) as Map); - - @override - String toString() { - return 'PartnerUserDto(id: $id, email: $email, name: $name, inTimeline: $inTimeline, profileImagePath: $profileImagePath)'; - } - - @override - bool operator ==(covariant PartnerUserDto other) { - if (identical(this, other)) { - return true; - } - - return other.id == id && - other.email == email && - other.name == name && - other.inTimeline == inTimeline && - other.profileImagePath == profileImagePath; - } - - @override - int get hashCode { - return id.hashCode ^ email.hashCode ^ name.hashCode ^ inTimeline.hashCode ^ profileImagePath.hashCode; - } +@freezed +abstract class PartnerUserDto with _$PartnerUserDto { + const factory PartnerUserDto({ + required String id, + required String email, + required String name, + required bool inTimeline, + String? profileImagePath, + }) = _PartnerUserDto; } class User { diff --git a/mobile/lib/domain/models/user_metadata.model.dart b/mobile/lib/domain/models/user_metadata.model.dart index 8bce1aca13..2b1da0a809 100644 --- a/mobile/lib/domain/models/user_metadata.model.dart +++ b/mobile/lib/domain/models/user_metadata.model.dart @@ -11,105 +11,32 @@ enum UserMetadataKey { license, } -class Onboarding { - final bool isOnboarded; +@freezed +abstract class Onboarding with _$Onboarding { + const Onboarding._(); - const Onboarding({required this.isOnboarded}); - - Onboarding copyWith({bool? isOnboarded}) { - return Onboarding(isOnboarded: isOnboarded ?? this.isOnboarded); - } - - Map toMap() { - final onboarding = {}; - onboarding["isOnboarded"] = isOnboarded; - return onboarding; - } + const factory Onboarding({required bool isOnboarded}) = _Onboarding; factory Onboarding.fromMap(Map map) { return Onboarding(isOnboarded: map["isOnboarded"]! as bool); } - - @override - String toString() { - return '''Onboarding { -isOnboarded: $isOnboarded, -}'''; - } - - @override - bool operator ==(covariant Onboarding other) { - if (identical(this, other)) { - return true; - } - - return isOnboarded == other.isOnboarded; - } - - @override - int get hashCode => isOnboarded.hashCode; } -class Preferences { - final bool foldersEnabled; - final bool memoriesEnabled; - final bool peopleEnabled; - final bool ratingsEnabled; - final bool sharedLinksEnabled; - final bool tagsEnabled; - final AvatarColor userAvatarColor; - final bool showSupportBadge; - final int minimumFaces; +@freezed +abstract class Preferences with _$Preferences { + const Preferences._(); - const Preferences({ - this.foldersEnabled = false, - this.memoriesEnabled = true, - this.peopleEnabled = true, - this.ratingsEnabled = false, - this.sharedLinksEnabled = true, - this.tagsEnabled = false, - this.userAvatarColor = AvatarColor.primary, - this.showSupportBadge = true, - this.minimumFaces = 3, - }); - - Preferences copyWith({ - bool? foldersEnabled, - bool? memoriesEnabled, - bool? peopleEnabled, - bool? ratingsEnabled, - bool? sharedLinksEnabled, - bool? tagsEnabled, - AvatarColor? userAvatarColor, - bool? showSupportBadge, - int? minimumFaces, - }) { - return Preferences( - foldersEnabled: foldersEnabled ?? this.foldersEnabled, - memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled, - peopleEnabled: peopleEnabled ?? this.peopleEnabled, - ratingsEnabled: ratingsEnabled ?? this.ratingsEnabled, - sharedLinksEnabled: sharedLinksEnabled ?? this.sharedLinksEnabled, - tagsEnabled: tagsEnabled ?? this.tagsEnabled, - userAvatarColor: userAvatarColor ?? this.userAvatarColor, - showSupportBadge: showSupportBadge ?? this.showSupportBadge, - minimumFaces: minimumFaces ?? this.minimumFaces, - ); - } - - Map toMap() { - final preferences = {}; - preferences["folders-Enabled"] = foldersEnabled; - preferences["memories-Enabled"] = memoriesEnabled; - preferences["people-Enabled"] = peopleEnabled; - preferences["ratings-Enabled"] = ratingsEnabled; - preferences["sharedLinks-Enabled"] = sharedLinksEnabled; - preferences["tags-Enabled"] = tagsEnabled; - preferences["avatar-Color"] = userAvatarColor.value; - preferences["purchase-ShowSupportBadge"] = showSupportBadge; - preferences["minimumFaces"] = minimumFaces; - return preferences; - } + const factory Preferences({ + @Default(false) bool foldersEnabled, + @Default(true) bool memoriesEnabled, + @Default(true) bool peopleEnabled, + @Default(false) bool ratingsEnabled, + @Default(true) bool sharedLinksEnabled, + @Default(false) bool tagsEnabled, + @Default(AvatarColor.primary) AvatarColor userAvatarColor, + @Default(true) bool showSupportBadge, + @Default(3) int minimumFaces, + }) = _Preferences; factory Preferences.fromMap(Map map) { return Preferences( @@ -127,75 +54,14 @@ class Preferences { minimumFaces: (map["people"] as Map?)?["minimumFaces"] as int? ?? 3, ); } - - @override - String toString() { - return '''Preferences: { -foldersEnabled: $foldersEnabled, -memoriesEnabled: $memoriesEnabled, -peopleEnabled: $peopleEnabled, -ratingsEnabled: $ratingsEnabled, -sharedLinksEnabled: $sharedLinksEnabled, -tagsEnabled: $tagsEnabled, -userAvatarColor: $userAvatarColor, -showSupportBadge: $showSupportBadge, -minimumFaces: $minimumFaces, -}'''; - } - - @override - bool operator ==(covariant Preferences other) { - if (identical(this, other)) { - return true; - } - - return other.foldersEnabled == foldersEnabled && - other.memoriesEnabled == memoriesEnabled && - other.peopleEnabled == peopleEnabled && - other.ratingsEnabled == ratingsEnabled && - other.sharedLinksEnabled == sharedLinksEnabled && - other.tagsEnabled == tagsEnabled && - other.userAvatarColor == userAvatarColor && - other.showSupportBadge == showSupportBadge && - other.minimumFaces == minimumFaces; - } - - @override - int get hashCode { - return foldersEnabled.hashCode ^ - memoriesEnabled.hashCode ^ - peopleEnabled.hashCode ^ - ratingsEnabled.hashCode ^ - sharedLinksEnabled.hashCode ^ - tagsEnabled.hashCode ^ - userAvatarColor.hashCode ^ - showSupportBadge.hashCode ^ - minimumFaces.hashCode; - } } -class License { - final DateTime activatedAt; - final String activationKey; - final String licenseKey; +@freezed +abstract class License with _$License { + const License._(); - const License({required this.activatedAt, required this.activationKey, required this.licenseKey}); - - License copyWith({DateTime? activatedAt, String? activationKey, String? licenseKey}) { - return License( - activatedAt: activatedAt ?? this.activatedAt, - activationKey: activationKey ?? this.activationKey, - licenseKey: licenseKey ?? this.licenseKey, - ); - } - - Map toMap() { - final license = {}; - license["activatedAt"] = activatedAt; - license["activationKey"] = activationKey; - license["licenseKey"] = licenseKey; - return license; - } + const factory License({required DateTime activatedAt, required String activationKey, required String licenseKey}) = + _License; factory License.fromMap(Map map) { return License( @@ -204,27 +70,6 @@ class License { licenseKey: map["licenseKey"]! as String, ); } - - @override - String toString() { - return '''License { -activatedAt: $activatedAt, -activationKey: $activationKey, -licenseKey: $licenseKey, -}'''; - } - - @override - bool operator ==(covariant License other) { - if (identical(this, other)) { - return true; - } - - return activatedAt == other.activatedAt && activationKey == other.activationKey && licenseKey == other.licenseKey; - } - - @override - int get hashCode => activatedAt.hashCode ^ activationKey.hashCode ^ licenseKey.hashCode; } // Model for a user metadata stored in the server diff --git a/mobile/lib/models/auth/auxilary_endpoint.model.dart b/mobile/lib/models/auth/auxilary_endpoint.model.dart index cd11918bed..8686ae9461 100644 --- a/mobile/lib/models/auth/auxilary_endpoint.model.dart +++ b/mobile/lib/models/auth/auxilary_endpoint.model.dart @@ -1,34 +1,14 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; -class AuxilaryEndpoint { - final String url; - final AuxCheckStatus status; +import 'package:freezed_annotation/freezed_annotation.dart'; - const AuxilaryEndpoint({required this.url, required this.status}); +part 'auxilary_endpoint.model.freezed.dart'; - AuxilaryEndpoint copyWith({String? url, AuxCheckStatus? status}) { - return AuxilaryEndpoint(url: url ?? this.url, status: status ?? this.status); - } +@Freezed(fromJson: false, toJson: false) +abstract class AuxilaryEndpoint with _$AuxilaryEndpoint { + const AuxilaryEndpoint._(); - @override - String toString() => 'AuxilaryEndpoint(url: $url, status: $status)'; - - @override - bool operator ==(covariant AuxilaryEndpoint other) { - if (identical(this, other)) { - return true; - } - - return other.url == url && other.status == status; - } - - @override - int get hashCode => url.hashCode ^ status.hashCode; - - Map toMap() { - return {'url': url, 'status': status.toMap()}; - } + const factory AuxilaryEndpoint({required String url, required AuxCheckStatus status}) = _AuxilaryEndpoint; factory AuxilaryEndpoint.fromMap(Map map) { return AuxilaryEndpoint( @@ -37,50 +17,23 @@ class AuxilaryEndpoint { ); } - String toJson() => json.encode(toMap()); - factory AuxilaryEndpoint.fromJson(String source) => AuxilaryEndpoint.fromMap(json.decode(source) as Map); } -class AuxCheckStatus { - final String name; - const AuxCheckStatus({required this.name}); - const AuxCheckStatus._(this.name); +// TODO(agg23): Should be an enum +@freezed +abstract class AuxCheckStatus with _$AuxCheckStatus { + const AuxCheckStatus._(); - static const loading = AuxCheckStatus._('loading'); - static const valid = AuxCheckStatus._('valid'); - static const error = AuxCheckStatus._('error'); - static const unknown = AuxCheckStatus._('unknown'); + const factory AuxCheckStatus({required String name}) = _AuxCheckStatus; - @override - bool operator ==(covariant AuxCheckStatus other) { - if (identical(this, other)) { - return true; - } - - return other.name == name; - } - - @override - int get hashCode => name.hashCode; - - AuxCheckStatus copyWith({String? name}) { - return AuxCheckStatus(name: name ?? this.name); - } - - Map toMap() { - return {'name': name}; - } + static const loading = AuxCheckStatus(name: 'loading'); + static const valid = AuxCheckStatus(name: 'valid'); + static const error = AuxCheckStatus(name: 'error'); + static const unknown = AuxCheckStatus(name: 'unknown'); factory AuxCheckStatus.fromMap(Map map) { return AuxCheckStatus(name: map['name'] as String); } - - String toJson() => json.encode(toMap()); - - factory AuxCheckStatus.fromJson(String source) => AuxCheckStatus.fromMap(json.decode(source) as Map); - - @override - String toString() => 'AuxCheckStatus(name: $name)'; } diff --git a/mobile/lib/models/auth/biometric_status.model.dart b/mobile/lib/models/auth/biometric_status.model.dart index c5c417d06d..691fee3468 100644 --- a/mobile/lib/models/auth/biometric_status.model.dart +++ b/mobile/lib/models/auth/biometric_status.model.dart @@ -1,32 +1,10 @@ -import 'package:collection/collection.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:local_auth/local_auth.dart'; -class BiometricStatus { - final List availableBiometrics; - final bool canAuthenticate; +part 'biometric_status.model.freezed.dart'; - const BiometricStatus({required this.availableBiometrics, required this.canAuthenticate}); - - @override - String toString() => 'BiometricStatus(availableBiometrics: $availableBiometrics, canAuthenticate: $canAuthenticate)'; - - BiometricStatus copyWith({List? availableBiometrics, bool? canAuthenticate}) { - return BiometricStatus( - availableBiometrics: availableBiometrics ?? this.availableBiometrics, - canAuthenticate: canAuthenticate ?? this.canAuthenticate, - ); - } - - @override - bool operator ==(covariant BiometricStatus other) { - if (identical(this, other)) { - return true; - } - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.availableBiometrics, availableBiometrics) && other.canAuthenticate == canAuthenticate; - } - - @override - int get hashCode => availableBiometrics.hashCode ^ canAuthenticate.hashCode; +@freezed +abstract class BiometricStatus with _$BiometricStatus { + const factory BiometricStatus({required List availableBiometrics, required bool canAuthenticate}) = + _BiometricStatus; } diff --git a/mobile/lib/models/cast/cast_manager_state.dart b/mobile/lib/models/cast/cast_manager_state.dart index 9727bc7ed8..0fd3dc877b 100644 --- a/mobile/lib/models/cast/cast_manager_state.dart +++ b/mobile/lib/models/cast/cast_manager_state.dart @@ -1,85 +1,18 @@ -import 'dart:convert'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'cast_manager_state.freezed.dart'; enum CastDestinationType { googleCast } enum CastState { idle, playing, paused, buffering } -class CastManagerState { - final bool isCasting; - final String receiverName; - final CastState castState; - final Duration currentTime; - final Duration duration; - - const CastManagerState({ - required this.isCasting, - required this.receiverName, - required this.castState, - required this.currentTime, - required this.duration, - }); - - CastManagerState copyWith({ - bool? isCasting, - String? receiverName, - CastState? castState, - Duration? currentTime, - Duration? duration, - }) { - return CastManagerState( - isCasting: isCasting ?? this.isCasting, - receiverName: receiverName ?? this.receiverName, - castState: castState ?? this.castState, - currentTime: currentTime ?? this.currentTime, - duration: duration ?? this.duration, - ); - } - - Map toMap() { - final result = {}; - - result.addAll({'isCasting': isCasting}); - result.addAll({'receiverName': receiverName}); - result.addAll({'castState': castState}); - result.addAll({'currentTime': currentTime.inSeconds}); - result.addAll({'duration': duration.inSeconds}); - - return result; - } - - factory CastManagerState.fromMap(Map map) { - return CastManagerState( - isCasting: map['isCasting'] ?? false, - receiverName: map['receiverName'] ?? '', - castState: map['castState'] ?? CastState.idle, - currentTime: Duration(seconds: map['currentTime']?.toInt() ?? 0), - duration: Duration(seconds: map['duration']?.toInt() ?? 0), - ); - } - - String toJson() => json.encode(toMap()); - - factory CastManagerState.fromJson(String source) => CastManagerState.fromMap(json.decode(source)); - - @override - String toString() => - 'CastManagerState(isCasting: $isCasting, receiverName: $receiverName, castState: $castState, currentTime: $currentTime, duration: $duration)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is CastManagerState && - other.isCasting == isCasting && - other.receiverName == receiverName && - other.castState == castState && - other.currentTime == currentTime && - other.duration == duration; - } - - @override - int get hashCode => - isCasting.hashCode ^ receiverName.hashCode ^ castState.hashCode ^ currentTime.hashCode ^ duration.hashCode; +@freezed +abstract class CastManagerState with _$CastManagerState { + const factory CastManagerState({ + required bool isCasting, + required String receiverName, + required CastState castState, + required Duration currentTime, + required Duration duration, + }) = _CastManagerState; } diff --git a/mobile/lib/models/download/download_state.model.dart b/mobile/lib/models/download/download_state.model.dart index bed92d98b6..7e47d60969 100644 --- a/mobile/lib/models/download/download_state.model.dart +++ b/mobile/lib/models/download/download_state.model.dart @@ -1,88 +1,20 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; - -import 'package:background_downloader/background_downloader.dart'; -import 'package:collection/collection.dart'; - -class DownloadInfo { - final String fileName; - final double progress; - // enum - final TaskStatus status; - - const DownloadInfo({required this.fileName, required this.progress, required this.status}); - - DownloadInfo copyWith({String? fileName, double? progress, TaskStatus? status}) { - return DownloadInfo( - fileName: fileName ?? this.fileName, - progress: progress ?? this.progress, - status: status ?? this.status, - ); - } - - Map toMap() { - return {'fileName': fileName, 'progress': progress, 'status': status.index}; - } - - factory DownloadInfo.fromMap(Map map) { - return DownloadInfo( - fileName: map['fileName'] as String, - progress: map['progress'] as double, - status: TaskStatus.values[map['status'] as int], - ); - } - - String toJson() => json.encode(toMap()); - - factory DownloadInfo.fromJson(String source) => DownloadInfo.fromMap(json.decode(source) as Map); - - @override - String toString() => 'DownloadInfo(fileName: $fileName, progress: $progress, status: $status)'; - - @override - bool operator ==(covariant DownloadInfo other) { - if (identical(this, other)) { - return true; - } - - return other.fileName == fileName && other.progress == progress && other.status == status; - } - - @override - int get hashCode => fileName.hashCode ^ progress.hashCode ^ status.hashCode; -} - -class DownloadState { - // enum - final TaskStatus downloadStatus; - final Map taskProgress; - final bool showProgress; - const DownloadState({required this.downloadStatus, required this.taskProgress, required this.showProgress}); - - DownloadState copyWith({TaskStatus? downloadStatus, Map? taskProgress, bool? showProgress}) { - return DownloadState( - downloadStatus: downloadStatus ?? this.downloadStatus, - taskProgress: taskProgress ?? this.taskProgress, - showProgress: showProgress ?? this.showProgress, - ); - } - - @override - String toString() => - 'DownloadState(downloadStatus: $downloadStatus, taskProgress: $taskProgress, showProgress: $showProgress)'; - - @override - bool operator ==(covariant DownloadState other) { - if (identical(this, other)) { - return true; - } - final mapEquals = const DeepCollectionEquality().equals; - - return other.downloadStatus == downloadStatus && - mapEquals(other.taskProgress, taskProgress) && - other.showProgress == showProgress; - } - - @override - int get hashCode => downloadStatus.hashCode ^ taskProgress.hashCode ^ showProgress.hashCode; -} +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:background_downloader/background_downloader.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'download_state.model.freezed.dart'; + +@freezed +abstract class DownloadInfo with _$DownloadInfo { + const factory DownloadInfo({required String fileName, required double progress, required TaskStatus status}) = + _DownloadInfo; +} + +@freezed +abstract class DownloadState with _$DownloadState { + const factory DownloadState({ + required TaskStatus downloadStatus, + required Map taskProgress, + required bool showProgress, + }) = _DownloadState; +} diff --git a/mobile/lib/models/search/search_curated_content.model.dart b/mobile/lib/models/search/search_curated_content.model.dart index 58c4c73264..8778c9b407 100644 --- a/mobile/lib/models/search/search_curated_content.model.dart +++ b/mobile/lib/models/search/search_curated_content.model.dart @@ -1,54 +1,20 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; - -/// A wrapper for [CuratedLocationsResponseDto] objects -/// and [CuratedObjectsResponseDto] to be displayed in -/// a view -class SearchCuratedContent { - /// The label to show associated with this curated object - final String label; - - /// The subtitle to show below the label - final String? subtitle; - - /// The id to lookup the asset from the server - final String id; - - const SearchCuratedContent({required this.label, required this.id, this.subtitle}); - - SearchCuratedContent copyWith({String? label, String? subtitle, String? id}) { - return SearchCuratedContent(label: label ?? this.label, subtitle: subtitle ?? this.subtitle, id: id ?? this.id); - } - - Map toMap() { - return {'label': label, 'subtitle': subtitle, 'id': id}; - } - - factory SearchCuratedContent.fromMap(Map map) { - return SearchCuratedContent( - label: map['label'] as String, - subtitle: map['subtitle'] as String?, - id: map['id'] as String, - ); - } - - String toJson() => json.encode(toMap()); - - factory SearchCuratedContent.fromJson(String source) => - SearchCuratedContent.fromMap(json.decode(source) as Map); - - @override - String toString() => 'CuratedContent(label: $label, subtitle: $subtitle, id: $id)'; - - @override - bool operator ==(covariant SearchCuratedContent other) { - if (identical(this, other)) { - return true; - } - - return other.label == label && other.subtitle == subtitle && other.id == id; - } - - @override - int get hashCode => label.hashCode ^ id.hashCode; -} +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'search_curated_content.model.freezed.dart'; + +/// A wrapper for [CuratedLocationsResponseDto] objects +/// and [CuratedObjectsResponseDto] to be displayed in +/// a view +@freezed +abstract class SearchCuratedContent with _$SearchCuratedContent { + const factory SearchCuratedContent({ + /// The label to show associated with this curated object + required String label, + + /// The id to lookup the asset from the server + required String id, + + /// The subtitle to show below the label + String? subtitle, + }) = _SearchCuratedContent; +} diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 825acb32c6..7e5e1b819e 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -1,10 +1,13 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/utils/option.dart'; +part 'search_filter.model.freezed.dart'; + class SearchLocationFilter { String? country; String? state; @@ -177,52 +180,10 @@ class SearchRatingFilter { int get hashCode => rating.hashCode; } -class SearchDisplayFilters { - bool isNotInAlbum = false; - bool isArchive = false; - bool isFavorite = false; - SearchDisplayFilters({required this.isNotInAlbum, required this.isArchive, required this.isFavorite}); - - SearchDisplayFilters copyWith({bool? isNotInAlbum, bool? isArchive, bool? isFavorite}) { - return SearchDisplayFilters( - isNotInAlbum: isNotInAlbum ?? this.isNotInAlbum, - isArchive: isArchive ?? this.isArchive, - isFavorite: isFavorite ?? this.isFavorite, - ); - } - - Map toMap() { - return {'isNotInAlbum': isNotInAlbum, 'isArchive': isArchive, 'isFavorite': isFavorite}; - } - - factory SearchDisplayFilters.fromMap(Map map) { - return SearchDisplayFilters( - isNotInAlbum: map['isNotInAlbum'] as bool, - isArchive: map['isArchive'] as bool, - isFavorite: map['isFavorite'] as bool, - ); - } - - String toJson() => json.encode(toMap()); - - factory SearchDisplayFilters.fromJson(String source) => - SearchDisplayFilters.fromMap(json.decode(source) as Map); - - @override - String toString() => - 'SearchDisplayFilters(isNotInAlbum: $isNotInAlbum, isArchive: $isArchive, isFavorite: $isFavorite)'; - - @override - bool operator ==(covariant SearchDisplayFilters other) { - if (identical(this, other)) { - return true; - } - - return other.isNotInAlbum == isNotInAlbum && other.isArchive == isArchive && other.isFavorite == isFavorite; - } - - @override - int get hashCode => isNotInAlbum.hashCode ^ isArchive.hashCode ^ isFavorite.hashCode; +@freezed +abstract class SearchDisplayFilters with _$SearchDisplayFilters { + const factory SearchDisplayFilters({required bool isNotInAlbum, required bool isArchive, required bool isFavorite}) = + _SearchDisplayFilters; } class SearchFilter { diff --git a/mobile/lib/presentation/actions/similar_photos.action.dart b/mobile/lib/presentation/actions/similar_photos.action.dart index 3c0b7b129b..d8f63f245d 100644 --- a/mobile/lib/presentation/actions/similar_photos.action.dart +++ b/mobile/lib/presentation/actions/similar_photos.action.dart @@ -31,7 +31,7 @@ class SimilarPhotosAction extends ActionBuilder { location: .new(), camera: .new(), date: .new(), - display: .new(isNotInAlbum: false, isArchive: false, isFavorite: false), + display: const .new(isNotInAlbum: false, isArchive: false, isFavorite: false), rating: .new(), mediaType: .other, ), diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 12935b4f23..63e48e0c33 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -59,7 +59,7 @@ class DriftSearchPage extends HookConsumerWidget { location: SearchLocationFilter(), camera: SearchCameraFilter(), date: SearchDateFilter(), - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), + display: const SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), rating: SearchRatingFilter(), mediaType: AssetType.other, language: "${context.locale.languageCode}-${context.locale.countryCode}", @@ -455,9 +455,9 @@ class DriftSearchPage extends HookConsumerWidget { void handleOnSelect(Map value) { display = display.copyWith( - isNotInAlbum: value[DisplayOption.notInAlbum], - isArchive: value[DisplayOption.archive], - isFavorite: value[DisplayOption.favorite], + isNotInAlbum: value[DisplayOption.notInAlbum] ?? display.isNotInAlbum, + isArchive: value[DisplayOption.archive] ?? display.isArchive, + isFavorite: value[DisplayOption.favorite] ?? display.isFavorite, ); } @@ -465,7 +465,7 @@ class DriftSearchPage extends HookConsumerWidget { displayOptionCurrentFilterWidget.value = null; search( filter.value.copyWith( - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), + display: const SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), ), ); } diff --git a/mobile/lib/presentation/widgets/timeline/timeline.state.dart b/mobile/lib/presentation/widgets/timeline/timeline.state.dart index bcdc3b4783..adad06b987 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.state.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.state.dart @@ -12,45 +12,17 @@ import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; part 'timeline.state.freezed.dart'; -class TimelineArgs { - final double maxWidth; - final double maxHeight; - final double spacing; - final int columnCount; - final bool showStorageIndicator; - final bool withStack; - final GroupAssetsBy? groupBy; - - const TimelineArgs({ - required this.maxWidth, - required this.maxHeight, - this.spacing = kTimelineSpacing, - this.columnCount = kTimelineColumnCount, - this.showStorageIndicator = false, - this.withStack = false, - this.groupBy, - }); - - @override - bool operator ==(covariant TimelineArgs other) { - return spacing == other.spacing && - maxWidth == other.maxWidth && - maxHeight == other.maxHeight && - columnCount == other.columnCount && - showStorageIndicator == other.showStorageIndicator && - withStack == other.withStack && - groupBy == other.groupBy; - } - - @override - int get hashCode => - maxWidth.hashCode ^ - maxHeight.hashCode ^ - spacing.hashCode ^ - columnCount.hashCode ^ - showStorageIndicator.hashCode ^ - withStack.hashCode ^ - groupBy.hashCode; +@freezed +abstract class TimelineArgs with _$TimelineArgs { + const factory TimelineArgs({ + required double maxWidth, + required double maxHeight, + @Default(kTimelineSpacing) double spacing, + @Default(kTimelineColumnCount) int columnCount, + @Default(false) bool showStorageIndicator, + @Default(false) bool withStack, + GroupAssetsBy? groupBy, + }) = _TimelineArgs; } @freezed diff --git a/mobile/lib/presentation/widgets/timeline/timeline_drag_region.dart b/mobile/lib/presentation/widgets/timeline/timeline_drag_region.dart index 9ffcc3b23b..892d0c6102 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline_drag_region.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline_drag_region.dart @@ -4,6 +4,9 @@ import 'package:collection/collection.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'timeline_drag_region.freezed.dart'; class TimelineDragRegion extends StatefulWidget { final Widget child; @@ -206,21 +209,7 @@ class _TimelineAssetIndexProxy extends RenderProxyBox { _TimelineAssetIndexProxy({required this.index}); } -class TimelineAssetIndex { - final int assetIndex; - final int segmentIndex; - - const TimelineAssetIndex({required this.assetIndex, required this.segmentIndex}); - - @override - bool operator ==(covariant TimelineAssetIndex other) { - if (identical(this, other)) { - return true; - } - - return other.assetIndex == assetIndex && other.segmentIndex == segmentIndex; - } - - @override - int get hashCode => assetIndex.hashCode ^ segmentIndex.hashCode; +@freezed +abstract class TimelineAssetIndex with _$TimelineAssetIndex { + const factory TimelineAssetIndex({required int assetIndex, required int segmentIndex}) = _TimelineAssetIndex; } diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart index 1756d0fd30..c3e1098e4b 100644 --- a/mobile/lib/providers/backup/drift_backup.provider.dart +++ b/mobile/lib/providers/backup/drift_backup.provider.dart @@ -44,87 +44,22 @@ abstract class DriftUploadStatus with _$DriftUploadStatus { enum BackupError { none, syncFailed } -class DriftBackupState { - final int totalCount; - final int backupCount; - final int remainderCount; - final int processingCount; +@freezed +abstract class DriftBackupState with _$DriftBackupState { + const DriftBackupState._(); - final bool isSyncing; - final BackupError error; - - final Map uploadItems; - - final Map iCloudDownloadProgress; - - const DriftBackupState({ - required this.totalCount, - required this.backupCount, - required this.remainderCount, - required this.processingCount, - required this.isSyncing, - this.error = BackupError.none, - required this.uploadItems, - this.iCloudDownloadProgress = const {}, - }); - - DriftBackupState copyWith({ - int? totalCount, - int? backupCount, - int? remainderCount, - int? processingCount, - bool? isSyncing, - BackupError? error, - Map? uploadItems, - Map? iCloudDownloadProgress, - }) { - return DriftBackupState( - totalCount: totalCount ?? this.totalCount, - backupCount: backupCount ?? this.backupCount, - remainderCount: remainderCount ?? this.remainderCount, - processingCount: processingCount ?? this.processingCount, - isSyncing: isSyncing ?? this.isSyncing, - error: error ?? this.error, - uploadItems: uploadItems ?? this.uploadItems, - iCloudDownloadProgress: iCloudDownloadProgress ?? this.iCloudDownloadProgress, - ); - } + const factory DriftBackupState({ + required int totalCount, + required int backupCount, + required int remainderCount, + required int processingCount, + required bool isSyncing, + @Default(BackupError.none) BackupError error, + required Map uploadItems, + @Default({}) Map iCloudDownloadProgress, + }) = _DriftBackupState; int get errorCount => uploadItems.values.where((item) => item.isFailed == true).length; - - @override - String toString() { - return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, isSyncing: $isSyncing, error: $error, uploadItems: $uploadItems, iCloudDownloadProgress: $iCloudDownloadProgress)'; - } - - @override - bool operator ==(covariant DriftBackupState other) { - if (identical(this, other)) { - return true; - } - final mapEquals = const DeepCollectionEquality().equals; - - return other.totalCount == totalCount && - other.backupCount == backupCount && - other.remainderCount == remainderCount && - other.processingCount == processingCount && - other.isSyncing == isSyncing && - other.error == error && - mapEquals(other.iCloudDownloadProgress, iCloudDownloadProgress) && - mapEquals(other.uploadItems, uploadItems); - } - - @override - int get hashCode { - return totalCount.hashCode ^ - backupCount.hashCode ^ - remainderCount.hashCode ^ - processingCount.hashCode ^ - isSyncing.hashCode ^ - error.hashCode ^ - uploadItems.hashCode ^ - iCloudDownloadProgress.hashCode; - } } final driftBackupProvider = StateNotifierProvider((ref) { diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index 4457b9f831..b76a8213c6 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -2,7 +2,7 @@ import 'dart:async'; -import 'package:collection/collection.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -17,30 +17,17 @@ import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:logging/logging.dart'; -class RemoteAlbumState { - final List albums; +part 'remote_album.provider.freezed.dart'; - const RemoteAlbumState({required this.albums}); +@Freezed(toStringOverride: false) +abstract class RemoteAlbumState with _$RemoteAlbumState { + const RemoteAlbumState._(); - RemoteAlbumState copyWith({List? albums}) { - return RemoteAlbumState(albums: albums ?? this.albums); - } + const factory RemoteAlbumState({required List albums}) = _RemoteAlbumState; + // Explicitly don't log albums @override String toString() => 'RemoteAlbumState(albums: ${albums.length})'; - - @override - bool operator ==(covariant RemoteAlbumState other) { - if (identical(this, other)) { - return true; - } - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.albums, albums); - } - - @override - int get hashCode => albums.hashCode; } class RemoteAlbumNotifier extends Notifier { diff --git a/mobile/lib/providers/search/search_filter.provider.dart b/mobile/lib/providers/search/search_filter.provider.dart index b171de50c7..5da0a11fe7 100644 --- a/mobile/lib/providers/search/search_filter.provider.dart +++ b/mobile/lib/providers/search/search_filter.provider.dart @@ -1,34 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/services/search.service.dart'; import 'package:openapi/api.dart'; -class SearchSuggestionArgs { - SearchSuggestionType type; - final String? locationCountry; - final String? locationState; - final String? make; - final String? model; +part 'search_filter.provider.freezed.dart'; - SearchSuggestionArgs({required this.type, this.locationCountry, this.locationState, this.make, this.model}); - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is SearchSuggestionArgs && - other.type == type && - other.locationCountry == locationCountry && - other.locationState == locationState && - other.make == make && - other.model == model; - } - - @override - int get hashCode { - return type.hashCode ^ locationCountry.hashCode ^ locationState.hashCode ^ make.hashCode ^ model.hashCode; - } +@freezed +abstract class SearchSuggestionArgs with _$SearchSuggestionArgs { + const factory SearchSuggestionArgs({ + required SearchSuggestionType type, + String? locationCountry, + String? locationState, + String? make, + String? model, + }) = _SearchSuggestionArgs; } final getSearchSuggestionsProvider = FutureProvider.autoDispose.family, SearchSuggestionArgs>(( diff --git a/mobile/lib/providers/timeline/multiselect.provider.dart b/mobile/lib/providers/timeline/multiselect.provider.dart index 36de8d5e4f..f74201e983 100644 --- a/mobile/lib/providers/timeline/multiselect.provider.dart +++ b/mobile/lib/providers/timeline/multiselect.provider.dart @@ -1,22 +1,27 @@ // ignore_for_file: use-ref-and-state-synchronously -import 'package:collection/collection.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +part 'multiselect.provider.freezed.dart'; + final multiSelectProvider = NotifierProvider( MultiSelectNotifier.new, dependencies: [timelineServiceProvider], ); -class MultiSelectState { - final Set selectedAssets; - final Set lockedSelectionAssets; - final bool forceEnable; +@freezed +abstract class MultiSelectState with _$MultiSelectState { + const MultiSelectState._(); - const MultiSelectState({required this.selectedAssets, required this.lockedSelectionAssets, this.forceEnable = false}); + const factory MultiSelectState({ + required Set selectedAssets, + required Set lockedSelectionAssets, + @Default(false) bool forceEnable, + }) = _MultiSelectState; bool get isEnabled => selectedAssets.isNotEmpty; @@ -29,37 +34,6 @@ class MultiSelectState { bool get onlyLocal => selectedAssets.any((asset) => asset.storage == AssetState.local); bool get onlyRemote => selectedAssets.any((asset) => asset.storage == AssetState.remote); - - MultiSelectState copyWith({ - Set? selectedAssets, - Set? lockedSelectionAssets, - bool? forceEnable, - }) { - return MultiSelectState( - selectedAssets: selectedAssets ?? this.selectedAssets, - lockedSelectionAssets: lockedSelectionAssets ?? this.lockedSelectionAssets, - forceEnable: forceEnable ?? this.forceEnable, - ); - } - - @override - String toString() => - 'MultiSelectState(selectedAssets: $selectedAssets, lockedSelectionAssets: $lockedSelectionAssets, forceEnable: $forceEnable)'; - - @override - bool operator ==(covariant MultiSelectState other) { - if (identical(this, other)) { - return true; - } - final setEquals = const DeepCollectionEquality().equals; - - return setEquals(other.selectedAssets, selectedAssets) && - setEquals(other.lockedSelectionAssets, lockedSelectionAssets) && - other.forceEnable == forceEnable; - } - - @override - int get hashCode => selectedAssets.hashCode ^ lockedSelectionAssets.hashCode ^ forceEnable.hashCode; } class MultiSelectNotifier extends Notifier { diff --git a/mobile/lib/providers/upload_profile_image.provider.dart b/mobile/lib/providers/upload_profile_image.provider.dart index 5e7f7fc732..9b5e83622e 100644 --- a/mobile/lib/providers/upload_profile_image.provider.dart +++ b/mobile/lib/providers/upload_profile_image.provider.dart @@ -1,60 +1,18 @@ -import 'dart:convert'; - +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/utils/debug_print.dart'; +part 'upload_profile_image.provider.freezed.dart'; + enum UploadProfileStatus { idle, loading, success, failure } -class UploadProfileImageState { - // enum - final UploadProfileStatus status; - final String profileImagePath; - const UploadProfileImageState({required this.status, required this.profileImagePath}); - - UploadProfileImageState copyWith({UploadProfileStatus? status, String? profileImagePath}) { - return UploadProfileImageState( - status: status ?? this.status, - profileImagePath: profileImagePath ?? this.profileImagePath, - ); - } - - Map toMap() { - final result = {}; - - result.addAll({'status': status.index}); - result.addAll({'profileImagePath': profileImagePath}); - - return result; - } - - factory UploadProfileImageState.fromMap(Map map) { - return UploadProfileImageState( - status: UploadProfileStatus.values[map['status'] ?? 0], - profileImagePath: map['profileImagePath'] ?? '', - ); - } - - String toJson() => json.encode(toMap()); - - factory UploadProfileImageState.fromJson(String source) => UploadProfileImageState.fromMap(json.decode(source)); - - @override - String toString() => 'UploadProfileImageState(status: $status, profileImagePath: $profileImagePath)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is UploadProfileImageState && other.status == status && other.profileImagePath == profileImagePath; - } - - @override - int get hashCode => status.hashCode ^ profileImagePath.hashCode; +@freezed +abstract class UploadProfileImageState with _$UploadProfileImageState { + const factory UploadProfileImageState({required UploadProfileStatus status, required String profileImagePath}) = + _UploadProfileImageState; } class UploadProfileImageNotifier extends StateNotifier { diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart index 5312107f6c..b2a134389f 100644 --- a/mobile/lib/services/background_upload.service.dart +++ b/mobile/lib/services/background_upload.service.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart'; @@ -25,6 +26,8 @@ import 'package:logging/logging.dart'; import 'package:openapi/api.dart' as api; import 'package:path/path.dart' as p; +part 'background_upload.service.freezed.dart'; + final backgroundUploadServiceProvider = Provider((ref) { final service = BackgroundUploadService( ref.watch(uploadRepositoryProvider), @@ -39,20 +42,15 @@ final backgroundUploadServiceProvider = Provider((ref) { }); /// Metadata for upload tasks to track live photo handling -class UploadTaskMetadata { - final String localAssetId; - final bool isLivePhotos; - final String livePhotoVideoId; +@Freezed(fromJson: false, toJson: false) +abstract class UploadTaskMetadata with _$UploadTaskMetadata { + const UploadTaskMetadata._(); - const UploadTaskMetadata({required this.localAssetId, required this.isLivePhotos, required this.livePhotoVideoId}); - - UploadTaskMetadata copyWith({String? localAssetId, bool? isLivePhotos, String? livePhotoVideoId}) { - return UploadTaskMetadata( - localAssetId: localAssetId ?? this.localAssetId, - isLivePhotos: isLivePhotos ?? this.isLivePhotos, - livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, - ); - } + const factory UploadTaskMetadata({ + required String localAssetId, + required bool isLivePhotos, + required String livePhotoVideoId, + }) = _UploadTaskMetadata; Map toMap() { return { @@ -70,28 +68,10 @@ class UploadTaskMetadata { ); } - String toJson() => json.encode(toMap()); - factory UploadTaskMetadata.fromJson(String source) => UploadTaskMetadata.fromMap(json.decode(source) as Map); - @override - String toString() => - 'UploadTaskMetadata(localAssetId: $localAssetId, isLivePhotos: $isLivePhotos, livePhotoVideoId: $livePhotoVideoId)'; - - @override - bool operator ==(covariant UploadTaskMetadata other) { - if (identical(this, other)) { - return true; - } - - return other.localAssetId == localAssetId && - other.isLivePhotos == isLivePhotos && - other.livePhotoVideoId == livePhotoVideoId; - } - - @override - int get hashCode => localAssetId.hashCode ^ isLivePhotos.hashCode ^ livePhotoVideoId.hashCode; + String toJson() => json.encode(toMap()); } /// Service for handling background uploads using iOS URLSession (background_downloader) diff --git a/mobile/lib/widgets/search/search_filter/camera_picker.dart b/mobile/lib/widgets/search/search_filter/camera_picker.dart index 6a025bdb69..24a463eedb 100644 --- a/mobile/lib/widgets/search/search_filter/camera_picker.dart +++ b/mobile/lib/widgets/search/search_filter/camera_picker.dart @@ -21,7 +21,9 @@ class CameraPicker extends HookConsumerWidget { final selectedMake = useState(filter?.make); final selectedModel = useState(filter?.model); - final make = ref.watch(getSearchSuggestionsProvider(SearchSuggestionArgs(type: SearchSuggestionType.cameraMake))); + final make = ref.watch( + getSearchSuggestionsProvider(const SearchSuggestionArgs(type: SearchSuggestionType.cameraMake)), + ); final models = ref.watch( getSearchSuggestionsProvider( From 565fbe49178162efd06240ce4640e66f0368bebb Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:41:11 +0530 Subject: [PATCH 17/49] chore: migrate mobile translations accessor (1) (#30667) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/bin/generate_keys.dart | 30 +++++++++++++---- mobile/lib/main.dart | 6 +--- .../models/server_info/server_info.model.dart | 8 ++--- mobile/lib/pages/common/app_log.page.dart | 5 +-- .../lib/pages/common/app_log_detail.page.dart | 6 ++-- mobile/lib/pages/common/download_panel.dart | 18 +++++------ .../pages/common/headers_settings.page.dart | 9 +++--- mobile/lib/pages/common/tab_shell.page.dart | 10 +++--- .../pages/share_intent/share_intent.page.dart | 24 +++++++------- .../pages/dev/media_stat.page.dart | 10 +++--- .../pages/drift_asset_troubleshoot.page.dart | 16 +++++----- mobile/lib/providers/local_auth.provider.dart | 13 ++++---- .../lib/providers/sync_status.provider.dart | 11 ++++--- .../repositories/biometric.repository.dart | 4 +-- .../services/foreground_upload.service.dart | 23 ++++++------- mobile/lib/utils/action_button.utils.dart | 6 ++-- mobile/lib/utils/bootstrap.dart | 20 ++++++------ mobile/lib/utils/people.utils.dart | 9 +++--- .../common/app_bar_dialog/app_bar_dialog.dart | 32 +++++++++++-------- .../app_bar_dialog/app_bar_profile_info.dart | 4 +-- .../app_bar_dialog/app_bar_server_info.dart | 10 +++--- .../server_update_notification.dart | 6 ++-- .../lib/widgets/common/date_time_picker.dart | 17 +++++----- .../widgets/common/dropdown_search_menu.dart | 4 +-- .../widgets/common/immich_sliver_app_bar.dart | 21 +++++++----- .../lib/widgets/common/location_picker.dart | 32 +++++++++---------- .../common/mesmerizing_sliver_app_bar.dart | 4 +-- .../widgets/common/scaffold_error_body.dart | 8 +++-- .../common/selection_sliver_app_bar.dart | 6 ++-- mobile/lib/widgets/common/tag_picker.dart | 12 +++---- 30 files changed, 207 insertions(+), 177 deletions(-) diff --git a/mobile/bin/generate_keys.dart b/mobile/bin/generate_keys.dart index 1ce643be12..160f2c5b5c 100644 --- a/mobile/bin/generate_keys.dart +++ b/mobile/bin/generate_keys.dart @@ -97,6 +97,15 @@ const _kIntParamNames = [ 'quantity', ]; +const _kParamTypeOverrides = { + 'advanced_settings_clear_image_cache_success.size': 'String', + 'backup_controller_page_storage_format.total': 'String', + 'backup_controller_page_storage_format.used': 'String', + 'cleanup_found_assets_with_size.size': 'String', +}; + +final _usedParamTypeOverrides = {}; + void main() async { final sourceFile = File('../i18n/en.json'); if (!await sourceFile.exists()) { @@ -143,6 +152,11 @@ class TranslationParam { Future _generateTranslations(Map translations, File output) async { final root = _buildTranslationTree('', translations); + final staleOverrides = _kParamTypeOverrides.keys.toSet().difference(_usedParamTypeOverrides); + if (staleOverrides.isNotEmpty) { + throw StateError('_kParamTypeOverrides has entries matching no plain {placeholder}: ${staleOverrides.join(', ')}'); + } + final buffer = StringBuffer(''' // DO NOT EDIT. This is code generated via generate_keys.dart @@ -194,21 +208,22 @@ class Translations extends _BaseTranslations { await output.writeAsString(buffer.toString()); } -TranslationNode _buildTranslationTree(String key, dynamic value) { +TranslationNode _buildTranslationTree(String key, dynamic value, [String parentKey = '']) { + final fullKey = parentKey.isEmpty ? key : '$parentKey.$key'; if (value is Map) { final children = {}; for (final entry in value.entries) { - children[entry.key] = _buildTranslationTree(entry.key, entry.value); + children[entry.key] = _buildTranslationTree(entry.key, entry.value, fullKey); } return TranslationNode(key: key, children: children); } else { final stringValue = value.toString(); - final params = _extractParams(stringValue); + final params = _extractParams(stringValue, fullKey); return TranslationNode(key: key, value: stringValue, params: params); } } -List _extractParams(String value) { +List _extractParams(String value, String fullKey) { final params = {}; final icuRegex = RegExp(r'\{(\w+),\s*(plural|select|number|date|time)([^}]*(?:\{[^}]*\}[^}]*)*)\}'); @@ -264,8 +279,11 @@ List _extractParams(String value) { continue; } - String type; - if (_kIntParamNames.contains(name.toLowerCase())) { + final String type; + if (_kParamTypeOverrides['$fullKey.$name'] case final override?) { + type = override; + _usedParamTypeOverrides.add('$fullKey.$name'); + } else if (_kIntParamNames.contains(name.toLowerCase())) { type = 'int'; } else { type = 'Object'; diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 317733f1de..d2fd945cbf 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -16,7 +16,6 @@ import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/domain/services/background_worker.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; @@ -273,10 +272,7 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve darkTheme: getThemeData(colorScheme: immichTheme.dark, locale: context.locale), theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale), builder: (context, child) => ImmichTranslationProvider( - translations: ImmichTranslations( - submit: "submit".t(context: context), - password: "password".t(context: context), - ), + translations: ImmichTranslations(submit: context.t.submit, password: context.t.password), child: ImmichThemeProvider(colorScheme: context.colorScheme, child: child!), ), routerConfig: router.config( diff --git a/mobile/lib/models/server_info/server_info.model.dart b/mobile/lib/models/server_info/server_info.model.dart index adc55d3f4e..1ed7dd6f8b 100644 --- a/mobile/lib/models/server_info/server_info.model.dart +++ b/mobile/lib/models/server_info/server_info.model.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/server_info/server_config.model.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; import 'package:immich_mobile/models/server_info/server_features.model.dart'; @@ -16,9 +16,9 @@ enum VersionStatus { String get message => switch (this) { VersionStatus.upToDate => "", - VersionStatus.clientOutOfDate => "app_update_available".tr(), - VersionStatus.serverOutOfDate => "server_update_available".tr(), - VersionStatus.error => "unable_to_check_version".tr(), + VersionStatus.clientOutOfDate => StaticTranslations.instance.app_update_available, + VersionStatus.serverOutOfDate => StaticTranslations.instance.server_update_available, + VersionStatus.error => StaticTranslations.instance.unable_to_check_version, }; } diff --git a/mobile/lib/pages/common/app_log.page.dart b/mobile/lib/pages/common/app_log.page.dart index 370d6a106d..d0b5096288 100644 --- a/mobile/lib/pages/common/app_log.page.dart +++ b/mobile/lib/pages/common/app_log.page.dart @@ -1,15 +1,16 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/immich_logger.service.dart'; +import 'package:intl/intl.dart'; @RoutePage() class AppLogPage extends HookWidget { @@ -50,7 +51,7 @@ class AppLogPage extends HookWidget { return Scaffold( appBar: AppBar( - title: Text('logs'.tr(), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0)), + title: Text(context.t.logs, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0)), scrolledUnderElevation: 1, elevation: 2, actions: [ diff --git a/mobile/lib/pages/common/app_log_detail.page.dart b/mobile/lib/pages/common/app_log_detail.page.dart index bc3d26a019..8e7eaf3ba8 100644 --- a/mobile/lib/pages/common/app_log_detail.page.dart +++ b/mobile/lib/pages/common/app_log_detail.page.dart @@ -1,12 +1,12 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; @RoutePage() class AppLogDetailPage extends HookWidget { @@ -44,7 +44,7 @@ class AppLogDetailPage extends HookWidget { context.scaffoldMessenger.showSnackBar( SnackBar( content: Text( - "copied_to_clipboard".tr(), + context.t.copied_to_clipboard, style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), ), @@ -106,7 +106,7 @@ class AppLogDetailPage extends HookWidget { } return Scaffold( - appBar: AppBar(title: Text("log_detail_title".tr())), + appBar: AppBar(title: Text(context.t.log_detail_title)), body: SafeArea( child: ListView( children: [ diff --git a/mobile/lib/pages/common/download_panel.dart b/mobile/lib/pages/common/download_panel.dart index 2015948fad..9ee5833b83 100644 --- a/mobile/lib/pages/common/download_panel.dart +++ b/mobile/lib/pages/common/download_panel.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'package:background_downloader/background_downloader.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; class DownloadPanel extends ConsumerWidget { @@ -67,14 +67,14 @@ class DownloadTaskTile extends StatelessWidget { final progressPercent = (progress * 100).round(); String getStatusText() => switch (status) { - TaskStatus.running => 'downloading'.tr(), - TaskStatus.complete => 'download_complete'.tr(), - TaskStatus.failed => 'download_failed'.tr(), - TaskStatus.canceled => 'download_canceled'.tr(), - TaskStatus.paused => 'download_paused'.tr(), - TaskStatus.enqueued => 'download_enqueue'.tr(), - TaskStatus.notFound => 'download_notfound'.tr(), - TaskStatus.waitingToRetry => 'download_waiting_to_retry'.tr(), + TaskStatus.running => context.t.downloading, + TaskStatus.complete => context.t.download_complete, + TaskStatus.failed => context.t.download_failed, + TaskStatus.canceled => context.t.download_canceled, + TaskStatus.paused => context.t.download_paused, + TaskStatus.enqueued => context.t.download_enqueue, + TaskStatus.notFound => context.t.download_notfound, + TaskStatus.waitingToRetry => context.t.download_waiting_to_retry, }; return SizedBox( diff --git a/mobile/lib/pages/common/headers_settings.page.dart b/mobile/lib/pages/common/headers_settings.page.dart index e0228eecac..24c5da1c10 100644 --- a/mobile/lib/pages/common/headers_settings.page.dart +++ b/mobile/lib/pages/common/headers_settings.page.dart @@ -1,5 +1,4 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -64,7 +63,7 @@ class HeaderSettingsPage extends HookConsumerWidget { headers.value = headers.value.toList(); }, icon: const Icon(Icons.add_outlined), - tooltip: 'header_settings_add_header_tip'.tr(), + tooltip: context.t.header_settings_add_header_tip, ), ], ), @@ -111,7 +110,7 @@ class HeaderKeyValueSettings extends StatelessWidget { String? emptyFieldValidator(String? value) { if (value == null || value.isEmpty) { - return 'header_settings_field_validator_msg'.tr(); + return StaticTranslations.instance.header_settings_field_validator_msg; } return null; @@ -129,7 +128,7 @@ class HeaderKeyValueSettings extends StatelessWidget { child: TextFormField( controller: keyController, decoration: InputDecoration( - labelText: 'header_settings_header_name_input'.tr(), + labelText: context.t.header_settings_header_name_input, border: const OutlineInputBorder(), ), autocorrect: false, @@ -157,7 +156,7 @@ class HeaderKeyValueSettings extends StatelessWidget { child: TextFormField( controller: valueController, decoration: InputDecoration( - labelText: 'header_settings_header_value_input'.tr(), + labelText: context.t.header_settings_header_value_input, border: const OutlineInputBorder(), ), autocorrect: false, diff --git a/mobile/lib/pages/common/tab_shell.page.dart b/mobile/lib/pages/common/tab_shell.page.dart index f834e4ac51..5b620e9c91 100644 --- a/mobile/lib/pages/common/tab_shell.page.dart +++ b/mobile/lib/pages/common/tab_shell.page.dart @@ -1,13 +1,13 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; @@ -34,24 +34,24 @@ class _TabShellPageState extends ConsumerState { final navigationDestinations = [ NavigationDestination( - label: 'photos'.tr(), + label: context.t.photos, icon: const Icon(Icons.photo_library_outlined), selectedIcon: Icon(Icons.photo_library, color: context.primaryColor), ), NavigationDestination( - label: 'search'.tr(), + label: context.t.search, icon: const Icon(Icons.search_rounded), selectedIcon: Icon(Icons.search, color: context.primaryColor), enabled: !isReadonlyModeEnabled, ), NavigationDestination( - label: 'albums'.tr(), + label: context.t.albums, icon: const Icon(Icons.photo_album_outlined), selectedIcon: Icon(Icons.photo_album_rounded, color: context.primaryColor), enabled: !isReadonlyModeEnabled, ), NavigationDestination( - label: 'library'.tr(), + label: context.t.library$, icon: const Icon(Icons.space_dashboard_outlined), selectedIcon: Icon(Icons.space_dashboard_rounded, color: context.primaryColor), enabled: !isReadonlyModeEnabled, diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart index db5e583d7a..5b651fbc8f 100644 --- a/mobile/lib/pages/share_intent/share_intent.page.dart +++ b/mobile/lib/pages/share_intent/share_intent.page.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; @@ -58,7 +58,7 @@ class ShareIntentPage extends ConsumerWidget { appBar: AppBar( title: Column( children: [ - const Text('upload_to_immich').tr(namedArgs: {'count': candidates.length.toString()}), + Text(context.t.upload_to_immich(count: candidates.length)), Text( currentEndpoint, style: context.textTheme.labelMedium?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), @@ -130,7 +130,7 @@ class ShareIntentPage extends ConsumerWidget { height: 48, child: ElevatedButton( onPressed: (isUploading || isUploaded) ? null : upload, - child: (isUploading || isUploaded) ? UploadingText(candidates: candidates) : const Text('upload').tr(), + child: (isUploading || isUploaded) ? UploadingText(candidates: candidates) : Text(context.t.upload), ), ), ), @@ -149,9 +149,7 @@ class UploadingText extends StatelessWidget { return element.status == UploadStatus.complete; }).length; - return const Text( - "shared_intent_upload_button_progress_text", - ).tr(namedArgs: {'current': uploadedCount.toString(), 'total': candidates.length.toString()}); + return Text(context.t.shared_intent_upload_button_progress_text(current: uploadedCount, total: candidates.length)); } } @@ -168,7 +166,7 @@ class UploadStatusIcon extends StatelessWidget { return Icon( Icons.check_circle_outline_rounded, color: context.colorScheme.onSurface.withAlpha(100), - semanticLabel: 'not_selected'.tr(), + semanticLabel: context.t.not_selected, ); } @@ -176,7 +174,7 @@ class UploadStatusIcon extends StatelessWidget { UploadStatus.enqueued => Icon( Icons.check_circle_rounded, color: context.primaryColor, - semanticLabel: 'enqueued'.tr(), + semanticLabel: context.t.enqueued, ), UploadStatus.running => Stack( alignment: AlignmentDirectional.center, @@ -191,7 +189,7 @@ class UploadStatusIcon extends StatelessWidget { backgroundColor: context.colorScheme.surfaceContainerLow, strokeWidth: 3, value: value, - semanticsLabel: 'uploading'.tr(), + semanticsLabel: context.t.uploading, ), ), ), @@ -201,8 +199,12 @@ class UploadStatusIcon extends StatelessWidget { ), ], ), - UploadStatus.complete => Icon(Icons.check_circle_rounded, color: Colors.green, semanticLabel: 'completed'.tr()), - UploadStatus.failed => Icon(Icons.error_rounded, color: Colors.red, semanticLabel: 'failed'.tr()), + UploadStatus.complete => Icon( + Icons.check_circle_rounded, + color: Colors.green, + semanticLabel: context.t.completed, + ), + UploadStatus.failed => Icon(Icons.error_rounded, color: Colors.red, semanticLabel: context.t.failed), }; return statusIcon; diff --git a/mobile/lib/presentation/pages/dev/media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart index 4c18a09200..b5a95156d8 100644 --- a/mobile/lib/presentation/pages/dev/media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -1,9 +1,9 @@ import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; @@ -56,7 +56,7 @@ class LocalMediaSummaryPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('local_media_summary'.tr())), + appBar: AppBar(title: Text(context.t.local_media_summary)), body: Consumer( builder: (ctx, ref, __) { final db = ref.watch(driftProvider); @@ -79,7 +79,7 @@ class LocalMediaSummaryPage extends StatelessWidget { const Divider(), Padding( padding: const EdgeInsets.only(left: 15), - child: Text("album_summary".tr(), style: ctx.textTheme.titleMedium), + child: Text(ctx.t.album_summary, style: ctx.textTheme.titleMedium), ), ], ), @@ -136,7 +136,7 @@ class RemoteMediaSummaryPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('remote_media_summary'.tr())), + appBar: AppBar(title: Text(context.t.remote_media_summary)), body: Consumer( builder: (ctx, ref, __) { final db = ref.watch(driftProvider); @@ -159,7 +159,7 @@ class RemoteMediaSummaryPage extends StatelessWidget { const Divider(), Padding( padding: const EdgeInsets.only(left: 15), - child: Text("album_summary".tr(), style: ctx.textTheme.titleMedium), + child: Text(ctx.t.album_summary, style: ctx.textTheme.titleMedium), ), ], ), diff --git a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart index 7e98d25cf0..81d765239b 100644 --- a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart +++ b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart @@ -1,12 +1,12 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; @RoutePage() @@ -18,7 +18,7 @@ class AssetTroubleshootPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('asset_troubleshoot'.tr())), + appBar: AppBar(title: Text(context.t.asset_troubleshoot)), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(16.0), @@ -42,7 +42,7 @@ class _AssetDetailsView extends StatelessWidget { _AssetPropertiesSection(asset: asset), const SizedBox(height: 16), Text( - 'matching_assets'.tr(), + context.t.matching_assets, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), if (asset.checksum != null) ...[ @@ -52,12 +52,12 @@ class _AssetDetailsView extends StatelessWidget { ] else ...[ _PropertySectionCard( title: 'Local Assets', - properties: [_PropertyItem(label: 'Status', value: 'no_checksum_local'.tr())], + properties: [_PropertyItem(label: 'Status', value: context.t.no_checksum_local)], ), const SizedBox(height: 16), _PropertySectionCard( title: 'Remote Assets', - properties: [_PropertyItem(label: 'Status', value: 'no_checksum_remote'.tr())], + properties: [_PropertyItem(label: 'Status', value: context.t.no_checksum_remote)], ), ], ], @@ -243,7 +243,7 @@ class _LocalAssetsSection extends ConsumerWidget { if (localAssets.isEmpty) { return _PropertySectionCard( title: 'Local Assets', - properties: [_PropertyItem(label: 'Status', value: 'no_local_assets_found'.tr())], + properties: [_PropertyItem(label: 'Status', value: context.t.no_local_assets_found)], ); } @@ -302,7 +302,7 @@ class _RemoteAssetSection extends ConsumerWidget { if (remoteAsset == null) { return _PropertySectionCard( title: 'Remote Assets', - properties: [_PropertyItem(label: 'Status', value: 'no_remote_assets_found'.tr())], + properties: [_PropertyItem(label: 'Status', value: context.t.no_remote_assets_found)], ); } @@ -356,7 +356,7 @@ class _PropertyItem extends StatelessWidget { ), Expanded( child: Text( - value ?? 'not_available'.tr(), + value ?? context.t.not_available, style: TextStyle(color: Theme.of(context).colorScheme.secondary), ), ), diff --git a/mobile/lib/providers/local_auth.provider.dart b/mobile/lib/providers/local_auth.provider.dart index 58bd4fb0f8..d99f28823f 100644 --- a/mobile/lib/providers/local_auth.provider.dart +++ b/mobile/lib/providers/local_auth.provider.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/auth/biometric_status.model.dart'; import 'package:immich_mobile/services/local_auth.service.dart'; import 'package:immich_mobile/services/secure_storage.service.dart'; @@ -43,6 +43,7 @@ class LocalAuthNotifier extends StateNotifier { } Future authenticate(BuildContext context, String? message) async { + final t = StaticTranslations.instance; String errorMessage = ""; try { @@ -51,20 +52,20 @@ class LocalAuthNotifier extends StateNotifier { switch (error.code) { case "NotEnrolled": _log.warning("User is not enrolled in biometrics"); - errorMessage = "biometric_no_options".tr(); + errorMessage = t.biometric_no_options; case "NotAvailable": _log.warning("Biometric authentication is not available"); - errorMessage = "biometric_not_available".tr(); + errorMessage = t.biometric_not_available; case "LockedOut": _log.warning("User is locked out of biometric authentication"); - errorMessage = "biometric_locked_out".tr(); + errorMessage = t.biometric_locked_out; default: _log.warning("Failed to authenticate with unknown reason"); - errorMessage = 'failed_to_authenticate'.tr(); + errorMessage = t.failed_to_authenticate; } } catch (error) { _log.warning("Error during authentication: $error"); - errorMessage = 'failed_to_authenticate'.tr(); + errorMessage = t.failed_to_authenticate; } finally { if (errorMessage.isNotEmpty) { context.showSnackBar( diff --git a/mobile/lib/providers/sync_status.provider.dart b/mobile/lib/providers/sync_status.provider.dart index 7a70514ca7..eb183fbb13 100644 --- a/mobile/lib/providers/sync_status.provider.dart +++ b/mobile/lib/providers/sync_status.provider.dart @@ -1,7 +1,7 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; part 'sync_status.provider.freezed.dart'; @@ -12,11 +12,12 @@ enum SyncStatus { error; String localized() { + final t = StaticTranslations.instance; return switch (this) { - SyncStatus.idle => "idle".tr(), - SyncStatus.syncing => "running".tr(), - SyncStatus.success => "success".tr(), - SyncStatus.error => "error".tr(), + SyncStatus.idle => t.idle, + SyncStatus.syncing => t.running, + SyncStatus.success => t.success, + SyncStatus.error => t.error, }; } } diff --git a/mobile/lib/repositories/biometric.repository.dart b/mobile/lib/repositories/biometric.repository.dart index 8bd5a85542..7da4bfe757 100644 --- a/mobile/lib/repositories/biometric.repository.dart +++ b/mobile/lib/repositories/biometric.repository.dart @@ -1,5 +1,5 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/auth/biometric_status.model.dart'; import 'package:local_auth/local_auth.dart'; @@ -19,6 +19,6 @@ class BiometricRepository { } Future authenticate(String? message) async { - return _localAuth.authenticate(localizedReason: message ?? 'please_auth_to_access'.tr()); + return _localAuth.authenticate(localizedReason: message ?? StaticTranslations.instance.please_auth_to_access); } } diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index 11cbd03017..88576bbb4d 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/network_capability_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; @@ -241,16 +241,17 @@ class ForegroundUploadService { Completer? cancelToken, { required UploadCallbacks callbacks, }) async { + final t = StaticTranslations.instance; + final assetNotFoundOnDevice = CurrentPlatform.isAndroid + ? t.asset_not_found_on_device_android + : t.asset_not_found_on_device_ios; File? file; File? livePhotoFile; try { final entity = await _storageRepository.getAssetEntityForAsset(asset); if (entity == null) { - callbacks.onError?.call( - asset.localId!, - CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(), - ); + callbacks.onError?.call(asset.localId!, assetNotFoundOnDevice); return; } @@ -284,10 +285,7 @@ class ForegroundUploadService { file = await _storageRepository.getFileForAsset(asset.id); if (file == null) { _logger.warning("Failed to get file ${asset.id} - ${asset.name}"); - callbacks.onError?.call( - asset.localId!, - CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(), - ); + callbacks.onError?.call(asset.localId!, assetNotFoundOnDevice); return; } @@ -296,17 +294,14 @@ class ForegroundUploadService { livePhotoFile = await _storageRepository.getMotionFileForAsset(asset); if (livePhotoFile == null) { _logger.warning("Failed to obtain motion part of the livePhoto - ${asset.name}"); - callbacks.onError?.call( - asset.localId!, - CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(), - ); + callbacks.onError?.call(asset.localId!, assetNotFoundOnDevice); } } } if (file == null) { _logger.warning("Failed to obtain file from iCloud for asset ${asset.id} - ${asset.name}"); - callbacks.onError?.call(asset.localId!, "asset_not_found_on_icloud".t()); + callbacks.onError?.call(asset.localId!, t.asset_not_found_on_icloud); return; } diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 28a9442dc7..4e0a5efff5 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -1,5 +1,4 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; @@ -7,6 +6,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; @@ -203,13 +203,13 @@ enum ActionButtonType { ), ActionButtonType.setProfilePicture => ActionMenuItem(action: SetProfilePictureAction(asset: context.asset)), ActionButtonType.openInfo => BaseActionButton( - label: 'info'.tr(), + label: StaticTranslations.instance.info, iconData: Icons.info_outline, menuItem: true, onPressed: () => EventStream.shared.emit(const ViewerShowDetailsEvent()), ), ActionButtonType.viewInTimeline => BaseActionButton( - label: 'view_in_timeline'.tr(), + label: StaticTranslations.instance.view_in_timeline, iconData: Icons.image_search, iconOnly: iconOnly, menuItem: menuItem, diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index 37ad748a57..efe6403559 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -2,7 +2,7 @@ import 'package:background_downloader/background_downloader.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; @@ -15,31 +15,33 @@ import 'package:photo_manager/photo_manager.dart'; import 'package:sqlite3/common.dart'; void configureFileDownloaderNotifications() { + final t = StaticTranslations.instance; + FileDownloader().configureNotificationForGroup( kDownloadGroupImage, - running: TaskNotification('downloading_media'.t(), '${'file_name_text'.t()}: {filename}'), - complete: TaskNotification('download_finished'.t(), '${'file_name_text'.t()}: {filename}'), + running: TaskNotification(t.downloading_media, '${t.file_name_text}: {filename}'), + complete: TaskNotification(t.download_finished, '${t.file_name_text}: {filename}'), progressBar: true, ); FileDownloader().configureNotificationForGroup( kDownloadGroupVideo, - running: TaskNotification('downloading_media'.t(), '${'file_name_text'.t()}: {filename}'), - complete: TaskNotification('download_finished'.t(), '${'file_name_text'.t()}: {filename}'), + running: TaskNotification(t.downloading_media, '${t.file_name_text}: {filename}'), + complete: TaskNotification(t.download_finished, '${t.file_name_text}: {filename}'), progressBar: true, ); FileDownloader().configureNotificationForGroup( kManualUploadGroup, - running: TaskNotification('uploading_media'.t(), 'backup_background_service_in_progress_notification'.t()), - complete: TaskNotification('upload_finished'.t(), 'backup_background_service_complete_notification'.t()), + running: TaskNotification(t.uploading_media, t.backup_background_service_in_progress_notification), + complete: TaskNotification(t.upload_finished, t.backup_background_service_complete_notification), groupNotificationId: kManualUploadGroup, ); FileDownloader().configureNotificationForGroup( kBackupGroup, - running: TaskNotification('uploading_media'.t(), 'backup_background_service_in_progress_notification'.t()), - complete: TaskNotification('upload_finished'.t(), 'backup_background_service_complete_notification'.t()), + running: TaskNotification(t.uploading_media, t.backup_background_service_in_progress_notification), + complete: TaskNotification(t.upload_finished, t.backup_background_service_complete_notification), groupNotificationId: kBackupGroup, ); } diff --git a/mobile/lib/utils/people.utils.dart b/mobile/lib/utils/people.utils.dart index 18ae8a4528..8da5a5a3eb 100644 --- a/mobile/lib/utils/people.utils.dart +++ b/mobile/lib/utils/people.utils.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/people/person_edit_birthday_modal.widget.dart'; import 'package:immich_mobile/presentation/widgets/people/person_edit_name_modal.widget.dart'; @@ -8,12 +8,13 @@ String formatAge(DateTime birthDate, DateTime referenceDate) { final int ageInYears = _calculateAge(birthDate, referenceDate); final int ageInMonths = _calculateAgeInMonths(birthDate, referenceDate); + final t = StaticTranslations.instance; if (ageInMonths <= 11) { - return "person_age_months".t(args: {'months': ageInMonths.toString()}); + return t.person_age_months(months: ageInMonths); } else if (ageInMonths > 12 && ageInMonths <= 23) { - return "person_age_year_months".t(args: {'months': (ageInMonths - 12).toString()}); + return t.person_age_year_months(months: ageInMonths - 12); } else { - return "person_age_years".t(args: {'years': ageInYears.toString()}); + return t.person_age_years(years: ageInYears); } } diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index b390d2a423..dd62813e3a 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; @@ -78,20 +78,24 @@ class ImmichAppBarDialog extends HookConsumerWidget { title: Text( text, style: theme.textTheme.labelLarge?.copyWith(color: theme.textTheme.labelLarge?.color?.withAlpha(250)), - ).tr(), + ), onTap: onTap, trailing: trailing, ); } ListTile buildSettingButton() { - return buildActionButton(Icons.settings_outlined, "settings", () => context.pushRoute(const SettingsRoute())); + return buildActionButton( + Icons.settings_outlined, + context.t.settings, + () => context.pushRoute(const SettingsRoute()), + ); } ListTile buildFreeUpSpaceButton() { return buildActionButton( Icons.cleaning_services_outlined, - "free_up_space", + context.t.free_up_space, () => context.pushRoute(SettingsSubRoute(section: SettingSection.freeUpSpace)), ); } @@ -99,7 +103,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ListTile buildAppLogButton() { return buildActionButton( Icons.assignment_outlined, - "profile_drawer_app_logs", + context.t.profile_drawer_app_logs, () => context.pushRoute(const AppLogRoute()), ); } @@ -107,7 +111,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ListTile buildSignOutButton() { return buildActionButton( Icons.logout_rounded, - "sign_out", + context.t.sign_out, () async { if (isLoggingOut.value) { return; @@ -163,16 +167,16 @@ class ImmichAppBarDialog extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, spacing: 12, children: [ - Text("backup_controller_page_server_storage".tr(), style: context.textTheme.labelLarge), + Text(context.t.backup_controller_page_server_storage, style: context.textTheme.labelLarge), LinearProgressIndicator( minHeight: 10.0, value: percentage, borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), Text( - 'backup_controller_page_storage_format', + context.t.backup_controller_page_storage_format(used: usedDiskSpace, total: totalDiskSpace), style: context.textTheme.bodySmall, - ).tr(namedArgs: {'used': usedDiskSpace, 'total': totalDiskSpace}), + ), ], ), ); @@ -189,7 +193,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ContextHelper(context).pop(); unawaited(launchUrl(Uri.parse('https://docs.immich.app'), mode: LaunchMode.externalApplication)); }, - child: Text("documentation", style: context.textTheme.bodySmall).tr(), + child: Text(context.t.documentation, style: context.textTheme.bodySmall), ), const SizedBox(width: 20, child: Text("•", textAlign: TextAlign.center)), InkWell( @@ -199,7 +203,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { launchUrl(Uri.parse('https://github.com/immich-app/immich'), mode: LaunchMode.externalApplication), ); }, - child: Text("profile_drawer_github", style: context.textTheme.bodySmall).tr(), + child: Text(context.t.profile_drawer_github, style: context.textTheme.bodySmall), ), const SizedBox(width: 20, child: Text("•", textAlign: TextAlign.center)), InkWell( @@ -219,7 +223,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { applicationVersion: packageInfo.version, ); }, - child: Text("licenses", style: context.textTheme.bodySmall).tr(), + child: Text(context.t.licenses, style: context.textTheme.bodySmall), ), ], ), @@ -237,10 +241,10 @@ class ImmichAppBarDialog extends HookConsumerWidget { minLeadingWidth: 20, tileColor: theme.primaryColor.withAlpha(80), title: Text( - "profile_drawer_readonly_mode", + context.t.profile_drawer_readonly_mode, style: theme.textTheme.labelLarge?.copyWith(color: theme.textTheme.labelLarge?.color?.withAlpha(250)), textAlign: TextAlign.center, - ).tr(), + ), ), ); } diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index 51cdd7c5a3..bd8e883ef0 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; @@ -68,7 +68,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { SnackBar( duration: const Duration(seconds: 2), content: Text( - (isReadonlyModeEnabled ? "readonly_mode_disabled" : "readonly_mode_enabled").tr(), + isReadonlyModeEnabled ? context.t.readonly_mode_disabled : context.t.readonly_mode_enabled, style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), ), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index 348e3aa14c..901ffb2496 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -46,20 +46,20 @@ class AppBarServerInfo extends HookConsumerWidget { children: [ if (showVersionWarning) ...[const ServerUpdateNotification(), divider], _ServerInfoItem( - label: "server_info_box_app_version".tr(), + label: context.t.server_info_box_app_version, text: "${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}", ), divider, _ServerInfoItem( - label: "server_version".tr(), + label: context.t.server_version, text: serverInfoState.serverVersion.major > 0 ? "${serverInfoState.serverVersion}" : "--", ), divider, - _ServerInfoItem(label: "server_info_box_server_url".tr(), text: getServerUrl() ?? '--', tooltip: true), + _ServerInfoItem(label: context.t.server_info_box_server_url, text: getServerUrl() ?? '--', tooltip: true), if (serverInfoState.latestVersion != null) ...[ divider, _ServerInfoItem( - label: "latest_version".tr(), + label: context.t.latest_version, text: serverInfoState.latestVersion!.major > 0 ? "${serverInfoState.latestVersion!}" : "--", tooltip: true, icon: serverInfoState.versionStatus == VersionStatus.serverOutOfDate diff --git a/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart b/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart index 806ac87c19..8261f71ad8 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'dart:io'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -76,8 +76,8 @@ class ServerUpdateNotification extends HookConsumerWidget { tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: serverInfoState.versionStatus == VersionStatus.clientOutOfDate - ? Text("action_common_update".tr(context: context)) - : Text("view".tr()), + ? Text(context.t.action_common_update) + : Text(context.t.view), ), ], ], diff --git a/mobile/lib/widgets/common/date_time_picker.dart b/mobile/lib/widgets/common/date_time_picker.dart index 3aedd55de5..86154c308e 100644 --- a/mobile/lib/widgets/common/date_time_picker.dart +++ b/mobile/lib/widgets/common/date_time_picker.dart @@ -1,10 +1,11 @@ import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; 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/generated/translations.g.dart'; import 'package:immich_mobile/widgets/common/dropdown_search_menu.dart'; +import 'package:intl/intl.dart'; import 'package:timezone/timezone.dart' as tz; import 'package:timezone/timezone.dart'; @@ -116,26 +117,26 @@ class _DateTimePicker extends HookWidget { TextButton( onPressed: () => context.pop(), child: Text( - "cancel", + context.t.cancel, style: context.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: context.colorScheme.error, ), - ).tr(), + ), ), TextButton( onPressed: popWithDateTime, child: Text( - "action_common_update", + context.t.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("date_and_time", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)).tr(), + Text(context.t.date_and_time, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 32), ListTile( tileColor: context.colorScheme.surfaceContainerHighest, @@ -151,8 +152,8 @@ class _DateTimePicker extends HookWidget { const SizedBox(height: 24), DropdownSearchMenu( trailingIcon: Icon(Icons.arrow_drop_down, color: context.primaryColor), - hintText: "timezone".tr(), - label: const Text('timezone').tr(), + hintText: context.t.timezone, + label: Text(context.t.timezone), textStyle: context.textTheme.bodyMedium, onSelected: (value) => tzOffset.value = value, initialSelection: tzOffset.value, diff --git a/mobile/lib/widgets/common/dropdown_search_menu.dart b/mobile/lib/widgets/common/dropdown_search_menu.dart index 54568c392a..ba40bb98d5 100644 --- a/mobile/lib/widgets/common/dropdown_search_menu.dart +++ b/mobile/lib/widgets/common/dropdown_search_menu.dart @@ -1,11 +1,11 @@ import 'dart:async'; 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'; +import 'package:immich_mobile/generated/translations.g.dart'; class DropdownSearchMenu extends HookWidget { const DropdownSearchMenu({ @@ -81,7 +81,7 @@ class DropdownSearchMenu extends HookWidget { autofocus: true, focusNode: focusNode, controller: textEditingController, - decoration: inputDecoration.copyWith(hintText: "search_timezone".tr()), + decoration: inputDecoration.copyWith(hintText: context.t.search_timezone), maxLines: 1, style: context.textTheme.bodyMedium, expands: false, diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 54495365db..4c6b2a7211 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -2,12 +2,12 @@ import 'dart:async'; import 'dart:math' as math; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; @@ -120,7 +120,7 @@ class _ProfileIndicator extends ConsumerWidget { SnackBar( duration: const Duration(seconds: 2), content: Text( - (isReadonlyModeEnabled ? "readonly_mode_disabled" : "readonly_mode_enabled").tr(), + isReadonlyModeEnabled ? context.t.readonly_mode_disabled : context.t.readonly_mode_enabled, style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), ), @@ -143,7 +143,7 @@ class _ProfileIndicator extends ConsumerWidget { ? context.colorScheme.error : context.primaryColor, size: widgetSize / 2 - 3, - semanticLabel: 'new_version_available'.tr(), + semanticLabel: context.t.new_version_available, ), ), backgroundColor: Colors.transparent, @@ -153,7 +153,7 @@ class _ProfileIndicator extends ConsumerWidget { child: user == null ? const Icon(Icons.face_outlined, size: widgetSize) : Semantics( - label: "logged_in_as".tr(namedArgs: {"user": user.name}), + label: context.t.logged_in_as(user: user.name), child: AbsorbPointer( child: Builder( builder: (context) => UserCircleAvatar( @@ -201,7 +201,12 @@ class _BackupIndicator extends ConsumerWidget { if (!backupEnabled) { return _BadgeLabel( - Icon(Icons.cloud_off_rounded, size: 9, color: iconColor, semanticLabel: 'backup_controller_page_backup'.tr()), + Icon( + Icons.cloud_off_rounded, + size: 9, + color: iconColor, + semanticLabel: context.t.backup_controller_page_backup, + ), ); } @@ -211,7 +216,7 @@ class _BackupIndicator extends ConsumerWidget { Icons.warning_rounded, size: 12, color: context.colorScheme.error, - semanticLabel: 'backup_controller_page_backup'.tr(), + semanticLabel: context.t.backup_controller_page_backup, ), backgroundColor: context.colorScheme.errorContainer, ); @@ -229,7 +234,7 @@ class _BackupIndicator extends ConsumerWidget { strokeWidth: 2, strokeCap: StrokeCap.round, valueColor: AlwaysStoppedAnimation(iconColor), - semanticsLabel: 'backup_controller_page_backup'.tr(), + semanticsLabel: context.t.backup_controller_page_backup, ), ), ), @@ -237,7 +242,7 @@ class _BackupIndicator extends ConsumerWidget { } return _BadgeLabel( - Icon(Icons.check_outlined, size: 9, color: iconColor, semanticLabel: 'backup_controller_page_backup'.tr()), + Icon(Icons.check_outlined, size: 9, color: iconColor, semanticLabel: context.t.backup_controller_page_backup), ); } } diff --git a/mobile/lib/widgets/common/location_picker.dart b/mobile/lib/widgets/common/location_picker.dart index c7eb827781..cf3240977b 100644 --- a/mobile/lib/widgets/common/location_picker.dart +++ b/mobile/lib/widgets/common/location_picker.dart @@ -1,10 +1,10 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -73,11 +73,11 @@ class _LocationPicker extends HookWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("edit_location_dialog_title", style: context.textTheme.titleMedium).tr(), + Text(context.t.edit_location_dialog_title, style: context.textTheme.titleMedium), Align( alignment: Alignment.center, child: TextButton.icon( - icon: const Text("location_picker_choose_on_map").tr(), + icon: Text(context.t.location_picker_choose_on_map), label: const Icon(Icons.map_outlined, size: 16), onPressed: onMapTap, ), @@ -85,9 +85,9 @@ class _LocationPicker extends HookWidget { const SizedBox(height: 12), _ManualPickerInput( controller: latitudeController, - decorationText: "latitude", - hintText: "location_picker_latitude_hint", - errorText: "location_picker_latitude_error", + decorationText: context.t.latitude, + hintText: context.t.location_picker_latitude_hint, + errorText: context.t.location_picker_latitude_error, focusNode: latitiudeFocusNode, validator: _validateLat, onUpdated: onLatitudeUpdated, @@ -95,9 +95,9 @@ class _LocationPicker extends HookWidget { const SizedBox(height: 24), _ManualPickerInput( controller: longitudeController, - decorationText: "longitude", - hintText: "location_picker_longitude_hint", - errorText: "location_picker_longitude_error", + decorationText: context.t.longitude, + hintText: context.t.location_picker_longitude_hint, + errorText: context.t.location_picker_longitude_error, focusNode: longitudeFocusNode, validator: _validateLong, onUpdated: onLongitudeEditingCompleted, @@ -109,19 +109,19 @@ class _LocationPicker extends HookWidget { TextButton( onPressed: () => ContextHelper(context).pop(), child: Text( - "cancel", + context.t.cancel, style: context.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: context.colorScheme.error, ), - ).tr(), + ), ), TextButton( onPressed: () => context.maybePop(latlng), child: Text( - "action_common_update", + context.t.action_common_update, style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor), - ).tr(), + ), ), ], ); @@ -163,13 +163,13 @@ class _ManualPickerInput extends HookWidget { textInputAction: TextInputAction.done, autofocus: false, decoration: InputDecoration( - labelText: decorationText.tr(), + labelText: decorationText, labelStyle: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), floatingLabelBehavior: FloatingLabelBehavior.auto, border: const OutlineInputBorder(), - hintText: hintText.tr(), + hintText: hintText, hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14), - errorText: isValid.value ? null : errorText.tr(), + errorText: isValid.value ? null : errorText, ), onEditingComplete: onEditingComplete, keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), diff --git a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart index 0ceb98aa76..ac53bca72f 100644 --- a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart @@ -8,7 +8,7 @@ import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; @@ -237,7 +237,7 @@ class _ItemCountTextState extends ConsumerState<_ItemCountText> { final assetCount = ref.watch(timelineServiceProvider.select((s) => s.totalAssets)); return Text( - 'items_count'.t(context: context, args: {"count": assetCount}), + context.t.items_count(count: assetCount), style: context.textTheme.labelLarge?.copyWith( // letterSpacing: 0.2, fontWeight: FontWeight.bold, diff --git a/mobile/lib/widgets/common/scaffold_error_body.dart b/mobile/lib/widgets/common/scaffold_error_body.dart index 2e2d8fb506..5f3fb1cb0d 100644 --- a/mobile/lib/widgets/common/scaffold_error_body.dart +++ b/mobile/lib/widgets/common/scaffold_error_body.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; // Error widget to be used in Scaffold when an AsyncError is received class ScaffoldErrorBody extends StatelessWidget { @@ -15,7 +15,11 @@ class ScaffoldErrorBody extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text("scaffold_body_error_occurred", style: context.textTheme.displayMedium, textAlign: TextAlign.center).tr(), + Text( + context.t.scaffold_body_error_occurred, + style: context.textTheme.displayMedium, + textAlign: TextAlign.center, + ), if (withIcon) Center( child: Padding( diff --git a/mobile/lib/widgets/common/selection_sliver_app_bar.dart b/mobile/lib/widgets/common/selection_sliver_app_bar.dart index d60124041c..f6fb2fc88f 100644 --- a/mobile/lib/widgets/common/selection_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/selection_sliver_app_bar.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; class SelectionSliverAppBar extends ConsumerStatefulWidget { @@ -43,12 +43,12 @@ class _SelectionSliverAppBarState extends ConsumerState { }, ), centerTitle: true, - title: Text("Select {count}".t(context: context, args: {'count': filteredAssets.length.toString()})), + title: Text(context.t.selected_count(count: filteredAssets.length)), actions: [ TextButton( onPressed: () => onDone(filteredAssets), child: Text( - 'done'.t(context: context), + context.t.done, style: context.textTheme.titleSmall?.copyWith(color: context.colorScheme.primary), ), ), diff --git a/mobile/lib/widgets/common/tag_picker.dart b/mobile/lib/widgets/common/tag_picker.dart index 82228f5b0b..2b4f07536f 100644 --- a/mobile/lib/widgets/common/tag_picker.dart +++ b/mobile/lib/widgets/common/tag_picker.dart @@ -1,10 +1,10 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/tag.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/tag.provider.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; @@ -41,19 +41,19 @@ class _TagPickerModal extends HookWidget { TextButton( onPressed: () => context.pop(), child: Text( - "cancel", + context.t.cancel, style: context.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: context.colorScheme.error, ), - ).tr(), + ), ), TextButton( onPressed: () => context.pop((selectedTagIds.value, newTagValues.value)), child: Text( - "action_common_update", + context.t.action_common_update, style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor), - ).tr(), + ), ), ], content: SizedBox( @@ -98,7 +98,7 @@ class TagPicker extends HookConsumerWidget { onChanged: (value) => searchQuery.value = value, onTapOutside: (_) => formFocus.unfocus(), filled: true, - hintText: 'filter_tags'.tr(), + hintText: context.t.filter_tags, ), ), Padding( From 835c35e9408332762e71afd64286a505cb2a9a6c Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:41:12 +0530 Subject: [PATCH 18/49] chore: migrate mobile translations accessor (2) (#30668) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../presentation/pages/drift_memory.page.dart | 4 +- .../add_action_button.widget.dart | 19 ++- .../like_activity_action_button.widget.dart | 9 +- .../motion_photo_action_button.widget.dart | 4 +- .../appears_in_details.widget.dart | 4 +- .../date_time_details.widget.dart | 10 +- .../location_details.widget.dart | 6 +- .../asset_details/people_details.widget.dart | 8 +- .../asset_details/rating_details.widget.dart | 4 +- .../technical_details.widget.dart | 4 +- .../asset_viewer/asset_viewer.page.dart | 4 +- .../motion_photo_button.widget.dart | 4 +- .../asset_viewer/rating_bar.widget.dart | 7 +- .../asset_viewer/sheet_tile.widget.dart | 8 +- .../memory/memory_bottom_info.widget.dart | 5 +- .../widgets/memory/memory_lane.widget.dart | 4 +- .../lib/widgets/asset_grid/delete_dialog.dart | 138 +++++++++--------- .../lib/widgets/asset_viewer/cast_dialog.dart | 22 +-- .../lib/widgets/memories/memory_epilogue.dart | 18 +-- .../image_viewer_quality_setting.dart | 10 +- .../image_viewer_tap_to_navigate_setting.dart | 8 +- .../slideshow_settings.dart | 47 ++---- .../video_viewer_settings.dart | 19 +-- .../sync_status_and_actions.dart | 99 ++++--------- .../settings/free_up_space_settings.dart | 137 +++++++---------- .../networking_settings/endpoint_input.dart | 6 +- .../external_network_preference.dart | 6 +- .../local_network_preference.dart | 31 ++-- .../networking_settings.dart | 31 ++-- .../settings/ssl_client_cert_settings.dart | 29 ++-- 30 files changed, 304 insertions(+), 401 deletions(-) diff --git a/mobile/lib/presentation/pages/drift_memory.page.dart b/mobile/lib/presentation/pages/drift_memory.page.dart index 31371fe581..3ad7d90657 100644 --- a/mobile/lib/presentation/pages/drift_memory.page.dart +++ b/mobile/lib/presentation/pages/drift_memory.page.dart @@ -8,7 +8,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/memory/memory_bottom_info.widget.dart'; import 'package:immich_mobile/presentation/widgets/memory/memory_card.widget.dart'; @@ -248,7 +248,7 @@ class DriftMemoryPage extends HookConsumerWidget { } final yearsAgo = DateTime.now().year - memories[mIndex].data.year; - final title = 'years_ago'.t(context: context, args: {'years': yearsAgo.toString()}); + final title = context.t.years_ago(years: yearsAgo); // Build horizontal page final assetController = memoryAssetPageControllers[mIndex]; return Column( diff --git a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart index cfb5887c7c..2466a52ead 100644 --- a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart @@ -1,13 +1,12 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; @@ -52,11 +51,11 @@ class _AddActionButtonState extends ConsumerState { return [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Text("add_to_bottom_bar".tr(), style: context.textTheme.labelMedium), + child: Text(context.t.add_to_bottom_bar, style: context.textTheme.labelMedium), ), BaseActionButton( iconData: Icons.photo_album_outlined, - label: "album".tr(), + label: context.t.album, menuItem: true, onPressed: () => _handleMenuSelection(AddToMenuItem.album), ), @@ -65,7 +64,7 @@ class _AddActionButtonState extends ConsumerState { const Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Text("move_to".tr(), style: context.textTheme.labelMedium), + child: Text(context.t.move_to, style: context.textTheme.labelMedium), ), const ActionMenuItem(action: ArchiveAction(source: .viewer)), const ActionMenuItem(action: LockAction(source: .viewer)), @@ -120,7 +119,7 @@ class _AddActionButtonState extends ConsumerState { } if (!result.success) { - ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error); + ImmichToast.show(context: context, msg: context.t.scaffold_body_error_occurred, toastType: ToastType.error); return; } @@ -128,7 +127,7 @@ class _AddActionButtonState extends ConsumerState { if (result.count > 0) { ImmichToast.show( context: context, - msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), + msg: context.t.add_to_album_bottom_sheet_added(album: album.name), ); // Refresh the "Appears in" list on the asset's info panel. @@ -136,13 +135,13 @@ class _AddActionButtonState extends ConsumerState { } else if (result.failedCount > 0) { ImmichToast.show( context: context, - msg: 'assets_cannot_be_added_to_album_count'.t(context: context, args: {'count': result.failedCount}), + msg: context.t.assets_cannot_be_added_to_album_count(count: result.failedCount), toastType: ToastType.error, ); } else { ImmichToast.show( context: context, - msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}), + msg: context.t.add_to_album_bottom_sheet_already_exists(album: album.name), ); } @@ -183,7 +182,7 @@ class _AddActionButtonState extends ConsumerState { builder: (context, controller, child) { return BaseActionButton( iconData: Icons.add, - label: "add_to_bottom_bar".tr(), + label: context.t.add_to_bottom_bar, onPressed: () => controller.isOpen ? controller.close() : controller.open(), ); }, diff --git a/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart index 46a5c2c41e..e39a76ef02 100644 --- a/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart @@ -1,9 +1,8 @@ import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/activities/activity.model.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/activity.provider.dart'; @@ -48,7 +47,7 @@ class LikeActivityActionButton extends ConsumerWidget { return BaseActionButton( maxWidth: 60, iconData: liked != null ? Icons.thumb_up : Icons.thumb_up_off_alt, - label: "like".t(context: context), + label: context.t.like, onPressed: () => onTap(liked), iconOnly: iconOnly, menuItem: menuItem, @@ -58,11 +57,11 @@ class LikeActivityActionButton extends ConsumerWidget { // default to empty heart during loading loading: () => BaseActionButton( iconData: Icons.thumb_up_off_alt, - label: "like".t(context: context), + label: context.t.like, iconOnly: iconOnly, menuItem: menuItem, ), - error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])), + error: (error, stack) => Text(context.t.error_saving_image(error: error.toString())), ); } } diff --git a/mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart index 878c589b21..6be8a6b023 100644 --- a/mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; @@ -16,7 +16,7 @@ class MotionPhotoActionButton extends ConsumerWidget { return BaseActionButton( iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded, - label: "play_motion_photo".t(context: context), + label: context.t.play_motion_photo, onPressed: ref.watch(isPlayingMotionVideoProvider.notifier).toggle, iconOnly: iconOnly, menuItem: menuItem, diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/appears_in_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/appears_in_details.widget.dart index dffda47b84..efd2ee00a1 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/appears_in_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/appears_in_details.widget.dart @@ -7,7 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; @@ -52,7 +52,7 @@ class AppearsInDetails extends ConsumerWidget { spacing: 12, children: [ SheetTile( - title: 'appears_in'.t(context: context), + title: context.t.appears_in, titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), Padding( diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart index eff5705eae..9f426f6db7 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart @@ -8,7 +8,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/duration_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; @@ -100,7 +100,7 @@ class _SheetAssetDescriptionState extends ConsumerState<_SheetAssetDescription> ImmichToast.show( context: context, - msg: 'exif_bottom_sheet_description_error'.t(context: context), + msg: context.t.exif_bottom_sheet_description_error, toastType: ToastType.error, ); } @@ -111,9 +111,9 @@ class _SheetAssetDescriptionState extends ConsumerState<_SheetAssetDescription> @override Widget build(BuildContext context) { final currentDescription = widget.exif.description ?? ''; - final hintText = (widget.isEditable ? 'exif_bottom_sheet_description' : 'exif_bottom_sheet_no_description').t( - context: context, - ); + final hintText = widget.isEditable + ? context.t.exif_bottom_sheet_description + : context.t.exif_bottom_sheet_no_description; if (_controller.text != currentDescription && !_descriptionFocus.hasFocus) { _controller.text = currentDescription; } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart index c6d048e458..47ecb53d8a 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart @@ -6,7 +6,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart'; @@ -75,7 +75,7 @@ class _LocationDetailsState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ SheetTile( - title: 'location'.t(context: context), + title: context.t.location, titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), trailing: hasCoordinates && editLocation != null ? const Icon(Icons.edit_location_alt, size: 20) : null, onTap: editLocation?.onAction, @@ -107,7 +107,7 @@ class _LocationDetailsState extends ConsumerState { ), if (!hasCoordinates) SheetTile( - title: "add_a_location".t(context: context), + title: context.t.add_a_location, titleStyle: context.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: context.primaryColor, diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart index 72db236b3c..739fe0a0d9 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart @@ -7,7 +7,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/people/person_edit_name_modal.widget.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; @@ -52,7 +52,7 @@ class PeopleDetails extends ConsumerWidget { Padding( padding: const EdgeInsets.only(left: 16, top: 16, bottom: 16), child: Text( - "people".t(context: context), + context.t.people, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ), @@ -89,7 +89,7 @@ class PeopleDetails extends ConsumerWidget { duration: Durations.short4, ); }, - error: (error, stack) => Text("error_loading_people".t(context: context), style: context.textTheme.bodyMedium), + error: (error, stack) => Text(context.t.errors.failed_to_load_people, style: context.textTheme.bodyMedium), loading: () => const SizedBox.shrink(), ); } @@ -135,7 +135,7 @@ class _Avatar extends StatelessWidget { GestureDetector( onTap: () => onNameTap?.call(), child: Text( - "add_a_name".t(context: context), + context.t.add_a_name, style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), maxLines: 2, overflow: TextOverflow.ellipsis, diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart index 352838c761..f07fb5c112 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart @@ -4,7 +4,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/rating_bar.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; @@ -32,7 +32,7 @@ class RatingDetails extends ConsumerWidget { spacing: 8, children: [ Text( - 'rating'.t(context: context), + context.t.rating, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), RatingBar( diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart index e92b671562..e36cd9967b 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart @@ -4,7 +4,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; @@ -27,7 +27,7 @@ class TechnicalDetails extends ConsumerWidget { return Column( children: [ SheetTile( - title: 'details'.t(context: context), + title: context.t.details, titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), _buildFileInfoTile(context, ref, asset, exifInfo), diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index 731d8f3481..ad330309ab 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:math' as math; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -14,6 +13,7 @@ import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/scroll_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_status_floating_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_page.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_preloader.dart'; @@ -210,7 +210,7 @@ class _AssetViewerState extends ConsumerState { SnackBar( duration: const Duration(seconds: 2), content: Text( - "local_asset_cast_failed".tr(), + context.t.local_asset_cast_failed, style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), ), diff --git a/mobile/lib/presentation/widgets/asset_viewer/motion_photo_button.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/motion_photo_button.widget.dart index 08d6b59423..78f234d39b 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/motion_photo_button.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/motion_photo_button.widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; @@ -66,7 +66,7 @@ class _MotionButton extends StatelessWidget { ), const SizedBox(width: 8), Text( - CurrentPlatform.isAndroid ? 'motion'.t(context: context) : 'live'.t(context: context), + CurrentPlatform.isAndroid ? context.t.motion : context.t.live, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), ], diff --git a/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart index 0ed37869be..fb5cfff255 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class RatingBar extends StatefulWidget { final double initialRating; @@ -129,10 +129,7 @@ class _RatingBarState extends State { }); widget.onClearRating?.call(); }, - child: Text( - 'rating_clear'.t(context: context), - style: TextStyle(color: context.themeData.colorScheme.primary), - ), + child: Text(context.t.rating_clear, style: TextStyle(color: context.themeData.colorScheme.primary)), ), ), ], diff --git a/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart index ac6f79a155..8ff0befd7b 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -29,11 +29,7 @@ class SheetTile extends ConsumerWidget { void copyTitle(BuildContext context, WidgetRef ref) { unawaited(Clipboard.setData(ClipboardData(text: title))); - ImmichToast.show( - context: context, - msg: 'copied_to_clipboard'.t(context: context), - toastType: ToastType.info, - ); + ImmichToast.show(context: context, msg: context.t.copied_to_clipboard, toastType: ToastType.info); ref.read(hapticFeedbackProvider.notifier).selectionClick(); } diff --git a/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart b/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart index fc9bfb34e7..2bafdc0843 100644 --- a/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart @@ -1,12 +1,13 @@ // ignore_for_file: require_trailing_commas import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:intl/intl.dart'; class DriftMemoryBottomInfo extends StatelessWidget { final DriftMemory memory; @@ -36,7 +37,7 @@ class DriftMemoryBottomInfo extends StatelessWidget { ], ), Tooltip( - message: 'view_in_timeline'.tr(), + message: context.t.view_in_timeline, child: MaterialButton( minWidth: 0, onPressed: () async { diff --git a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart index 94a91437b0..1732249523 100644 --- a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart @@ -4,7 +4,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/pages/drift_memory.page.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; @@ -53,7 +53,7 @@ class DriftMemoryCard extends StatelessWidget { @override Widget build(BuildContext context) { final yearsAgo = DateTime.now().year - memory.data.year; - final title = 'years_ago'.t(context: context, args: {'years': yearsAgo.toString()}); + final title = context.t.years_ago(years: yearsAgo); return Center( child: Stack( children: [ diff --git a/mobile/lib/widgets/asset_grid/delete_dialog.dart b/mobile/lib/widgets/asset_grid/delete_dialog.dart index ff5aac617a..9b0367a463 100644 --- a/mobile/lib/widgets/asset_grid/delete_dialog.dart +++ b/mobile/lib/widgets/asset_grid/delete_dialog.dart @@ -1,69 +1,69 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class DeleteLocalOnlyDialog extends StatelessWidget { - final void Function(bool onlyMerged) onDeleteLocal; - - const DeleteLocalOnlyDialog({super.key, required this.onDeleteLocal}); - - @override - Widget build(BuildContext context) { - void onDeleteBackedUpOnly() { - context.pop(true); - onDeleteLocal(true); - } - - void onForceDelete() { - context.pop(false); - onDeleteLocal(false); - } - - return AlertDialog( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), - title: const Text("delete_dialog_title").tr(), - content: const Text("delete_dialog_alert_local_non_backed_up").tr(), - actions: [ - SizedBox( - width: double.infinity, - height: 48, - child: FilledButton( - onPressed: () => context.pop(), - style: FilledButton.styleFrom( - backgroundColor: context.colorScheme.surfaceDim, - foregroundColor: context.primaryColor, - ), - child: const Text("cancel", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - ), - ), - const SizedBox(height: 8), - SizedBox( - width: double.infinity, - height: 48, - - child: FilledButton( - onPressed: onDeleteBackedUpOnly, - style: FilledButton.styleFrom( - backgroundColor: context.colorScheme.errorContainer, - foregroundColor: context.colorScheme.onErrorContainer, - ), - child: const Text( - "delete_local_dialog_ok_backed_up_only", - style: TextStyle(fontWeight: FontWeight.bold), - ).tr(), - ), - ), - const SizedBox(height: 8), - SizedBox( - width: double.infinity, - height: 48, - child: FilledButton( - onPressed: onForceDelete, - style: FilledButton.styleFrom(backgroundColor: Colors.red[400], foregroundColor: Colors.white), - child: const Text("delete_local_dialog_ok_force", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - ), - ), - ], - ); - } -} +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; + +class DeleteLocalOnlyDialog extends StatelessWidget { + final void Function(bool onlyMerged) onDeleteLocal; + + const DeleteLocalOnlyDialog({super.key, required this.onDeleteLocal}); + + @override + Widget build(BuildContext context) { + void onDeleteBackedUpOnly() { + context.pop(true); + onDeleteLocal(true); + } + + void onForceDelete() { + context.pop(false); + onDeleteLocal(false); + } + + return AlertDialog( + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), + title: Text(context.t.delete_dialog_title), + content: Text(context.t.delete_dialog_alert_local_non_backed_up), + actions: [ + SizedBox( + width: double.infinity, + height: 48, + child: FilledButton( + onPressed: () => context.pop(), + style: FilledButton.styleFrom( + backgroundColor: context.colorScheme.surfaceDim, + foregroundColor: context.primaryColor, + ), + child: Text(context.t.cancel, style: const TextStyle(fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + height: 48, + + child: FilledButton( + onPressed: onDeleteBackedUpOnly, + style: FilledButton.styleFrom( + backgroundColor: context.colorScheme.errorContainer, + foregroundColor: context.colorScheme.onErrorContainer, + ), + child: Text( + context.t.delete_local_dialog_ok_backed_up_only, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + height: 48, + child: FilledButton( + onPressed: onForceDelete, + style: FilledButton.styleFrom(backgroundColor: Colors.red[400], foregroundColor: Colors.white), + child: Text(context.t.delete_local_dialog_ok_force, style: const TextStyle(fontWeight: FontWeight.bold)), + ), + ), + ], + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/cast_dialog.dart b/mobile/lib/widgets/asset_viewer/cast_dialog.dart index f91d8f5b27..70b1b3d3e5 100644 --- a/mobile/lib/widgets/asset_viewer/cast_dialog.dart +++ b/mobile/lib/widgets/asset_viewer/cast_dialog.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/cast/cast_manager_state.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; @@ -23,7 +23,7 @@ class CastDialog extends ConsumerWidget { } return AlertDialog( - title: const Text("cast", style: TextStyle(fontWeight: FontWeight.bold)).tr(), + title: Text(context.t.cast, style: const TextStyle(fontWeight: FontWeight.bold)), content: SizedBox( width: 250, height: 250, @@ -31,13 +31,13 @@ class CastDialog extends ConsumerWidget { future: ref.watch(castProvider.notifier).getDevices(), builder: (context, snapshot) { if (snapshot.hasError) { - return Text('error_saving_image'.tr(args: [snapshot.error.toString()])); + return Text(context.t.error_saving_image(error: snapshot.error.toString())); } else if (!snapshot.hasData) { return const SizedBox(height: 48, child: Center(child: CircularProgressIndicator())); } if (snapshot.data!.isEmpty) { - return const Text('no_cast_devices_found').tr(); + return Text(context.t.no_cast_devices_found); } final devices = snapshot.data!; @@ -47,12 +47,12 @@ class CastDialog extends ConsumerWidget { final List sectionedList = []; if (connected.isNotEmpty) { - sectionedList.add("connected_device"); + sectionedList.add(context.t.connected_device); sectionedList.addAll(connected); } if (others.isNotEmpty) { - sectionedList.add("discovered_devices"); + sectionedList.add(context.t.discovered_devices); sectionedList.addAll(others); } @@ -66,7 +66,7 @@ class CastDialog extends ConsumerWidget { // It's a section header return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text(item, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)).tr(), + child: Text(item, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), ); } else { final (deviceName, type, deviceObj) = item as (String, CastDestinationType, dynamic); @@ -110,16 +110,16 @@ class CastDialog extends ConsumerWidget { TextButton( onPressed: () => ref.read(castProvider.notifier).disconnect(), child: Text( - "stop_casting", + context.t.stop_casting, style: TextStyle(color: context.colorScheme.secondary, fontWeight: FontWeight.bold), - ).tr(), + ), ), TextButton( onPressed: () => context.pop(), child: Text( - "close", + context.t.close, style: TextStyle(color: context.colorScheme.primary, fontWeight: FontWeight.bold), - ).tr(), + ), ), ], ); diff --git a/mobile/lib/widgets/memories/memory_epilogue.dart b/mobile/lib/widgets/memories/memory_epilogue.dart index b866ed049a..0335b586ac 100644 --- a/mobile/lib/widgets/memories/memory_epilogue.dart +++ b/mobile/lib/widgets/memories/memory_epilogue.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class MemoryEpilogue extends StatefulWidget { final Function()? onStartOver; @@ -45,23 +45,23 @@ class _MemoryEpilogueState extends State with TickerProviderStat ), const SizedBox(height: 16.0), Text( - "memories_all_caught_up", + context.t.memories_all_caught_up, style: context.textTheme.headlineMedium?.copyWith(color: Colors.white), - ).tr(), + ), const SizedBox(height: 16.0), Text( - "memories_check_back_tomorrow", + context.t.memories_check_back_tomorrow, style: context.textTheme.bodyMedium?.copyWith(color: Colors.white), - ).tr(), + ), const SizedBox(height: 16.0), TextButton( onPressed: widget.onStartOver, child: Text( - "memories_start_over", + context.t.memories_start_over, style: context.textTheme.displayMedium?.copyWith( color: context.isDarkTheme ? context.colorScheme.primary : context.colorScheme.inversePrimary, ), - ).tr(), + ), ), ], ), @@ -85,9 +85,9 @@ class _MemoryEpilogueState extends State with TickerProviderStat ), ), Text( - "memories_swipe_to_close", + context.t.memories_swipe_to_close, style: context.textTheme.bodyMedium?.copyWith(color: Colors.white), - ).tr(), + ), ], ), ), diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart index 82a2d804a3..d1b1e3d0f2 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; @@ -23,14 +23,14 @@ class ImageViewerQualitySetting extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ SettingGroupTitle( - title: "photos".t(context: context), + title: context.t.photos, icon: Icons.image_outlined, - subtitle: "setting_image_viewer_help".t(context: context), + subtitle: context.t.setting_image_viewer_help, ), SettingsSwitchListTile( valueNotifier: isOriginal, - title: "setting_image_viewer_original_title".t(context: context), - subtitle: "setting_image_viewer_original_subtitle".t(context: context), + title: context.t.setting_image_viewer_original_title, + subtitle: context.t.setting_image_viewer_original_subtitle, onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), ], diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart index fc3466c3c6..ed07028bcf 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @@ -21,11 +21,11 @@ class ImageViewerTapToNavigateSetting extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsSubTitle(title: "setting_image_navigation_title".tr()), + SettingsSubTitle(title: context.t.setting_image_navigation_title), SettingsSwitchListTile( valueNotifier: tapToNavigate, - title: "setting_image_navigation_enable_title".tr(), - subtitle: "setting_image_navigation_enable_subtitle".tr(), + title: context.t.setting_image_navigation_enable_title, + subtitle: context.t.setting_image_navigation_enable_subtitle, ), ], ); diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart index d8ccd4f0ed..bc4d9a1235 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart'; @@ -39,40 +39,28 @@ class SlideshowSettings extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingGroupTitle( - title: 'slideshow'.t(context: context), - icon: Icons.slideshow_outlined, - ), + SettingGroupTitle(title: context.t.slideshow, icon: Icons.slideshow_outlined), SettingsSwitchListTile( valueNotifier: useRepeat, - title: "slideshow_repeat".t(context: context), - subtitle: "slideshow_repeat_description".t(context: context), + title: context.t.slideshow_repeat, + subtitle: context.t.slideshow_repeat_description, ), SettingsSliderListTile( valueNotifier: useDuration, - text: "duration".t(context: context), + text: context.t.duration, minValue: 5, noDivisons: 5, maxValue: 30, ), Padding( padding: const EdgeInsets.only(top: 20), - child: SettingsSubTitle(title: 'look'.t(context: context)), + child: SettingsSubTitle(title: context.t.look), ), SettingsRadioListTile( groups: [ - SettingsRadioGroup( - title: 'contain'.t(context: context), - value: SlideshowLook.contain, - ), - SettingsRadioGroup( - title: 'cover'.t(context: context), - value: SlideshowLook.cover, - ), - SettingsRadioGroup( - title: 'blurred_background'.t(context: context), - value: SlideshowLook.blurredBackground, - ), + SettingsRadioGroup(title: context.t.contain, value: SlideshowLook.contain), + SettingsRadioGroup(title: context.t.cover, value: SlideshowLook.cover), + SettingsRadioGroup(title: context.t.blurred_background, value: SlideshowLook.blurredBackground), ], groupBy: useLook.value, onRadioChanged: (value) { @@ -83,24 +71,15 @@ class SlideshowSettings extends HookConsumerWidget { ), Padding( padding: const EdgeInsets.only(top: 20), - child: SettingsSubTitle(title: 'direction'.t(context: context)), + child: SettingsSubTitle(title: context.t.direction), ), Padding( padding: const EdgeInsets.only(bottom: 32), child: SettingsRadioListTile( groups: [ - SettingsRadioGroup( - title: 'forward'.t(context: context), - value: SlideshowDirection.forward, - ), - SettingsRadioGroup( - title: 'backward'.t(context: context), - value: SlideshowDirection.backward, - ), - SettingsRadioGroup( - title: 'shuffle'.t(context: context), - value: SlideshowDirection.shuffle, - ), + SettingsRadioGroup(title: context.t.forward, value: SlideshowDirection.forward), + SettingsRadioGroup(title: context.t.backward, value: SlideshowDirection.backward), + SettingsRadioGroup(title: context.t.shuffle, value: SlideshowDirection.shuffle), ], groupBy: useDirection.value, onRadioChanged: (value) { diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart index 7bea439f0c..2a575061ca 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @@ -31,24 +31,21 @@ class VideoViewerSettings extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingGroupTitle( - title: "videos".t(context: context), - icon: Icons.video_camera_back_outlined, - ), + SettingGroupTitle(title: context.t.videos, icon: Icons.video_camera_back_outlined), SettingsSwitchListTile( valueNotifier: useAutoPlayVideo, - title: "setting_video_viewer_auto_play_title".t(context: context), - subtitle: "setting_video_viewer_auto_play_subtitle".t(context: context), + title: context.t.setting_video_viewer_auto_play_title, + subtitle: context.t.setting_video_viewer_auto_play_subtitle, ), SettingsSwitchListTile( valueNotifier: useLoopVideo, - title: "setting_video_viewer_looping_title".t(context: context), - subtitle: "loop_videos_description".t(context: context), + title: context.t.setting_video_viewer_looping_title, + subtitle: context.t.loop_videos_description, ), SettingsSwitchListTile( valueNotifier: useOriginalVideo, - title: "setting_video_viewer_original_video_title".t(context: context), - subtitle: "setting_video_viewer_original_video_subtitle".t(context: context), + title: context.t.setting_video_viewer_original_video_title, + subtitle: context.t.setting_video_viewer_original_video_subtitle, ), ], ); diff --git a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart index 097f72e1e1..d2edef832e 100644 --- a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart +++ b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; @@ -45,9 +44,7 @@ class SyncStatusAndActions extends HookConsumerWidget { return; } - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Database file not found".t(context: context))), - ); + context.scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Database file not found'))); return; } @@ -77,17 +74,13 @@ class SyncStatusAndActions extends HookConsumerWidget { return; } - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Database exported successfully".t(context: context))), - ); + context.scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Database exported successfully'))); } catch (e) { if (!context.mounted) { return; } - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Failed to export database: $e".t(context: context))), - ); + context.scaffoldMessenger.showSnackBar(SnackBar(content: Text('Failed to export database: $e'))); } } @@ -138,10 +131,10 @@ class SyncStatusAndActions extends HookConsumerWidget { const _SyncStatsCounts(), const Divider(height: 10), const SizedBox(height: 16), - SettingGroupTitle(title: "jobs".t(context: context)), + SettingGroupTitle(title: context.t.jobs), SettingListTile( - title: "sync_local".t(context: context), - subtitle: "tap_to_run_job".t(context: context), + title: context.t.sync_local, + subtitle: context.t.tap_to_run_job, leading: const Icon(Icons.sync), trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).localSyncStatus), onTap: () { @@ -149,8 +142,8 @@ class SyncStatusAndActions extends HookConsumerWidget { }, ), SettingListTile( - title: "sync_remote".t(context: context), - subtitle: "tap_to_run_job".t(context: context), + title: context.t.sync_remote, + subtitle: context.t.tap_to_run_job, leading: const Icon(Icons.cloud_sync), trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).remoteSyncStatus), onTap: () { @@ -159,16 +152,16 @@ class SyncStatusAndActions extends HookConsumerWidget { ), if (CurrentPlatform.isIOS && serverVersion.isAtLeast(major: 2, minor: 5)) SettingListTile( - title: "Sync Cloud Ids".t(context: context), + title: 'Sync Cloud Ids', leading: const Icon(Icons.cloud_circle_rounded), - subtitle: "tap_to_run_job".t(context: context), + subtitle: context.t.tap_to_run_job, trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).cloudIdSyncStatus), onTap: ref.watch(backgroundSyncProvider).syncCloudIds, ), SettingListTile( - title: "hash_asset".t(context: context), + title: context.t.hash_asset, leading: const Icon(Icons.tag), - subtitle: "tap_to_run_job".t(context: context), + subtitle: context.t.tap_to_run_job, trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).hashJobStatus), onTap: () { unawaited(ref.read(backgroundSyncProvider).hashAssets()); @@ -176,27 +169,21 @@ class SyncStatusAndActions extends HookConsumerWidget { ), const Divider(height: 1), const SizedBox(height: 16), - SettingGroupTitle(title: "actions".t(context: context)), + SettingGroupTitle(title: context.t.actions), ListTile( - title: Text( - "clear_file_cache".t(context: context), - style: const TextStyle(fontWeight: FontWeight.w500), - ), + title: Text(context.t.clear_file_cache, style: const TextStyle(fontWeight: FontWeight.w500)), leading: const Icon(Icons.playlist_remove_rounded), onTap: clearFileCache, ), ListTile( - title: Text( - "export_database".t(context: context), - style: const TextStyle(fontWeight: FontWeight.w500), - ), - subtitle: Text("export_database_description".t(context: context)), + title: Text(context.t.export_database, style: const TextStyle(fontWeight: FontWeight.w500)), + subtitle: Text(context.t.export_database_description), leading: const Icon(Icons.download), onTap: exportDatabase, ), ListTile( title: Text( - "reset_sqlite".t(context: context), + context.t.reset_sqlite, style: TextStyle(color: context.colorScheme.error, fontWeight: FontWeight.w500), ), leading: Icon(Icons.settings_backup_restore_rounded, color: context.colorScheme.error), @@ -282,7 +269,7 @@ class _SyncStatsCounts extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingGroupTitle(title: "assets".t(context: context)), + SettingGroupTitle(title: context.t.assets), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), // 1. Wrap in IntrinsicHeight @@ -295,24 +282,16 @@ class _SyncStatsCounts extends ConsumerWidget { spacing: 8.0, children: [ Expanded( - child: EntityCountTile( - label: "local".t(context: context), - count: localAssetCount, - icon: Icons.smartphone, - ), + child: EntityCountTile(label: context.t.local, count: localAssetCount, icon: Icons.smartphone), ), Expanded( - child: EntityCountTile( - label: "remote".t(context: context), - count: remoteAssetCount, - icon: Icons.cloud, - ), + child: EntityCountTile(label: context.t.remote, count: remoteAssetCount, icon: Icons.cloud), ), ], ), ), ), - SettingGroupTitle(title: "albums".t(context: context)), + SettingGroupTitle(title: context.t.albums), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: IntrinsicHeight( @@ -323,24 +302,16 @@ class _SyncStatsCounts extends ConsumerWidget { spacing: 8.0, children: [ Expanded( - child: EntityCountTile( - label: "local".t(context: context), - count: localAlbumCount, - icon: Icons.smartphone, - ), + child: EntityCountTile(label: context.t.local, count: localAlbumCount, icon: Icons.smartphone), ), Expanded( - child: EntityCountTile( - label: "remote".t(context: context), - count: remoteAlbumCount, - icon: Icons.cloud, - ), + child: EntityCountTile(label: context.t.remote, count: remoteAlbumCount, icon: Icons.cloud), ), ], ), ), ), - SettingGroupTitle(title: "other".t(context: context)), + SettingGroupTitle(title: context.t.other), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: IntrinsicHeight( @@ -351,18 +322,10 @@ class _SyncStatsCounts extends ConsumerWidget { spacing: 8.0, children: [ Expanded( - child: EntityCountTile( - label: "memories".t(context: context), - count: memoryCount, - icon: Icons.calendar_today, - ), + child: EntityCountTile(label: context.t.memories, count: memoryCount, icon: Icons.calendar_today), ), Expanded( - child: EntityCountTile( - label: "hashed_assets".t(context: context), - count: localHashedCount, - icon: Icons.tag, - ), + child: EntityCountTile(label: context.t.hashed_assets, count: localHashedCount, icon: Icons.tag), ), ], ), @@ -371,7 +334,7 @@ class _SyncStatsCounts extends ConsumerWidget { // To be removed once the experimental feature is stable if (CurrentPlatform.isAndroid && appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) ...[ - SettingGroupTitle(title: "trash".t(context: context)), + SettingGroupTitle(title: context.t.trash), Consumer( builder: (context, ref, _) { final counts = ref.watch(trashedAssetsCountProvider); @@ -387,17 +350,13 @@ class _SyncStatsCounts extends ConsumerWidget { children: [ Expanded( child: EntityCountTile( - label: "local".t(context: context), + label: context.t.local, count: c.total, icon: Icons.delete_outline, ), ), Expanded( - child: EntityCountTile( - label: "hashed_assets".t(context: context), - count: c.hashed, - icon: Icons.tag, - ), + child: EntityCountTile(label: context.t.hashed_assets, count: c.hashed, icon: Icons.tag), ), ], ), diff --git a/mobile/lib/widgets/settings/free_up_space_settings.dart b/mobile/lib/widgets/settings/free_up_space_settings.dart index 8d4785d664..5885601171 100644 --- a/mobile/lib/widgets/settings/free_up_space_settings.dart +++ b/mobile/lib/widgets/settings/free_up_space_settings.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; @@ -9,12 +8,13 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/cleanup.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; +import 'package:intl/intl.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; class FreeUpSpaceSettings extends ConsumerStatefulWidget { @@ -236,21 +236,21 @@ class _FreeUpSpaceSettingsState extends ConsumerState { final parts = []; if (state.keepMediaType == AssetKeepType.photosOnly) { - parts.add('all_photos'.t(context: context)); + parts.add(context.t.all_photos); } else if (state.keepMediaType == AssetKeepType.videosOnly) { - parts.add('all_videos'.t(context: context)); + parts.add(context.t.all_videos); } if (state.keepFavorites) { - parts.add('favorites'.t(context: context)); + parts.add(context.t.favorites); } if (state.keepAlbumIds.isNotEmpty) { - parts.add('keep_albums_count'.t(context: context, args: {'count': state.keepAlbumIds.length.toString()})); + parts.add(context.t.keep_albums_count(count: state.keepAlbumIds.length)); } if (parts.isEmpty) { - return 'none'.t(context: context); + return context.t.none; } return parts.join(', '); @@ -274,7 +274,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { borderRadius: const BorderRadius.all(Radius.circular(12)), border: Border.all(color: context.primaryColor.withValues(alpha: 0.25)), ), - child: Text('free_up_space_description'.t(context: context), style: context.textTheme.bodyMedium), + child: Text(context.t.free_up_space_description, style: context.textTheme.bodyMedium), ), ), @@ -307,7 +307,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { color: hasKeepSettings ? context.colorScheme.primary : context.colorScheme.onSurfaceVariant, ), title: Text( - 'keep_on_device'.t(context: context), + context.t.keep_on_device, style: context.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, color: hasKeepSettings ? context.colorScheme.primary : null, @@ -315,8 +315,8 @@ class _FreeUpSpaceSettingsState extends ConsumerState { ), subtitle: Text( hasKeepSettings - ? 'keeping'.t(context: context, args: {'items': getKeepSettingsSummary()}) - : 'keep_on_device_hint'.t(context: context), + ? context.t.keeping(items: getKeepSettingsSummary()) + : context.t.keep_on_device_hint, style: context.textTheme.bodySmall?.copyWith( color: hasKeepSettings ? context.colorScheme.primary : context.colorScheme.onSurfaceVariant, ), @@ -327,12 +327,12 @@ class _FreeUpSpaceSettingsState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('keep_description'.t(context: context), style: subtitleStyle), + Text(context.t.keep_description, style: subtitleStyle), const SizedBox(height: 4), SwitchListTile( contentPadding: EdgeInsets.zero, title: Text( - 'keep_favorites'.t(context: context), + context.t.keep_favorites, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), @@ -352,7 +352,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { ), const SizedBox(height: 16), Text( - 'always_keep'.t(context: context), + context.t.always_keep, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), const SizedBox(height: 4), @@ -362,12 +362,12 @@ class _FreeUpSpaceSettingsState extends ConsumerState { const ButtonSegment(value: AssetKeepType.none, label: Text('—')), ButtonSegment( value: AssetKeepType.photosOnly, - label: Text('photos'.t(context: context)), + label: Text(context.t.photos), icon: const Icon(Icons.photo), ), ButtonSegment( value: AssetKeepType.videosOnly, - label: Text('videos'.t(context: context)), + label: Text(context.t.videos), icon: const Icon(Icons.videocam), ), ], @@ -381,8 +381,8 @@ class _FreeUpSpaceSettingsState extends ConsumerState { const SizedBox(height: 8), Text( state.keepMediaType == AssetKeepType.photosOnly - ? 'always_keep_photos_hint'.t(context: context) - : 'always_keep_videos_hint'.t(context: context), + ? context.t.always_keep_photos_hint + : context.t.always_keep_videos_hint, style: context.textTheme.bodySmall?.copyWith( color: context.colorScheme.onSurfaceVariant, ), @@ -413,7 +413,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { Step( stepStyle: styleForState(step1State), title: Text( - 'select_cutoff_date'.t(context: context), + context.t.select_cutoff_date, style: context.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: step1State == StepState.complete @@ -433,7 +433,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { content: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('cutoff_date_description'.t(context: context), style: subtitleStyle), + Text(context.t.cutoff_date_description, style: subtitleStyle), const SizedBox(height: 16), GridView.count( shrinkWrap: true, @@ -445,39 +445,39 @@ class _FreeUpSpaceSettingsState extends ConsumerState { children: [ _DatePresetCard( value: '30', - unit: 'cutoff_day'.t(context: context, args: {'count': '30'}), + unit: context.t.cutoff_day(count: 30), onTap: () => _setPresetDate(30), isSelected: _isPresetSelected(30), ), _DatePresetCard( value: '60', - unit: 'cutoff_day'.t(context: context, args: {'count': '60'}), + unit: context.t.cutoff_day(count: 60), onTap: () => _setPresetDate(60), isSelected: _isPresetSelected(60), ), _DatePresetCard( value: '90', - unit: 'cutoff_day'.t(context: context, args: {'count': '90'}), + unit: context.t.cutoff_day(count: 90), onTap: () => _setPresetDate(90), isSelected: _isPresetSelected(90), ), _DatePresetCard( value: '1', - unit: 'cutoff_year'.t(context: context, args: {'count': '1'}), + unit: context.t.cutoff_year(count: 1), onTap: () => _setPresetDate(365), isSelected: _isPresetSelected(365), ), _DatePresetCard( value: '2', - unit: 'cutoff_year'.t(context: context, args: {'count': '2'}), + unit: context.t.cutoff_year(count: 2), onTap: () => _setPresetDate(730), isSelected: _isPresetSelected(730), ), _DatePresetCard( value: '3', - unit: 'cutoff_year'.t(context: context, args: {'count': '3'}), + unit: context.t.cutoff_year(count: 3), onTap: () => _setPresetDate(1095), isSelected: _isPresetSelected(1095), ), @@ -487,14 +487,14 @@ class _FreeUpSpaceSettingsState extends ConsumerState { OutlinedButton.icon( onPressed: _selectDate, icon: const Icon(Icons.calendar_today), - label: Text('custom_date'.t(context: context)), + label: Text(context.t.custom_date), style: OutlinedButton.styleFrom(minimumSize: const Size(double.infinity, 48)), ), const SizedBox(height: 16), ElevatedButton.icon( onPressed: hasDate ? _goToScanStep : null, icon: const Icon(Icons.arrow_forward), - label: Text('continue'.t(context: context)), + label: Text(context.t.continue$), style: ElevatedButton.styleFrom(minimumSize: const Size(double.infinity, 48)), ), ], @@ -507,7 +507,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { Step( stepStyle: styleForState(step2State), title: Text( - 'scan'.t(context: context), + context.t.scan, style: context.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: step2State == StepState.complete @@ -520,17 +520,11 @@ class _FreeUpSpaceSettingsState extends ConsumerState { subtitle: _hasScanned ? Text( state.totalBytes > 0 - ? 'cleanup_found_assets_with_size'.t( - context: context, - args: { - 'count': state.assetsToDelete.length.toString(), - 'size': formatBytes(state.totalBytes), - }, + ? context.t.cleanup_found_assets_with_size( + count: state.assetsToDelete.length, + size: formatBytes(state.totalBytes), ) - : 'cleanup_found_assets'.t( - context: context, - args: {'count': state.assetsToDelete.length.toString()}, - ), + : context.t.cleanup_found_assets(count: state.assetsToDelete.length), style: context.textTheme.bodyMedium?.copyWith( color: state.assetsToDelete.isNotEmpty ? context.colorScheme.primary @@ -541,7 +535,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { : null, content: Column( children: [ - Text('cleanup_step3_description'.t(context: context), style: subtitleStyle), + Text(context.t.cleanup_step3_description, style: subtitleStyle), if (CurrentPlatform.isIOS) ...[ const SizedBox(height: 12), Container( @@ -556,7 +550,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { const SizedBox(width: 12), Expanded( child: Text( - 'cleanup_icloud_shared_albums_excluded'.t(context: context), + context.t.cleanup_icloud_shared_albums_excluded, style: context.textTheme.labelLarge, ), ), @@ -577,7 +571,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { : ElevatedButton.icon( onPressed: state.isScanning ? null : _scanAssets, icon: const Icon(Icons.search), - label: Text(_hasScanned ? 'rescan'.t(context: context) : 'scan'.t(context: context)), + label: Text(_hasScanned ? context.t.rescan : context.t.scan), style: ElevatedButton.styleFrom(minimumSize: const Size(double.infinity, 48)), ), if (_hasScanned && state.assetsToDelete.isEmpty) ...[ @@ -593,10 +587,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { const Icon(Icons.info, color: Colors.orange), const SizedBox(width: 12), Expanded( - child: Text( - 'cleanup_no_assets_found'.t(context: context), - style: context.textTheme.bodyMedium, - ), + child: Text(context.t.cleanup_no_assets_found, style: context.textTheme.bodyMedium), ), ], ), @@ -612,7 +603,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { Step( stepStyle: styleForState(step3State, isDestructive: true), title: Text( - 'move_to_device_trash'.t(context: context), + context.t.move_to_device_trash, style: context.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: step3State == StepState.disabled @@ -635,12 +626,9 @@ class _FreeUpSpaceSettingsState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'cleanup_step4_summary'.t( - context: context, - args: { - 'count': state.assetsToDelete.length.toString(), - 'date': DateFormat.yMMMd().format(state.selectedDate!), - }, + context.t.cleanup_step4_summary( + count: state.assetsToDelete.length, + date: DateFormat.yMMMd().format(state.selectedDate!), ), style: context.textTheme.labelLarge?.copyWith(fontSize: 15), ), @@ -652,7 +640,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { OutlinedButton.icon( onPressed: () => _showAssetsPreview(state.assetsToDelete), icon: const Icon(Icons.preview), - label: Text('preview'.t(context: context)), + label: Text(context.t.preview), style: OutlinedButton.styleFrom(minimumSize: const Size(double.infinity, 48)), ), const SizedBox(height: 12), @@ -665,11 +653,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : const Icon(Icons.delete_forever), - label: Text( - state.isDeleting - ? 'cleanup_deleting'.t(context: context) - : 'move_to_device_trash'.t(context: context), - ), + label: Text(state.isDeleting ? context.t.cleanup_deleting : context.t.move_to_device_trash), style: ElevatedButton.styleFrom( backgroundColor: context.colorScheme.error, foregroundColor: context.colorScheme.onError, @@ -701,26 +685,20 @@ class _DeleteConfirmationDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text('cleanup_confirm_prompt_title'.t(context: context)), + title: Text(context.t.cleanup_confirm_prompt_title), content: Text( - 'cleanup_confirm_description'.t( - context: context, - args: {'count': assetCount.toString(), 'date': DateFormat.yMMMd().format(cutoffDate)}, - ), + context.t.cleanup_confirm_description(count: assetCount, date: DateFormat.yMMMd().format(cutoffDate)), style: context.textTheme.labelLarge?.copyWith(fontSize: 15), ), actions: [ - TextButton( - onPressed: () => ContextHelper(context).pop(false), - child: Text('cancel'.t(context: context)), - ), + TextButton(onPressed: () => ContextHelper(context).pop(false), child: Text(context.t.cancel)), ElevatedButton( onPressed: () => ContextHelper(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: context.colorScheme.error, foregroundColor: context.colorScheme.onError, ), - child: Text('confirm'.t(context: context)), + child: Text(context.t.confirm), ), ], ); @@ -736,29 +714,24 @@ class _DeleteSuccessDialog extends StatelessWidget { Widget build(BuildContext context) { return AlertDialog( icon: Icon(Icons.check_circle, color: context.colorScheme.primary, size: 48), - title: Text('success'.t(context: context)), + title: Text(context.t.success), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text( - 'cleanup_deleted_assets'.t(context: context, args: {'count': deletedCount.toString()}), + context.t.cleanup_deleted_assets(count: deletedCount), style: context.textTheme.labelLarge?.copyWith(fontSize: 16), textAlign: TextAlign.center, ), const SizedBox(height: 16), Text( - 'cleanup_trash_hint'.t(context: context), + context.t.cleanup_trash_hint, style: context.textTheme.labelLarge?.copyWith(fontSize: 16, color: context.primaryColor), textAlign: TextAlign.center, ), ], ), - actions: [ - ElevatedButton( - onPressed: () => ContextHelper(context).pop(), - child: Text('done'.t(context: context)), - ), - ], + actions: [ElevatedButton(onPressed: () => ContextHelper(context).pop(), child: Text(context.t.done))], ); } } @@ -824,7 +797,7 @@ class _KeepAlbumsSection extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'keep_albums'.t(context: context), + context.t.keep_albums, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), @@ -834,13 +807,13 @@ class _KeepAlbumsSection extends ConsumerWidget { child: Padding(padding: EdgeInsets.all(16.0), child: CircularProgressIndicator(strokeWidth: 2)), ), error: (error, stack) => Text( - 'error_loading_albums'.t(context: context), + context.t.error_loading_albums, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.error), ), data: (albums) { if (albums.isEmpty) { return Text( - 'no_albums_found'.t(context: context), + context.t.no_albums_found, style: context.textTheme.bodyMedium?.copyWith( color: context.colorScheme.onSurface.withValues(alpha: 0.6), ), @@ -870,7 +843,7 @@ class _KeepAlbumsSection extends ConsumerWidget { if (albumIds.isNotEmpty) ...[ const SizedBox(height: 8), Text( - 'keep_albums_count'.t(context: context, args: {'count': albumIds.length.toString()}), + context.t.keep_albums_count(count: albumIds.length), style: context.textTheme.bodySmall?.copyWith( color: context.colorScheme.primary, fontWeight: FontWeight.w500, diff --git a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart index 162f1252a8..349b32287b 100644 --- a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart +++ b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart'; @@ -78,11 +78,11 @@ class EndpointInputState extends ConsumerState { try { if (url == null || url.isEmpty || !Uri.parse(url).isAbsolute) { isInputValid = false; - return 'validate_endpoint_error'.tr(); + return context.t.validate_endpoint_error; } } catch (_) { isInputValid = false; - return 'validate_endpoint_error'.tr(); + return context.t.validate_endpoint_error; } isInputValid = true; diff --git a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart index 81ffb4b19b..63b094b440 100644 --- a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart @@ -5,7 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; @@ -103,7 +103,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 24), - child: Text("external_network_sheet_info".t(context: context), style: context.textTheme.bodyMedium), + child: Text(context.t.external_network_sheet_info, style: context.textTheme.bodyMedium), ), const SizedBox(height: 4), Divider(color: context.colorScheme.surfaceContainerHighest), @@ -135,7 +135,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { height: 48, child: OutlinedButton.icon( icon: const Icon(Icons.add), - label: Text('add_endpoint'.t(context: context)), + label: Text(context.t.add_endpoint), onPressed: enabled ? () { entries.value = [ diff --git a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart index 8b787bcaea..3c4fa73543 100644 --- a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart @@ -1,11 +1,10 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/network.provider.dart'; import 'package:immich_ui/immich_ui.dart'; @@ -34,9 +33,12 @@ class LocalNetworkPreference extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('cancel'.tr().toUpperCase(), style: const TextStyle(color: Colors.red)), + child: Text(context.t.cancel.toUpperCase(), style: const TextStyle(color: Colors.red)), + ), + TextButton( + onPressed: () => Navigator.pop(context, controller.text), + child: Text(context.t.save.toUpperCase()), ), - TextButton(onPressed: () => Navigator.pop(context, controller.text), child: Text('save'.tr().toUpperCase())), ], ), ); @@ -73,7 +75,12 @@ class LocalNetworkPreference extends HookConsumerWidget { } Future handleEditWifiName() async { - final wifiName = await _showEditDialog(context, "wifi_name".tr(), "your_wifi_name".tr(), wifiNameText.value); + final wifiName = await _showEditDialog( + context, + context.t.wifi_name, + context.t.your_wifi_name, + wifiNameText.value, + ); if (wifiName != null) { await saveWifiName(wifiName); @@ -83,7 +90,7 @@ class LocalNetworkPreference extends HookConsumerWidget { Future handleEditServerEndpoint() async { final localEndpoint = await _showEditDialog( context, - "server_endpoint".tr(), + context.t.server_endpoint, "http://local-ip:2283", localEndpointText.value, isUrlField: true, @@ -104,7 +111,7 @@ class LocalNetworkPreference extends HookConsumerWidget { context.showSnackBar( SnackBar( content: Text( - "get_wifiname_error".tr(), + context.t.get_wifiname_error, style: context.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w500, color: context.colorScheme.onSecondary, @@ -149,7 +156,7 @@ class LocalNetworkPreference extends HookConsumerWidget { children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 24), - child: Text("local_network_sheet_info".tr(), style: context.textTheme.bodyMedium), + child: Text(context.t.local_network_sheet_info, style: context.textTheme.bodyMedium), ), const SizedBox(height: 4), Divider(color: context.colorScheme.surfaceContainerHighest), @@ -157,9 +164,9 @@ class LocalNetworkPreference extends HookConsumerWidget { enabled: enabled, contentPadding: const EdgeInsets.only(left: 24, right: 8), leading: const Icon(Icons.wifi_rounded), - title: Text("wifi_name".tr()), + title: Text(context.t.wifi_name), subtitle: wifiNameText.value.isEmpty - ? Text("enter_wifi_name".tr()) + ? Text(context.t.enter_wifi_name) : Text( wifiNameText.value, style: context.textTheme.labelLarge?.copyWith( @@ -177,7 +184,7 @@ class LocalNetworkPreference extends HookConsumerWidget { enabled: enabled, contentPadding: const EdgeInsets.only(left: 24, right: 8), leading: const Icon(Icons.lan_rounded), - title: Text("server_endpoint".t(context: context)), + title: Text(context.t.server_endpoint), subtitle: localEndpointText.value.isEmpty ? const Text("http://local-ip:2283") : Text( @@ -199,7 +206,7 @@ class LocalNetworkPreference extends HookConsumerWidget { height: 48, child: OutlinedButton.icon( icon: const Icon(Icons.wifi_find_rounded), - label: Text('use_current_connection'.t(context: context)), + label: Text(context.t.use_current_connection), onPressed: enabled ? autofillCurrentNetwork : null, ), ), diff --git a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart index dad827230a..581bae4324 100644 --- a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart +++ b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart @@ -1,11 +1,10 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/network.provider.dart'; @@ -39,8 +38,8 @@ class NetworkingSettings extends HookConsumerWidget { context: context, builder: (context) { return AlertDialog( - title: Text("location_permission".tr()), - content: Text("location_permission_content".tr()), + title: Text(context.t.location_permission), + content: Text(context.t.location_permission_content), actions: [ TextButton( onPressed: () async { @@ -52,7 +51,7 @@ class NetworkingSettings extends HookConsumerWidget { Navigator.pop(context, isGrant); }, - child: Text("grant_permission".tr()), + child: Text(context.t.grant_permission), ), ], ); @@ -69,8 +68,8 @@ class NetworkingSettings extends HookConsumerWidget { context: context, builder: (context) { return AlertDialog( - title: Text("background_location_permission".tr()), - content: Text("background_location_permission_content".tr()), + title: Text(context.t.background_location_permission), + content: Text(context.t.background_location_permission_content), actions: [ TextButton( onPressed: () async { @@ -82,7 +81,7 @@ class NetworkingSettings extends HookConsumerWidget { Navigator.pop(context, isGrant); }, - child: Text("grant_permission".tr()), + child: Text(context.t.grant_permission), ), ], ); @@ -107,7 +106,7 @@ class NetworkingSettings extends HookConsumerWidget { children: [ const SizedBox(height: 8), SettingGroupTitle( - title: "current_server_address".t(context: context), + title: context.t.current_server_address, icon: (currentEndpoint?.startsWith('https') ?? false) ? Icons.https_outlined : Icons.http_outlined, ), Padding( @@ -136,20 +135,14 @@ class NetworkingSettings extends HookConsumerWidget { SettingsSwitchListTile( enabled: true, valueNotifier: featureEnabled, - title: "automatic_endpoint_switching_title".tr(), - subtitle: "automatic_endpoint_switching_subtitle".tr(), + title: context.t.automatic_endpoint_switching_title, + subtitle: context.t.automatic_endpoint_switching_subtitle, ), const SizedBox(height: 8), - SettingGroupTitle( - title: "local_network".t(context: context), - icon: Icons.home_outlined, - ), + SettingGroupTitle(title: context.t.local_network, icon: Icons.home_outlined), LocalNetworkPreference(enabled: featureEnabled.value), const SizedBox(height: 16), - SettingGroupTitle( - title: "external_network".t(context: context), - icon: Icons.dns_outlined, - ), + SettingGroupTitle(title: context.t.external_network, icon: Icons.dns_outlined), ExternalNetworkPreference(enabled: featureEnabled.value), ], ); diff --git a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart index 77ad7ee179..f37aa04b33 100644 --- a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart +++ b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart @@ -1,10 +1,10 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/platform/network_api.g.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:logging/logging.dart'; @@ -44,12 +44,15 @@ class _SslClientCertSettingsState extends State { contentPadding: const EdgeInsets.symmetric(horizontal: 20), horizontalTitleGap: 20, isThreeLine: true, - title: Text("client_cert_title".tr(), style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500)), + title: Text( + context.t.client_cert_title, + style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), + ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "client_cert_subtitle".tr(), + context.t.client_cert_subtitle, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), const SizedBox(height: 6), @@ -58,8 +61,8 @@ class _SslClientCertSettingsState extends State { mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ - ElevatedButton(onPressed: importCert, child: Text("client_cert_import".tr())), - ElevatedButton(onPressed: !isCertExist ? null : removeCert, child: Text("remove".tr())), + ElevatedButton(onPressed: importCert, child: Text(context.t.client_cert_import)), + ElevatedButton(onPressed: !isCertExist ? null : removeCert, child: Text(context.t.remove)), ], ), ], @@ -79,20 +82,20 @@ class _SslClientCertSettingsState extends State { Future importCert() async { try { final styling = ClientCertPrompt( - title: "client_cert_password_title".tr(), - message: "client_cert_password_message".tr(), - cancel: "cancel".tr(), - confirm: "confirm".tr(), + title: context.t.client_cert_password_title, + message: context.t.client_cert_password_message, + cancel: context.t.cancel, + confirm: context.t.confirm, ); await networkApi.selectCertificate(styling); setState(() => isCertExist = true); - showMessage("client_cert_import_success_msg".tr()); + showMessage(StaticTranslations.instance.client_cert_import_success_msg); } catch (e) { if (_isCancellation(e)) { return; } _log.severe("Error importing client cert", e); - showMessage("client_cert_invalid_msg".tr()); + showMessage(StaticTranslations.instance.client_cert_invalid_msg); } } @@ -100,13 +103,13 @@ class _SslClientCertSettingsState extends State { try { await networkApi.removeCertificate(); setState(() => isCertExist = false); - showMessage("client_cert_remove_msg".tr()); + showMessage(StaticTranslations.instance.client_cert_remove_msg); } catch (e) { if (_isCancellation(e)) { return; } _log.severe("Error removing client cert", e); - showMessage("client_cert_invalid_msg".tr()); + showMessage(StaticTranslations.instance.client_cert_invalid_msg); } } From d799698c51c5d045115f464653fd35cb6eb81d3c Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:41:12 +0530 Subject: [PATCH 19/49] chore: migrate mobile translations accessor (3) (#30670) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../lib/pages/backup/drift_backup.page.dart | 44 ++++++------ .../drift_backup_album_selection.page.dart | 48 +++++-------- .../drift_backup_asset_detail.page.dart | 9 ++- .../backup/drift_backup_options.page.dart | 9 +-- .../backup/drift_upload_detail.page.dart | 40 +++++------ mobile/lib/pages/common/settings.page.dart | 69 ++++++++++++------- .../search/map/map_location_picker.page.dart | 7 +- .../lib/pages/settings/sync_status.page.dart | 4 +- .../backup/backup_toggle_button.widget.dart | 6 +- .../presentation/widgets/map/map.widget.dart | 4 +- .../widgets/map/map_settings_sheet.dart | 7 +- .../presentation/widgets/map/map_utils.dart | 18 ++--- mobile/lib/utils/map_utils.dart | 18 ++--- .../lib/widgets/backup/backup_info_card.dart | 9 ++- .../map_settings_time_dropdown.dart | 20 +++--- .../map/map_settings/map_theme_picker.dart | 4 +- .../widgets/settings/advanced_settings.dart | 34 ++++----- .../asset_list_group_settings.dart | 14 ++-- .../asset_list_layout_settings.dart | 10 +-- .../asset_list_settings.dart | 4 +- .../drift_backup_settings.dart | 48 +++++-------- .../widgets/settings/language_settings.dart | 10 +-- .../settings/notification_setting.dart | 20 +++--- .../preference_settings/haptic_setting.dart | 9 +-- .../primary_color_setting.dart | 10 +-- .../preference_settings/theme_setting.dart | 15 ++-- 26 files changed, 227 insertions(+), 263 deletions(-) diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index e2472f1063..ad2674b81b 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; @@ -10,7 +9,6 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; @@ -108,7 +106,7 @@ class _DriftBackupPageState extends ConsumerState { return Scaffold( appBar: AppBar( elevation: 0, - title: Text("backup_controller_page_backup".t()), + title: Text(context.t.backup_controller_page_backup), leading: IconButton( onPressed: () { unawaited(context.maybePop(true)); @@ -122,7 +120,7 @@ class _DriftBackupPageState extends ConsumerState { unawaited(context.pushRoute(const DriftBackupOptionsRoute())); }, icon: const Icon(Icons.settings_outlined), - tooltip: "backup_options".t(context: context), + tooltip: context.t.backup_options, ), ], ), @@ -319,7 +317,7 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { Widget buildSelectedAlbumName() { - String text = "backup_controller_page_backup_selected".tr(); + String text = context.t.backup_controller_page_backup_selected; final albums = ref .read(backupAlbumProvider) .where((album) => album.backupSelection == BackupSelection.selected) @@ -328,7 +326,7 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { if (albums.isNotEmpty) { for (var album in albums) { if (album.name == "Recent" || album.name == "Recents") { - text += "${album.name} (${'all'.tr()}), "; + text += "${album.name} (${context.t.all}), "; } else { text += "${album.name}, "; } @@ -345,7 +343,7 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( - "backup_controller_page_none_selected".tr(), + context.t.backup_controller_page_none_selected, style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), ), ); @@ -353,7 +351,7 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { } Widget buildExcludedAlbumName() { - String text = "backup_controller_page_excluded".tr(); + String text = context.t.backup_controller_page_excluded; final albums = ref .read(backupAlbumProvider) .where((album) => album.backupSelection == BackupSelection.excluded) @@ -385,16 +383,16 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { borderOnForeground: false, child: ListTile( minVerticalPadding: 18, - title: Text("backup_controller_page_albums", style: context.textTheme.titleMedium).tr(), + title: Text(context.t.backup_controller_page_albums, style: context.textTheme.titleMedium), subtitle: Padding( padding: const EdgeInsets.only(top: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "backup_controller_page_to_backup", + context.t.backup_controller_page_to_backup, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), + ), buildSelectedAlbumName(), buildExcludedAlbumName(), ], @@ -413,7 +411,7 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { } unawaited(ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id)); }, - child: const Text("select", style: TextStyle(fontWeight: FontWeight.bold)).tr(), + child: Text(context.t.select, style: const TextStyle(fontWeight: FontWeight.bold)), ), ), ); @@ -428,8 +426,8 @@ class _TotalCard extends ConsumerWidget { final totalCount = ref.watch(driftBackupProvider.select((p) => p.totalCount)); return BackupInfoCard( - title: "total".tr(), - subtitle: "backup_controller_page_total_sub".tr(), + title: context.t.total, + subtitle: context.t.backup_controller_page_total_sub, info: totalCount.toString(), ); } @@ -444,8 +442,8 @@ class _BackupCard extends ConsumerWidget { final syncStatus = ref.watch(syncStatusProvider); return BackupInfoCard( - title: "backup_controller_page_backup".tr(), - subtitle: "backup_controller_page_backup_sub".tr(), + title: context.t.backup_controller_page_backup, + subtitle: context.t.backup_controller_page_backup_sub, info: backupCount.toString(), isLoading: syncStatus.isRemoteSyncing, ); @@ -472,11 +470,11 @@ class _RemainderCard extends ConsumerWidget { ListTile( minVerticalPadding: 18, isThreeLine: true, - title: Text("backup_controller_page_remainder".t(context: context), style: context.textTheme.titleMedium), + title: Text(context.t.backup_controller_page_remainder, style: context.textTheme.titleMedium), subtitle: Padding( padding: const EdgeInsets.only(top: 4.0, right: 18.0), child: Text( - "backup_controller_page_remainder_sub".t(context: context), + context.t.backup_controller_page_remainder_sub, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ), @@ -509,11 +507,11 @@ class _RemainderCard extends ConsumerWidget { ], ), Text( - "backup_info_card_assets", + context.t.backup_info_card_assets, style: context.textTheme.labelLarge?.copyWith( color: context.colorScheme.onSurface.withAlpha(syncStatus.isRemoteSyncing ? 50 : 255), ), - ).tr(), + ), ], ), ), @@ -530,7 +528,7 @@ class _RemainderCard extends ConsumerWidget { ), onTap: () => context.pushRoute(const DriftBackupAssetDetailRoute()), title: Text( - "view_details".t(context: context), + context.t.view_details, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), ), trailing: Icon(Icons.arrow_forward_ios, size: 16, color: context.colorScheme.onSurfaceVariant), @@ -622,7 +620,7 @@ class _PreparingStatusState extends ConsumerState { Row( children: [ Text( - "preparing".t(context: context), + context.t.preparing, style: context.textTheme.labelLarge?.copyWith( color: context.colorScheme.onSurface.withAlpha(200), ), @@ -652,7 +650,7 @@ class _PreparingStatusState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - "ready_for_upload".t(context: context), + context.t.ready_for_upload, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), ), const SizedBox(height: 2), diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart index de0ad52b5a..26719c2884 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -1,13 +1,12 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; @@ -154,13 +153,13 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState setState(() => _searchQuery = value.trim()), ) - : const Text("backup_album_selection_page_select_albums").t(context: context), + : Text(context.t.backup_album_selection_page_select_albums), actions: [ if (!_isSearchMode) IconButton( @@ -194,9 +193,9 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState> result = ref.watch(driftBackupCandidateProvider); return Scaffold( - appBar: AppBar(title: Text('backup_controller_page_remainder'.t(context: context))), + appBar: AppBar(title: Text(context.t.backup_controller_page_remainder)), body: result.when( data: (List candidates) { return ListView.separated( @@ -66,7 +65,7 @@ class DriftBackupAssetDetailPage extends ConsumerWidget { ); }, error: (error, stackTrace) => Text( - 'error_saving_image'.tr(args: [error.toString()]), + context.t.error_saving_image(error: error.toString()), style: TextStyle(color: context.colorScheme.error), ), loading: () => @@ -102,7 +101,7 @@ class DriftBackupAssetDetailPage extends ConsumerWidget { ); }, error: (Object error, StackTrace stackTrace) { - return Center(child: Text('error_saving_image'.tr(args: [error.toString()]))); + return Center(child: Text(context.t.error_saving_image(error: error.toString()))); }, loading: () { return const SizedBox(height: 48, width: 48, child: Center(child: CircularProgressIndicator.adaptive())); diff --git a/mobile/lib/pages/backup/drift_backup_options.page.dart b/mobile/lib/pages/backup/drift_backup_options.page.dart index ccd486e04d..994f09766c 100644 --- a/mobile/lib/pages/backup/drift_backup_options.page.dart +++ b/mobile/lib/pages/backup/drift_backup_options.page.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; @@ -51,10 +51,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { } ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("network_requirements_updated".t(context: context)), - duration: const Duration(seconds: 4), - ), + SnackBar(content: Text(context.t.network_requirements_updated), duration: const Duration(seconds: 4)), ); final backupNotifier = ref.read(driftBackupProvider.notifier); @@ -72,7 +69,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { } }, child: Scaffold( - appBar: AppBar(title: Text("backup_options".t(context: context))), + appBar: AppBar(title: Text(context.t.backup_options)), body: const DriftBackupSettings(), ), ); diff --git a/mobile/lib/pages/backup/drift_upload_detail.page.dart b/mobile/lib/pages/backup/drift_upload_detail.page.dart index 5ed43672d9..6993601c62 100644 --- a/mobile/lib/pages/backup/drift_upload_detail.page.dart +++ b/mobile/lib/pages/backup/drift_upload_detail.page.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; @@ -80,7 +80,7 @@ class _DriftUploadDetailPageState extends ConsumerState { return Scaffold( appBar: AppBar( - title: Text("upload_details".t(context: context)), + title: Text(context.t.upload_details), backgroundColor: context.colorScheme.surface, elevation: 0, scrolledUnderElevation: 1, @@ -125,7 +125,7 @@ class _DriftUploadDetailPageState extends ConsumerState { SliverToBoxAdapter( child: _buildSectionHeader( context, - title: "uploading".t(context: context), + title: context.t.uploading, count: uploadingItems.length, color: context.colorScheme.primary, ), @@ -151,7 +151,7 @@ class _DriftUploadDetailPageState extends ConsumerState { SliverToBoxAdapter( child: _buildSectionHeader( context, - title: "errors_text".t(context: context), + title: context.t.errors_text, count: failedItems.length, color: context.colorScheme.error, ), @@ -231,7 +231,7 @@ class _DriftUploadDetailPageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "downloading_from_icloud".t(context: context), + context.t.downloading_from_icloud, style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -318,7 +318,7 @@ class _DriftUploadDetailPageState extends ConsumerState { const SizedBox(height: 2), Text( isFailed - ? item.error ?? "unable_to_upload_file".t(context: context) + ? item.error ?? context.t.errors.unable_to_upload_file : "${formatHumanReadableBytes(item.fileSize, 1)} • ${item.networkSpeedAsString}", style: context.textTheme.labelLarge?.copyWith( color: isFailed @@ -395,7 +395,7 @@ class _DriftUploadDetailPageState extends ConsumerState { ), const SizedBox(height: 4), Text( - item.error ?? "unable_to_upload_file".t(context: context), + item.error ?? context.t.errors.unable_to_upload_file, style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.error), maxLines: 4, overflow: TextOverflow.ellipsis, @@ -556,7 +556,7 @@ class FileDetailDialog extends ConsumerWidget { const SizedBox(width: 8), Expanded( child: Text( - "details".t(context: context), + context.t.details, style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor), ), ), @@ -595,20 +595,14 @@ class FileDetailDialog extends ConsumerWidget { const SizedBox(height: 24), if (asset != null) _buildInfoSection(context, [ - _buildInfoRow(context, "filename".t(context: context), path.basename(uploadStatus.filename)), - _buildInfoRow(context, "local_id".t(context: context), asset.id), - _buildInfoRow( - context, - "file_size".t(context: context), - formatHumanReadableBytes(uploadStatus.fileSize, 2), - ), - if (asset.width != null) _buildInfoRow(context, "width".t(context: context), "${asset.width}px"), - if (asset.height != null) - _buildInfoRow(context, "height".t(context: context), "${asset.height}px"), - _buildInfoRow(context, "created_at".t(context: context), asset.createdAt.toString()), - _buildInfoRow(context, "updated_at".t(context: context), asset.updatedAt.toString()), - if (asset.checksum != null) - _buildInfoRow(context, "checksum".t(context: context), asset.checksum!), + _buildInfoRow(context, context.t.filename, path.basename(uploadStatus.filename)), + _buildInfoRow(context, context.t.local_id, asset.id), + _buildInfoRow(context, context.t.file_size, formatHumanReadableBytes(uploadStatus.fileSize, 2)), + if (asset.width != null) _buildInfoRow(context, context.t.width, "${asset.width}px"), + if (asset.height != null) _buildInfoRow(context, context.t.height, "${asset.height}px"), + _buildInfoRow(context, context.t.created_at, asset.createdAt.toString()), + _buildInfoRow(context, context.t.updated_at, asset.updatedAt.toString()), + if (asset.checksum != null) _buildInfoRow(context, context.t.checksum, asset.checksum!), ]), ], ), @@ -620,7 +614,7 @@ class FileDetailDialog extends ConsumerWidget { TextButton( onPressed: () => Navigator.of(context).pop(), child: Text( - "close".t(), + context.t.close, style: TextStyle(fontWeight: FontWeight.w600, color: context.primaryColor), ), ), diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index d102fe0d3b..2e941a70a3 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -1,5 +1,4 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -18,21 +17,45 @@ import 'package:immich_mobile/widgets/settings/preference_settings/preference_se import 'package:immich_mobile/widgets/settings/settings_card.dart'; enum SettingSection { - advanced('advanced', Icons.build_outlined, "advanced_settings_tile_subtitle"), - assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"), - backup('backup', Icons.cloud_upload_outlined, "backup_settings_subtitle"), - freeUpSpace('free_up_space', Icons.cleaning_services_outlined, "free_up_space_settings_subtitle"), - languages('language', Icons.language, "setting_languages_subtitle"), - networking('networking_settings', Icons.wifi, "networking_subtitle"), - notifications('notifications', Icons.notifications_none_rounded, "setting_notifications_subtitle"), - preferences('preferences_settings_title', Icons.interests_outlined, "preferences_settings_subtitle"), - timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined, "asset_list_settings_subtitle"), - beta('sync_status', Icons.sync_outlined, "sync_status_subtitle"); + advanced(Icons.build_outlined), + assetViewer(Icons.image_outlined), + backup(Icons.cloud_upload_outlined), + freeUpSpace(Icons.cleaning_services_outlined), + languages(Icons.language), + networking(Icons.wifi), + notifications(Icons.notifications_none_rounded), + preferences(Icons.interests_outlined), + timeline(Icons.auto_awesome_mosaic_outlined), + beta(Icons.sync_outlined); - final String title; - final String subtitle; final IconData icon; + String title(Translations t) => switch (this) { + SettingSection.advanced => t.advanced, + SettingSection.assetViewer => t.asset_viewer_settings_title, + SettingSection.backup => t.backup, + SettingSection.freeUpSpace => t.free_up_space, + SettingSection.languages => t.language, + SettingSection.networking => t.networking_settings, + SettingSection.notifications => t.notifications, + SettingSection.preferences => t.preferences_settings_title, + SettingSection.timeline => t.asset_list_settings_title, + SettingSection.beta => t.sync_status, + }; + + String subtitle(Translations t) => switch (this) { + SettingSection.advanced => t.advanced_settings_tile_subtitle, + SettingSection.assetViewer => t.asset_viewer_settings_subtitle, + SettingSection.backup => t.backup_settings_subtitle, + SettingSection.freeUpSpace => t.free_up_space_settings_subtitle, + SettingSection.languages => t.setting_languages_subtitle, + SettingSection.networking => t.networking_subtitle, + SettingSection.notifications => t.setting_notifications_subtitle, + SettingSection.preferences => t.preferences_settings_subtitle, + SettingSection.timeline => t.asset_list_settings_subtitle, + SettingSection.beta => t.sync_status_subtitle, + }; + Widget get widget => switch (this) { SettingSection.advanced => const AdvancedSettings(), SettingSection.assetViewer => const AssetViewerSettings(), @@ -46,7 +69,7 @@ enum SettingSection { SettingSection.beta => const SyncStatusAndActions(), }; - const SettingSection(this.title, this.icon, this.subtitle); + const SettingSection(this.icon); } @RoutePage() @@ -55,9 +78,8 @@ class SettingsPage extends StatelessWidget { @override Widget build(BuildContext context) { - context.locale; return Scaffold( - appBar: AppBar(centerTitle: false, title: const Text('settings').tr()), + appBar: AppBar(centerTitle: false, title: Text(context.t.settings)), body: context.isMobile ? const _MobileLayout() : const _TabletLayout(), ); } @@ -73,15 +95,15 @@ class _MobileLayout extends StatelessWidget { ? [ SettingsCard( icon: Icons.sync_outlined, - title: 'sync_status'.tr(), - subtitle: 'sync_status_subtitle'.tr(), + title: context.t.sync_status, + subtitle: context.t.sync_status_subtitle, settingRoute: const SyncStatusRoute(), ), ] : [ SettingsCard( - title: setting.title.tr(), - subtitle: setting.subtitle.tr(), + title: setting.title(context.t), + subtitle: setting.subtitle(context.t), icon: setting.icon, settingRoute: SettingsSubRoute(section: setting), ), @@ -116,7 +138,7 @@ class _TabletLayout extends HookWidget { ...SettingSection.values.map( (s) => SliverToBoxAdapter( child: ListTile( - title: Text(s.title).tr(), + title: Text(s.title(context.t)), leading: Icon(s.icon), selected: s.index == selectedSection.value.index, selectedColor: context.primaryColor, @@ -127,7 +149,7 @@ class _TabletLayout extends HookWidget { ), SliverToBoxAdapter( child: ListTile( - title: Text('whats_new'.tr()), + title: Text(context.t.whats_new), leading: const Icon(Icons.auto_awesome_outlined), onTap: () => context.pushRoute(const WhatsNewRoute()), ), @@ -150,9 +172,8 @@ class SettingsSubPage extends StatelessWidget { @override Widget build(BuildContext context) { - context.locale; return Scaffold( - appBar: AppBar(centerTitle: false, title: Text(section.title).tr()), + appBar: AppBar(centerTitle: false, title: Text(section.title(context.t))), body: section.widget, ); } diff --git a/mobile/lib/pages/search/map/map_location_picker.page.dart b/mobile/lib/pages/search/map/map_location_picker.page.dart index e8a507bfdc..b1d79b5150 100644 --- a/mobile/lib/pages/search/map/map_location_picker.page.dart +++ b/mobile/lib/pages/search/map/map_location_picker.page.dart @@ -2,12 +2,12 @@ import 'dart:async'; import 'dart:math'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/utils/map_utils.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -148,10 +148,7 @@ class _BottomBar extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - ElevatedButton( - onPressed: onUseLocation, - child: const Text("map_location_picker_page_use_location").tr(), - ), + ElevatedButton(onPressed: onUseLocation, child: Text(context.t.map_location_picker_page_use_location)), ElevatedButton(onPressed: onGetCurrentLocation, child: const Icon(Icons.my_location)), ], ), diff --git a/mobile/lib/pages/settings/sync_status.page.dart b/mobile/lib/pages/settings/sync_status.page.dart index 58750e9e30..6a0af1230d 100644 --- a/mobile/lib/pages/settings/sync_status.page.dart +++ b/mobile/lib/pages/settings/sync_status.page.dart @@ -1,6 +1,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart'; @RoutePage() @@ -12,7 +12,7 @@ class SyncStatusPage extends StatelessWidget { return Scaffold( appBar: AppBar( elevation: 0, - title: const Text("sync_status").t(context: context), + title: Text(context.t.sync_status), leading: IconButton( onPressed: () => context.maybePop(true), splashRadius: 24, diff --git a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart index b424298f5b..383ec17952 100644 --- a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart +++ b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; @@ -135,7 +135,7 @@ class BackupToggleButtonState extends ConsumerState with Sin children: [ Flexible( child: Text( - "enable_backup".t(context: context), + context.t.enable_backup, style: context.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: context.primaryColor, @@ -148,7 +148,7 @@ class BackupToggleButtonState extends ConsumerState with Sin Padding( padding: const EdgeInsets.only(top: 2), child: Text( - "upload_error_with_count".t(context: context, args: {'count': '$errorCount'}), + context.t.upload_error_with_count(count: errorCount), style: context.textTheme.labelMedium?.copyWith(color: context.colorScheme.error), ), ), diff --git a/mobile/lib/presentation/widgets/map/map.widget.dart b/mobile/lib/presentation/widgets/map/map.widget.dart index 918d64054b..ab2db23d8b 100644 --- a/mobile/lib/presentation/widgets/map/map.widget.dart +++ b/mobile/lib/presentation/widgets/map/map.widget.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; @@ -164,7 +164,7 @@ class _DriftMapState extends ConsumerState { context: context, gravity: ToastGravity.BOTTOM, toastType: ToastType.error, - msg: "map_cannot_get_user_location".t(context: context), + msg: context.t.map_cannot_get_user_location, ); } return; diff --git a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart index 2680850bfe..c2a5678ee9 100644 --- a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart +++ b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/time_range.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; import 'package:immich_mobile/widgets/map/map_settings/map_custom_time_range.dart'; @@ -50,18 +49,18 @@ class _DriftMapSettingsSheetState extends ConsumerState { ), const Divider(height: 30, thickness: 1), MapSettingsListTile( - title: "map_settings_only_show_favorites".t(context: context), + title: context.t.map_settings_only_show_favorites, selected: mapState.onlyFavorites, onChanged: (favoriteOnly) => ref.read(mapStateProvider.notifier).switchFavoriteOnly(favoriteOnly), ), MapSettingsListTile( - title: "map_settings_include_show_archived".t(context: context), + title: context.t.map_settings_include_show_archived, selected: mapState.includeArchived, onChanged: (includeArchive) => ref.read(mapStateProvider.notifier).switchIncludeArchived(includeArchive), ), MapSettingsListTile( - title: "map_settings_include_show_partners".t(context: context), + title: context.t.map_settings_include_show_partners, selected: mapState.withPartners, onChanged: (withPartners) => ref.read(mapStateProvider.notifier).switchWithPartners(withPartners), ), diff --git a/mobile/lib/presentation/widgets/map/map_utils.dart b/mobile/lib/presentation/widgets/map/map_utils.dart index 34116b144b..3ed6ceee25 100644 --- a/mobile/lib/presentation/widgets/map/map_utils.dart +++ b/mobile/lib/presentation/widgets/map/map_utils.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:logging/logging.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -124,10 +124,10 @@ class MapUtils { class _LocationServiceDisabledDialog extends ConfirmDialog { _LocationServiceDisabledDialog(BuildContext context) : super( - title: 'map_location_service_disabled_title'.t(context: context), - content: 'map_location_service_disabled_content'.t(context: context), - cancel: 'cancel'.t(context: context), - ok: 'yes'.t(context: context), + title: context.t.map_location_service_disabled_title, + content: context.t.map_location_service_disabled_content, + cancel: context.t.cancel, + ok: context.t.yes, onOk: () async { await Geolocator.openLocationSettings(); }, @@ -137,10 +137,10 @@ class _LocationServiceDisabledDialog extends ConfirmDialog { class _LocationPermissionDisabledDialog extends ConfirmDialog { _LocationPermissionDisabledDialog(BuildContext context) : super( - title: 'map_no_location_permission_title'.t(context: context), - content: 'map_no_location_permission_content'.t(context: context), - cancel: 'cancel'.t(context: context), - ok: 'yes'.t(context: context), + title: context.t.map_no_location_permission_title, + content: context.t.map_no_location_permission_content, + cancel: context.t.cancel, + ok: context.t.yes, onOk: () {}, ); } diff --git a/mobile/lib/utils/map_utils.dart b/mobile/lib/utils/map_utils.dart index d5e6f957a5..2a89b63422 100644 --- a/mobile/lib/utils/map_utils.dart +++ b/mobile/lib/utils/map_utils.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/map/map_marker.model.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:logging/logging.dart'; @@ -121,10 +121,10 @@ class MapUtils { class _LocationServiceDisabledDialog extends ConfirmDialog { _LocationServiceDisabledDialog() : super( - title: 'map_location_service_disabled_title'.tr(), - content: 'map_location_service_disabled_content'.tr(), - cancel: 'cancel'.tr(), - ok: 'yes'.tr(), + title: StaticTranslations.instance.map_location_service_disabled_title, + content: StaticTranslations.instance.map_location_service_disabled_content, + cancel: StaticTranslations.instance.cancel, + ok: StaticTranslations.instance.yes, onOk: () async { await Geolocator.openLocationSettings(); }, @@ -134,10 +134,10 @@ class _LocationServiceDisabledDialog extends ConfirmDialog { class _LocationPermissionDisabledDialog extends ConfirmDialog { _LocationPermissionDisabledDialog() : super( - title: 'map_no_location_permission_title'.tr(), - content: 'map_no_location_permission_content'.tr(), - cancel: 'cancel'.tr(), - ok: 'yes'.tr(), + title: StaticTranslations.instance.map_no_location_permission_title, + content: StaticTranslations.instance.map_no_location_permission_content, + cancel: StaticTranslations.instance.cancel, + ok: StaticTranslations.instance.yes, onOk: () {}, ); } diff --git a/mobile/lib/widgets/backup/backup_info_card.dart b/mobile/lib/widgets/backup/backup_info_card.dart index 7911679577..4efd940e36 100644 --- a/mobile/lib/widgets/backup/backup_info_card.dart +++ b/mobile/lib/widgets/backup/backup_info_card.dart @@ -1,8 +1,7 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class BackupInfoCard extends StatelessWidget { final String title; @@ -73,11 +72,11 @@ class BackupInfoCard extends StatelessWidget { ], ), Text( - "backup_info_card_assets", + context.t.backup_info_card_assets, style: context.textTheme.labelLarge?.copyWith( color: context.colorScheme.onSurface.withAlpha(isLoading ? 50 : 255), ), - ).tr(), + ), ], ), ), @@ -93,7 +92,7 @@ class BackupInfoCard extends StatelessWidget { ), onTap: onTap, title: Text( - "view_details".t(context: context), + context.t.view_details, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), ), trailing: Icon(Icons.arrow_forward_ios, size: 16, color: context.colorScheme.onSurfaceVariant), diff --git a/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart b/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart index 2a4dacaff7..eeebc28dda 100644 --- a/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart +++ b/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart @@ -1,7 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class MapTimeDropDown extends StatelessWidget { final int relativeTime; @@ -20,7 +19,7 @@ class MapTimeDropDown extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "date_range".t(context: context), + context.t.date_range, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), Flexible( @@ -30,24 +29,21 @@ class MapTimeDropDown extends StatelessWidget { initialSelection: relativeTime, onSelected: (value) => onTimeChange(value!), dropdownMenuEntries: [ - DropdownMenuEntry(value: 0, label: "all".t(context: context)), - DropdownMenuEntry(value: 1, label: "map_settings_date_range_option_day".t(context: context)), - DropdownMenuEntry(value: 7, label: "map_settings_date_range_option_days".tr(namedArgs: {'days': "7"})), - DropdownMenuEntry( - value: 30, - label: "map_settings_date_range_option_days".tr(namedArgs: {'days': "30"}), - ), + DropdownMenuEntry(value: 0, label: context.t.all), + DropdownMenuEntry(value: 1, label: context.t.map_settings_date_range_option_day), + DropdownMenuEntry(value: 7, label: context.t.map_settings_date_range_option_days(days: 7)), + DropdownMenuEntry(value: 30, label: context.t.map_settings_date_range_option_days(days: 30)), DropdownMenuEntry( value: now .difference(DateTime(now.year - 1, now.month, now.day, now.hour, now.minute, now.second)) .inDays, - label: "map_settings_date_range_option_year".t(context: context), + label: context.t.map_settings_date_range_option_year, ), DropdownMenuEntry( value: now .difference(DateTime(now.year - 3, now.month, now.day, now.hour, now.minute, now.second)) .inDays, - label: "map_settings_date_range_option_years".t(args: {'years': "3"}), + label: context.t.map_settings_date_range_option_years(years: 3), ), ], ), diff --git a/mobile/lib/widgets/map/map_settings/map_theme_picker.dart b/mobile/lib/widgets/map/map_settings/map_theme_picker.dart index e66f08c221..ec83d5085e 100644 --- a/mobile/lib/widgets/map/map_settings/map_theme_picker.dart +++ b/mobile/lib/widgets/map/map_settings/map_theme_picker.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -18,7 +18,7 @@ class MapThemePicker extends StatelessWidget { padding: const EdgeInsets.only(bottom: 20), child: Center( child: Text( - "map_settings_theme_settings".t(context: context), + context.t.map_settings_theme_settings, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), ), diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index cb035a22e9..9e80f8715c 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -2,12 +2,12 @@ import 'dart:async'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; @@ -71,8 +71,8 @@ class AdvancedSettings extends HookConsumerWidget { SettingsSwitchListTile( enabled: true, valueNotifier: advancedTroubleshooting, - title: "advanced_settings_troubleshooting_title".tr(), - subtitle: "advanced_settings_troubleshooting_subtitle".tr(), + title: context.t.advanced_settings_troubleshooting_title, + subtitle: context.t.advanced_settings_troubleshooting_subtitle, ), if (isManageMediaSupported.value) Column( @@ -80,8 +80,8 @@ class AdvancedSettings extends HookConsumerWidget { SettingsSwitchListTile( enabled: true, valueNotifier: manageLocalMediaAndroid, - title: "advanced_settings_sync_remote_deletions_title".tr(), - subtitle: "advanced_settings_sync_remote_deletions_subtitle".tr(), + title: context.t.advanced_settings_sync_remote_deletions_title, + subtitle: context.t.advanced_settings_sync_remote_deletions_subtitle, onChanged: (value) async { if (value) { final result = await ref.read(permissionRepositoryProvider).requestManageMediaPermission(); @@ -91,9 +91,9 @@ class AdvancedSettings extends HookConsumerWidget { }, ), SettingsActionTile( - title: "manage_media_access_title".tr(), - statusText: manageMediaAndroidPermission.value ? "allowed".tr() : "not_allowed".tr(), - subtitle: "manage_media_access_rationale".tr(), + title: context.t.manage_media_access_title, + statusText: manageMediaAndroidPermission.value ? context.t.allowed : context.t.not_allowed, + subtitle: context.t.manage_media_access_rationale, statusColor: manageLocalMediaAndroid.value && !manageMediaAndroidPermission.value ? const Color.fromARGB(255, 243, 188, 106) : null, @@ -105,7 +105,7 @@ class AdvancedSettings extends HookConsumerWidget { ], ), SettingsSliderListTile( - text: "advanced_settings_log_level_title".tr(namedArgs: {'level': logLevel}), + text: context.t.advanced_settings_log_level_title(level: logLevel), valueNotifier: levelId, maxValue: 8, minValue: 1, @@ -114,15 +114,15 @@ class AdvancedSettings extends HookConsumerWidget { ), SettingsSwitchListTile( valueNotifier: preferRemote, - title: "advanced_settings_prefer_remote_title".tr(), - subtitle: "advanced_settings_prefer_remote_subtitle".tr(), + title: context.t.advanced_settings_prefer_remote_title, + subtitle: context.t.advanced_settings_prefer_remote_subtitle, ), const CustomProxyHeaderSettings(), const SslClientCertSettings(), SettingsSwitchListTile( valueNotifier: readonlyModeEnabled, - title: "advanced_settings_readonly_mode_title".tr(), - subtitle: "advanced_settings_readonly_mode_subtitle".tr(), + title: context.t.advanced_settings_readonly_mode_title, + subtitle: context.t.advanced_settings_readonly_mode_subtitle, onChanged: (value) { readonlyModeEnabled.value = value; ref.read(readonlyModeProvider.notifier).setReadonlyMode(value); @@ -130,7 +130,7 @@ class AdvancedSettings extends HookConsumerWidget { SnackBar( duration: const Duration(seconds: 2), content: Text( - (value ? "readonly_mode_enabled" : "readonly_mode_disabled").tr(), + value ? context.t.readonly_mode_enabled : context.t.readonly_mode_disabled, style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), ), @@ -138,7 +138,7 @@ class AdvancedSettings extends HookConsumerWidget { }, ), ListTile( - title: Text("advanced_settings_clear_image_cache".tr(), style: const TextStyle(fontWeight: FontWeight.w500)), + title: Text(context.t.advanced_settings_clear_image_cache, style: const TextStyle(fontWeight: FontWeight.w500)), leading: const Icon(Icons.playlist_remove_rounded), onTap: () async { final int clearedBytes; @@ -153,7 +153,7 @@ class AdvancedSettings extends HookConsumerWidget { SnackBar( duration: const Duration(seconds: 2), content: Text( - "advanced_settings_clear_image_cache_error".tr(), + context.t.advanced_settings_clear_image_cache_error, style: context.textTheme.bodyLarge?.copyWith(color: context.themeData.colorScheme.error), ), ), @@ -171,7 +171,7 @@ class AdvancedSettings extends HookConsumerWidget { SnackBar( duration: const Duration(seconds: 2), content: Text( - "advanced_settings_clear_image_cache_success".tr(namedArgs: {'size': clearedMB}), + context.t.advanced_settings_clear_image_cache_success(size: clearedMB), style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), ), diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart index a5ced2ef5f..a68ad0fe5e 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; @@ -34,20 +34,14 @@ class GroupSettings extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingGroupTitle( - title: "asset_list_group_by_sub_title".t(context: context), - icon: Icons.group_work_outlined, - ), + SettingGroupTitle(title: context.t.asset_list_group_by_sub_title, icon: Icons.group_work_outlined), SettingsRadioListTile( groups: [ SettingsRadioGroup( - title: 'asset_list_layout_settings_group_by_month_day'.t(context: context), + title: context.t.asset_list_layout_settings_group_by_month_day, value: GroupAssetsBy.day, ), - SettingsRadioGroup( - title: 'month'.t(context: context), - value: GroupAssetsBy.month, - ), + SettingsRadioGroup(title: context.t.month, value: GroupAssetsBy.month), ], groupBy: groupBy.value, onRadioChanged: changeGroupValue, diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart index 5b4c4bb89f..6ecd76dd78 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart @@ -1,10 +1,9 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; @@ -23,13 +22,10 @@ class LayoutSettings extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingGroupTitle( - title: "asset_list_layout_sub_title".t(context: context), - icon: Icons.view_module_outlined, - ), + SettingGroupTitle(title: context.t.asset_list_layout_sub_title, icon: Icons.view_module_outlined), SettingsSliderListTile( valueNotifier: tilesPerRow, - text: 'theme_setting_asset_list_tiles_per_row_title'.tr(namedArgs: {'count': "${tilesPerRow.value}"}), + text: context.t.theme_setting_asset_list_tiles_per_row_title(count: tilesPerRow.value), label: "${tilesPerRow.value}", maxValue: 6, minValue: 2, diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart index 1f3f4bbcf0..08fe897c5f 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_group_settings.dart'; @@ -21,7 +21,7 @@ class AssetListSettings extends HookConsumerWidget { final assetListSetting = [ SettingsSwitchListTile( valueNotifier: storageIndicator, - title: 'theme_setting_asset_list_storage_indicator_title'.tr(), + title: context.t.theme_setting_asset_list_storage_indicator_title, onChanged: (value) { unawaited(ref.read(settingsProvider).write(.timelineStorageIndicator, value)); ref.invalidate(appSettingsServiceProvider); diff --git a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart index e7d56ed66a..ae9ff87a91 100644 --- a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart @@ -8,7 +8,6 @@ import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; @@ -26,26 +25,17 @@ class DriftBackupSettings extends StatelessWidget { Widget build(BuildContext context) { return SettingsSubPageScaffold( settings: [ - SettingGroupTitle( - title: "network_requirements".t(context: context), - icon: Icons.cell_tower, - ), + SettingGroupTitle(title: context.t.network_requirements, icon: Icons.cell_tower), const _UseCellularForVideosButton(), const _UseCellularForPhotosButton(), if (CurrentPlatform.isAndroid) ...[ const Divider(), - SettingGroupTitle( - title: "background_options".t(context: context), - icon: Icons.charging_station_rounded, - ), + SettingGroupTitle(title: context.t.background_options, icon: Icons.charging_station_rounded), const _BackupOnlyWhenChargingButton(), const _BackupDelaySlider(), ], const Divider(), - SettingGroupTitle( - title: "backup_albums_sync".t(context: context), - icon: Icons.sync, - ), + SettingGroupTitle(title: context.t.backup_albums_sync, icon: Icons.sync), const _AlbumSyncActionButton(), ], ); @@ -108,8 +98,8 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> Column( children: [ SettingListTile( - title: "sync_albums".t(context: context), - subtitle: "sync_upload_album_setting_subtitle".t(context: context), + title: context.t.sync_albums, + subtitle: context.t.sync_upload_album_setting_subtitle, trailing: Switch( value: albumSyncEnable, onChanged: (bool newValue) async { @@ -131,8 +121,8 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> ? SettingListTile( onTap: _manualSyncAlbums, contentPadding: const EdgeInsets.only(left: 32, right: 16), - title: "organize_into_albums".t(context: context), - subtitle: "organize_into_albums_description".t(context: context), + title: context.t.organize_into_albums, + subtitle: context.t.organize_into_albums_description, trailing: isAlbumSyncInProgress ? const SizedBox( width: 32, @@ -161,15 +151,15 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> class _BackupSwitchTile extends ConsumerWidget { final SettingsKey metadataKey; final bool Function(AppConfig) selector; - final String titleKey; - final String subtitleKey; + final String title; + final String subtitle; final void Function(bool)? onChanged; const _BackupSwitchTile({ required this.metadataKey, required this.selector, - required this.titleKey, - required this.subtitleKey, + required this.title, + required this.subtitle, this.onChanged, }); @@ -179,8 +169,8 @@ class _BackupSwitchTile extends ConsumerWidget { return Padding( padding: const EdgeInsets.only(left: 8.0), child: SettingListTile( - title: titleKey.t(context: context), - subtitle: subtitleKey.t(context: context), + title: title, + subtitle: subtitle, trailing: Switch( value: value, onChanged: (bool newValue) async { @@ -201,8 +191,8 @@ class _UseCellularForVideosButton extends StatelessWidget { return _BackupSwitchTile( metadataKey: SettingsKey.backupUseCellularForVideos, selector: (c) => c.backup.useCellularForVideos, - titleKey: "videos", - subtitleKey: "network_requirement_videos_upload", + title: context.t.videos, + subtitle: context.t.network_requirement_videos_upload, ); } } @@ -215,8 +205,8 @@ class _UseCellularForPhotosButton extends StatelessWidget { return _BackupSwitchTile( metadataKey: SettingsKey.backupUseCellularForPhotos, selector: (c) => c.backup.useCellularForPhotos, - titleKey: "photos", - subtitleKey: "network_requirement_photos_upload", + title: context.t.photos, + subtitle: context.t.network_requirement_photos_upload, ); } } @@ -230,8 +220,8 @@ class _BackupOnlyWhenChargingButton extends ConsumerWidget { return _BackupSwitchTile( metadataKey: SettingsKey.backupRequireCharging, selector: (c) => c.backup.requireCharging, - titleKey: "charging", - subtitleKey: "charging_requirement_mobile_backup", + title: context.t.charging, + subtitle: context.t.charging_requirement_mobile_backup, onChanged: (value) { unawaited(fgService.configure(requireCharging: value)); }, diff --git a/mobile/lib/widgets/settings/language_settings.dart b/mobile/lib/widgets/settings/language_settings.dart index e8478da453..61f2088979 100644 --- a/mobile/lib/widgets/settings/language_settings.dart +++ b/mobile/lib/widgets/settings/language_settings.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; @@ -152,7 +152,7 @@ class _LanguageSearchBar extends StatelessWidget { child: SearchField( autofocus: false, contentPadding: const EdgeInsets.all(12), - hintText: 'language_search_hint'.t(context: context), + hintText: context.t.language_search_hint, prefixIcon: const Icon(Icons.search_rounded), suffixIcon: controller.text.isNotEmpty ? IconButton(icon: const Icon(Icons.clear_rounded), onPressed: onClear) @@ -179,12 +179,12 @@ class _LanguageNotFound extends StatelessWidget { Icon(Icons.search_off_rounded, size: 64, color: context.colorScheme.onSurface.withValues(alpha: 0.4)), const SizedBox(height: 8), Text( - 'language_no_results_title'.t(context: context), + context.t.language_no_results_title, style: context.textTheme.titleMedium?.copyWith(color: context.colorScheme.onSurface), ), const SizedBox(height: 4), Text( - 'language_no_results_subtitle'.t(context: context), + context.t.language_no_results_subtitle, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.8)), ), ], @@ -214,7 +214,7 @@ class _LanguageApplyButton extends StatelessWidget { child: isLoading ? const SizedBox.square(dimension: 24, child: CircularProgressIndicator(strokeWidth: 2)) : Text( - 'setting_languages_apply'.t(context: context), + context.t.setting_languages_apply, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16.0), ), ), diff --git a/mobile/lib/widgets/settings/notification_setting.dart b/mobile/lib/widgets/settings/notification_setting.dart index 0bab4e051a..c3d175999d 100644 --- a/mobile/lib/widgets/settings/notification_setting.dart +++ b/mobile/lib/widgets/settings/notification_setting.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/permission.provider.dart'; import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; @@ -29,10 +29,10 @@ class NotificationSetting extends HookConsumerWidget { showDialog( context: context, builder: (ctx) => AlertDialog( - content: const Text('notification_permission_dialog_content').tr(), + content: Text(ctx.t.notification_permission_dialog_content), actions: [ - TextButton(child: const Text('cancel').tr(), onPressed: () => ctx.pop()), - TextButton(onPressed: () => openAppNotificationSettings(ctx), child: const Text('settings').tr()), + TextButton(child: Text(ctx.t.cancel), onPressed: () => ctx.pop()), + TextButton(onPressed: () => openAppNotificationSettings(ctx), child: Text(ctx.t.settings)), ], ), ), @@ -43,9 +43,9 @@ class NotificationSetting extends HookConsumerWidget { if (!hasPermission) SettingsButtonListTile( icon: Icons.notifications_outlined, - title: 'notification_permission_list_tile_title'.tr(), - subtileText: 'notification_permission_list_tile_content'.tr(), - buttonText: 'notification_permission_list_tile_enable_button'.tr(), + title: context.t.notification_permission_list_tile_title, + subtileText: context.t.notification_permission_list_tile_content, + buttonText: context.t.notification_permission_list_tile_enable_button, onButtonTap: () => ref.read(notificationPermissionProvider.notifier).requestNotificationPermission().then((permission) { if (permission == PermissionStatus.permanentlyDenied) { @@ -56,9 +56,9 @@ class NotificationSetting extends HookConsumerWidget { else SettingsButtonListTile( icon: Icons.notifications_active_outlined, - title: 'notification_enabled_list_tile_title'.tr(), - subtileText: 'notification_enabled_list_tile_content'.tr(), - buttonText: 'notification_enabled_list_tile_open_button'.tr(), + title: context.t.notification_enabled_list_tile_title, + subtileText: context.t.notification_enabled_list_tile_content, + buttonText: context.t.notification_enabled_list_tile_open_button, onButtonTap: () => openAppSettings(), ), ]; diff --git a/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart b/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart index c5003a9ced..9aaae683ab 100644 --- a/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; @@ -21,13 +21,10 @@ class HapticSetting extends HookWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingGroupTitle( - title: "haptic_feedback_title".t(context: context), - icon: Icons.vibration_outlined, - ), + SettingGroupTitle(title: context.t.haptic_feedback_title, icon: Icons.vibration_outlined), SettingsSwitchListTile( valueNotifier: isHapticFeedbackEnabled, - title: 'enabled'.t(context: context), + title: context.t.enabled, onChanged: onHapticFeedbackChange, ), ], diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart index 7964ec3861..37235a182a 100644 --- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/theme/color_scheme.dart'; @@ -97,7 +97,7 @@ class PrimaryColorSetting extends HookConsumerWidget { children: [ Align( alignment: Alignment.center, - child: Text("theme_setting_primary_color_title".tr(), style: context.textTheme.titleLarge), + child: Text(context.t.theme_setting_primary_color_title, style: context.textTheme.titleLarge), ), if (DynamicTheme.isAvailable) Container( @@ -110,7 +110,7 @@ class PrimaryColorSetting extends HookConsumerWidget { tileColor: context.colorScheme.surfaceContainerHigh, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15))), title: Text( - 'theme_setting_system_primary_color_title'.tr(), + context.t.theme_setting_system_primary_color_title, style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), value: themeConfig.dynamicTheme, @@ -157,11 +157,11 @@ class PrimaryColorSetting extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "theme_setting_primary_color_title".tr(), + context.t.theme_setting_primary_color_title, style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), ), Text( - "theme_setting_primary_color_subtitle".tr(), + context.t.theme_setting_primary_color_subtitle, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ], diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index 8281b2262a..cf1a22c45f 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; @@ -52,26 +52,23 @@ class ThemeSetting extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingGroupTitle( - title: "theme".t(context: context), - icon: Icons.color_lens_outlined, - ), + SettingGroupTitle(title: context.t.theme, icon: Icons.color_lens_outlined), SettingsSwitchListTile( valueNotifier: isSystemTheme, - title: 'theme_setting_system_theme_switch'.t(context: context), + title: context.t.theme_setting_system_theme_switch, onChanged: onSystemThemeChange, ), if (currentTheme.value != ThemeMode.system) SettingsSwitchListTile( valueNotifier: isDarkTheme, - title: 'map_settings_dark_mode'.t(context: context), + title: context.t.map_settings_dark_mode, onChanged: onThemeChange, ), const PrimaryColorSetting(), SettingsSwitchListTile( valueNotifier: colorfulInterface, - title: "theme_setting_colorful_interface_title".t(context: context), - subtitle: 'theme_setting_colorful_interface_subtitle'.t(context: context), + title: context.t.theme_setting_colorful_interface_title, + subtitle: context.t.theme_setting_colorful_interface_subtitle, onChanged: onSurfaceColorSettingChange, ), ], From a8b3b30abaca33b9a3198d56dbcd78be09bcbd03 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:41:13 +0530 Subject: [PATCH 20/49] chore: migrate mobile translations accessor (4) (#30671) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../lib/pages/library/folder/folder.page.dart | 21 +- .../pages/library/locked/pin_auth.page.dart | 10 +- .../library/shared_link/shared_link.page.dart | 6 +- .../pages/drift_album_options.page.dart | 28 +-- .../pages/drift_archive.page.dart | 7 +- .../pages/drift_create_album.page.dart | 28 +-- .../pages/drift_library.page.dart | 35 ++- .../pages/drift_local_album.page.dart | 7 +- .../pages/drift_locked_folder.page.dart | 4 +- .../pages/drift_people_collection.page.dart | 8 +- .../pages/drift_remote_album.page.dart | 51 ++-- .../presentation/pages/drift_trash.page.dart | 3 +- .../widgets/album/album_selector.widget.dart | 45 ++-- .../widgets/album/album_tile.dart | 4 +- .../album/drift_activity_text_field.dart | 4 +- .../album/new_album_name_modal.widget.dart | 14 +- .../album/pending_uploads_banner.widget.dart | 13 +- .../archive_bottom_sheet.widget.dart | 8 +- .../local_album_bottom_sheet.widget.dart | 8 +- .../remote_album_bottom_sheet.widget.dart | 12 +- .../person_edit_birthday_modal.widget.dart | 17 +- .../people/person_edit_name_modal.widget.dart | 17 +- .../people/person_option_sheet.widget.dart | 6 +- .../drift_album_option.widget.dart | 28 +-- .../album/album_sort_by_options.provider.dart | 25 +- .../common/local_album_sliver_app_bar.dart | 4 +- .../widgets/common/person_sliver_app_bar.dart | 8 +- .../common/remote_album_sliver_app_bar.dart | 4 +- .../search/search_filter/people_picker.dart | 224 +++++++++--------- 29 files changed, 305 insertions(+), 344 deletions(-) diff --git a/mobile/lib/pages/library/folder/folder.page.dart b/mobile/lib/pages/library/folder/folder.page.dart index 69631efe4e..0264ceee16 100644 --- a/mobile/lib/pages/library/folder/folder.page.dart +++ b/mobile/lib/pages/library/folder/folder.page.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -10,6 +9,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; import 'package:immich_mobile/models/folder/root_folder.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; @@ -20,6 +20,7 @@ import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:intl/intl.dart'; RecursiveFolder? _findFolderInStructure(RootFolder rootFolder, RecursiveFolder targetFolder) { for (final folder in rootFolder.subfolders) { @@ -81,7 +82,7 @@ class FolderPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: Text(currentFolder.value?.name ?? tr("folders")), + title: Text(currentFolder.value?.name ?? context.t.folders), elevation: 0, centerTitle: false, actions: [IconButton(icon: const Icon(Icons.swap_vert), onPressed: onToggleSortOrder)], @@ -96,8 +97,8 @@ class FolderPage extends HookConsumerWidget { }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) { - ImmichToast.show(context: context, msg: "failed_to_load_folder".tr(), toastType: ToastType.error); - return Center(child: const Text("failed_to_load_folder").tr()); + ImmichToast.show(context: context, msg: context.t.failed_to_load_folder, toastType: ToastType.error); + return Center(child: Text(context.t.failed_to_load_folder)); }, ), ); @@ -125,16 +126,16 @@ class FolderContent extends HookConsumerWidget { }, [folder]); if (folder == null) { - return Center(child: const Text("folder_not_found").tr()); + return Center(child: Text(context.t.folder_not_found)); } String getSubtitle(int subFolderCount) { if (subFolderCount > 0) { - return "$subFolderCount ${tr("folders")}".toLowerCase(); + return "$subFolderCount ${context.t.folders}".toLowerCase(); } if (subFolderCount == 1) { - return "1 ${tr("folder")}".toLowerCase(); + return "1 ${context.t.folder}".toLowerCase(); } return ""; @@ -147,7 +148,7 @@ class FolderContent extends HookConsumerWidget { child: folderRenderlist.when( data: (folderAssets) { if (folder!.subfolders.isEmpty && folderAssets.isEmpty) { - return Center(child: const Text("empty_folder").tr()); + return Center(child: Text(context.t.empty_folder)); } return ListView( @@ -211,8 +212,8 @@ class FolderContent extends HookConsumerWidget { }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) { - ImmichToast.show(context: context, msg: "failed_to_load_assets".tr(), toastType: ToastType.error); - return Center(child: const Text("failed_to_load_assets").tr()); + ImmichToast.show(context: context, msg: context.t.failed_to_load_assets, toastType: ToastType.error); + return Center(child: Text(context.t.failed_to_load_assets)); }, ), ), diff --git a/mobile/lib/pages/library/locked/pin_auth.page.dart b/mobile/lib/pages/library/locked/pin_auth.page.dart index 81e08d853b..db0e009d8c 100644 --- a/mobile/lib/pages/library/locked/pin_auth.page.dart +++ b/mobile/lib/pages/library/locked/pin_auth.page.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' show useState; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/local_auth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/forms/pin_registration_form.dart'; @@ -31,7 +31,7 @@ class PinAuthPage extends HookConsumerWidget { context.showSnackBar( SnackBar( - content: Text('biometric_auth_enabled'.tr(), style: context.textTheme.labelLarge), + content: Text(context.t.biometric_auth_enabled, style: context.textTheme.labelLarge), duration: const Duration(seconds: 3), backgroundColor: context.colorScheme.primaryContainer, ), @@ -52,7 +52,7 @@ class PinAuthPage extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ PinVerificationForm( - description: 'enable_biometric_auth_description'.tr(), + description: context.t.enable_biometric_auth_description, onSuccess: (pinCode) { Navigator.pop(buildContext); unawaited(registerBiometric(pinCode)); @@ -71,7 +71,7 @@ class PinAuthPage extends HookConsumerWidget { } return Scaffold( - appBar: AppBar(title: Text('locked_folder'.tr())), + appBar: AppBar(title: Text(context.t.locked_folder)), body: ListView( shrinkWrap: true, children: [ @@ -97,7 +97,7 @@ class PinAuthPage extends HookConsumerWidget { icon: const Icon(Icons.fingerprint, size: 28), onPressed: () => unawaited(enableBiometricAuth()), label: Text( - 'use_biometric'.tr(), + context.t.use_biometric, style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor, fontSize: 18), ), ), diff --git a/mobile/lib/pages/library/shared_link/shared_link.page.dart b/mobile/lib/pages/library/shared_link/shared_link.page.dart index 18fc235dd8..70cd4e2129 100644 --- a/mobile/lib/pages/library/shared_link/shared_link.page.dart +++ b/mobile/lib/pages/library/shared_link/shared_link.page.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/shared_link/shared_link.model.dart'; import 'package:immich_mobile/providers/shared_link.provider.dart'; import 'package:immich_mobile/widgets/shared_link/shared_link_item.dart'; @@ -36,7 +36,7 @@ class SharedLinkPage extends HookConsumerWidget { children: [ Icon(Icons.link_off, size: 100, color: Theme.of(context).colorScheme.onSurface.withAlpha(128)), const SizedBox(height: 20), - const Text("you_dont_have_any_shared_links", style: TextStyle(fontSize: 14)).tr(), + Text(context.t.you_dont_have_any_shared_links, style: const TextStyle(fontSize: 14)), ], ), ); @@ -68,7 +68,7 @@ class SharedLinkPage extends HookConsumerWidget { } return Scaffold( - appBar: AppBar(title: const Text("shared_link_app_bar_title").tr(), elevation: 0, centerTitle: false), + appBar: AppBar(title: Text(context.t.shared_link_app_bar_title), elevation: 0, centerTitle: false), body: SafeArea( child: sharedLinks.widgetWhen( onError: (error, stackTrace) => buildNoShares(), diff --git a/mobile/lib/presentation/pages/drift_album_options.page.dart b/mobile/lib/presentation/pages/drift_album_options.page.dart index 7b4c48a6b0..59cabf8daf 100644 --- a/mobile/lib/presentation/pages/drift_album_options.page.dart +++ b/mobile/lib/presentation/pages/drift_album_options.page.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; @@ -37,7 +37,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { ContextHelper(context).pop(); ImmichToast.show( context: context, - msg: "shared_album_section_people_action_error".t(context: context), + msg: context.t.shared_album_section_people_action_error, toastType: ToastType.error, gravity: ToastGravity.BOTTOM, ); @@ -87,7 +87,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { ImmichToast.show( context: context, - msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}), + msg: context.t.users_added_to_album_count(count: newUsers.length), toastType: ToastType.success, ); } catch (e) { @@ -106,7 +106,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { actions = [ ListTile( leading: const Icon(Icons.exit_to_app_rounded), - title: const Text("leave_album").t(context: context), + title: Text(context.t.leave_album), onTap: leaveAlbum, ), ]; @@ -116,7 +116,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { actions = [ ListTile( leading: const Icon(Icons.person_remove_rounded), - title: const Text("remove_user").t(context: context), + title: Text(context.t.remove_user), onTap: () => removeUserFromAlbum(user), ), ]; @@ -146,7 +146,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { leading: owner != null ? UserCircleAvatar(user: owner) : const SizedBox(), title: Text(album.ownerName, style: const TextStyle(fontWeight: FontWeight.w500)), subtitle: Text(owner?.email ?? "", style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), - trailing: Text("owner", style: context.textTheme.labelLarge).t(context: context), + trailing: Text(context.t.owner, style: context.textTheme.labelLarge), ); } else { final usersProvider = ref.read(driftUsersProvider); @@ -162,7 +162,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { leading: UserCircleAvatar(user: user), title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)), subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), - trailing: Text("owner", style: context.textTheme.labelLarge).t(context: context), + trailing: Text(context.t.owner, style: context.textTheme.labelLarge), ); }, orElse: () => const SizedBox(), @@ -207,7 +207,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { onPressed: () => context.maybePop(null), ), centerTitle: true, - title: Text("options".t(context: context)), + title: Text(context.t.options), ), body: ListView( children: [ @@ -222,19 +222,19 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor, dense: true, title: Text( - "comments_and_likes", + context.t.comments_and_likes, style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), - ).t(context: context), + ), subtitle: Text( - "let_others_respond", + context.t.let_others_respond, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).t(context: context), + ), ), - buildSectionTitle("shared_album_section_people_title".t(context: context)), + buildSectionTitle(context.t.shared_album_section_people_title), if (isOwner) ...[ ListTile( leading: const Icon(Icons.person_add_rounded), - title: Text("invite_people".t(context: context)), + title: Text(context.t.invite_people), onTap: () async => addUsers(), ), const Divider(indent: 16), diff --git a/mobile/lib/presentation/pages/drift_archive.page.dart b/mobile/lib/presentation/pages/drift_archive.page.dart index ee8417bcad..75c05eb070 100644 --- a/mobile/lib/presentation/pages/drift_archive.page.dart +++ b/mobile/lib/presentation/pages/drift_archive.page.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; @@ -28,10 +28,7 @@ class DriftArchivePage extends StatelessWidget { }), ], child: Timeline( - appBar: MesmerizingSliverAppBar( - title: 'archive'.t(context: context), - icon: Icons.archive_outlined, - ), + appBar: MesmerizingSliverAppBar(title: context.t.archive, icon: Icons.archive_outlined), bottomSheet: const ArchiveBottomSheet(), ), ); diff --git a/mobile/lib/presentation/pages/drift_create_album.page.dart b/mobile/lib/presentation/pages/drift_create_album.page.dart index 93534d62a5..f536b5b60f 100644 --- a/mobile/lib/presentation/pages/drift_create_album.page.dart +++ b/mobile/lib/presentation/pages/drift_create_album.page.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -52,9 +52,7 @@ class _DriftCreateAlbumPageState extends ConsumerState { bool get _canCreateAlbum => albumTitleController.text.trim().isNotEmpty && !isCreatingAlbum; String _getEffectiveTitle() { - return albumTitleController.text.isNotEmpty - ? albumTitleController.text - : 'create_album_page_untitled'.t(context: context); + return albumTitleController.text.isNotEmpty ? albumTitleController.text : context.t.create_album_page_untitled; } Widget _buildSliverAppBar() { @@ -93,7 +91,7 @@ class _DriftCreateAlbumPageState extends ConsumerState { Widget _buildEmptyState() { return Padding( padding: const EdgeInsets.only(top: 0, left: 18), - child: Text('create_shared_album_page_share_add_assets', style: context.textTheme.labelLarge).t(), + child: Text(context.t.create_shared_album_page_share_add_assets, style: context.textTheme.labelLarge), ); } @@ -112,9 +110,9 @@ class _DriftCreateAlbumPageState extends ConsumerState { label: Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( - 'create_shared_album_page_share_select_photos', + context.t.create_shared_album_page_share_select_photos, style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor), - ).t(), + ), ), ), ); @@ -148,7 +146,7 @@ class _DriftCreateAlbumPageState extends ConsumerState { }); if (albumTitleController.text.isEmpty) { - final untitledText = 'create_album_page_untitled'.t(); + final untitledText = context.t.create_album_page_untitled; albumTitleController.text = untitledText; } } @@ -196,7 +194,7 @@ class _DriftCreateAlbumPageState extends ConsumerState { return; } - ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.t()); + ImmichToast.show(context: context, toastType: ToastType.error, msg: context.t.errors.failed_to_create_album); } finally { if (mounted) { setState(() => isCreatingAlbum = false); @@ -241,7 +239,7 @@ class _DriftCreateAlbumPageState extends ConsumerState { AlbumActionFilledButton( iconData: Icons.add_photo_alternate_outlined, onPressed: onSelectPhotos, - labelText: "add_photos".t(), + labelText: context.t.add_photos, ), ], ), @@ -257,12 +255,12 @@ class _DriftCreateAlbumPageState extends ConsumerState { centerTitle: false, backgroundColor: context.scaffoldBackgroundColor, leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.close_rounded)), - title: const Text('create_album').t(), + title: Text(context.t.create_album), actions: [ TextButton( onPressed: _canCreateAlbum ? createAlbum : null, child: Text( - 'create'.t(), + context.t.create, style: TextStyle( fontWeight: FontWeight.bold, color: _canCreateAlbum ? context.primaryColor : context.themeData.disabledColor, @@ -320,7 +318,7 @@ class _AlbumTitleTextFieldState extends State<_AlbumTitleTextField> { style: TextStyle(fontSize: 28.0, color: context.colorScheme.onSurface, fontWeight: FontWeight.bold), controller: widget.textController, onTap: () { - if (widget.textController.text == 'create_album_page_untitled'.t(context: context)) { + if (widget.textController.text == context.t.create_album_page_untitled) { widget.textController.clear(); } }, @@ -343,7 +341,7 @@ class _AlbumTitleTextFieldState extends State<_AlbumTitleTextField> { borderSide: BorderSide(color: context.primaryColor.withValues(alpha: 0.3)), borderRadius: const BorderRadius.all(Radius.circular(16.0)), ), - hintText: 'add_a_title'.t(), + hintText: context.t.add_a_title, hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( fontSize: 28.0, fontWeight: FontWeight.bold, @@ -430,7 +428,7 @@ class _AlbumViewerEditableDescriptionState extends State<_AlbumViewerEditableDes focusColor: Colors.grey[300], fillColor: context.scaffoldBackgroundColor, filled: widget.focusNode.hasFocus, - hintText: 'add_a_description'.t(), + hintText: context.t.add_a_description, ), ), ); diff --git a/mobile/lib/presentation/pages/drift_library.page.dart b/mobile/lib/presentation/pages/drift_library.page.dart index 740de441e7..3118f2c4f0 100644 --- a/mobile/lib/presentation/pages/drift_library.page.dart +++ b/mobile/lib/presentation/pages/drift_library.page.dart @@ -1,11 +1,10 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/local_album_thumbnail.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/people/partner_user_avatar.widget.dart'; @@ -56,13 +55,13 @@ class _ActionButtonGrid extends ConsumerWidget { _ActionButton( icon: Icons.favorite_outline_rounded, onTap: () => context.pushRoute(const DriftFavoriteRoute()), - label: 'favorites'.t(context: context), + label: context.t.favorites, ), const SizedBox(width: 8), _ActionButton( icon: Icons.archive_outlined, onTap: () => context.pushRoute(const DriftArchiveRoute()), - label: 'archived'.t(context: context), + label: context.t.archived, ), ], ), @@ -72,14 +71,14 @@ class _ActionButtonGrid extends ConsumerWidget { _ActionButton( icon: Icons.link_outlined, onTap: () => context.pushRoute(const SharedLinkRoute()), - label: 'shared_links'.t(context: context), + label: context.t.shared_links, ), isTrashEnable ? const SizedBox(width: 8) : const SizedBox.shrink(), isTrashEnable ? _ActionButton( icon: Icons.delete_outline_rounded, onTap: () => context.pushRoute(const DriftTrashRoute()), - label: 'trash'.t(context: context), + label: context.t.trash, ) : const SizedBox.shrink(), ], @@ -193,7 +192,7 @@ class _PeopleCollectionCard extends ConsumerWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - 'people'.t(context: context), + context.t.people, style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, @@ -245,7 +244,7 @@ class _PlacesCollectionCard extends StatelessWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - 'places'.t(), + context.t.places, style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, @@ -303,9 +302,7 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget { }).toList(); }, error: (error, _) { - return [ - Center(child: Text('error_saving_image'.tr(args: [error.toString()]))), - ]; + return [Center(child: Text(context.t.error_saving_image(error: error.toString())))]; }, loading: () { return [const Center(child: CircularProgressIndicator())]; @@ -317,7 +314,7 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - 'on_this_device'.t(context: context), + context.t.on_this_device, style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, @@ -384,7 +381,7 @@ class _QuickAccessButtonList extends ConsumerWidget { ), leading: const Icon(Icons.folder_outlined, size: 26), title: Text( - 'folders'.t(context: context), + context.t.folders, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), ), onTap: () => context.pushRoute(FolderRoute()), @@ -392,7 +389,7 @@ class _QuickAccessButtonList extends ConsumerWidget { ListTile( leading: const Icon(Icons.lock_outline_rounded, size: 26), title: Text( - 'locked_folder'.t(context: context), + context.t.locked_folder, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), ), onTap: () => context.pushRoute(const DriftLockedFolderRoute()), @@ -400,7 +397,7 @@ class _QuickAccessButtonList extends ConsumerWidget { ListTile( leading: const Icon(Icons.group_outlined, size: 26), title: Text( - 'partners'.t(context: context), + context.t.partners, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), ), onTap: () => context.pushRoute(const PartnerRoute()), @@ -438,10 +435,10 @@ class _PartnerList extends StatelessWidget { ), contentPadding: const EdgeInsets.only(left: 12.0, right: 18.0), leading: PartnerUserAvatar(userId: partner.id, name: partner.name), - title: const Text( - "partner_list_user_photos", - style: TextStyle(fontWeight: FontWeight.w500), - ).t(context: context, args: {'user': partner.name}), + title: Text( + context.t.partner_list_user_photos(user: partner.name), + style: const TextStyle(fontWeight: FontWeight.w500), + ), onTap: () => context.pushRoute(DriftPartnerDetailRoute(partner: partner)), ); }, diff --git a/mobile/lib/presentation/pages/drift_local_album.page.dart b/mobile/lib/presentation/pages/drift_local_album.page.dart index 6b133f7a6f..71707688da 100644 --- a/mobile/lib/presentation/pages/drift_local_album.page.dart +++ b/mobile/lib/presentation/pages/drift_local_album.page.dart @@ -1,10 +1,9 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/presentation/widgets/images/local_album_thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; @@ -49,7 +48,7 @@ class _AlbumList extends ConsumerWidget { if (albums.isEmpty) { return SliverToBoxAdapter( child: Center( - child: Padding(padding: const EdgeInsets.all(20.0), child: Text('no_albums_yet'.tr())), + child: Padding(padding: const EdgeInsets.all(20.0), child: Text(context.t.no_albums_yet)), ), ); } @@ -67,7 +66,7 @@ class _AlbumList extends ConsumerWidget { leading: SizedBox(width: 80, height: 80, child: LocalAlbumThumbnail(albumId: album.id)), title: Text(album.name, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)), subtitle: Text( - 'items_count'.t(context: context, args: {'count': album.assetCount}), + context.t.items_count(count: album.assetCount), style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), onTap: () => context.pushRoute(LocalTimelineRoute(album: album)), diff --git a/mobile/lib/presentation/pages/drift_locked_folder.page.dart b/mobile/lib/presentation/pages/drift_locked_folder.page.dart index e8130f7059..6ebe7283d3 100644 --- a/mobile/lib/presentation/pages/drift_locked_folder.page.dart +++ b/mobile/lib/presentation/pages/drift_locked_folder.page.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; @@ -70,7 +70,7 @@ class _DriftLockedFolderPageState extends ConsumerState w : PopScope( onPopInvokedWithResult: (didPop, _) => didPop ? ref.read(authProvider.notifier).lockPinCode() : null, child: Timeline( - appBar: MesmerizingSliverAppBar(title: 'locked_folder'.t(context: context)), + appBar: MesmerizingSliverAppBar(title: context.t.locked_folder), bottomSheet: const LockedFolderBottomSheet(), ), ), diff --git a/mobile/lib/presentation/pages/drift_people_collection.page.dart b/mobile/lib/presentation/pages/drift_people_collection.page.dart index 416b7d587a..cb305395cc 100644 --- a/mobile/lib/presentation/pages/drift_people_collection.page.dart +++ b/mobile/lib/presentation/pages/drift_people_collection.page.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -49,10 +49,10 @@ class _DriftPeopleCollectionPageState extends ConsumerState _formFocus.unfocus(), onChanged: (value) => setState(() => _search = value), filled: true, - hintText: 'filter_people'.tr(), + hintText: context.t.filter_people, autofocus: true, ) - : Text('people'.tr()), + : Text(context.t.people), actions: [ IconButton( icon: Icon(_search != null ? Icons.close : Icons.search), @@ -107,7 +107,7 @@ class _DriftPeopleCollectionPageState extends ConsumerState showNameEditModal(context, person), child: person.name.isEmpty ? Text( - 'add_a_name'.tr(), + context.t.add_a_name, style: context.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, color: context.colorScheme.primary, diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart index f9e59da0a3..7669b6c765 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -7,7 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/album/pending_uploads_banner.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/remote_album/drift_album_option.widget.dart'; @@ -62,7 +62,7 @@ class _RemoteAlbumPageState extends ConsumerState { if (added > 0) { ImmichToast.show( context: context, - msg: "assets_added_to_album_count".t(context: context, args: {'count': added.toString()}), + msg: context.t.assets_added_to_album_count(count: added), toastType: ToastType.success, ); } @@ -88,7 +88,7 @@ class _RemoteAlbumPageState extends ConsumerState { ImmichToast.show( context: context, - msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}), + msg: context.t.users_added_to_album_count(count: newUsers.length), toastType: ToastType.success, ); } catch (e) { @@ -111,24 +111,21 @@ class _RemoteAlbumPageState extends ConsumerState { context: context, builder: (BuildContext context) { return AlertDialog( - title: Text('delete_album'.t(context: context)), + title: Text(context.t.delete_album), content: Column( mainAxisSize: MainAxisSize.min, children: [ - Text('album_delete_confirmation'.t(context: context, args: {'album': _album.name})), + Text(context.t.album_delete_confirmation(album: _album.name)), const SizedBox(height: 8), - Text('album_delete_confirmation_description'.t(context: context)), + Text(context.t.album_delete_confirmation_description), ], ), actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text('cancel'.t(context: context)), - ), + TextButton(onPressed: () => Navigator.of(context).pop(false), child: Text(context.t.cancel)), TextButton( onPressed: () => Navigator.of(context).pop(true), style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.error), - child: Text('delete_album'.t(context: context)), + child: Text(context.t.delete_album), ), ], ); @@ -146,11 +143,7 @@ class _RemoteAlbumPageState extends ConsumerState { return; } - ImmichToast.show( - context: context, - msg: 'album_deleted'.t(context: context), - toastType: ToastType.success, - ); + ImmichToast.show(context: context, msg: context.t.album_deleted, toastType: ToastType.success); unawaited(context.pushRoute(const DriftAlbumsRoute())); } catch (e) { @@ -160,7 +153,7 @@ class _RemoteAlbumPageState extends ConsumerState { ImmichToast.show( context: context, - msg: 'album_viewer_appbar_share_err_delete'.t(context: context), + msg: context.t.album_viewer_appbar_share_err_delete, toastType: ToastType.error, ); } @@ -288,11 +281,7 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { return; } - ImmichToast.show( - context: context, - msg: 'album_update_error'.t(context: context), - toastType: ToastType.error, - ); + ImmichToast.show(context: context, msg: context.t.errors.unable_to_update_album_info, toastType: ToastType.error); } } @@ -315,14 +304,14 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { children: [ Icon(Icons.edit_outlined, color: context.colorScheme.primary, size: 24), const SizedBox(width: 12), - Text('edit_album'.t(context: context), style: context.textTheme.titleMedium), + Text(context.t.edit_album, style: context.textTheme.titleMedium), ], ), const SizedBox(height: 24), // Album Name Text( - 'album_name'.t(context: context).toUpperCase(), + context.t.album_name.toUpperCase(), style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600), ), const SizedBox(height: 4), @@ -337,7 +326,7 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { ), validator: (value) { if (value == null || value.trim().isEmpty) { - return 'album_name_required'.t(context: context); + return context.t.name_required; } return null; @@ -347,7 +336,7 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { // Description Text( - 'description'.t(context: context).toUpperCase(), + context.t.description.toUpperCase(), style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600), ), const SizedBox(height: 4), @@ -367,15 +356,9 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - TextButton( - onPressed: () => Navigator.of(context).pop(null), - child: Text('cancel'.t(context: context)), - ), + TextButton(onPressed: () => Navigator.of(context).pop(null), child: Text(context.t.cancel)), const SizedBox(width: 12), - FilledButton( - onPressed: _handleSave, - child: Text('save'.t(context: context)), - ), + FilledButton(onPressed: _handleSave, child: Text(context.t.save)), ], ), ], diff --git a/mobile/lib/presentation/pages/drift_trash.page.dart b/mobile/lib/presentation/pages/drift_trash.page.dart index 36db74a658..4dd027494d 100644 --- a/mobile/lib/presentation/pages/drift_trash.page.dart +++ b/mobile/lib/presentation/pages/drift_trash.page.dart @@ -2,7 +2,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart'; @@ -35,7 +34,7 @@ class DriftTrashPage extends StatelessWidget { ], child: Timeline( appBar: SliverAppBar( - title: Text('trash'.t(context: context)), + title: Text(context.t.trash), floating: true, snap: true, pinned: true, diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 9babdb5b57..521f7d0f77 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -2,14 +2,13 @@ import 'dart:async'; import 'dart:math'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart'; @@ -326,7 +325,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { ), ), child: Text( - sortMode.label.t(context: context), + sortMode.label(context.t), style: context.textTheme.labelLarge?.copyWith( color: albumSortOption == sortMode ? context.colorScheme.onPrimary @@ -354,7 +353,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { : Icon(Icons.keyboard_arrow_up_rounded, color: context.colorScheme.onSurface), ), Text( - albumSortOption.label.t(context: context), + albumSortOption.label(context.t), style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(225)), ), isSorting @@ -413,7 +412,7 @@ class _SearchBar extends StatelessWidget { child: SearchField( autofocus: false, contentPadding: const EdgeInsets.all(16), - hintText: 'search_albums'.tr(), + hintText: context.t.search_albums, prefixIcon: const Icon(Icons.search_rounded), suffixIcon: searchController.text.isNotEmpty ? IconButton(icon: const Icon(Icons.clear_rounded), onPressed: onClearSearch) @@ -452,7 +451,7 @@ class _QuickFilterButtonRow extends StatelessWidget { runSpacing: 4, children: [ _QuickFilterButton( - label: 'all'.tr(), + label: context.t.all, isSelected: filterMode == QuickFilterMode.all, onTap: () { onChangeFilter(QuickFilterMode.all); @@ -460,7 +459,7 @@ class _QuickFilterButtonRow extends StatelessWidget { }, ), _QuickFilterButton( - label: 'shared_with_me'.tr(), + label: context.t.shared_with_me, isSelected: filterMode == QuickFilterMode.sharedWithMe, onTap: () { onChangeFilter(QuickFilterMode.sharedWithMe); @@ -468,7 +467,7 @@ class _QuickFilterButtonRow extends StatelessWidget { }, ), _QuickFilterButton( - label: 'my_albums'.tr(), + label: context.t.my_albums, isSelected: filterMode == QuickFilterMode.myAlbums, onTap: () { onChangeFilter(QuickFilterMode.myAlbums); @@ -571,7 +570,7 @@ class _AlbumList extends ConsumerWidget { if (albums.isEmpty) { return SliverToBoxAdapter( child: Center( - child: Padding(padding: const EdgeInsets.all(20.0), child: Text('album_search_not_found'.tr())), + child: Padding(padding: const EdgeInsets.all(20.0), child: Text(context.t.album_search_not_found)), ), ); } @@ -600,9 +599,9 @@ class _AlbumList extends ConsumerWidget { context: context, builder: (context) => ConfirmDialog( onOk: () => true, - title: "delete_album".t(context: context), - content: "album_delete_confirmation".t(context: context, args: {'album': album.name}), - ok: "delete".t(context: context), + title: context.t.delete_album, + content: context.t.album_delete_confirmation(album: album.name), + ok: context.t.delete, ), ); }, @@ -637,7 +636,7 @@ class _AlbumGrid extends StatelessWidget { if (albums.isEmpty) { return SliverToBoxAdapter( child: Center( - child: Padding(padding: const EdgeInsets.all(20.0), child: Text('album_search_not_found'.tr())), + child: Padding(padding: const EdgeInsets.all(20.0), child: Text(context.t.album_search_not_found)), ), ); } @@ -723,7 +722,7 @@ class _GridAlbumCard extends ConsumerWidget { style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), ), Text( - '${'items_count'.t(context: context, args: {'count': album.assetCount})} • ${album.ownerId != userId ? 'shared_by_user'.t(context: context, args: {'user': album.ownerName}) : 'owned'.t(context: context)}', + '${context.t.items_count(count: album.assetCount)} • ${album.ownerId != userId ? context.t.shared_by_user(user: album.ownerName) : context.t.owned}', maxLines: 1, overflow: TextOverflow.ellipsis, style: context.textTheme.labelMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), @@ -760,7 +759,7 @@ class AddToAlbumHeader extends ConsumerWidget { } if (newAlbum == null) { - ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr()); + ImmichToast.show(context: context, toastType: ToastType.error, msg: context.t.errors.failed_to_create_album); return; } @@ -774,7 +773,7 @@ class AddToAlbumHeader extends ConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("add_to_album", style: context.textTheme.titleSmall).tr(), + Text(context.t.add_to_album, style: context.textTheme.titleSmall), TextButton.icon( style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), // remove internal padding @@ -784,9 +783,9 @@ class AddToAlbumHeader extends ConsumerWidget { onPressed: onCreateAlbum, icon: Icon(Icons.add, color: context.primaryColor), label: Text( - "common_create_new_album", + context.t.common_create_new_album, style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14), - ).tr(), + ), ), ], ), @@ -822,13 +821,13 @@ class CreateAlbumButton extends ConsumerWidget { } if (album == null) { - ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr()); + ImmichToast.show(context: context, toastType: ToastType.error, msg: context.t.errors.failed_to_create_album); return; } ImmichToast.show( context: context, - msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), + msg: context.t.add_to_album_bottom_sheet_added(album: album.name), ); // Invalidate using the asset's remote ID to refresh the "Appears in" list @@ -843,7 +842,7 @@ class CreateAlbumButton extends ConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("add_to_album", style: context.textTheme.titleSmall).tr(), + Text(context.t.add_to_album, style: context.textTheme.titleSmall), TextButton.icon( style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), @@ -853,9 +852,9 @@ class CreateAlbumButton extends ConsumerWidget { onPressed: onCreateAlbum, icon: Icon(Icons.add, color: context.primaryColor), label: Text( - "common_create_new_album", + context.t.common_create_new_album, style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14), - ).tr(), + ), ), ], ), diff --git a/mobile/lib/presentation/widgets/album/album_tile.dart b/mobile/lib/presentation/widgets/album/album_tile.dart index 598666cda9..54fc27cfd1 100644 --- a/mobile/lib/presentation/widgets/album/album_tile.dart +++ b/mobile/lib/presentation/widgets/album/album_tile.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; @@ -27,7 +27,7 @@ class AlbumTile extends ConsumerWidget { style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), ), subtitle: Text( - '${'items_count'.t(context: context, args: {'count': album.assetCount})} • ${isOwner ? 'owned'.t(context: context) : 'shared_by_user'.t(context: context, args: {'user': album.ownerName})}', + '${context.t.items_count(count: album.assetCount)} • ${isOwner ? context.t.owned : context.t.shared_by_user(user: album.ownerName)}', overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), diff --git a/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart b/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart index 691b46f80d..55024d9b2b 100644 --- a/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart +++ b/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart @@ -1,7 +1,7 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; @@ -98,7 +98,7 @@ class _DriftActivityTextFieldState extends ConsumerState color: context.primaryColor, disabledColor: context.colorScheme.secondaryContainer, ), - hintText: !widget.isEnabled ? 'shared_album_activities_input_disable'.tr() : 'say_something'.tr(), + hintText: !widget.isEnabled ? context.t.shared_album_activities_input_disable : context.t.say_something, hintStyle: TextStyle(fontWeight: FontWeight.normal, fontSize: 14, color: Colors.grey[600]), ), onEditingComplete: onEditingComplete, diff --git a/mobile/lib/presentation/widgets/album/new_album_name_modal.widget.dart b/mobile/lib/presentation/widgets/album/new_album_name_modal.widget.dart index a5e21af489..255dbd4a0c 100644 --- a/mobile/lib/presentation/widgets/album/new_album_name_modal.widget.dart +++ b/mobile/lib/presentation/widgets/album/new_album_name_modal.widget.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class NewAlbumNameModal extends StatefulWidget { const NewAlbumNameModal({super.key}); @@ -21,31 +21,31 @@ class _NewAlbumNameModalState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: const Text("album_name", style: TextStyle(fontWeight: FontWeight.bold)).tr(), + title: Text(context.t.album_name, style: const TextStyle(fontWeight: FontWeight.bold)), content: SingleChildScrollView( child: TextFormField( controller: nameController, textCapitalization: TextCapitalization.words, autofocus: true, - decoration: InputDecoration(hintText: 'name'.tr(), border: const OutlineInputBorder()), + decoration: InputDecoration(hintText: context.t.name, border: const OutlineInputBorder()), ), ), actions: [ TextButton( onPressed: () => context.pop(null), child: Text( - "cancel", + context.t.cancel, style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold), - ).tr(), + ), ), TextButton( onPressed: () { context.pop(nameController.text.trim()); }, child: Text( - "create_album", + context.t.create_album, style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), + ), ), ], ); diff --git a/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart b/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart index 5622ed6b1b..c281f6b166 100644 --- a/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart +++ b/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/album/pending_album_uploads.provider.dart'; import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; @@ -115,7 +115,7 @@ class _PendingUploadsBannerContent extends StatelessWidget { const SizedBox(width: 12), Expanded( child: Text( - '${'uploading'.t(context: context)} $count$percentLabel', + '${context.t.uploading} $count$percentLabel', maxLines: 1, overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500), @@ -183,10 +183,7 @@ class _PendingUploadsSheet extends ConsumerWidget { child: Row( children: [ Expanded( - child: Text( - '${'uploading'.t(context: context)} (${pending.length})', - style: context.textTheme.titleMedium, - ), + child: Text('${context.t.uploading} (${pending.length})', style: context.textTheme.titleMedium), ), if (canAbort) TextButton.icon( @@ -199,14 +196,14 @@ class _PendingUploadsSheet extends ConsumerWidget { ref.read(pendingAlbumUploadsProvider(albumId).notifier).clear(); }, icon: const Icon(Icons.stop_circle_outlined, size: 18), - label: Text('cancel'.t(context: context)), + label: Text(context.t.cancel), style: TextButton.styleFrom(foregroundColor: context.colorScheme.error), ) else if (failedCount > 0) TextButton.icon( onPressed: () => ref.read(pendingAlbumUploadsProvider(albumId).notifier).clearFailed(), icon: const Icon(Icons.clear_rounded, size: 18), - label: Text('clear_failed_count'.t(context: context, args: {'count': failedCount})), + label: Text(context.t.clear_failed_count(count: failedCount)), style: TextButton.styleFrom(foregroundColor: context.colorScheme.error), ), ], diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart index 837397254c..c68ce8d9ca 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -1,8 +1,8 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; @@ -52,15 +52,15 @@ class _ArchiveBottomSheetState extends ConsumerState { } if (!result.success) { - ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error); + ImmichToast.show(context: context, msg: context.t.scaffold_body_error_occurred, toastType: ToastType.error); return; } ImmichToast.show( context: context, msg: result.count == 0 - ? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}) - : 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), + ? context.t.add_to_album_bottom_sheet_already_exists(album: album.name) + : context.t.add_to_album_bottom_sheet_added(album: album.name), ); } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart index 94e708015c..74d5ec96da 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart @@ -1,8 +1,8 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; @@ -45,15 +45,15 @@ class _LocalAlbumBottomSheetState extends ConsumerState { } if (!result.success) { - ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error); + ImmichToast.show(context: context, msg: context.t.scaffold_body_error_occurred, toastType: ToastType.error); return; } ImmichToast.show( context: context, msg: result.count == 0 - ? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}) - : 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), + ? context.t.add_to_album_bottom_sheet_already_exists(album: album.name) + : context.t.add_to_album_bottom_sheet_added(album: album.name), ); } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index cdae658b45..9500e6329d 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; @@ -58,19 +58,15 @@ class _RemoteAlbumBottomSheetState extends ConsumerState } if (!result.success) { - ImmichToast.show( - context: context, - msg: 'scaffold_body_error_occurred'.t(context: context), - toastType: ToastType.error, - ); + ImmichToast.show(context: context, msg: context.t.scaffold_body_error_occurred, toastType: ToastType.error); return; } ImmichToast.show( context: context, msg: result.count == 0 - ? 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name}) - : 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}), + ? context.t.add_to_album_bottom_sheet_already_exists(album: album.name) + : context.t.add_to_album_bottom_sheet_added(album: album.name), ); } diff --git a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart index b5a9dd235c..0b8a08a5d8 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart @@ -4,7 +4,7 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -49,7 +49,7 @@ class _DriftPersonNameEditFormState extends ConsumerState context.pop(null), child: Text( - "cancel", + context.t.cancel, style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold), - ).tr(), + ), ), TextButton( onPressed: () => saveBirthday(), child: Text( - "save", + context.t.save, style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), + ), ), ], ); diff --git a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart index bb63de3695..3b0e62ab1a 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart @@ -1,10 +1,9 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -47,7 +46,7 @@ class _DriftPersonNameEditFormState extends ConsumerState context.pop(null), child: Text( - "cancel", + context.t.cancel, style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold), - ).tr(), + ), ), TextButton( onPressed: () => onEdit(widget.person.id, _formController.text), child: Text( - "save", + context.t.save, style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), + ), ), ], ); diff --git a/mobile/lib/presentation/widgets/people/person_option_sheet.widget.dart b/mobile/lib/presentation/widgets/people/person_option_sheet.widget.dart index ac2d4580c9..026a28bd7d 100644 --- a/mobile/lib/presentation/widgets/people/person_option_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_option_sheet.widget.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class PersonOptionSheet extends StatelessWidget { const PersonOptionSheet({super.key, this.onEditName, this.onEditBirthday, this.birthdayExists = false}); @@ -20,12 +20,12 @@ class PersonOptionSheet extends StatelessWidget { children: [ ListTile( leading: const Icon(Icons.edit), - title: Text('edit_name'.t(context: context), style: textStyle), + title: Text(context.t.edit_name, style: textStyle), onTap: onEditName, ), ListTile( leading: const Icon(Icons.cake), - title: Text((birthdayExists ? 'edit_birthday' : "add_birthday").t(context: context), style: textStyle), + title: Text(birthdayExists ? context.t.edit_birthday : context.t.add_birthday, style: textStyle), onTap: onEditBirthday, ), ], diff --git a/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart b/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart index cd94ef89a8..dc89a25788 100644 --- a/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart +++ b/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; class DriftRemoteAlbumOption extends StatelessWidget { @@ -36,19 +36,14 @@ class DriftRemoteAlbumOption extends StatelessWidget { if (onEditAlbum != null) { menuChildren.add( - BaseActionButton( - label: 'edit_album'.t(context: context), - iconData: Icons.edit, - onPressed: onEditAlbum, - menuItem: true, - ), + BaseActionButton(label: context.t.edit_album, iconData: Icons.edit, onPressed: onEditAlbum, menuItem: true), ); } if (onAddPhotos != null) { menuChildren.add( BaseActionButton( - label: 'add_photos'.t(context: context), + label: context.t.add_photos, iconData: Icons.add_a_photo, onPressed: onAddPhotos, menuItem: true, @@ -59,7 +54,7 @@ class DriftRemoteAlbumOption extends StatelessWidget { if (onAddUsers != null) { menuChildren.add( BaseActionButton( - label: 'album_viewer_page_share_add_users'.t(context: context), + label: context.t.album_viewer_page_share_add_users, iconData: Icons.group_add, onPressed: onAddUsers, menuItem: true, @@ -70,7 +65,7 @@ class DriftRemoteAlbumOption extends StatelessWidget { if (onLeaveAlbum != null) { menuChildren.add( BaseActionButton( - label: 'leave_album'.t(context: context), + label: context.t.leave_album, iconData: Icons.person_remove_rounded, onPressed: onLeaveAlbum, menuItem: true, @@ -81,7 +76,7 @@ class DriftRemoteAlbumOption extends StatelessWidget { if (onToggleAlbumOrder != null) { menuChildren.add( BaseActionButton( - label: 'change_display_order'.t(context: context), + label: context.t.change_display_order, iconData: Icons.swap_vert_rounded, onPressed: onToggleAlbumOrder, menuItem: true, @@ -92,7 +87,7 @@ class DriftRemoteAlbumOption extends StatelessWidget { if (onCreateSharedLink != null) { menuChildren.add( BaseActionButton( - label: 'create_shared_link'.t(context: context), + label: context.t.create_shared_link, iconData: Icons.link, onPressed: onCreateSharedLink, menuItem: true, @@ -102,12 +97,7 @@ class DriftRemoteAlbumOption extends StatelessWidget { if (onShowOptions != null) { menuChildren.add( - BaseActionButton( - label: 'options'.t(context: context), - iconData: Icons.settings, - onPressed: onShowOptions, - menuItem: true, - ), + BaseActionButton(label: context.t.options, iconData: Icons.settings, onPressed: onShowOptions, menuItem: true), ); } @@ -115,7 +105,7 @@ class DriftRemoteAlbumOption extends StatelessWidget { menuChildren.add(const Divider(height: 1)); menuChildren.add( BaseActionButton( - label: 'delete_album'.t(context: context), + label: context.t.delete_album, iconData: Icons.delete, iconColor: context.isDarkTheme ? Colors.red[400] : Colors.red[800], onPressed: onDeleteAlbum, diff --git a/mobile/lib/providers/album/album_sort_by_options.provider.dart b/mobile/lib/providers/album/album_sort_by_options.provider.dart index ec4ae71d03..015a846de8 100644 --- a/mobile/lib/providers/album/album_sort_by_options.provider.dart +++ b/mobile/lib/providers/album/album_sort_by_options.provider.dart @@ -1,19 +1,28 @@ import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; // Store index allows us to re-arrange the values without affecting the saved prefs enum AlbumSortMode { - title(1, "library_page_sort_title", SortOrder.asc), - assetCount(4, "library_page_sort_asset_count", SortOrder.desc), - lastModified(3, "library_page_sort_last_modified", SortOrder.desc), - created(0, "library_page_sort_created", SortOrder.desc), - mostRecent(2, "sort_recent", SortOrder.desc), - mostOldest(5, "sort_oldest", SortOrder.asc); + title(1, SortOrder.asc), + assetCount(4, SortOrder.desc), + lastModified(3, SortOrder.desc), + created(0, SortOrder.desc), + mostRecent(2, SortOrder.desc), + mostOldest(5, SortOrder.asc); final int storeIndex; - final String label; final SortOrder defaultOrder; - const AlbumSortMode(this.storeIndex, this.label, this.defaultOrder); + const AlbumSortMode(this.storeIndex, this.defaultOrder); + + String label(Translations t) => switch (this) { + AlbumSortMode.title => t.library_page_sort_title, + AlbumSortMode.assetCount => t.library_page_sort_asset_count, + AlbumSortMode.lastModified => t.library_page_sort_last_modified, + AlbumSortMode.created => t.library_page_sort_created, + AlbumSortMode.mostRecent => t.sort_recent, + AlbumSortMode.mostOldest => t.sort_oldest, + }; SortOrder effectiveOrder(bool isReverse) => isReverse ? defaultOrder.reverse() : defaultOrder; } diff --git a/mobile/lib/widgets/common/local_album_sliver_app_bar.dart b/mobile/lib/widgets/common/local_album_sliver_app_bar.dart index 85d8f4bb01..1de1d1a262 100644 --- a/mobile/lib/widgets/common/local_album_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/local_album_sliver_app_bar.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class LocalAlbumsSliverAppBar extends StatelessWidget { const LocalAlbumsSliverAppBar({super.key}); @@ -15,7 +15,7 @@ class LocalAlbumsSliverAppBar extends StatelessWidget { shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), automaticallyImplyLeading: true, centerTitle: true, - title: Text("on_this_device".t(context: context)), + title: Text(context.t.on_this_device), ); } } diff --git a/mobile/lib/widgets/common/person_sliver_app_bar.dart b/mobile/lib/widgets/common/person_sliver_app_bar.dart index 0e6829243c..4b159447e6 100644 --- a/mobile/lib/widgets/common/person_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/person_sliver_app_bar.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; @@ -261,7 +261,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S ), ) : Text( - 'add_a_name'.tr(), + context.t.add_a_name, style: context.textTheme.titleLarge?.copyWith( color: Colors.grey[400], fontSize: 36, @@ -293,7 +293,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S ) else Text( - 'add_birthday'.tr(), + context.t.add_birthday, style: context.textTheme.labelLarge?.copyWith( color: Colors.grey[400], height: 1.2, @@ -344,7 +344,7 @@ class _ItemCountTextState extends ConsumerState<_ItemCountText> { final assetCount = ref.watch(timelineServiceProvider.select((s) => s.totalAssets)); return Text( - 'items_count'.t(context: context, args: {"count": assetCount}), + context.t.items_count(count: assetCount), style: context.textTheme.labelLarge?.copyWith( fontWeight: FontWeight.bold, color: Colors.white, diff --git a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart index 09a0367039..4b9532eb88 100644 --- a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/datetime_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; @@ -318,7 +318,7 @@ class _ItemCountTextState extends ConsumerState<_ItemCountText> { final assetCount = ref.watch(timelineServiceProvider.select((s) => s.totalAssets)); return Text( - 'items_count'.t(context: context, args: {"count": assetCount}), + context.t.items_count(count: assetCount), style: context.textTheme.labelLarge?.copyWith( color: Colors.white, shadows: [const Shadow(offset: Offset(0, 2), blurRadius: 12, color: Colors.black87)], diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index 0732b30a63..b4e73e97f9 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -1,112 +1,112 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; -import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; - -class PeoplePicker extends HookConsumerWidget { - const PeoplePicker({super.key, required this.onSelect, this.filter}); - - final Function(Set) onSelect; - final Set? filter; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final formFocus = useFocusNode(); - const imageSize = 60.0; - final searchQuery = useState(''); - final people = ref.watch(getAllPeopleProvider); - final selectedPeople = useState>(filter ?? {}); - - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: SearchField( - focusNode: formFocus, - onChanged: (value) => searchQuery.value = value, - onTapOutside: (_) => formFocus.unfocus(), - filled: true, - hintText: 'filter_people'.tr(), - ), - ), - Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 0), - child: Divider(color: context.colorScheme.surfaceContainerHighest, thickness: 1), - ), - Expanded( - child: people.widgetWhen( - onData: (people) { - final filtered = people - .where( - (person) => person.name.toLowerCase().removeDiacritics().contains( - searchQuery.value.toLowerCase().removeDiacritics(), - ), - ) - .toList(); - return ListView.builder( - shrinkWrap: true, - itemCount: filtered.length, - padding: const EdgeInsets.all(8), - itemBuilder: (context, index) { - final person = filtered[index]; - final isSelected = selectedPeople.value.contains(person); - - return Padding( - key: ValueKey(person.id), - padding: const EdgeInsets.only(bottom: 2.0), - child: LargeLeadingTile( - title: Text( - person.name, - style: context.textTheme.bodyLarge?.copyWith( - fontSize: 20, - fontWeight: FontWeight.w500, - color: isSelected ? context.colorScheme.onPrimary : context.colorScheme.onSurface, - ), - ), - leading: SizedBox( - height: imageSize, - child: Material( - shape: const CircleBorder(side: BorderSide.none), - elevation: 3, - child: CircleAvatar( - key: ValueKey(person.id), - maxRadius: imageSize / 2, - backgroundImage: RemoteImageProvider( - url: getFaceThumbnailUrl(person.id, updatedAt: person.updatedAt), - ), - ), - ), - ), - onTap: () { - if (selectedPeople.value.contains(person)) { - selectedPeople.value.remove(person); - } else { - selectedPeople.value.add(person); - } - - selectedPeople.value = {...selectedPeople.value}; - onSelect(selectedPeople.value); - }, - selected: isSelected, - selectedTileColor: context.primaryColor, - tileColor: context.primaryColor.withAlpha(25), - ), - ); - }, - ); - }, - ), - ), - ], - ); - } -} +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/person.model.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/string_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/pages/common/large_leading_tile.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; +import 'package:immich_mobile/providers/search/people.provider.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/widgets/common/search_field.dart'; + +class PeoplePicker extends HookConsumerWidget { + const PeoplePicker({super.key, required this.onSelect, this.filter}); + + final Function(Set) onSelect; + final Set? filter; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formFocus = useFocusNode(); + const imageSize = 60.0; + final searchQuery = useState(''); + final people = ref.watch(getAllPeopleProvider); + final selectedPeople = useState>(filter ?? {}); + + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: SearchField( + focusNode: formFocus, + onChanged: (value) => searchQuery.value = value, + onTapOutside: (_) => formFocus.unfocus(), + filled: true, + hintText: context.t.filter_people, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 0), + child: Divider(color: context.colorScheme.surfaceContainerHighest, thickness: 1), + ), + Expanded( + child: people.widgetWhen( + onData: (people) { + final filtered = people + .where( + (person) => person.name.toLowerCase().removeDiacritics().contains( + searchQuery.value.toLowerCase().removeDiacritics(), + ), + ) + .toList(); + return ListView.builder( + shrinkWrap: true, + itemCount: filtered.length, + padding: const EdgeInsets.all(8), + itemBuilder: (context, index) { + final person = filtered[index]; + final isSelected = selectedPeople.value.contains(person); + + return Padding( + key: ValueKey(person.id), + padding: const EdgeInsets.only(bottom: 2.0), + child: LargeLeadingTile( + title: Text( + person.name, + style: context.textTheme.bodyLarge?.copyWith( + fontSize: 20, + fontWeight: FontWeight.w500, + color: isSelected ? context.colorScheme.onPrimary : context.colorScheme.onSurface, + ), + ), + leading: SizedBox( + height: imageSize, + child: Material( + shape: const CircleBorder(side: BorderSide.none), + elevation: 3, + child: CircleAvatar( + key: ValueKey(person.id), + maxRadius: imageSize / 2, + backgroundImage: RemoteImageProvider( + url: getFaceThumbnailUrl(person.id, updatedAt: person.updatedAt), + ), + ), + ), + ), + onTap: () { + if (selectedPeople.value.contains(person)) { + selectedPeople.value.remove(person); + } else { + selectedPeople.value.add(person); + } + + selectedPeople.value = {...selectedPeople.value}; + onSelect(selectedPeople.value); + }, + selected: isSelected, + selectedTileColor: context.primaryColor, + tileColor: context.primaryColor.withAlpha(25), + ), + ); + }, + ); + }, + ), + ), + ], + ); + } +} From 675408b003f6429dd99881ed8cc4a70cdf7d91ab Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:41:13 +0530 Subject: [PATCH 21/49] chore: migrate mobile translations accessor (5) (#30672) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/analysis_options.yaml | 12 +++ .../login_input_validation_test.dart | 29 ++--- .../test_utils/login_helper.dart | 43 ++------ .../domain/models/feature_message.model.dart | 66 +++++------- .../services/feature_message.service.dart | 2 +- .../lib/extensions/translate_extensions.dart | 46 -------- .../shared_link/shared_link_edit.page.dart | 4 +- .../presentation/actions/delete.action.dart | 7 +- .../pages/cleanup_preview.page.dart | 4 +- .../pages/download_info.page.dart | 9 +- .../pages/drift_favorite.page.dart | 7 +- .../presentation/pages/drift_place.page.dart | 6 +- .../pages/drift_recently_added.page.dart | 4 +- .../pages/drift_recently_taken.page.dart | 4 +- .../pages/drift_slideshow.page.dart | 4 +- .../pages/drift_user_selection.page.dart | 8 +- .../presentation/pages/drift_video.page.dart | 4 +- .../pages/edit/drift_edit.page.dart | 17 +-- .../pages/feature_message/whats_new.page.dart | 5 +- .../profile/profile_picture_crop.page.dart | 10 +- .../pages/search/drift_search.page.dart | 100 ++++++++---------- .../favorite_bottom_sheet.widget.dart | 13 +-- .../general_bottom_sheet.widget.dart | 8 +- .../partner_detail_bottom_sheet.widget.dart | 8 +- .../feature_message_dialog.widget.dart | 5 +- .../widgets/search/quick_date_picker.dart | 24 ++--- .../activities/dismissible_activity.dart | 7 +- .../common/app_bar_dialog/app_bar_dialog.dart | 6 +- mobile/lib/widgets/common/confirm_dialog.dart | 27 ++--- .../widgets/forms/change_password_form.dart | 24 ++--- .../lib/widgets/forms/login/login_form.dart | 61 ++++++----- .../widgets/forms/pin_registration_form.dart | 12 +-- .../widgets/forms/pin_verification_form.dart | 4 +- .../search/search_filter/camera_picker.dart | 6 +- .../search_filter/display_option_picker.dart | 8 +- .../filter_bottom_sheet_scaffold.dart | 6 +- .../search/search_filter/location_picker.dart | 8 +- .../search_filter/media_type_picker.dart | 8 +- .../search_filter/star_rating_picker.dart | 4 +- .../lib/widgets/search/search_row_title.dart | 6 +- .../widgets/shared_link/shared_link_item.dart | 4 +- 41 files changed, 268 insertions(+), 372 deletions(-) delete mode 100644 mobile/lib/extensions/translate_extensions.dart diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 7e314e8d81..fb5d96697b 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -97,6 +97,18 @@ dart_code_metrics: exclude-paths: - 'lib/utils/debug_print.dart' severity: perf + - name: tr + description: 'Use context.t. or StaticTranslations.instance. instead of tr().' + - type: String + entries: + - name: tr + description: 'Use context.t. or StaticTranslations.instance. instead of String.tr().' + - name: plural + description: 'Use the generated plural accessor instead of String.plural().' + - type: Text + entries: + - name: tr + description: 'Use Text(context.t.) instead of Text().tr().' # All rules from "recommended" preset # Show potential errors # - avoid-cascade-after-if-null diff --git a/mobile/integration_test/module_login/login_input_validation_test.dart b/mobile/integration_test/module_login/login_input_validation_test.dart index 1b47b6d8cc..504940f643 100644 --- a/mobile/integration_test/module_login/login_input_validation_test.dart +++ b/mobile/integration_test/module_login/login_input_validation_test.dart @@ -1,5 +1,5 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import '../test_utils/general_helper.dart'; @@ -7,45 +7,32 @@ void main() async { await ImmichTestHelper.initialize(); group("Login input validation test", () { - immichWidgetTest("Test leading/trailing whitespace", - (tester, helper) async { + immichWidgetTest("Test leading/trailing whitespace", (tester, helper) async { await helper.loginHelper.waitForLoginScreen(); await helper.loginHelper.acknowledgeNewServerVersion(); - await helper.loginHelper.enterCredentials( - email: " demo@immich.app", - ); + await helper.loginHelper.enterCredentials(email: " demo@immich.app"); await tester.pump(const Duration(milliseconds: 300)); - expect( - find.text("login_form_err_leading_whitespace".tr()), - findsOneWidget, - ); + expect(find.text(StaticTranslations.instance.login_form_err_leading_whitespace), findsOneWidget); - await helper.loginHelper.enterCredentials( - email: "demo@immich.app ", - ); + await helper.loginHelper.enterCredentials(email: "demo@immich.app "); await tester.pump(const Duration(milliseconds: 300)); - expect( - find.text("login_form_err_trailing_whitespace".tr()), - findsOneWidget, - ); + expect(find.text(StaticTranslations.instance.login_form_err_trailing_whitespace), findsOneWidget); }); immichWidgetTest("Test invalid email", (tester, helper) async { await helper.loginHelper.waitForLoginScreen(); await helper.loginHelper.acknowledgeNewServerVersion(); - await helper.loginHelper.enterCredentials( - email: "demo.immich.app", - ); + await helper.loginHelper.enterCredentials(email: "demo.immich.app"); await tester.pump(const Duration(milliseconds: 300)); - expect(find.text("login_form_err_invalid_email".tr()), findsOneWidget); + expect(find.text(StaticTranslations.instance.login_form_err_invalid_email), findsOneWidget); }); }); } diff --git a/mobile/integration_test/test_utils/login_helper.dart b/mobile/integration_test/test_utils/login_helper.dart index cfbc5a9214..ad61b77a53 100644 --- a/mobile/integration_test/test_utils/login_helper.dart +++ b/mobile/integration_test/test_utils/login_helper.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'general_helper.dart'; @@ -23,11 +23,7 @@ class ImmichTestLoginHelper { return true; } - Future enterCredentials({ - String server = "", - String email = "", - String password = "", - }) async { + Future enterCredentials({String server = "", String email = "", String password = ""}) async { final loginForms = find.byType(TextFormField); await tester.enterText(loginForms.at(0), email); @@ -44,49 +40,30 @@ class ImmichTestLoginHelper { } Future enterCredentialsOf(LoginCredentials credentials) async { - await enterCredentials( - server: credentials.server, - email: credentials.email, - password: credentials.password, - ); + await enterCredentials(server: credentials.server, email: credentials.email, password: credentials.password); } Future pressLoginButton() async { - await pumpUntilFound( - tester, - find.textContaining("login_form_button_text".tr()), - ); - final button = find.textContaining("login_form_button_text".tr()); + await pumpUntilFound(tester, find.textContaining(StaticTranslations.instance.login)); + final button = find.textContaining(StaticTranslations.instance.login); await tester.tap(button); } Future assertLoginSuccess() async { - await pumpUntilFound(tester, find.text("home_page_building_timeline".tr())); + await pumpUntilFound(tester, find.text(StaticTranslations.instance.home_page_building_timeline)); } Future assertLoginFailed() async { - await pumpUntilFound(tester, find.text("login_form_failed_login".tr())); + await pumpUntilFound(tester, find.text(StaticTranslations.instance.login_form_failed_login)); } } enum LoginCredentials { - testInstance( - "https://flutter-int-test.preview.immich.app", - "demo@immich.app", - "demo", - ), + testInstance("https://flutter-int-test.preview.immich.app", "demo@immich.app", "demo"), - testInstanceButWithWrongPassword( - "https://flutter-int-test.preview.immich.app", - "demo@immich.app", - "wrong", - ), + testInstanceButWithWrongPassword("https://flutter-int-test.preview.immich.app", "demo@immich.app", "wrong"), - wrongInstanceUrl( - "https://does-not-exist.preview.immich.app", - "demo@immich.app", - "demo", - ); + wrongInstanceUrl("https://does-not-exist.preview.immich.app", "demo@immich.app", "demo"); const LoginCredentials(this.server, this.email, this.password); diff --git a/mobile/lib/domain/models/feature_message.model.dart b/mobile/lib/domain/models/feature_message.model.dart index b3c9c459d3..817076c0e9 100644 --- a/mobile/lib/domain/models/feature_message.model.dart +++ b/mobile/lib/domain/models/feature_message.model.dart @@ -1,21 +1,40 @@ import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/utils/semver.dart'; -class FeatureHighlight { +enum FeatureHighlight { + shareQuality(image: 'assets/feature_message/share_quality.webp'), + slideshow(image: 'assets/feature_message/slideshow.webp'), + recentlyAdded(image: 'assets/feature_message/recently_added.webp'), + ocr(image: 'assets/feature_message/ocr.webp'), + openInImmich(image: 'assets/feature_message/open_in_immich.webp', platform: [.android]), + uploadToAlbum(); + /// Asset path of the feature screenshot, or null to show a placeholder. final String? image; - final String titleKey; - final String bodyKey; final List platform; - const FeatureHighlight({ - this.image, - required this.titleKey, - required this.bodyKey, - this.platform = const [.iOS, .android], - }); + const FeatureHighlight({this.image, this.platform = const [.iOS, .android]}); bool get isVisibleOnCurrentPlatform => platform.contains(defaultTargetPlatform); + + String title(Translations t) => switch (this) { + FeatureHighlight.shareQuality => t.share_quality_title, + FeatureHighlight.slideshow => t.slideshow_title, + FeatureHighlight.recentlyAdded => t.recently_added_title, + FeatureHighlight.ocr => t.ocr_title, + FeatureHighlight.openInImmich => t.open_in_immich_title, + FeatureHighlight.uploadToAlbum => t.upload_to_album_title, + }; + + String body(Translations t) => switch (this) { + FeatureHighlight.shareQuality => t.share_quality_body, + FeatureHighlight.slideshow => t.slideshow_body, + FeatureHighlight.recentlyAdded => t.recently_added_body, + FeatureHighlight.ocr => t.ocr_body, + FeatureHighlight.openInImmich => t.open_in_immich_body, + FeatureHighlight.uploadToAlbum => t.upload_to_album_body, + }; } /// The release this batch of highlights was authored for. Content-defined: @@ -24,31 +43,4 @@ const featureMessageRelease = SemVer(major: 3, minor: 0, patch: 0); /// Highlights relevant to the current platform. List get visibleFeatureMessageHighlights => - featureMessageHighlights.where((h) => h.isVisibleOnCurrentPlatform).toList(); - -const List featureMessageHighlights = [ - FeatureHighlight( - image: 'assets/feature_message/share_quality.webp', - titleKey: 'share_quality_title', - bodyKey: 'share_quality_body', - ), - FeatureHighlight( - image: 'assets/feature_message/slideshow.webp', - titleKey: 'slideshow_title', - bodyKey: 'slideshow_body', - ), - FeatureHighlight( - image: 'assets/feature_message/recently_added.webp', - titleKey: 'recently_added_title', - bodyKey: 'recently_added_body', - ), - - FeatureHighlight(image: 'assets/feature_message/ocr.webp', titleKey: 'ocr_title', bodyKey: 'ocr_body'), - FeatureHighlight( - image: 'assets/feature_message/open_in_immich.webp', - titleKey: 'open_in_immich_title', - bodyKey: 'open_in_immich_body', - platform: [.android], - ), - FeatureHighlight(titleKey: 'upload_to_album_title', bodyKey: 'upload_to_album_body'), -]; + FeatureHighlight.values.where((h) => h.isVisibleOnCurrentPlatform).toList(); diff --git a/mobile/lib/domain/services/feature_message.service.dart b/mobile/lib/domain/services/feature_message.service.dart index ff0fdda8f3..5eb58f0823 100644 --- a/mobile/lib/domain/services/feature_message.service.dart +++ b/mobile/lib/domain/services/feature_message.service.dart @@ -9,7 +9,7 @@ class FeatureMessageService { bool shouldShow() { final seen = _settingsRepository.appConfig.featureMessage.seenRelease; - return featureMessageHighlights.isNotEmpty && featureMessageRelease > seen; + return visibleFeatureMessageHighlights.isNotEmpty && featureMessageRelease > seen; } Future markSeen() => _settingsRepository.write(SettingsKey.featureMessageSeenRelease, featureMessageRelease); diff --git a/mobile/lib/extensions/translate_extensions.dart b/mobile/lib/extensions/translate_extensions.dart deleted file mode 100644 index b01203a90c..0000000000 --- a/mobile/lib/extensions/translate_extensions.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:intl/message_format.dart'; - -extension StringTranslateExtension on String { - String t({BuildContext? context, Map? args}) { - return _translateHelper(context, this, args); - } -} - -extension TextTranslateExtension on Text { - Text t({BuildContext? context, Map? args}) { - return Text( - _translateHelper(context, data ?? '', args), - key: key, - style: style, - strutStyle: strutStyle, - textAlign: textAlign, - textDirection: textDirection, - locale: locale, - softWrap: softWrap, - overflow: overflow, - textScaler: textScaler, - maxLines: maxLines, - semanticsLabel: semanticsLabel, - textWidthBasis: textWidthBasis, - textHeightBehavior: textHeightBehavior, - ); - } -} - -String _translateHelper(BuildContext? context, String key, [Map? args]) { - if (key.isEmpty) { - return ''; - } - try { - final translatedMessage = key.tr(context: context); - return args != null - ? MessageFormat(translatedMessage, locale: Intl.defaultLocale ?? 'en').format(args) - : translatedMessage; - } catch (e) { - dPrint(() => 'Translation failed for key "$key". Error: $e'); - return key; - } -} diff --git a/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart b/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart index 23c24a64b0..6c90571428 100644 --- a/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart +++ b/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart @@ -427,8 +427,8 @@ class SharedLinkEditPage extends HookConsumerWidget { return showDialog( context: context, builder: (BuildContext context) => ConfirmDialog( - title: "delete_shared_link_dialog_title", - content: "confirm_delete_shared_link", + title: context.t.delete_shared_link_dialog_title, + content: context.t.confirm_delete_shared_link, onOk: () async { await ref.read(sharedLinkServiceProvider).deleteSharedLink(existingLink!.id); ref.invalidate(sharedLinksStateProvider); diff --git a/mobile/lib/presentation/actions/delete.action.dart b/mobile/lib/presentation/actions/delete.action.dart index 31d01b32ef..ea7423354a 100644 --- a/mobile/lib/presentation/actions/delete.action.dart +++ b/mobile/lib/presentation/actions/delete.action.dart @@ -127,8 +127,11 @@ class DeleteAction extends AssetActionBuilder { final assetService = ref.read(assetServiceProvider); final confirmed = await showDialog( context: context, - builder: (_) => - const ConfirmDialog(title: 'delete_dialog_title', content: 'delete_dialog_alert', ok: 'delete_permanently'), + builder: (_) => ConfirmDialog( + title: context.t.delete_dialog_title, + content: context.t.delete_dialog_alert, + ok: context.t.delete_permanently, + ), ); if (confirmed != true || !context.mounted) { return null; diff --git a/mobile/lib/presentation/pages/cleanup_preview.page.dart b/mobile/lib/presentation/pages/cleanup_preview.page.dart index 556ed6412f..45f8c80c91 100644 --- a/mobile/lib/presentation/pages/cleanup_preview.page.dart +++ b/mobile/lib/presentation/pages/cleanup_preview.page.dart @@ -5,7 +5,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; @@ -19,7 +19,7 @@ class CleanupPreviewPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('cleanup_preview_title'.t(context: context, args: {'count': assets.length.toString()})), + title: Text(context.t.cleanup_preview_title(count: assets.length)), centerTitle: true, elevation: 0, scrolledUnderElevation: 0, diff --git a/mobile/lib/presentation/pages/download_info.page.dart b/mobile/lib/presentation/pages/download_info.page.dart index 8073d3098c..09a65b4586 100644 --- a/mobile/lib/presentation/pages/download_info.page.dart +++ b/mobile/lib/presentation/pages/download_info.page.dart @@ -4,7 +4,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/pages/common/download_panel.dart'; import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; @@ -21,10 +21,7 @@ class DownloadInfoPage extends ConsumerWidget { } return Scaffold( - appBar: AppBar( - title: Text("download".t(context: context)), - actions: const [], - ), + appBar: AppBar(title: Text(context.t.download), actions: const []), body: ListView.builder( physics: const ClampingScrollPhysics(), shrinkWrap: true, @@ -49,7 +46,7 @@ class DownloadInfoPage extends ConsumerWidget { }, style: OutlinedButton.styleFrom(side: BorderSide(color: context.colorScheme.primary)), child: Text( - 'clear_all'.t(context: context), + context.t.clear_all, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary), ), ), diff --git a/mobile/lib/presentation/pages/drift_favorite.page.dart b/mobile/lib/presentation/pages/drift_favorite.page.dart index 2bc3f26363..dd4384906b 100644 --- a/mobile/lib/presentation/pages/drift_favorite.page.dart +++ b/mobile/lib/presentation/pages/drift_favorite.page.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; @@ -28,10 +28,7 @@ class DriftFavoritePage extends StatelessWidget { }), ], child: Timeline( - appBar: MesmerizingSliverAppBar( - title: 'favorites'.t(context: context), - icon: Icons.favorite_outline, - ), + appBar: MesmerizingSliverAppBar(title: context.t.favorites, icon: Icons.favorite_outline), bottomSheet: const FavoriteBottomSheet(), ), ); diff --git a/mobile/lib/presentation/pages/drift_place.page.dart b/mobile/lib/presentation/pages/drift_place.page.dart index 10b9ca7ae4..68824e2752 100644 --- a/mobile/lib/presentation/pages/drift_place.page.dart +++ b/mobile/lib/presentation/pages/drift_place.page.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; @@ -62,10 +62,10 @@ class _PlaceSliverAppBar extends HookWidget { onTapOutside: (_) => searchFocusNode.unfocus(), onChanged: (value) => search.value = value, filled: true, - hintText: 'filter_places'.t(context: context), + hintText: context.t.filter_places, autofocus: true, ) - : Text('places'.t(context: context)), + : Text(context.t.places), actions: [ IconButton( icon: Icon(search.value != null ? Icons.close : Icons.search), diff --git a/mobile/lib/presentation/pages/drift_recently_added.page.dart b/mobile/lib/presentation/pages/drift_recently_added.page.dart index c33cd4370d..841fb9a671 100644 --- a/mobile/lib/presentation/pages/drift_recently_added.page.dart +++ b/mobile/lib/presentation/pages/drift_recently_added.page.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -26,7 +26,7 @@ class DriftRecentlyAddedPage extends StatelessWidget { return timelineService; }), ], - child: Timeline(appBar: MesmerizingSliverAppBar(title: 'recently_added'.t())), + child: Timeline(appBar: MesmerizingSliverAppBar(title: context.t.recently_added)), ); } } diff --git a/mobile/lib/presentation/pages/drift_recently_taken.page.dart b/mobile/lib/presentation/pages/drift_recently_taken.page.dart index ceb121b124..22d89770fe 100644 --- a/mobile/lib/presentation/pages/drift_recently_taken.page.dart +++ b/mobile/lib/presentation/pages/drift_recently_taken.page.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -26,7 +26,7 @@ class DriftRecentlyTakenPage extends StatelessWidget { return timelineService; }), ], - child: Timeline(appBar: MesmerizingSliverAppBar(title: 'recently_taken'.t())), + child: Timeline(appBar: MesmerizingSliverAppBar(title: context.t.recently_taken)), ); } } diff --git a/mobile/lib/presentation/pages/drift_slideshow.page.dart b/mobile/lib/presentation/pages/drift_slideshow.page.dart index 48b29b3499..824d7d7c6b 100644 --- a/mobile/lib/presentation/pages/drift_slideshow.page.dart +++ b/mobile/lib/presentation/pages/drift_slideshow.page.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/domain/models/config/slideshow_config.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/scroll_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; @@ -411,7 +411,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si children: [ AppBar( backgroundColor: context.scaffoldBackgroundColor, - title: Text("slideshow".t(context: context)), + title: Text(context.t.slideshow), actions: [ IconButton( onPressed: _paused ? _play : _pause, diff --git a/mobile/lib/presentation/pages/drift_user_selection.page.dart b/mobile/lib/presentation/pages/drift_user_selection.page.dart index a145b96a70..215a5d1071 100644 --- a/mobile/lib/presentation/pages/drift_user_selection.page.dart +++ b/mobile/lib/presentation/pages/drift_user_selection.page.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -9,6 +8,7 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -86,7 +86,7 @@ class DriftUserSelectionPage extends HookConsumerWidget { Padding( padding: const EdgeInsets.all(16.0), child: Text( - 'suggestions'.tr(), + context.t.suggestions, style: const TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold), ), ), @@ -118,7 +118,7 @@ class DriftUserSelectionPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text('invite_to_album').tr(), + title: Text(context.t.invite_to_album), elevation: 0, centerTitle: false, leading: IconButton( @@ -130,7 +130,7 @@ class DriftUserSelectionPage extends HookConsumerWidget { actions: [ TextButton( onPressed: sharedUsersList.value.isEmpty ? null : () => addNewUsersHandler(), - child: const Text("add", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), + child: Text(context.t.add, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), ), ], ), diff --git a/mobile/lib/presentation/pages/drift_video.page.dart b/mobile/lib/presentation/pages/drift_video.page.dart index eef05acdce..b51ebc25b8 100644 --- a/mobile/lib/presentation/pages/drift_video.page.dart +++ b/mobile/lib/presentation/pages/drift_video.page.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -26,7 +26,7 @@ class DriftVideoPage extends StatelessWidget { return timelineService; }), ], - child: Timeline(appBar: MesmerizingSliverAppBar(title: 'videos'.t())), + child: Timeline(appBar: MesmerizingSliverAppBar(title: context.t.videos)), ); } } diff --git a/mobile/lib/presentation/pages/edit/drift_edit.page.dart b/mobile/lib/presentation/pages/edit/drift_edit.page.dart index 0fe5d28850..6237169431 100644 --- a/mobile/lib/presentation/pages/edit/drift_edit.page.dart +++ b/mobile/lib/presentation/pages/edit/drift_edit.page.dart @@ -9,6 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/aspect_ratios.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/theme/theme_data.dart'; @@ -63,14 +64,14 @@ class _DriftEditImagePageState extends ConsumerState with Ti return; } - ImmichToast.show(context: context, msg: 'success'.tr(), toastType: ToastType.success); + ImmichToast.show(context: context, msg: context.t.success, toastType: ToastType.success); Navigator.of(context).pop(); } catch (e) { if (!mounted) { return; } - ImmichToast.show(context: context, msg: 'error_title'.tr(), toastType: ToastType.error); + ImmichToast.show(context: context, msg: context.t.error_title, toastType: ToastType.error); } finally { ref.read(editorStateProvider.notifier).setIsEditing(false); } @@ -80,17 +81,17 @@ class _DriftEditImagePageState extends ConsumerState with Ti return showDialog( context: context, builder: (context) => AlertDialog( - title: Text('editor_discard_edits_title'.tr()), - content: Text('editor_discard_edits_prompt'.tr()), + title: Text(context.t.editor_discard_edits_title), + content: Text(context.t.editor_discard_edits_prompt), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), style: ButtonStyle( foregroundColor: WidgetStateProperty.all(context.themeData.colorScheme.onSurfaceVariant), ), - child: Text('cancel'.tr()), + child: Text(context.t.cancel), ), - TextButton(onPressed: () => Navigator.of(context).pop(true), child: Text('confirm'.tr())), + TextButton(onPressed: () => Navigator.of(context).pop(true), child: Text(context.t.confirm)), ], ), ); @@ -116,7 +117,7 @@ class _DriftEditImagePageState extends ConsumerState with Ti child: Scaffold( appBar: AppBar( backgroundColor: Colors.black, - title: Text("edit".tr()), + title: Text(context.t.edit), leading: ImmichCloseButton(onPressed: () => Navigator.of(context).maybePop()), actions: [_SaveEditsButton(onSave: _saveEditedImage)], ), @@ -336,7 +337,7 @@ class _ResetEditsButton extends ConsumerWidget { final editorNotifier = ref.watch(editorStateProvider.notifier); return ImmichTextButton( - labelText: 'reset'.tr(), + labelText: context.t.reset, onPressed: editorNotifier.resetEdits, variant: ImmichVariant.ghost, expanded: false, diff --git a/mobile/lib/presentation/pages/feature_message/whats_new.page.dart b/mobile/lib/presentation/pages/feature_message/whats_new.page.dart index e3c3f8ed6e..fb82f59577 100644 --- a/mobile/lib/presentation/pages/feature_message/whats_new.page.dart +++ b/mobile/lib/presentation/pages/feature_message/whats_new.page.dart @@ -1,5 +1,4 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/feature_message.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -60,10 +59,10 @@ class _HighlightCard extends StatelessWidget { ), ), const SizedBox(height: 12), - Text(highlight.titleKey.tr(), style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), + Text(highlight.title(context.t), style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: 4), Text( - highlight.bodyKey.tr(), + highlight.body(context.t), style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant), ), ], diff --git a/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart b/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart index 523781b274..a5c069671c 100644 --- a/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart +++ b/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart @@ -2,13 +2,13 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:crop_image/crop_image.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; @@ -93,7 +93,7 @@ class _ProfilePictureCropPageState extends ConsumerState ImmichToast.show( context: context, - msg: 'profile_picture_set'.tr(), + msg: context.t.profile_picture_set, gravity: ToastGravity.BOTTOM, toastType: ToastType.success, ); @@ -102,7 +102,7 @@ class _ProfilePictureCropPageState extends ConsumerState } else { ImmichToast.show( context: context, - msg: 'errors.unable_to_set_profile_picture'.tr(), + msg: context.t.errors.unable_to_set_profile_picture, toastType: ToastType.error, gravity: ToastGravity.BOTTOM, ); @@ -114,7 +114,7 @@ class _ProfilePictureCropPageState extends ConsumerState ImmichToast.show( context: context, - msg: 'errors.unable_to_set_profile_picture'.tr(), + msg: context.t.errors.unable_to_set_profile_picture, toastType: ToastType.error, gravity: ToastGravity.BOTTOM, ); @@ -135,7 +135,7 @@ class _ProfilePictureCropPageState extends ConsumerState return Scaffold( appBar: AppBar( backgroundColor: context.scaffoldBackgroundColor, - title: Text("set_profile_picture".tr()), + title: Text(context.t.set_profile_picture), leading: _isLoading ? null : const ImmichCloseButton(), actions: [ if (_isLoading) diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 63e48e0c33..0c6b0d8ae4 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -12,7 +12,6 @@ import 'package:immich_mobile/domain/models/tag.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart'; @@ -48,9 +47,7 @@ class DriftSearchPage extends HookConsumerWidget { serverFeatures.smartSearch ? TextSearchType.context : TextSearchType.filename, ); final searchHintText = useState( - serverFeatures.smartSearch - ? 'sunrise_on_the_beach'.t(context: context) - : 'file_name_or_extension'.t(context: context), + serverFeatures.smartSearch ? context.t.sunrise_on_the_beach : context.t.file_name_or_extension, ); final textSearchController = useTextEditingController(); final filter = useState( @@ -142,7 +139,7 @@ class DriftSearchPage extends HookConsumerWidget { } void handleApply() { - final label = people.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context)).join(', '); + final label = people.map((e) => e.name != '' ? e.name : context.t.no_name).join(', '); peopleCurrentFilterWidget.value = label.isNotEmpty ? Text(label, style: context.textTheme.labelLarge) : null; search(filter.value.copyWith(people: people)); } @@ -154,7 +151,7 @@ class DriftSearchPage extends HookConsumerWidget { child: FractionallySizedBox( heightFactor: 0.8, child: FilterBottomSheetScaffold( - title: 'search_filter_people_title'.t(context: context), + title: context.t.search_filter_people_title, expanded: true, onSearch: handleApply, onClear: handleClear, @@ -191,7 +188,7 @@ class DriftSearchPage extends HookConsumerWidget { child: FractionallySizedBox( heightFactor: 0.8, child: FilterBottomSheetScaffold( - title: 'search_filter_tags_title'.t(context: context), + title: context.t.search_filter_tags_title, expanded: true, onSearch: handleApply, onClear: handleClear, @@ -232,7 +229,7 @@ class DriftSearchPage extends HookConsumerWidget { isScrollControlled: true, isDismissible: true, child: FilterBottomSheetScaffold( - title: 'search_filter_location_title'.t(context: context), + title: context.t.search_filter_location_title, onSearch: handleApply, onClear: handleClear, child: Padding( @@ -277,7 +274,7 @@ class DriftSearchPage extends HookConsumerWidget { isScrollControlled: true, isDismissible: true, child: FilterBottomSheetScaffold( - title: 'search_filter_camera_title'.t(context: context), + title: context.t.search_filter_camera_title, onSearch: handleApply, onClear: handleClear, child: Padding( @@ -333,14 +330,14 @@ class DriftSearchPage extends HookConsumerWidget { lastDate: lastDate, currentDate: DateTime.now(), initialDateRange: dateRange, - helpText: 'search_filter_date_title'.t(context: context), - cancelText: 'cancel'.t(context: context), - confirmText: 'select'.t(context: context), - saveText: 'save'.t(context: context), - errorFormatText: 'invalid_date_format'.t(context: context), - errorInvalidText: 'invalid_date'.t(context: context), - fieldStartHintText: 'start_date'.t(context: context), - fieldEndHintText: 'end_date'.t(context: context), + helpText: context.t.search_filter_date_title, + cancelText: context.t.cancel, + confirmText: context.t.select, + saveText: context.t.save, + errorFormatText: context.t.invalid_date_format, + errorInvalidText: context.t.invalid_date, + fieldStartHintText: context.t.start_date, + fieldEndHintText: context.t.end_date, initialEntryMode: DatePickerEntryMode.calendar, keyboardType: TextInputType.text, ); @@ -357,7 +354,7 @@ class DriftSearchPage extends HookConsumerWidget { showFilterBottomSheet( context: context, child: FilterBottomSheetScaffold( - title: "pick_date_range".tr(), + title: context.t.pick_date_range, expanded: true, onClear: () => datePicked(null), child: QuickDatePicker( @@ -392,7 +389,7 @@ class DriftSearchPage extends HookConsumerWidget { void handleApply() { mediaTypeCurrentFilterWidget.value = mediaType != AssetType.other ? Text( - mediaType == AssetType.image ? 'image'.t(context: context) : 'video'.t(context: context), + mediaType == AssetType.image ? context.t.image : context.t.video, style: context.textTheme.labelLarge, ) : null; @@ -403,7 +400,7 @@ class DriftSearchPage extends HookConsumerWidget { showFilterBottomSheet( context: context, child: FilterBottomSheetScaffold( - title: 'search_filter_media_type_title'.t(context: context), + title: context.t.search_filter_media_type_title, onSearch: handleApply, onClear: handleClear, child: MediaTypePicker(onSelect: handleOnSelected, filter: filter.value.mediaType), @@ -427,10 +424,7 @@ class DriftSearchPage extends HookConsumerWidget { void handleApply() { ratingCurrentFilterWidget.value = rating.rating.isSome - ? Text( - 'rating_count'.t(args: {'count': rating.rating.unwrapOrNull ?? 0}), - style: context.textTheme.labelLarge, - ) + ? Text(context.t.rating_count(count: rating.rating.unwrapOrNull ?? 0), style: context.textTheme.labelLarge) : null; search(filter.value.copyWith(rating: rating)); } @@ -440,7 +434,7 @@ class DriftSearchPage extends HookConsumerWidget { context: context, isScrollControlled: true, child: FilterBottomSheetScaffold( - title: 'rating'.t(context: context), + title: context.t.rating, onSearch: handleApply, onClear: handleClear, child: StarRatingPicker(onSelect: handleOnSelected, filter: filter.value.rating), @@ -472,9 +466,9 @@ class DriftSearchPage extends HookConsumerWidget { void handleApply() { final filterText = [ - if (display.isNotInAlbum) 'search_filter_display_option_not_in_album'.t(context: context), - if (display.isArchive) 'archive'.t(context: context), - if (display.isFavorite) 'favorite'.t(context: context), + if (display.isNotInAlbum) context.t.search_filter_display_option_not_in_album, + if (display.isArchive) context.t.archive, + if (display.isFavorite) context.t.favorite, ]; displayOptionCurrentFilterWidget.value = filterText.isNotEmpty ? Text(filterText.join(', '), style: context.textTheme.labelLarge) @@ -486,7 +480,7 @@ class DriftSearchPage extends HookConsumerWidget { showFilterBottomSheet( context: context, child: FilterBottomSheetScaffold( - title: 'display_options'.t(context: context), + title: context.t.display_options, onSearch: handleApply, onClear: handleClear, child: DisplayOptionPicker(onSelect: handleOnSelect, filter: filter.value.display), @@ -534,7 +528,7 @@ class DriftSearchPage extends HookConsumerWidget { } }, icon: const Icon(Icons.more_vert_rounded), - tooltip: 'show_text_search_menu'.tr(), + tooltip: context.t.show_text_search_menu, ); }, menuChildren: [ @@ -544,7 +538,7 @@ class DriftSearchPage extends HookConsumerWidget { child: ListTile( leading: const Icon(Icons.image_search_rounded), title: Text( - 'search_by_context'.t(context: context), + context.t.search_by_context, style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, color: textSearchType.value == TextSearchType.context ? context.colorScheme.primary : null, @@ -555,7 +549,7 @@ class DriftSearchPage extends HookConsumerWidget { ), onPressed: () { textSearchType.value = TextSearchType.context; - searchHintText.value = 'sunrise_on_the_beach'.t(context: context); + searchHintText.value = context.t.sunrise_on_the_beach; }, ), ), @@ -563,7 +557,7 @@ class DriftSearchPage extends HookConsumerWidget { child: ListTile( leading: const Icon(Icons.abc_rounded), title: Text( - 'search_filter_filename'.t(context: context), + context.t.search_filter_filename, style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, color: textSearchType.value == TextSearchType.filename ? context.colorScheme.primary : null, @@ -574,14 +568,14 @@ class DriftSearchPage extends HookConsumerWidget { ), onPressed: () { textSearchType.value = TextSearchType.filename; - searchHintText.value = 'file_name_or_extension'.t(context: context); + searchHintText.value = context.t.file_name_or_extension; }, ), MenuItemButton( child: ListTile( leading: const Icon(Icons.text_snippet_outlined), title: Text( - 'search_by_description'.t(context: context), + context.t.search_by_description, style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, color: textSearchType.value == TextSearchType.description ? context.colorScheme.primary : null, @@ -592,7 +586,7 @@ class DriftSearchPage extends HookConsumerWidget { ), onPressed: () { textSearchType.value = TextSearchType.description; - searchHintText.value = 'search_by_description_example'.t(context: context); + searchHintText.value = context.t.search_by_description_example; }, ), FeatureCheck( @@ -601,7 +595,7 @@ class DriftSearchPage extends HookConsumerWidget { child: ListTile( leading: const Icon(Icons.document_scanner_outlined), title: Text( - 'search_by_ocr'.t(context: context), + context.t.search_by_ocr, style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, color: textSearchType.value == TextSearchType.ocr ? context.colorScheme.primary : null, @@ -612,7 +606,7 @@ class DriftSearchPage extends HookConsumerWidget { ), onPressed: () { textSearchType.value = TextSearchType.ocr; - searchHintText.value = 'search_by_ocr_example'.t(context: context); + searchHintText.value = context.t.search_by_ocr_example; }, ), ), @@ -663,52 +657,52 @@ class DriftSearchPage extends HookConsumerWidget { SearchFilterChip( icon: Icons.people_alt_outlined, onTap: showPeoplePicker, - label: 'people'.t(context: context), + label: context.t.people, currentFilter: peopleCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.location_on_outlined, onTap: showLocationPicker, - label: 'search_filter_location'.t(context: context), + label: context.t.search_filter_location, currentFilter: locationCurrentFilterWidget.value, ), if (userPreferences.valueOrNull?.tagsEnabled ?? false) SearchFilterChip( icon: Icons.sell_outlined, onTap: showTagPicker, - label: 'tags'.t(context: context), + label: context.t.tags, currentFilter: tagCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.camera_alt_outlined, onTap: showCameraPicker, - label: 'camera'.t(context: context), + label: context.t.camera, currentFilter: cameraCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.date_range_outlined, onTap: showQuickDatePicker, - label: 'search_filter_date'.t(context: context), + label: context.t.search_filter_date, currentFilter: dateRangeCurrentFilterWidget.value, ), SearchFilterChip( key: const Key('media_type_chip'), icon: Icons.video_collection_outlined, onTap: showMediaTypePicker, - label: 'search_filter_media_type'.t(context: context), + label: context.t.search_filter_media_type, currentFilter: mediaTypeCurrentFilterWidget.value, ), if (userPreferences.valueOrNull?.ratingsEnabled ?? false) SearchFilterChip( icon: Icons.star_outline_rounded, onTap: showStarRatingPicker, - label: 'search_filter_star_rating'.t(context: context), + label: context.t.search_filter_star_rating, currentFilter: ratingCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.display_settings_outlined, onTap: showDisplayOptionPicker, - label: 'search_filter_display_options'.t(context: context), + label: context.t.search_filter_display_options, currentFilter: displayOptionCurrentFilterWidget.value, ), ], @@ -772,7 +766,7 @@ class _SearchResultGrid extends ConsumerWidget { padding: const EdgeInsets.symmetric(vertical: 32), child: Center( child: Text( - 'search_no_more_result'.t(context: context), + context.t.search_no_more_result, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant), ), ), @@ -837,7 +831,7 @@ class _SearchNoResults extends StatelessWidget { Icon(Icons.search_off_rounded, size: 72, color: context.colorScheme.onSurfaceVariant), const SizedBox(height: 24), Text( - 'search_no_result'.t(context: context), + context.t.search_no_result, textAlign: TextAlign.center, style: context.textTheme.bodyLarge?.copyWith(color: context.colorScheme.onSurfaceVariant), ), @@ -865,9 +859,7 @@ class _SearchSuggestions extends StatelessWidget { ), ), const SizedBox(height: 16), - Center( - child: Text('search_page_search_photos_videos'.t(context: context), style: context.textTheme.labelLarge), - ), + Center(child: Text(context.t.search_page_search_photos_videos, style: context.textTheme.labelLarge)), const SizedBox(height: 32), const Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: _QuickLinkList()), ], @@ -900,7 +892,7 @@ class _QuickLinkList extends StatelessWidget { physics: const NeverScrollableScrollPhysics(), children: [ _QuickLink( - title: 'recently_taken'.t(context: context), + title: context.t.recently_taken, icon: Icons.schedule_outlined, isTop: true, onTap: () => context.pushRoute(const DriftRecentlyTakenRoute()), @@ -911,12 +903,12 @@ class _QuickLinkList extends StatelessWidget { onTap: () => context.pushRoute(const DriftRecentlyAddedRoute()), ), _QuickLink( - title: 'videos'.t(context: context), + title: context.t.videos, icon: Icons.play_circle_outline_rounded, onTap: () => context.pushRoute(const DriftVideoRoute()), ), _QuickLink( - title: 'favorites'.t(context: context), + title: context.t.favorites, icon: Icons.favorite_border_rounded, isBottom: true, onTap: () => context.pushRoute(const DriftFavoriteRoute()), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index 3322035320..5ac6869c57 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; @@ -43,28 +43,25 @@ class FavoriteBottomSheet extends ConsumerWidget { } if (selectedAssets.length != remoteAssets.length) { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_some_local_assets'.t(context: context), - ); + ImmichToast.show(context: context, msg: context.t.add_to_album_bottom_sheet_some_local_assets); } // Only report the failure when nothing was added; if some succeeded we show "added". if (result.added > 0) { ImmichToast.show( context: context, - msg: 'add_to_album_bottom_sheet_added'.t(args: {"album": album.name}), + msg: context.t.add_to_album_bottom_sheet_added(album: album.name), ); } else if (result.failed > 0) { ImmichToast.show( context: context, - msg: 'assets_cannot_be_added_to_album_count'.t(context: context, args: {'count': result.failed}), + msg: context.t.assets_cannot_be_added_to_album_count(count: result.failed), toastType: ToastType.error, ); } else { ImmichToast.show( context: context, - msg: 'add_to_album_bottom_sheet_already_exists'.t(args: {"album": album.name}), + msg: context.t.add_to_album_bottom_sheet_already_exists(album: album.name), ); } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index d5407e768f..1fb78703a1 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -1,8 +1,8 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; @@ -54,14 +54,14 @@ class _GeneralBottomSheetState extends ConsumerState { } if (!result.success) { - ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error); + ImmichToast.show(context: context, msg: context.t.scaffold_body_error_occurred, toastType: ToastType.error); return; } ImmichToast.show( context: context, msg: result.count == 0 - ? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}) - : 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), + ? context.t.add_to_album_bottom_sheet_already_exists(album: album.name) + : context.t.add_to_album_bottom_sheet_added(album: album.name), ); } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart index c764669cf6..0c63cde140 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart @@ -1,8 +1,8 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/download.action.dart'; @@ -44,15 +44,15 @@ class _PartnerDetailBottomSheetState extends ConsumerState asDateTimeRange(); @@ -13,12 +13,9 @@ sealed class DateFilterInputModel { if (date.end.difference(date.start).inHours < 24) { return DateFormat.yMMMd().format(date.start.toLocal()); } else { - return 'search_filter_date_interval'.t( - context: context, - args: { - "start": DateFormat.yMMMd().format(date.start.toLocal()), - "end": DateFormat.yMMMd().format(date.end.toLocal()), - }, + return context.t.search_filter_date_interval( + start: DateFormat.yMMMd().format(date.start.toLocal()), + end: DateFormat.yMMMd().format(date.end.toLocal()), ); } } @@ -38,7 +35,7 @@ class RecentMonthRangeFilter extends DateFilterInputModel { @override String asHumanReadable(BuildContext context) { - return 'last_months'.t(context: context, args: {"count": monthDelta.toString()}); + return context.t.last_months(count: monthDelta); } } @@ -62,7 +59,7 @@ class YearFilter extends DateFilterInputModel { @override String asHumanReadable(BuildContext context) { - return 'in_year'.tr(namedArgs: {"year": year.toString()}); + return context.t.in_year(year: year); } } @@ -121,14 +118,13 @@ class QuickDatePicker extends HookWidget { return null; } - Text _monthLabel(BuildContext context, int monthsFromNow) => - const Text('last_months').t(context: context, args: {"count": monthsFromNow.toString()}); + Text _monthLabel(BuildContext context, int monthsFromNow) => Text(context.t.last_months(count: monthsFromNow)); Widget _yearPicker(BuildContext context) { final size = MediaQuery.of(context).size; return Row( children: [ - const Text("in_year_selector").tr(), + Text(context.t.in_year_selector), const SizedBox(width: 15), Expanded( child: DropdownMenu( @@ -158,7 +154,7 @@ class QuickDatePicker extends HookWidget { child: IgnorePointer( ignoring: true, child: RadioListTile( - title: const Text('pick_custom_range').tr(), + title: Text(context.t.pick_custom_range), subtitle: hasPreviousInput ? Text(currentInput!.asHumanReadable(context)) : null, secondary: hasPreviousInput ? const Icon(Icons.edit) : null, value: _QuickPickerType.custom, diff --git a/mobile/lib/widgets/activities/dismissible_activity.dart b/mobile/lib/widgets/activities/dismissible_activity.dart index c056f5ee35..f1305ba3aa 100644 --- a/mobile/lib/widgets/activities/dismissible_activity.dart +++ b/mobile/lib/widgets/activities/dismissible_activity.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; class DismissibleActivity extends StatelessWidget { @@ -23,9 +24,9 @@ class DismissibleActivity extends StatelessWidget { context: context, builder: (context) => ConfirmDialog( onOk: () {}, - title: "shared_album_activity_remove_title", - content: "shared_album_activity_remove_content", - ok: "delete", + title: context.t.shared_album_activity_remove_title, + content: context.t.shared_album_activity_remove_content, + ok: context.t.delete, ), ) : Future.value(false), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index dd62813e3a..fffa6746f6 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -122,9 +122,9 @@ class ImmichAppBarDialog extends HookConsumerWidget { context: context, builder: (BuildContext ctx) { return ConfirmDialog( - title: "app_bar_signout_dialog_title", - content: "app_bar_signout_dialog_content", - ok: "yes", + title: context.t.app_bar_signout_dialog_title, + content: context.t.app_bar_signout_dialog_content, + ok: context.t.yes, onOk: () async { isLoggingOut.value = true; await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false); diff --git a/mobile/lib/widgets/common/confirm_dialog.dart b/mobile/lib/widgets/common/confirm_dialog.dart index 9c3f4344c6..74b7f4a04c 100644 --- a/mobile/lib/widgets/common/confirm_dialog.dart +++ b/mobile/lib/widgets/common/confirm_dialog.dart @@ -1,22 +1,15 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class ConfirmDialog extends StatelessWidget { final Function? onOk; final String title; final String content; - final String cancel; - final String ok; + final String? cancel; + final String? ok; - const ConfirmDialog({ - super.key, - this.onOk, - required this.title, - required this.content, - this.cancel = "cancel", - this.ok = "backup_controller_page_background_battery_info_ok", - }); + const ConfirmDialog({super.key, this.onOk, required this.title, required this.content, this.cancel, this.ok}); @override Widget build(BuildContext context) { @@ -27,22 +20,22 @@ class ConfirmDialog extends StatelessWidget { return AlertDialog( shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), - title: Text(title).tr(), - content: Text(content).tr(), + title: Text(title), + content: Text(content), actions: [ TextButton( onPressed: () => context.pop(false), child: Text( - cancel, + cancel ?? context.t.cancel, style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), + ), ), TextButton( onPressed: onOkPressed, child: Text( - ok, + ok ?? context.t.backup_controller_page_background_battery_info_ok, style: TextStyle(color: context.colorScheme.error, fontWeight: FontWeight.bold), - ).tr(), + ), ), ], ); diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index 229eaa7ec6..65a4c6f88e 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.dart @@ -1,10 +1,10 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -29,13 +29,13 @@ class ChangePasswordForm extends HookConsumerWidget { alignment: WrapAlignment.start, children: [ Text( - 'change_password'.tr(), + context.t.change_password, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: context.primaryColor), ), Padding( padding: const EdgeInsets.symmetric(vertical: 24.0), child: Text( - 'change_password_form_description'.tr(namedArgs: {'name': authState.name}), + context.t.change_password_form_description(name: authState.name), style: TextStyle(fontSize: 14, color: context.colorScheme.onSurface, fontWeight: FontWeight.w600), ), ), @@ -62,7 +62,7 @@ class ChangePasswordForm extends HookConsumerWidget { if (!isSuccess && context.mounted) { ImmichToast.show( context: context, - msg: "login_password_changed_error".tr(), + msg: context.t.login_password_changed_error, toastType: ToastType.error, gravity: ToastGravity.TOP, ); @@ -82,7 +82,7 @@ class ChangePasswordForm extends HookConsumerWidget { AutoRouter.of(context).back(); ImmichToast.show( context: context, - msg: "login_password_changed_success".tr(), + msg: context.t.login_password_changed_success, toastType: ToastType.success, gravity: ToastGravity.TOP, ); @@ -92,7 +92,7 @@ class ChangePasswordForm extends HookConsumerWidget { TextButton.icon( icon: const Icon(Icons.arrow_back), onPressed: () => AutoRouter.of(context).back(), - label: const Text('back').tr(), + label: Text(context.t.back), ), ], ), @@ -116,9 +116,9 @@ class PasswordInput extends StatelessWidget { obscureText: true, controller: controller, decoration: InputDecoration( - labelText: 'change_password_form_new_password'.tr(), + labelText: context.t.change_password_form_new_password, border: const OutlineInputBorder(), - hintText: 'change_password_form_new_password'.tr(), + hintText: context.t.change_password_form_new_password, ), ); } @@ -132,7 +132,7 @@ class ConfirmPasswordInput extends StatelessWidget { String? _validateInput(String? email) { if (confirmController.value != originalController.value) { - return 'change_password_form_password_mismatch'.tr(); + return StaticTranslations.instance.change_password_form_password_mismatch; } return null; } @@ -143,8 +143,8 @@ class ConfirmPasswordInput extends StatelessWidget { obscureText: true, controller: confirmController, decoration: InputDecoration( - labelText: 'change_password_form_confirm_password'.tr(), - hintText: 'change_password_form_reenter_new_password'.tr(), + labelText: context.t.change_password_form_confirm_password, + hintText: context.t.change_password_form_reenter_new_password, border: const OutlineInputBorder(), ), validator: _validateInput, @@ -166,7 +166,7 @@ class ChangePasswordButton extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), ), onPressed: onPressed, - child: Text('change_password'.tr(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), + child: Text(context.t.change_password, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), ); } } diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index e4c7516c43..5185c5c318 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -5,7 +5,6 @@ import 'dart:math'; import 'package:auto_route/auto_route.dart'; import 'package:crypto/crypto.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; @@ -14,7 +13,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; @@ -43,20 +42,21 @@ class LoginForm extends HookConsumerWidget { final log = Logger('LoginForm'); - String? _validateUrl(String? url) => normalizeAndValidateServerUrl(url) ? null : 'login_form_err_invalid_url'.tr(); + String? _validateUrl(String? url) => + normalizeAndValidateServerUrl(url) ? null : StaticTranslations.instance.login_form_err_invalid_url; String? _validateEmail(String? email) { if (email == null || email == '') { return null; } if (email.endsWith(' ')) { - return 'login_form_err_trailing_whitespace'.tr(); + return StaticTranslations.instance.login_form_err_trailing_whitespace; } if (email.startsWith(' ')) { - return 'login_form_err_leading_whitespace'.tr(); + return StaticTranslations.instance.login_form_err_leading_whitespace; } if (email.contains(' ') || !email.contains('@')) { - return 'login_form_err_invalid_email'.tr(); + return StaticTranslations.instance.login_form_err_invalid_email; } return null; } @@ -96,7 +96,7 @@ class LoginForm extends HookConsumerWidget { // Guard empty URL if (serverUrl.isEmpty) { - ImmichToast.show(context: context, msg: "login_form_server_empty".tr(), toastType: ToastType.error); + ImmichToast.show(context: context, msg: context.t.login_form_server_empty, toastType: ToastType.error); } try { @@ -127,7 +127,7 @@ class LoginForm extends HookConsumerWidget { ImmichToast.show( context: context, - msg: e.message ?? 'login_form_api_exception'.tr(), + msg: e.message ?? context.t.login_form_api_exception, toastType: ToastType.error, gravity: ToastGravity.TOP, ); @@ -140,7 +140,7 @@ class LoginForm extends HookConsumerWidget { ImmichToast.show( context: context, - msg: 'login_form_handshake_exception'.tr(), + msg: context.t.login_form_handshake_exception, toastType: ToastType.error, gravity: ToastGravity.TOP, ); @@ -153,7 +153,7 @@ class LoginForm extends HookConsumerWidget { ImmichToast.show( context: context, - msg: 'login_form_server_error'.tr(), + msg: context.t.login_form_server_error, toastType: ToastType.error, gravity: ToastGravity.TOP, ); @@ -210,15 +210,15 @@ class LoginForm extends HookConsumerWidget { shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), elevation: 5, title: Text( - 'manage_media_access_title', + context.t.manage_media_access_title, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: context.primaryColor), - ).tr(), + ), content: SingleChildScrollView( child: ListBody( children: [ - const Text('manage_media_access_subtitle', style: TextStyle(fontSize: 14)).tr(), + Text(context.t.manage_media_access_subtitle, style: const TextStyle(fontSize: 14)), const SizedBox(height: 4), - const Text('manage_media_access_rationale', style: TextStyle(fontSize: 12)).tr(), + Text(context.t.manage_media_access_rationale, style: const TextStyle(fontSize: 12)), ], ), ), @@ -226,7 +226,7 @@ class LoginForm extends HookConsumerWidget { TextButton( onPressed: () => Navigator.of(context).pop(), child: Text( - 'cancel'.tr(), + context.t.cancel, style: TextStyle(fontWeight: FontWeight.w600, color: context.primaryColor), ), ), @@ -236,7 +236,7 @@ class LoginForm extends HookConsumerWidget { Navigator.of(context).pop(); }, child: Text( - 'manage_media_access_settings'.tr(), + context.t.manage_media_access_settings, style: TextStyle(fontWeight: FontWeight.w600, color: context.primaryColor), ), ), @@ -290,7 +290,7 @@ class LoginForm extends HookConsumerWidget { ImmichToast.show( context: context, - msg: "login_form_failed_login".tr(), + msg: context.t.login_form_failed_login, toastType: ToastType.error, gravity: ToastGravity.TOP, ); @@ -348,7 +348,7 @@ class LoginForm extends HookConsumerWidget { ImmichToast.show( context: context, - msg: "login_form_failed_get_oauth_server_config".tr(), + msg: context.t.login_form_failed_get_oauth_server_config, toastType: ToastType.error, gravity: ToastGravity.TOP, ); @@ -404,7 +404,7 @@ class LoginForm extends HookConsumerWidget { ImmichToast.show( context: context, - msg: "login_form_failed_get_oauth_server_disable".tr(), + msg: context.t.login_form_failed_get_oauth_server_disable, toastType: ToastType.info, gravity: ToastGravity.TOP, ); @@ -451,19 +451,19 @@ class LoginForm extends HookConsumerWidget { children: [ ImmichForm( onSubmit: getServerAuthSettings, - submitText: 'next'.t(context: context), + submitText: context.t.next, submitIcon: Icons.arrow_forward_rounded, builder: (_, form) => ImmichURLInput( controller: serverEndpointController, - label: 'login_form_endpoint_url'.t(context: context), - hintText: 'login_form_endpoint_hint'.t(context: context), + label: context.t.login_form_endpoint_url, + hintText: context.t.login_form_endpoint_hint, validator: _validateUrl, keyboardAction: .next, onSubmit: (_) => form.submit(), ), ), ImmichTextButton( - labelText: 'settings'.t(context: context), + labelText: context.t.settings, icon: Icons.settings, variant: ImmichVariant.ghost, onPressed: () => context.pushRoute(const SettingsRoute()), @@ -488,15 +488,15 @@ class LoginForm extends HookConsumerWidget { if (isPasswordLoginEnable.value) ImmichForm( onSubmit: login, - submitText: 'login'.t(context: context), + submitText: context.t.login, submitIcon: Icons.login_rounded, builder: (context, form) => Column( spacing: ImmichSpacing.md, children: [ ImmichTextInput( controller: emailController, - label: 'email'.t(context: context), - hintText: 'login_form_email_hint'.t(context: context), + label: context.t.email, + hintText: context.t.login_form_email_hint, validator: _validateEmail, keyboardAction: TextInputAction.next, keyboardType: TextInputType.emailAddress, @@ -506,8 +506,8 @@ class LoginForm extends HookConsumerWidget { ImmichPasswordInput( controller: passwordController, focusNode: passwordFocusNode, - label: 'password'.t(context: context), - hintText: 'login_form_password_hint'.t(context: context), + label: context.t.password, + hintText: context.t.login_form_password_hint, keyboardAction: TextInputAction.go, onSubmit: (_) => form.submit(), ), @@ -526,10 +526,9 @@ class LoginForm extends HookConsumerWidget { ) : const SizedBox.shrink(), ), - if (!isOauthEnable.value && !isPasswordLoginEnable.value) - Center(child: const Text('login_disabled').tr()), + if (!isOauthEnable.value && !isPasswordLoginEnable.value) Center(child: Text(context.t.login_disabled)), ImmichTextButton( - labelText: 'back'.t(context: context), + labelText: context.t.back, icon: Icons.arrow_back, variant: ImmichVariant.ghost, onPressed: () => serverEndpoint.value = null, diff --git a/mobile/lib/widgets/forms/pin_registration_form.dart b/mobile/lib/widgets/forms/pin_registration_form.dart index ebd299bcce..bc14388021 100644 --- a/mobile/lib/widgets/forms/pin_registration_form.dart +++ b/mobile/lib/widgets/forms/pin_registration_form.dart @@ -1,8 +1,8 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/widgets/forms/pin_input.dart'; @@ -58,7 +58,7 @@ class PinRegistrationForm extends HookConsumerWidget { SizedBox( width: context.width * 0.7, child: Text( - 'setup_pin_code'.tr(), + context.t.setup_pin_code, style: context.textTheme.labelLarge!.copyWith(fontSize: 24), textAlign: TextAlign.center, ), @@ -66,7 +66,7 @@ class PinRegistrationForm extends HookConsumerWidget { SizedBox( width: context.width * 0.8, child: Text( - 'new_pin_code_subtitle'.tr(), + context.t.new_pin_code_subtitle, style: context.textTheme.bodyLarge!.copyWith(fontSize: 16), textAlign: TextAlign.center, ), @@ -74,7 +74,7 @@ class PinRegistrationForm extends HookConsumerWidget { const SizedBox(height: 32), PinInput( controller: newPinCodeController, - label: 'new_pin_code'.tr(), + label: context.t.new_pin_code, length: 6, autoFocus: true, hasError: hasError.value, @@ -87,7 +87,7 @@ class PinRegistrationForm extends HookConsumerWidget { const SizedBox(height: 32), PinInput( controller: confirmPinCodeController, - label: 'confirm_new_pin_code'.tr(), + label: context.t.confirm_new_pin_code, length: 6, hasError: hasError.value, onChanged: (input) { @@ -102,7 +102,7 @@ class PinRegistrationForm extends HookConsumerWidget { child: Row( children: [ Expanded( - child: ElevatedButton(onPressed: createNewPinCode, child: Text('create'.tr())), + child: ElevatedButton(onPressed: createNewPinCode, child: Text(context.t.create)), ), ], ), diff --git a/mobile/lib/widgets/forms/pin_verification_form.dart b/mobile/lib/widgets/forms/pin_verification_form.dart index de2acaa1f7..6f1ba25fd0 100644 --- a/mobile/lib/widgets/forms/pin_verification_form.dart +++ b/mobile/lib/widgets/forms/pin_verification_form.dart @@ -1,8 +1,8 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/widgets/forms/pin_input.dart'; @@ -60,7 +60,7 @@ class PinVerificationForm extends HookConsumerWidget { SizedBox( width: context.width * 0.7, child: Text( - description ?? 'enter_your_pin_code_subtitle'.tr(), + description ?? context.t.enter_your_pin_code_subtitle, style: context.textTheme.labelLarge!.copyWith(fontSize: 18), textAlign: TextAlign.center, ), diff --git a/mobile/lib/widgets/search/search_filter/camera_picker.dart b/mobile/lib/widgets/search/search_filter/camera_picker.dart index 24a463eedb..aad5fda18f 100644 --- a/mobile/lib/widgets/search/search_filter/camera_picker.dart +++ b/mobile/lib/widgets/search/search_filter/camera_picker.dart @@ -1,8 +1,8 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/providers/search/search_filter.provider.dart'; import 'package:immich_mobile/widgets/search/search_filter/common/dropdown.dart'; @@ -37,7 +37,7 @@ class CameraPicker extends HookConsumerWidget { AsyncData(:final value) => value.map((e) => DropdownMenuEntry(value: e, label: e)).toList(), _ => [], }, - label: const Text('make').tr(), + label: Text(context.t.make), controller: makeTextController, leadingIcon: const Icon(Icons.photo_camera_rounded), onSelected: (value) { @@ -56,7 +56,7 @@ class CameraPicker extends HookConsumerWidget { AsyncData(:final value) => value.map((e) => DropdownMenuEntry(value: e, label: e)).toList(), _ => [], }, - label: const Text('model').tr(), + label: Text(context.t.model), controller: modelTextController, leadingIcon: const Icon(Icons.camera), onSelected: (value) { diff --git a/mobile/lib/widgets/search/search_filter/display_option_picker.dart b/mobile/lib/widgets/search/search_filter/display_option_picker.dart index a64eab7b71..83ddc98107 100644 --- a/mobile/lib/widgets/search/search_filter/display_option_picker.dart +++ b/mobile/lib/widgets/search/search_filter/display_option_picker.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; enum DisplayOption { notInAlbum, favorite, archive } @@ -23,7 +23,7 @@ class DisplayOptionPicker extends HookWidget { shrinkWrap: true, children: [ CheckboxListTile( - title: const Text('search_filter_display_option_not_in_album').tr(), + title: Text(context.t.search_filter_display_option_not_in_album), value: options.value[DisplayOption.notInAlbum], onChanged: (bool? value) { options.value = {...options.value, DisplayOption.notInAlbum: value!}; @@ -31,7 +31,7 @@ class DisplayOptionPicker extends HookWidget { }, ), CheckboxListTile( - title: const Text('favorite').tr(), + title: Text(context.t.favorite), value: options.value[DisplayOption.favorite], onChanged: (value) { options.value = {...options.value, DisplayOption.favorite: value!}; @@ -39,7 +39,7 @@ class DisplayOptionPicker extends HookWidget { }, ), CheckboxListTile( - title: const Text('archive').tr(), + title: Text(context.t.archive), value: options.value[DisplayOption.archive], onChanged: (value) { options.value = {...options.value, DisplayOption.archive: value!}; diff --git a/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart b/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart index 1c338b8828..a74ea41c38 100644 --- a/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart +++ b/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class FilterBottomSheetScaffold extends StatelessWidget { const FilterBottomSheetScaffold({ @@ -46,7 +46,7 @@ class FilterBottomSheetScaffold extends StatelessWidget { onClear(); context.pop(); }, - child: const Text('clear').tr(), + child: Text(context.t.clear), ), if (onSearch != null) const SizedBox(width: 8), if (onSearch != null) @@ -56,7 +56,7 @@ class FilterBottomSheetScaffold extends StatelessWidget { onSearch!(); context.pop(); }, - child: const Text('search_filter_apply').tr(), + child: Text(context.t.search_filter_apply), ), ], ), diff --git a/mobile/lib/widgets/search/search_filter/location_picker.dart b/mobile/lib/widgets/search/search_filter/location_picker.dart index f521a50f35..ab35145a28 100644 --- a/mobile/lib/widgets/search/search_filter/location_picker.dart +++ b/mobile/lib/widgets/search/search_filter/location_picker.dart @@ -1,7 +1,7 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/providers/search/search_filter.provider.dart'; import 'package:immich_mobile/widgets/search/search_filter/common/dropdown.dart'; @@ -61,7 +61,7 @@ class LocationPicker extends HookConsumerWidget { AsyncData(:final value) => value.map((e) => DropdownMenuEntry(value: e, label: e)).toList(), _ => [], }, - label: const Text('country').tr(), + label: Text(context.t.country), controller: countryTextController, onSelected: (value) { if (value.toString() == selectedCountry.value) { @@ -80,7 +80,7 @@ class LocationPicker extends HookConsumerWidget { AsyncData(:final value) => value.map((e) => DropdownMenuEntry(value: e, label: e)).toList(), _ => [], }, - label: const Text('state').tr(), + label: Text(context.t.state), controller: stateTextController, onSelected: (value) { if (value.toString() == selectedState.value) { @@ -98,7 +98,7 @@ class LocationPicker extends HookConsumerWidget { AsyncData(:final value) => value.map((e) => DropdownMenuEntry(value: e, label: e)).toList(), _ => [], }, - label: const Text('city').tr(), + label: Text(context.t.city), controller: cityTextController, onSelected: (value) { selectedCity.value = value.toString(); diff --git a/mobile/lib/widgets/search/search_filter/media_type_picker.dart b/mobile/lib/widgets/search/search_filter/media_type_picker.dart index ac89de8190..dd0767a97a 100644 --- a/mobile/lib/widgets/search/search_filter/media_type_picker.dart +++ b/mobile/lib/widgets/search/search_filter/media_type_picker.dart @@ -1,7 +1,7 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class MediaTypePicker extends HookWidget { const MediaTypePicker({super.key, required this.onSelect, this.filter}); @@ -21,9 +21,9 @@ class MediaTypePicker extends HookWidget { groupValue: selectedMediaType.value, child: Column( children: [ - RadioListTile(key: const Key("all"), title: const Text("all").tr(), value: AssetType.other), - RadioListTile(key: const Key("image"), title: const Text("image").tr(), value: AssetType.image), - RadioListTile(key: const Key("video"), title: const Text("video").tr(), value: AssetType.video), + RadioListTile(key: const Key("all"), title: Text(context.t.all), value: AssetType.other), + RadioListTile(key: const Key("image"), title: Text(context.t.image), value: AssetType.image), + RadioListTile(key: const Key("video"), title: Text(context.t.video), value: AssetType.video), ], ), ); diff --git a/mobile/lib/widgets/search/search_filter/star_rating_picker.dart b/mobile/lib/widgets/search/search_filter/star_rating_picker.dart index 864ddf2591..643f4f167c 100644 --- a/mobile/lib/widgets/search/search_filter/star_rating_picker.dart +++ b/mobile/lib/widgets/search/search_filter/star_rating_picker.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/utils/option.dart'; @@ -28,7 +28,7 @@ class StarRatingPicker extends HookWidget { 6, (index) => RadioListTile( key: Key("star_$index"), - title: Text('rating_count'.t(args: {'count': index})), + title: Text(context.t.rating_count(count: index)), value: index, ), ), diff --git a/mobile/lib/widgets/search/search_row_title.dart b/mobile/lib/widgets/search/search_row_title.dart index dc0a4ba6cb..cf4cc62c18 100644 --- a/mobile/lib/widgets/search/search_row_title.dart +++ b/mobile/lib/widgets/search/search_row_title.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class SearchRowTitle extends StatelessWidget { const SearchRowTitle({super.key, required this.onViewAllPressed, required this.title}); @@ -17,9 +17,9 @@ class SearchRowTitle extends StatelessWidget { TextButton( onPressed: onViewAllPressed, child: Text( - 'search_page_view_all_button', + context.t.search_page_view_all_button, style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ).tr(), + ), ), ], ); diff --git a/mobile/lib/widgets/shared_link/shared_link_item.dart b/mobile/lib/widgets/shared_link/shared_link_item.dart index d98925bdf8..2471c19cea 100644 --- a/mobile/lib/widgets/shared_link/shared_link_item.dart +++ b/mobile/lib/widgets/shared_link/shared_link_item.dart @@ -173,8 +173,8 @@ class SharedLinkItem extends ConsumerWidget { final confirmed = await showDialog( context: context, builder: (BuildContext context) => ConfirmDialog( - title: "delete_shared_link_dialog_title", - content: "confirm_delete_shared_link", + title: context.t.delete_shared_link_dialog_title, + content: context.t.confirm_delete_shared_link, onOk: () {}, ), ); From 4c4170077cefe541707ab507e713134053bb02cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:39:38 +0200 Subject: [PATCH 22/49] fix(deps): update typescript-projects (#30539) Co-authored-by: Daniel Dietzler --- docs/mise.toml | 2 +- mise.lock | 74 +- mise.toml | 2 +- package.json | 2 +- packages/e2e-auth-server/package.json | 2 +- packages/plugin-sdk/package.json | 2 +- packages/scripts/package.json | 2 +- pnpm-lock.yaml | 2709 ++++++++++++------------- 8 files changed, 1373 insertions(+), 1422 deletions(-) diff --git a/docs/mise.toml b/docs/mise.toml index c4fe5f1b68..da5d00940e 100644 --- a/docs/mise.toml +++ b/docs/mise.toml @@ -28,4 +28,4 @@ run = "prettier --write ." run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}" [tools] -wrangler = "4.114.0" +wrangler = "4.119.0" diff --git a/mise.lock b/mise.lock index a4b395500b..836712e0fd 100644 --- a/mise.lock +++ b/mise.lock @@ -104,17 +104,17 @@ version = "7.1.3-6" backend = "github:jellyfin/jellyfin-ffmpeg" [tools."github:jellyfin/jellyfin-ffmpeg".options] -asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz" +asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz" -[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"] -checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886" -url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz" -url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876" +[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"] +checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2" +url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz" +url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879" -[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"] -checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886" -url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz" -url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876" +[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"] +checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2" +url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz" +url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879" [[tools."github:jellyfin/jellyfin-ffmpeg"]] version = "7.1.3-6" @@ -133,17 +133,17 @@ version = "7.1.3-6" backend = "github:jellyfin/jellyfin-ffmpeg" [tools."github:jellyfin/jellyfin-ffmpeg".options] -asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz" +asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz" -[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"] -checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2" -url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz" -url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879" +[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"] +checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886" +url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz" +url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876" -[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"] -checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2" -url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz" -url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879" +[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"] +checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886" +url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz" +url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876" [[tools."github:webassembly/binaryen"]] version = "version_124" @@ -291,43 +291,43 @@ url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12. url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602547" [[tools.pnpm]] -version = "11.17.0" +version = "11.20.0" backend = "aqua:pnpm/pnpm" [tools.pnpm."platforms.linux-arm64"] -checksum = "sha256:730d17de742a3efbb020ba91d7acfc0456c6ba6ad1cd8eb49f4c229fe9f504d3" -url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-linux-arm64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422123" +checksum = "sha256:f00fc2041bb41742b7943bf2bb24183ad20320e8384824a8031eb94edf2f57a5" +url = "https://github.com/pnpm/pnpm/releases/download/v11.20.0/pnpm-linux-arm64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/500142378" provenance = "github-attestations" [tools.pnpm."platforms.linux-arm64-musl"] -checksum = "sha256:cc072c0e7bdd290f52fb9afdcea778ede69f96f0f4dae63623163fc11d4beec3" -url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-linux-arm64-musl.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422121" +checksum = "sha256:2a21235d3f0fbfbcb9e530b9a98f2c64dcee95d1d5d1c2d04428255c7b85f32c" +url = "https://github.com/pnpm/pnpm/releases/download/v11.20.0/pnpm-linux-arm64-musl.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/500142372" provenance = "github-attestations" [tools.pnpm."platforms.linux-x64"] -checksum = "sha256:bdb1db01bf0f757495405a59a09c5c287f315889dc98d3b14bc374b9fe43a0bf" -url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-linux-x64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422122" +checksum = "sha256:b4ad6ad2b21db2f8cd50af416c3aa148ba704c31c84893f465a770a01c2c4572" +url = "https://github.com/pnpm/pnpm/releases/download/v11.20.0/pnpm-linux-x64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/500142376" provenance = "github-attestations" [tools.pnpm."platforms.linux-x64-musl"] -checksum = "sha256:db2e4eeecab336bd41bbbc7a39626b166f1a15bb189efcf6a1dafdf159f23fe7" -url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-linux-x64-musl.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422116" +checksum = "sha256:db046881c027f1a2d69e9a530a21a62c2bced4ba8ff437e7300426ae56951fe4" +url = "https://github.com/pnpm/pnpm/releases/download/v11.20.0/pnpm-linux-x64-musl.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/500142377" provenance = "github-attestations" [tools.pnpm."platforms.macos-arm64"] -checksum = "sha256:1e9a35d76f9d382af365d95c64f3abf7815e24350653f5bb1a37e4546265bc2b" -url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-darwin-arm64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422117" +checksum = "sha256:4bc97fea72e5c92eec1fefc8d410c35d01e0d0d52f3160c59f38c32db58b5cd2" +url = "https://github.com/pnpm/pnpm/releases/download/v11.20.0/pnpm-darwin-arm64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/500142373" provenance = "github-attestations" [tools.pnpm."platforms.windows-x64"] -checksum = "sha256:3025c174a6dcffec14a071a5c854cb0c350a09a8c7c2857a75f75cc7b29b939c" -url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-win32-x64.zip" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422118" +checksum = "sha256:ea2528bdc3d96a1ff3c35587dc48ca692b39d77f08f26df4adeaaa9eb427024e" +url = "https://github.com/pnpm/pnpm/releases/download/v11.20.0/pnpm-win32-x64.zip" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/500142371" provenance = "github-attestations" [[tools.terragrunt]] diff --git a/mise.toml b/mise.toml index ad7fdf5a90..0039dbac2d 100644 --- a/mise.toml +++ b/mise.toml @@ -16,7 +16,7 @@ config_roots = [ [tools] node = "24.15.0" -pnpm = "11.17.0" +pnpm = "11.20.0" terragrunt = "1.1.1" opentofu = "1.12.5" "npm:@openapitools/openapi-generator-cli" = "2.40.1" diff --git a/package.json b/package.json index 2464012cee..75222c8220 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "format": "prettier --cache --check i18n/", "format:fix": "prettier --cache --write --list-different i18n" }, - "packageManager": "pnpm@11.17.0", + "packageManager": "pnpm@11.20.0", "engines": { "pnpm": ">=10.0.0" }, diff --git a/packages/e2e-auth-server/package.json b/packages/e2e-auth-server/package.json index b872dfe36d..eae4fd3e04 100644 --- a/packages/e2e-auth-server/package.json +++ b/packages/e2e-auth-server/package.json @@ -13,5 +13,5 @@ "oidc-provider": "^9.0.0", "tsx": "^4.20.6" }, - "packageManager": "pnpm@11.17.0" + "packageManager": "pnpm@11.20.0" } diff --git a/packages/plugin-sdk/package.json b/packages/plugin-sdk/package.json index 8afa00cfb4..4ae3ca76a7 100644 --- a/packages/plugin-sdk/package.json +++ b/packages/plugin-sdk/package.json @@ -27,7 +27,7 @@ "keywords": [], "author": "", "license": "GNU Affero General Public License version 3", - "packageManager": "pnpm@11.17.0", + "packageManager": "pnpm@11.20.0", "devDependencies": { "@extism/js-pdk": "^1.1.1", "@immich/sdk": "workspace:*", diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 66327e6150..ca08f10759 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -32,5 +32,5 @@ "vite": "^8.0.16", "vitest": "^4.1.8" }, - "packageManager": "pnpm@11.17.0" + "packageManager": "pnpm@11.20.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44d920aaa9..54faa1fa2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,16 +34,16 @@ importers: dependencies: '@docusaurus/core': specifier: ~3.10.0 - version: 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + version: 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/preset-classic': specifier: ~3.10.0 - version: 3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3) + version: 3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/theme-common': specifier: ~3.10.0 - version: 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + version: 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/theme-mermaid': specifier: ~3.10.0 - version: 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + version: 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@mdi/js': specifier: ^7.3.67 version: 7.4.47 @@ -52,25 +52,25 @@ importers: version: 1.6.1 '@mdx-js/react': specifier: ^3.0.0 - version: 3.1.1(@types/react@19.2.17)(react@19.2.8) + version: 3.1.1(@types/react@19.2.18)(react@19.2.8) autoprefixer: specifier: ^10.4.17 - version: 10.5.4(postcss@8.5.25) + version: 10.5.4(postcss@8.5.26) docusaurus-lunr-search: specifier: ^3.3.2 - version: 3.6.0(@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 3.6.0(@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) lunr: specifier: ^2.3.9 version: 2.3.9 postcss: specifier: ^8.4.25 - version: 8.5.25 + version: 8.5.26 prism-react-renderer: specifier: ^2.3.1 version: 2.4.1(react@19.2.8) raw-loader: specifier: ^4.0.2 - version: 4.0.2(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + version: 4.0.2(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) react: specifier: ^19.0.0 version: 19.2.8 @@ -79,20 +79,20 @@ importers: version: 19.2.8(react@19.2.8) tailwindcss: specifier: ^3.2.4 - version: 3.4.19(tsx@4.23.1)(yaml@2.9.0) + version: 3.4.19(tsx@4.23.9)(yaml@2.9.0) url: specifier: ^0.11.0 version: 0.11.4 devDependencies: '@docusaurus/module-type-aliases': specifier: ~3.10.0 - version: 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + version: 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/tsconfig': specifier: ^3.10.0 version: 3.10.2 '@docusaurus/types': specifier: ^3.10.0 - version: 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + version: 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@typescript/native': specifier: npm:typescript@^7.0.2 version: typescript@7.0.2 @@ -122,19 +122,19 @@ importers: version: link:../packages/sdk '@playwright/test': specifier: ^1.44.1 - version: 1.62.0 + version: 1.62.1 '@socket.io/component-emitter': specifier: ^3.1.2 version: 3.1.2 '@types/luxon': specifier: ^3.4.2 - version: 3.7.2 + version: 3.7.3 '@types/node': specifier: ^24.13.3 version: 24.13.3 '@types/pg': specifier: ^8.15.1 - version: 8.20.0 + version: 8.20.4 '@types/pngjs': specifier: ^6.0.4 version: 6.0.5 @@ -164,7 +164,7 @@ importers: version: 35.21.0 globals: specifier: ^17.0.0 - version: 17.7.0 + version: 17.9.0 luxon: specifier: ^3.4.4 version: 3.7.2 @@ -194,13 +194,13 @@ importers: version: '@typescript/typescript6@6.0.2' typescript-eslint: specifier: ^8.28.0 - version: 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + version: 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) utimes: specifier: ^5.2.1 version: 5.2.1(supports-color@8.1.1) vitest: specifier: ^4.0.0 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) packages/cli: dependencies: @@ -273,7 +273,7 @@ importers: version: 72.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) globals: specifier: ^17.0.0 - version: 17.7.0 + version: 17.9.0 mock-fs: specifier: ^5.2.0 version: 5.5.0 @@ -288,13 +288,13 @@ importers: version: '@typescript/typescript6@6.0.2' typescript-eslint: specifier: ^8.58.0 - version: 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + version: 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) vite: specifier: ^8.0.0 - version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + version: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) vitest: specifier: ^4.0.0 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) vitest-fetch-mock: specifier: ^0.4.0 version: 0.4.5(vitest@4.1.10) @@ -306,16 +306,16 @@ importers: devDependencies: '@types/oidc-provider': specifier: ^9.0.0 - version: 9.5.0 + version: 9.11.0 jose: specifier: ^6.0.0 - version: 6.2.4 + version: 6.2.8 oidc-provider: specifier: ^9.0.0 - version: 9.10.0(supports-color@8.1.1) + version: 9.11.2(supports-color@8.1.1) tsx: specifier: ^4.20.6 - version: 4.23.1 + version: 4.23.9 packages/plugin-core: devDependencies: @@ -330,7 +330,7 @@ importers: version: link:../sdk esbuild: specifier: ^0.28.0 - version: 0.28.1 + version: 0.28.2 typescript: specifier: ^7.0.0 version: 7.0.2 @@ -352,7 +352,7 @@ importers: version: 24.13.3 esbuild: specifier: ^0.28.0 - version: 0.28.1 + version: 0.28.2 tsc-alias: specifier: ^1.8.16 version: 1.9.1 @@ -374,13 +374,13 @@ importers: version: 24.13.3 '@types/semver': specifier: ^7.7.1 - version: 7.7.1 + version: 7.8.0 vite: specifier: ^8.0.16 - version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + version: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) vitest: specifier: ^4.1.8 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) packages/sdk: dependencies: @@ -408,7 +408,7 @@ importers: version: 0.5.2 '@nestjs/bullmq': specifier: ^11.0.1 - version: 11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(bullmq@5.81.2(supports-color@8.1.1)) + version: 11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(bullmq@5.81.3(supports-color@8.1.1)) '@nestjs/common': specifier: ^11.0.4 version: 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) @@ -486,7 +486,7 @@ importers: version: 2.3.0(supports-color@8.1.1) bullmq: specifier: ^5.51.0 - version: 5.81.2(supports-color@8.1.1) + version: 5.81.3(supports-color@8.1.1) chokidar: specifier: ^4.0.3 version: 4.0.3 @@ -534,10 +534,10 @@ importers: version: 5.11.1(supports-color@8.1.1) jose: specifier: ^6.0.0 - version: 6.2.4 + version: 6.2.8 js-yaml: specifier: ^5.0.0 - version: 5.2.2 + version: 5.2.3 jsonwebtoken: specifier: ^9.0.2 version: 9.0.3 @@ -576,7 +576,7 @@ importers: version: 5.5.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/swagger@11.4.6(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) nodemailer: specifier: ^9.0.0 - version: 9.0.3 + version: 9.0.4 openid-client: specifier: ^6.3.3 version: 6.8.4 @@ -621,7 +621,7 @@ importers: version: 4.8.3(supports-color@8.1.1) tailwindcss-preset-email: specifier: ^1.4.0 - version: 1.4.1(tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0)) + version: 1.4.1(tailwindcss@3.4.19(tsx@4.23.9)(yaml@2.9.0)) thumbhash: specifier: ^0.1.1 version: 0.1.1 @@ -646,7 +646,7 @@ importers: version: 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) '@nestjs/cli': specifier: ^11.0.2 - version: 11.0.24(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/node@24.13.3)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(prettier@3.9.6)(uglify-js@3.19.3) + version: 11.0.24(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/node@24.13.3)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(prettier@3.9.6)(uglify-js@3.19.3) '@nestjs/schematics': specifier: ^11.0.0 version: 11.1.0(@typescript/typescript6@6.0.2)(chokidar@4.0.3)(prettier@3.9.6) @@ -655,7 +655,7 @@ importers: version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@nestjs/platform-express@11.1.28) '@swc/core': specifier: ^1.4.14 - version: 1.15.46(@swc/helpers@0.5.23) + version: 1.15.47(@swc/helpers@0.5.23) '@types/archiver': specifier: ^7.0.0 version: 7.0.0 @@ -688,10 +688,10 @@ importers: version: 9.0.10 '@types/lodash': specifier: ^4.14.197 - version: 4.17.24 + version: 4.17.25 '@types/luxon': specifier: ^3.6.2 - version: 3.7.2 + version: 3.7.3 '@types/mock-fs': specifier: ^4.13.1 version: 4.13.4 @@ -712,10 +712,10 @@ importers: version: 6.0.5 '@types/react': specifier: ^19.0.0 - version: 19.2.17 + version: 19.2.18 '@types/semver': specifier: ^7.5.8 - version: 7.7.1 + version: 7.8.0 '@types/supertest': specifier: ^7.0.0 version: 7.2.1 @@ -730,7 +730,7 @@ importers: version: typescript@7.0.2 '@vitest/coverage-v8': specifier: ^4.0.0 - version: 4.1.10(vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.1.10(vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) eslint: specifier: ^10.0.0 version: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) @@ -745,7 +745,7 @@ importers: version: 72.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) globals: specifier: ^17.0.0 - version: 17.7.0 + version: 17.9.0 mock-fs: specifier: ^5.2.0 version: 5.5.0 @@ -766,31 +766,31 @@ importers: version: 7.2.2(supports-color@8.1.1) tailwindcss: specifier: ^3.4.0 - version: 3.4.19(tsx@4.23.1)(yaml@2.9.0) + version: 3.4.19(tsx@4.23.9)(yaml@2.9.0) testcontainers: specifier: ^12.0.0 - version: 12.0.4(supports-color@8.1.1) + version: 12.1.0(supports-color@8.1.1) typescript: specifier: npm:@typescript/typescript6@^6.0.2 version: '@typescript/typescript6@6.0.2' typescript-eslint: specifier: ^8.28.0 - version: 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + version: 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) unplugin-swc: specifier: ^1.4.5 - version: 1.5.9(@swc/core@1.15.46(@swc/helpers@0.5.23))(rollup@4.62.0) + version: 1.5.10(@swc/core@1.15.47(@swc/helpers@0.5.23))(rollup@4.62.0) vite-tsconfig-paths: specifier: ^6.0.0 - version: 6.1.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 6.1.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1)(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) vitest: specifier: ^3.0.0 - version: 3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + version: 3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) web: dependencies: '@formatjs/icu-messageformat-parser': specifier: ^3.0.0 - version: 3.5.15 + version: 3.5.16 '@immich/justified-layout-wasm': specifier: ^0.4.3 version: 0.4.3 @@ -799,7 +799,7 @@ importers: version: link:../packages/sdk '@immich/ui': specifier: ^0.83.0 - version: 0.83.0(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 0.83.0(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) '@mapbox/mapbox-gl-rtl-text': specifier: 0.4.0 version: 0.4.0 @@ -808,25 +808,25 @@ importers: version: 7.4.47 '@noble/hashes': specifier: ^2.2.0 - version: 2.2.0 + version: 2.3.0 '@photo-sphere-viewer/core': specifier: ^5.14.0 - version: 5.15.0 + version: 5.15.1 '@photo-sphere-viewer/equirectangular-video-adapter': specifier: ^5.14.0 - version: 5.15.0(@photo-sphere-viewer/core@5.15.0)(@photo-sphere-viewer/video-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)) + version: 5.15.1(@photo-sphere-viewer/core@5.15.1)(@photo-sphere-viewer/video-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1)) '@photo-sphere-viewer/markers-plugin': specifier: ^5.14.0 - version: 5.15.0(@photo-sphere-viewer/core@5.15.0) + version: 5.15.1(@photo-sphere-viewer/core@5.15.1) '@photo-sphere-viewer/resolution-plugin': specifier: ^5.14.0 - version: 5.15.0(@photo-sphere-viewer/core@5.15.0)(@photo-sphere-viewer/settings-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)) + version: 5.15.1(@photo-sphere-viewer/core@5.15.1)(@photo-sphere-viewer/settings-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1)) '@photo-sphere-viewer/settings-plugin': specifier: ^5.14.0 - version: 5.15.0(@photo-sphere-viewer/core@5.15.0) + version: 5.15.1(@photo-sphere-viewer/core@5.15.1) '@photo-sphere-viewer/video-plugin': specifier: ^5.14.0 - version: 5.15.0(@photo-sphere-viewer/core@5.15.0) + version: 5.15.1(@photo-sphere-viewer/core@5.15.1) '@types/geojson': specifier: ^7946.0.16 version: 7946.0.16 @@ -835,7 +835,7 @@ importers: version: 0.42.0 '@zoom-image/svelte': specifier: ^0.3.0 - version: 0.3.9(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 0.3.9(svelte@5.56.8(@typescript-eslint/types@8.66.0)) dom-to-image: specifier: ^2.6.0 version: 2.6.0 @@ -859,10 +859,10 @@ importers: version: 1.5.11 hls.js: specifier: ^1.6.16 - version: 1.6.16 + version: 1.6.17 intl-messageformat: specifier: ^11.0.0 - version: 11.2.12 + version: 11.2.13 justified-layout: specifier: ^4.1.0 version: 4.1.0 @@ -886,7 +886,7 @@ importers: version: 1.5.4 simple-icons: specifier: ^16.0.0 - version: 16.27.1 + version: 16.28.0 socket.io-client: specifier: ~4.8.0 version: 4.8.3(supports-color@8.1.1) @@ -895,16 +895,16 @@ importers: version: 5.2.2 svelte-i18n: specifier: ^4.0.1 - version: 4.0.1(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 4.0.1(svelte@5.56.8(@typescript-eslint/types@8.66.0)) svelte-jsoneditor: specifier: ^3.10.0 - version: 3.13.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 3.13.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) svelte-maplibre: specifier: ^1.2.5 - version: 1.3.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 1.3.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) svelte-persisted-store: specifier: ^0.12.0 - version: 0.12.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 0.12.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) tabbable: specifier: ^6.2.0 version: 6.5.0 @@ -913,7 +913,7 @@ importers: version: 3.6.0 tailwind-variants: specifier: ^3.2.2 - version: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.3) + version: 3.3.1(tailwind-merge@3.6.0)(tailwindcss@4.3.3) thumbhash: specifier: ^0.1.1 version: 0.1.1 @@ -938,31 +938,31 @@ importers: version: 3.1.2 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.10(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))) + version: 3.0.10(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))) '@sveltejs/enhanced-img': specifier: ^0.11.0 - version: 0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(rollup@4.62.0)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(rollup@4.62.0)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@sveltejs/kit': specifier: ^2.56.1 - version: 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@sveltejs/vite-plugin-svelte': specifier: 7.2.0 - version: 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@tailwindcss/vite': specifier: ^4.2.4 - version: 4.3.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.3.3(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@testing-library/jest-dom': specifier: ^7.0.0 version: 7.0.0(@testing-library/dom@10.4.1) '@testing-library/svelte': specifier: ^5.2.8 - version: 5.4.2(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))(vitest@4.1.10) + version: 5.4.2(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))(vitest@4.1.10) '@testing-library/user-event': specifier: ^14.5.2 - version: 14.6.1(@testing-library/dom@10.4.1) + version: 14.6.3(@testing-library/dom@10.4.1) '@trivago/prettier-plugin-sort-imports': specifier: ^6.0.2 - version: 6.0.2(prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)))(prettier@3.9.6)(supports-color@8.1.1)(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 6.0.2(prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.66.0)))(prettier@3.9.6)(supports-color@8.1.1)(svelte@5.56.8(@typescript-eslint/types@8.66.0)) '@types/chromecast-caf-sender': specifier: ^1.0.11 version: 1.0.11 @@ -977,7 +977,7 @@ importers: version: 4.17.12 '@types/luxon': specifier: ^3.4.2 - version: 3.7.2 + version: 3.7.3 '@types/qrcode': specifier: ^1.5.5 version: 1.5.6 @@ -1004,7 +1004,7 @@ importers: version: 7.0.2(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-svelte: specifier: ^3.12.4 - version: 3.22.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 3.22.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) eslint-plugin-unicorn: specifier: ^72.0.0 version: 72.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) @@ -1013,7 +1013,7 @@ importers: version: 1.4.2 globals: specifier: ^17.0.0 - version: 17.7.0 + version: 17.9.0 prettier: specifier: ^3.7.4 version: 3.9.6 @@ -1022,19 +1022,19 @@ importers: version: 4.2.0(prettier@3.9.6) prettier-plugin-svelte: specifier: ^4.0.0 - version: 4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.66.0)) rollup-plugin-visualizer: specifier: ^7.0.0 - version: 7.0.1(rolldown@1.1.5)(rollup@4.62.0) + version: 7.0.1(rolldown@1.2.3)(rollup@4.62.0) svelte: specifier: 5.56.8 - version: 5.56.8(@typescript-eslint/types@8.65.0) + version: 5.56.8(@typescript-eslint/types@8.66.0) svelte-check: specifier: ^4.4.6 - version: 4.7.3(@typescript/typescript6@6.0.2)(picomatch@4.0.5)(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 4.7.4(@typescript/typescript6@6.0.2)(picomatch@4.0.5)(svelte@5.56.8(@typescript-eslint/types@8.66.0)) svelte-eslint-parser: specifier: ^1.3.3 - version: 1.8.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 1.8.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) tailwindcss: specifier: ^4.2.4 version: 4.3.3 @@ -1043,13 +1043,13 @@ importers: version: '@typescript/typescript6@6.0.2' typescript-eslint: specifier: ^8.45.0 - version: 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + version: 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) vite: specifier: ^8.0.0 - version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + version: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) vitest: specifier: ^4.0.0 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) packages: @@ -2336,15 +2336,9 @@ packages: resolution: {integrity: sha512-xx0W3eav2uW1NRIpuHJWNwLTC15xPNjU4Uxi9NSnd3swYC96BE3vFiT93SD8s24kmAAWNwgZwfZ2fghGZ01Lcw==} engines: {node: '>=20.0'} - '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - '@esbuild/aix-ppc64@0.19.12': resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} engines: {node: '>=12'} @@ -2357,8 +2351,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.28.1': - resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -2375,8 +2369,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.28.1': - resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -2393,8 +2387,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.28.1': - resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -2411,8 +2405,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.28.1': - resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -2429,8 +2423,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.28.1': - resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -2447,8 +2441,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.28.1': - resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -2465,8 +2459,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.28.1': - resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -2483,8 +2477,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.28.1': - resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -2501,8 +2495,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.28.1': - resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -2519,8 +2513,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.28.1': - resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -2537,8 +2531,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.28.1': - resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -2555,8 +2549,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.28.1': - resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -2573,8 +2567,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.28.1': - resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -2591,8 +2585,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.28.1': - resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -2609,8 +2603,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.28.1': - resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -2627,8 +2621,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.28.1': - resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -2645,8 +2639,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.28.1': - resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -2657,8 +2651,8 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.28.1': - resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -2675,8 +2669,8 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.28.1': - resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -2687,8 +2681,8 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.28.1': - resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -2705,8 +2699,8 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.28.1': - resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -2717,8 +2711,8 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.28.1': - resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -2735,8 +2729,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.28.1': - resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -2753,8 +2747,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.28.1': - resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -2771,8 +2765,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.28.1': - resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -2789,8 +2783,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.28.1': - resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -2874,8 +2868,8 @@ packages: '@formatjs/icu-messageformat-parser@2.11.4': resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} - '@formatjs/icu-messageformat-parser@3.5.15': - resolution: {integrity: sha512-5o4grXKotAB3JqQuisLApHG43g17N+paoRTa92Jiz35Zvfemq0cVf4EDvuxyHAzmsJji7igaEowicLO/VmfJ8Q==} + '@formatjs/icu-messageformat-parser@3.5.16': + resolution: {integrity: sha512-kl6b/4D56gjGZi4ZewSmvXbalHwjOUI5ogEHPZqw42goeXTTrL7/yuPzvdrvr0QigDtvaOeb+UeMf62jks43Yg==} '@formatjs/icu-skeleton-parser@1.8.16': resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} @@ -3580,12 +3574,6 @@ packages: '@namnode/store@0.1.0': resolution: {integrity: sha512-4NGTldxKcmY0UuZ7OEkvCjs8ZEoeYB6M2UwMu74pdLiFMKxXbj9HdNk1Qn213bxX1O7bY5h+PLh5DZsTURZkYA==} - '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - '@nestjs/bull-shared@11.0.4': resolution: {integrity: sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==} peerDependencies: @@ -3735,8 +3723,8 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} - '@noble/hashes@2.2.0': - resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==} + '@noble/hashes@2.3.0': + resolution: {integrity: sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==} engines: {node: '>= 20.19.0'} '@nodelib/fs.scandir@2.1.5': @@ -3968,8 +3956,8 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 - '@oxc-project/types@0.139.0': - resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@oxc-project/types@0.143.0': + resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} @@ -4099,35 +4087,35 @@ packages: resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} engines: {node: '>=20.0.0'} - '@photo-sphere-viewer/core@5.15.0': - resolution: {integrity: sha512-Mf72l3R8iBf+Im5duv7IVkX3uiKU1BT6bzzXmdE43KahQgNftt9pYKqTzN1YjaYLAzWDBCFZj/bEK1dIZXk9Gg==} + '@photo-sphere-viewer/core@5.15.1': + resolution: {integrity: sha512-nhJviczUB1xiAT/I7mGba9iDIw7DvsiK34hbR9t7kIrg9MySG33uA+a4QUk2dEOP5PjcKMj58wod8I/dQ7ePiw==} - '@photo-sphere-viewer/equirectangular-video-adapter@5.15.0': - resolution: {integrity: sha512-Gro1wUA5qTla71uSeGlQFr9BenNXrI9Xee4a+dveafqrZJ35/n+nDhxrc8I4osykyvC6Foc0qgksAsaNsMKX/w==} + '@photo-sphere-viewer/equirectangular-video-adapter@5.15.1': + resolution: {integrity: sha512-510h1cDBAe+qoHVrR5IwUE/jnmzz0lA9mXHN7yzJDsrixPRh67gXEB3KIEc2Cgj/B1u1D2WK2W7OwhkQ40AX0A==} peerDependencies: - '@photo-sphere-viewer/core': 5.15.0 - '@photo-sphere-viewer/video-plugin': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 + '@photo-sphere-viewer/video-plugin': 5.15.1 - '@photo-sphere-viewer/markers-plugin@5.15.0': - resolution: {integrity: sha512-r1Kv4BkvcOf+77QU7k0pHzZ3O0IrX+ff7arttmb0D81HQHRl/fUcE+tXu+mGDj9B1zuSi1LcvKOsUI9m96owNw==} + '@photo-sphere-viewer/markers-plugin@5.15.1': + resolution: {integrity: sha512-57WiHy8fcQfwid9x07ADiQJGxzcFxclbR2yIqeggzfPDguwJlCKwX5tj8brTRqwouUiHpkb80QWxHByxt45LcA==} peerDependencies: - '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 - '@photo-sphere-viewer/resolution-plugin@5.15.0': - resolution: {integrity: sha512-8Rm9r9WJX5m8N0Q7ezUEHUgxWybroj6HhfaksINUm7o8WegqZ+Pp2izEEoXI57kkjfudYOp7abrwA/J3I+QRrA==} + '@photo-sphere-viewer/resolution-plugin@5.15.1': + resolution: {integrity: sha512-Sp14tgFv1XRkXbsFO+dWSgfwGjxB4ibsB+wXe64M4RI/NSDu+1Uzqtxj85Vbz7gOw3CaP1e8acXrd/IEg6CEHg==} peerDependencies: - '@photo-sphere-viewer/core': 5.15.0 - '@photo-sphere-viewer/settings-plugin': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 + '@photo-sphere-viewer/settings-plugin': 5.15.1 - '@photo-sphere-viewer/settings-plugin@5.15.0': - resolution: {integrity: sha512-xZot+fT5R+Jy7D/QEVoylIMg/R+l/iSBykQ1v8wqGkSrUqJdVkNv3w3ksycb9BYFt/78P8HU7AGvJgIF6J3EFQ==} + '@photo-sphere-viewer/settings-plugin@5.15.1': + resolution: {integrity: sha512-0FkCUUEsdTa4iLZu9nVLsjuh459hKKZeh+JiXfPzfrGLcLODLUwJugDjFHe/4KH7mmW9n7KbSUWBD8POvkUqqA==} peerDependencies: - '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 - '@photo-sphere-viewer/video-plugin@5.15.0': - resolution: {integrity: sha512-RG70vRu+suV7QT1WbnxVknm8KVU26wCZx4NUofFAENYSvyldlRxmVK3XqCh1WChWhcjrXqj3woz4vnzrykVOzQ==} + '@photo-sphere-viewer/video-plugin@5.15.1': + resolution: {integrity: sha512-jrZh2BGZA8BGzNj+gzDntfX8bd4kt0vIBsyAslSgPpkJUTRKxZ2nTIp2aP1OQ4EtpP3g4kbqrIAe6Avdn6pr0A==} peerDependencies: - '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 '@photostructure/tz-lookup@11.5.0': resolution: {integrity: sha512-0DVFriinZ7TeOnm9ytXeSL3NMFU87ZqMjgbPNkd8LgHFLcPg1BDyM1eewFYs+pPM+62S4fSP9Mtgijmn+6y95w==} @@ -4140,8 +4128,8 @@ packages: resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} engines: {node: ^14.18.0 || >=16.0.0} - '@playwright/test@1.62.0': - resolution: {integrity: sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==} + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} engines: {node: '>=20'} hasBin: true @@ -4380,97 +4368,92 @@ packages: '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 - '@rolldown/binding-android-arm64@1.1.5': - resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + '@rolldown/binding-android-arm64@1.2.3': + resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.1.5': - resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + '@rolldown/binding-darwin-arm64@1.2.3': + resolution: {integrity: sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.1.5': - resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + '@rolldown/binding-darwin-x64@1.2.3': + resolution: {integrity: sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.1.5': - resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + '@rolldown/binding-freebsd-x64@1.2.3': + resolution: {integrity: sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': + resolution: {integrity: sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.1.5': - resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + '@rolldown/binding-linux-arm64-gnu@1.2.3': + resolution: {integrity: sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.1.5': - resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + '@rolldown/binding-linux-arm64-musl@1.2.3': + resolution: {integrity: sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + '@rolldown/binding-linux-ppc64-gnu@1.2.3': + resolution: {integrity: sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.5': - resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + '@rolldown/binding-linux-s390x-gnu@1.2.3': + resolution: {integrity: sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.5': - resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + '@rolldown/binding-linux-x64-gnu@1.2.3': + resolution: {integrity: sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.1.5': - resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + '@rolldown/binding-linux-x64-musl@1.2.3': + resolution: {integrity: sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.1.5': - resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + '@rolldown/binding-openharmony-arm64@1.2.3': + resolution: {integrity: sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.1.5': - resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.1.5': - resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + '@rolldown/binding-win32-arm64-msvc@1.2.3': + resolution: {integrity: sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.5': - resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + '@rolldown/binding-win32-x64-msvc@1.2.3': + resolution: {integrity: sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -4675,8 +4658,8 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@sveltejs/acorn-typescript@1.0.11': - resolution: {integrity: sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==} + '@sveltejs/acorn-typescript@1.0.12': + resolution: {integrity: sha512-J1jNYG23QWd67UfrQSFHtjhV37r9mVi0gdc12A3MWPldOjRK35Xk+um+qACPVjgw3AleiqoyEAhok8Wm3q46NA==} peerDependencies: acorn: ^8.9.0 @@ -4692,8 +4675,8 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || >=7.0.0 - '@sveltejs/kit@2.70.1': - resolution: {integrity: sha512-nY9SPHGOZro3doud9vZXDBwl9tCZIouuJztjgSHs6PAIrv9M/z5O7eOhPV5xU7CgVHA976Jwu3BA1hIFvXztkA==} + '@sveltejs/kit@2.70.2': + resolution: {integrity: sha512-RzRoRpuR2KXqc5yMO0akQHDZeT4AslOlznGITURsqHaVbtyYP4Wn3eE3gxj9JcDyNYO0crkxhdwFHc+2vkVm6w==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -4708,8 +4691,8 @@ packages: typescript: optional: true - '@sveltejs/load-config@0.2.0': - resolution: {integrity: sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==} + '@sveltejs/load-config@0.2.2': + resolution: {integrity: sha512-K7dsJDQxBOF+f+epuhMactcjK2VP4MRkLKtwSykNtEI+cKVEyzrmmhQ1pmoxI800m4JKcyJ05L2M4yPwAGiBNw==} engines: {node: '>= 18.0.0'} '@sveltejs/vite-plugin-svelte@7.2.0': @@ -4797,86 +4780,86 @@ packages: resolution: {integrity: sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==} engines: {node: '>=14'} - '@swc/core-darwin-arm64@1.15.46': - resolution: {integrity: sha512-IsISIT22EfktVJrlvIpnAxG2u/A9aob9l99HMlx80x72WlFmFPk1V3UhkEzx86eJP8hw049KTFv/RISho2cq2Q==} + '@swc/core-darwin-arm64@1.15.47': + resolution: {integrity: sha512-GsoMtan3ojGGMGFbl31mmRu5ctZ56re8grGE8mO/OHJ8O+JRkzod02fe7X6ZQ8JvamA3imkEkx/h3u+vsOgPgA==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.15.46': - resolution: {integrity: sha512-4Tj4ppVIPCmUMpmGFiGtyEriwLyJ+yi/US4WfBrP/ok8COGddDZXLEzQETnKyK46mjvr1v0jevrS23zjoff7vA==} + '@swc/core-darwin-x64@1.15.47': + resolution: {integrity: sha512-leTi7Rx3KF4zcC637iqWgk9SoV8VXAD8ppQYXsep63px5A/UftOcxLN1pmr8Z1si/YvX90ompP/rHgpYkgwXWg==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.15.46': - resolution: {integrity: sha512-i8tUGnNjyOgMmfmgFSg4aeJLQoFyfpIHK5FjpQAwpRyQIqEUB2w1e8zIDQzY1WhOxx8NoS1S5iUL813Un4Sf5A==} + '@swc/core-linux-arm-gnueabihf@1.15.47': + resolution: {integrity: sha512-hBqHuoWKKIsKmDBn9qVeWqj5GWZhtlcczVaqQmNRXsDfq+voR5CxKRfamA367QjJXtceYuliLFfEL8QsskRM2g==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.15.46': - resolution: {integrity: sha512-c0OnhqzdhfOvv6qhNCcByepB+sNYOGZyhtr2Qa6ZCHvAWTYhSRw4j/u92Stue9PbZ/6q74b9nHzi76+kVzqQHQ==} + '@swc/core-linux-arm64-gnu@1.15.47': + resolution: {integrity: sha512-TBxvRz+B4K205TWHHZxWVxkC2RFNP/Mz3PNcECBos5PsKwxjg3QSJzdoebr0VCf0Bfh8HOPldKxAP/8XkFe9gA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] libc: [glibc] - '@swc/core-linux-arm64-musl@1.15.46': - resolution: {integrity: sha512-imyRpNEcUzFQFV2LE4jL68ErvmKEuZCbvZru77iQREunJ+bR4i658cupTgtG1mLYM3F1Tzy3Sb9xYb02KghWTg==} + '@swc/core-linux-arm64-musl@1.15.47': + resolution: {integrity: sha512-3Yu3Uq/VgytqsPjTMbkPU1ExADytbdWbruJYhA584E9jrpE2Ki+R6VVPoZCeAVk1Cb7QxcRTgblw6bSa6a/R+w==} engines: {node: '>=10'} cpu: [arm64] os: [linux] libc: [musl] - '@swc/core-linux-ppc64-gnu@1.15.46': - resolution: {integrity: sha512-ctEfcl/HcUeomK33cbySiHZm98GEDIxTm1EkpBsYCiHxElYBzvTXVeuQT2YwbUXn9XCrjiw4ipyUNk33k26qRg==} + '@swc/core-linux-ppc64-gnu@1.15.47': + resolution: {integrity: sha512-wfdMi5IaOaNtmh2/6geRoxIdNfqylUZFdtzTKS655y1axWfIWyx7As74vv0wVdjeCIZ3WmCI9odDd4rUttXOSQ==} engines: {node: '>=10'} cpu: [ppc64] os: [linux] libc: [glibc] - '@swc/core-linux-s390x-gnu@1.15.46': - resolution: {integrity: sha512-DxlMdnt84TtRVTv7WL/thWyz9+QU8QZNNoAP9rrk0P68LziuhfePp8MjQ44zIprpTHTsEwyziIuGUUN5iSC1bQ==} + '@swc/core-linux-s390x-gnu@1.15.47': + resolution: {integrity: sha512-3hHYBY0yx8Ez7GMRrkhXHQzMdR5IZA6Wq5Ee4svlgwvSECLpnAJ9+0AimEGUFDvuLwE7nV/2+PYe8+Nm4rvNcQ==} engines: {node: '>=10'} cpu: [s390x] os: [linux] libc: [glibc] - '@swc/core-linux-x64-gnu@1.15.46': - resolution: {integrity: sha512-SKxI7J6t90XPl8hRUqtJi9NfGdunN/E/vZMc7Bc0figeRdOPDBT+Tm8g7cx9xM0T0mewh2l+8dewa3Am27/P+A==} + '@swc/core-linux-x64-gnu@1.15.47': + resolution: {integrity: sha512-TjfhjgP/jGCfFHYC3JQPhJA1HwErbIJ9JfREDc1KNkvY6P0LodCgKVIlQ5deeTbkG7ih3bF5PHJLuLpaZjdRyQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] libc: [glibc] - '@swc/core-linux-x64-musl@1.15.46': - resolution: {integrity: sha512-qj9T6B7bosI0VEsrWOVXZN1OXxS8Tp63ywyrLxNdOycnUtLdkgYcoBsN5y8ImnDDsnwrEWZOy1e+J4xSe7mA3Q==} + '@swc/core-linux-x64-musl@1.15.47': + resolution: {integrity: sha512-CQpS8Ge/avfjZd0UEwG/sds83Uu32deQXcV1Jo3jD0mmvQQqtYAjpsDZXugmheeAwmt+YIuoVtVHro8LMYHqsQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] libc: [musl] - '@swc/core-win32-arm64-msvc@1.15.46': - resolution: {integrity: sha512-8p7l4c3LU+eA5g9Et1JPhNeMC1oQwXTGU+uah8DPIBX7YXzqswvaBtyKVmXefVGi/DJU1x3YJsc3mbAp9aWzSQ==} + '@swc/core-win32-arm64-msvc@1.15.47': + resolution: {integrity: sha512-0W8IKHsUTYiT7G2RqtOoVWk+89yzZikIiDUb/sCK6BmQDBhN91hQSfyUtW12jhEWLzYgcfmisfsZrmZE+84U1A==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.15.46': - resolution: {integrity: sha512-tUEnfr3Bn9u6FOjUb3PN9p+09qZC2j+wNDLKHzXXZn22rqGcUqR/ohCRSS+nG9B9+X+U+3FewNEHJkTmdIvMjQ==} + '@swc/core-win32-ia32-msvc@1.15.47': + resolution: {integrity: sha512-ZIp49d2Z4/ka2jO9otOg4hDvTdPmp86kVOgS2M5FCPI7eKKZ1W0boxWn+8XeZrfERtFGW0AlMRm4JhlJa7l3NA==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.15.46': - resolution: {integrity: sha512-Vux7UDzBJYQggSuPfcl2w9iu+IJpgpRCxHzgCaVkELnAXAE4XZMOTX9HNcaNiwfeIDqdu2rkr69RuDm6wY8neA==} + '@swc/core-win32-x64-msvc@1.15.47': + resolution: {integrity: sha512-2h8Iek95vnixkBRCo+H8p09+Q5ll2NgSMFrWTy0iKt7+/t+8/T5mBpiT6c0ZxSS7wcWjwZ9sGZkK70tTSYHdDw==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.15.46': - resolution: {integrity: sha512-Ri3em2mBpq3h2zSPliCYl63otDGqek8PPEfv2nWgRQEbZ/VBCNyypVTVQ6cEbTCXBhy+WE2T3fQb08moIyuYaw==} + '@swc/core@1.15.47': + resolution: {integrity: sha512-FbsO5JcfOjfH38W/rohBRBweJeERsAuIP4f377lmkmxTcq9exjtx4SkRuZY5CdfhR2CBVwDIJegBpJDffwNsOg==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -5020,8 +5003,8 @@ packages: vitest: optional: true - '@testing-library/user-event@14.6.1': - resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + '@testing-library/user-event@14.6.3': + resolution: {integrity: sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==} engines: {node: '>=12', npm: '>=6'} peerDependencies: '@testing-library/dom': '>=7.21.4' @@ -5061,9 +5044,6 @@ packages: '@turf/invariant@7.3.5': resolution: {integrity: sha512-ZVIvsBvjr8lO7WxC5zYNjRsjSDvyGvWkJMjuWaJjTU8x+1tmfNnw3gDX/TI2Sit83gcRYLYkNo23lB/udqx/Hg==} - '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - '@types/accepts@1.3.7': resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} @@ -5345,11 +5325,11 @@ packages: '@types/lodash-es@4.17.12': resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} - '@types/lodash@4.17.24': - resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + '@types/lodash@4.17.25': + resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} - '@types/luxon@3.7.2': - resolution: {integrity: sha512-gW+Oib+vUtGJBtNC8V9Reww0oIpusw+4m81uncg9REGZAJfqOQHfo/nkabnc7w0QReXyPqjrbWMJk6NuAkiX3Q==} + '@types/luxon@3.7.3': + resolution: {integrity: sha512-pE7BSbKHiojpl4v7iEzdfFLXXJaH5RPJxI3Wr4x3HU59l83PJfhcUx4mGPWq245+DCAENAzRF/+ZoYr2tJCe/g==} '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -5387,8 +5367,8 @@ packages: '@types/nodemailer@8.0.1': resolution: {integrity: sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==} - '@types/oidc-provider@9.5.0': - resolution: {integrity: sha512-eEzCRVTSqIHD9Bo/qRJ4XQWQ5Z/zBcG+Z2cGJluRsSuWx1RJihqRyPxhIEpMXTwPzHYRTQkVp7hwisQOwzzSAg==} + '@types/oidc-provider@9.11.0': + resolution: {integrity: sha512-wZMEmCGh8WGKYSUpxJgpPLIKTe4972OJUlWXaNzy5+vvnU097i8ijzApgHLihdg56cQ+PV+USYYcnh3W8kVISA==} '@types/parse5@5.0.3': resolution: {integrity: sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==} @@ -5399,8 +5379,8 @@ packages: '@types/pg@8.15.6': resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} - '@types/pg@8.20.0': - resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/pg@8.20.4': + resolution: {integrity: sha512-Jz7UDOlIiFJuacC0TlBoLyNtmwlA/wpIyPDd3tvUqlRM+HzkWy2xUgpFpaXtbfTAFF6sIGq5lsCDBdJnhky1Xg==} '@types/picomatch@4.0.3': resolution: {integrity: sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ==} @@ -5429,8 +5409,8 @@ packages: '@types/react-router@5.1.20': resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} '@types/readdir-glob@1.1.5': resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} @@ -5441,8 +5421,8 @@ packages: '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} - '@types/semver@7.7.1': - resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/semver@7.8.0': + resolution: {integrity: sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==} '@types/send@0.17.6': resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} @@ -5507,63 +5487,63 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.65.0': - resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} + '@typescript-eslint/eslint-plugin@8.66.0': + resolution: {integrity: sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.65.0 + '@typescript-eslint/parser': ^8.66.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.65.0': - resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} + '@typescript-eslint/parser@8.66.0': + resolution: {integrity: sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.65.0': - resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} + '@typescript-eslint/project-service@8.66.0': + resolution: {integrity: sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.65.0': - resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} + '@typescript-eslint/scope-manager@8.66.0': + resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.65.0': - resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} + '@typescript-eslint/tsconfig-utils@8.66.0': + resolution: {integrity: sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.65.0': - resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} + '@typescript-eslint/type-utils@8.66.0': + resolution: {integrity: sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.65.0': - resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} + '@typescript-eslint/types@8.66.0': + resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.65.0': - resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} + '@typescript-eslint/typescript-estree@8.66.0': + resolution: {integrity: sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.65.0': - resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} + '@typescript-eslint/utils@8.66.0': + resolution: {integrity: sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.65.0': - resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} + '@typescript-eslint/visitor-keys@8.66.0': + resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript/typescript-aix-ppc64@7.0.2': @@ -6139,9 +6119,9 @@ packages: bare-abort-controller: optional: true - bare-fs@4.7.4: - resolution: {integrity: sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==} - engines: {bare: '>=1.16.0'} + bare-fs@4.8.0: + resolution: {integrity: sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==} + engines: {bare: '>=1.28.0'} peerDependencies: bare-buffer: '*' peerDependenciesMeta: @@ -6165,8 +6145,8 @@ packages: bare-events: optional: true - bare-url@2.4.5: - resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==} + bare-url@2.5.1: + resolution: {integrity: sha512-cD5ciQuKlx+eumTCfqbfiL+fhQm+dHbVNB/cX4+d+I/nx4JsVop9VoFEPAs5RJ6I84QR6bZIBjpd2kjDOYeWcg==} base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -6283,8 +6263,8 @@ packages: resolution: {integrity: sha512-02yxLeyxF4dNl6SlY6/5HfRSrSdZ/sCPoxy2kZNP5dZZX8LSAD9aE2gtJIUgWrsQTiMPl3mxESyrobSwvRGisQ==} engines: {node: '>=18.20'} - bullmq@5.81.2: - resolution: {integrity: sha512-Hi9GaVCC6HE9bQP65j/FNv1aL1fcEukTF99ezS5pl1Ud+joCFpNWiPCV45mWdkEmpuacS0XJdMMFGKPIHCwoPg==} + bullmq@5.81.3: + resolution: {integrity: sha512-Q7uEH2G92rVjX3Yl2qw3+6VEO15uxehC6DfeTHQBQTehddcRDSDuquD9zuvDjxiCffXsPUaBwrRlvlbVcgKs7g==} engines: {node: '>=12.22.0'} peerDependencies: redis: '>=5.0.0' @@ -7268,8 +7248,8 @@ packages: engines: {node: '>= 16.0.0'} hasBin: true - devalue@5.8.2: - resolution: {integrity: sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==} + devalue@5.9.0: + resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -7525,8 +7505,8 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.28.1: - resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} engines: {node: '>=18'} hasBin: true @@ -8129,8 +8109,8 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} - globals@17.7.0: - resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} + globals@17.9.0: + resolution: {integrity: sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==} engines: {node: '>=18'} globalyzer@0.1.0: @@ -8278,8 +8258,8 @@ packages: hls-video-element@1.5.11: resolution: {integrity: sha512-tJJ65/52CDxj8XFyIve6zT9nVVdUIc6mqvKR25X0ycPKHk07rpjp4xxVteeCefDUBSf/tFLhlICFmn3KWj37xA==} - hls.js@1.6.16: - resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} + hls.js@1.6.17: + resolution: {integrity: sha512-NUplVGVuc1hSPwdB/9/cbRkUmLrYi75/hqiXKdA+l300pJNxDu96R7jRb2imDzWJqIUF4I5ThmAdp9GvOCXsuQ==} hogan.js@3.0.2: resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==} @@ -8515,8 +8495,8 @@ packages: intl-messageformat@10.7.18: resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} - intl-messageformat@11.2.12: - resolution: {integrity: sha512-KW70Xxfcvy7vV3qODfvShWkFDPMqKDAa4N+hSyVBWGNtVhTUFYaqlD/l88DaYPKiVcPP4rPQ3qnH7i5K82Mg7g==} + intl-messageformat@11.2.13: + resolution: {integrity: sha512-JaPaE6TIX+TAS5XLhDUh41geLw4QfBHX4s5pW8Km+L9fVC8HzB9yOuhbh4EMR/F1+8C6b9qk4763Cv+LdOG1kg==} invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -8779,8 +8759,8 @@ packages: joi@17.13.4: resolution: {integrity: sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==} - jose@6.2.4: - resolution: {integrity: sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==} + jose@6.2.8: + resolution: {integrity: sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==} js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -8799,8 +8779,8 @@ packages: resolution: {integrity: sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==} hasBin: true - js-yaml@5.2.2: - resolution: {integrity: sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==} + js-yaml@5.2.3: + resolution: {integrity: sha512-n+mUVyUX5bVv7G/G2zyIHOhdxfuU1dY2NOFzTQUWiMUbFss8b57NFlgCCaggU78wSw5KVS9cllzeLyzyR+n5nw==} hasBin: true jsdom@26.1.0: @@ -9732,8 +9712,8 @@ packages: resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} hasBin: true - msgpackr@2.0.4: - resolution: {integrity: sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==} + msgpackr@2.0.5: + resolution: {integrity: sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA==} multer@2.2.0: resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} @@ -9763,13 +9743,13 @@ packages: nan@2.28.0: resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==} - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@6.0.0: - resolution: {integrity: sha512-mkUH+rPkwU2qPadJ0oJZOjeZ5Mxn8Q1UhevwkTRWNuUZzyia3h4rhzK39hxaHTk0o2OxB8W2SQ6A8k23ZDi1pQ==} + nanoid@6.0.1: + resolution: {integrity: sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw==} engines: {node: ^22 || ^24 || >=26} hasBin: true @@ -9900,8 +9880,8 @@ packages: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} - nodemailer@9.0.3: - resolution: {integrity: sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==} + nodemailer@9.0.4: + resolution: {integrity: sha512-LmJNRVRtfSCULxcZpy0Cpg4WWenlUZ9+zbmTO+S7v9wD6XreYLjXRFtDjtV/4F0HT5p1GyZfA0Ux/myxHb18CQ==} engines: {node: '>=6.0.0'} nopt@1.0.10: @@ -9993,8 +9973,8 @@ packages: resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} engines: {node: '>=12.20.0'} - oidc-provider@9.10.0: - resolution: {integrity: sha512-Olmg6oxgHIviZnrf9yaey6vdaWG1f19UiyvpSj0gCgj6wl9YMcOw3uDv3FHj+x62EXeFj7GfZeTL09MCgIEKnw==} + oidc-provider@9.11.2: + resolution: {integrity: sha512-Z7CZzWtBfcNMSaIoNJwHmEbla6h4mW73soOAJM4Qo9XDALMgQ1J+amMg/FBZJD1BHoec1KLL5BHFDkvlgeUZqw==} on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -10247,8 +10227,8 @@ packages: peerDependencies: pg: '>=8.0' - pg-protocol@1.15.0: - resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + pg-protocol@1.16.0: + resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} @@ -10297,13 +10277,13 @@ packages: resolution: {integrity: sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==} engines: {node: '>=16.0.0'} - playwright-core@1.62.0: - resolution: {integrity: sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==} + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} engines: {node: '>=20'} hasBin: true - playwright@1.62.0: - resolution: {integrity: sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==} + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} engines: {node: '>=20'} hasBin: true @@ -10785,8 +10765,8 @@ packages: peerDependencies: postcss: ^8.4.31 - postcss@8.5.25: - resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -11282,8 +11262,8 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rolldown@1.1.5: - resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + rolldown@1.2.3: + resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -11520,8 +11500,8 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - simple-icons@16.27.1: - resolution: {integrity: sha512-slZF8iKxkv7Lb9SF3L1AcIl5iYLNvDrKKm8yV69cG66XvxJ12SX+bYTgxRwCY4bXSmklGXoXaKEyVmEypOCeqw==} + simple-icons@16.28.0: + resolution: {integrity: sha512-sQPR5AtK/ijRjou7zw7mlLp08oB6FH7i0lOy5XJ2zp9mJs/yejgiOn7KvQoe2q4YJIx6VmgUSW5AOefebPt5kg==} engines: {node: '>=0.12.18'} sirv@2.0.4: @@ -11794,13 +11774,13 @@ packages: peerDependencies: svelte: '>= 3.43.1 < 6' - svelte-check@4.7.3: - resolution: {integrity: sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==} + svelte-check@4.7.4: + resolution: {integrity: sha512-IW9ot9YqAoyv8FvyN+eb4ZTe8zgcKZrJLNYU6dzSKkGwEBsSPc4K7lmQ8bKn8W2YMXM6WDfZSSVOaGtekyUfOQ==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: '>=5.0.0' + typescript: ^5.0.0 || ^6.0.0 svelte-eslint-parser@1.8.0: resolution: {integrity: sha512-mikR1qwIVy3t5WthUoAXkMwxkXvabZP9FJgdx35Ei7EbGWmctva1Pih16Koeor/bdNNq8NXHlwKGS6NkYTawLg==} @@ -11921,15 +11901,17 @@ packages: tailwind-merge@3.6.0: resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} - tailwind-variants@3.2.2: - resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==} - engines: {node: '>=16.x', pnpm: '>=7.x'} + tailwind-variants@3.3.1: + resolution: {integrity: sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw==} + engines: {node: '>=16.9.x', pnpm: '>=7.x'} peerDependencies: tailwind-merge: '>=3.0.0' tailwindcss: '*' peerDependenciesMeta: tailwind-merge: optional: true + tailwindcss: + optional: true tailwindcss-email-variants@3.0.5: resolution: {integrity: sha512-BbULkY0/0VLXwSzAZP0wxyO0siXJARatreZTSW+U6dM+LuTzPsfG/SfxxuouINM1xtIoPGGU++jKkMyfYKJe7Q==} @@ -12033,8 +12015,9 @@ packages: engines: {node: '>=10'} hasBin: true - testcontainers@12.0.4: - resolution: {integrity: sha512-QIR/8xF1+F/26cIM+9B4yyxNTbKJxAv3hygZyhPRgZ8Q2AhlPZjDdpXRuk16V37X4bgJRI3hXFhoEICMBA7Adg==} + testcontainers@12.1.0: + resolution: {integrity: sha512-YjDLqIITuhGLMnM10yhg3oV6lIG5IMpz1R1DPBZoOOks83q7i7IVpeSWRTiyl7roozjiyLmwIoLK/KY8OnZmIA==} + engines: {node: '>= 22.22'} text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} @@ -12227,8 +12210,8 @@ packages: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} engines: {node: '>=0.6.x'} - tsx@4.23.1: - resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} + tsx@4.23.9: + resolution: {integrity: sha512-6q8uTORRGauQVjqMQnKUucLFoeXZAfw6zKvG35GLbdKWbLdeOtZ3H4mhyA5mxuUd2o2cRTskhj59nLLQseUvUw==} engines: {node: '>=18.0.0'} hasBin: true @@ -12283,8 +12266,8 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@8.65.0: - resolution: {integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==} + typescript-eslint@8.66.0: + resolution: {integrity: sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -12339,8 +12322,8 @@ packages: resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==} engines: {node: '>=18.17'} - undici@8.7.0: - resolution: {integrity: sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ==} + undici@8.10.0: + resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} engines: {node: '>=22.19.0'} unicode-canonical-property-names-ecmascript@2.0.1: @@ -12414,8 +12397,8 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unplugin-swc@1.5.9: - resolution: {integrity: sha512-RKwK3yf0M+MN17xZfF14bdKqfx0zMXYdtOdxLiE6jHAoidupKq3jGdJYANyIM1X/VmABhh1WpdO+/f4+Ol89+g==} + unplugin-swc@1.5.10: + resolution: {integrity: sha512-aNvKH601KYzc/fXyDQJPAk8obNPzA4GdGqx+qSjQoyweMLDMLs5VRbunjL4skIv/hyyAnSgrbxMwXHdRTzhmew==} peerDependencies: '@swc/core': ^1.2.108 @@ -12580,13 +12563,13 @@ packages: yaml: optional: true - vite@8.1.5: - resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + vite@8.2.0: + resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 + '@vitejs/devtools': ^0.4.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -14137,272 +14120,272 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-alpha-function@1.0.1(postcss@8.5.25)': + '@csstools/postcss-alpha-function@1.0.1(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.25)': + '@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.26)': dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - '@csstools/postcss-color-function-display-p3-linear@1.0.1(postcss@8.5.25)': + '@csstools/postcss-color-function-display-p3-linear@1.0.1(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-color-function@4.0.12(postcss@8.5.25)': + '@csstools/postcss-color-function@4.0.12(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-color-mix-function@3.0.12(postcss@8.5.25)': + '@csstools/postcss-color-mix-function@3.0.12(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-color-mix-variadic-function-arguments@1.0.2(postcss@8.5.25)': + '@csstools/postcss-color-mix-variadic-function-arguments@1.0.2(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-content-alt-text@2.0.8(postcss@8.5.25)': + '@csstools/postcss-content-alt-text@2.0.8(postcss@8.5.26)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-contrast-color-function@2.0.12(postcss@8.5.25)': + '@csstools/postcss-contrast-color-function@2.0.12(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-exponential-functions@2.0.9(postcss@8.5.25)': + '@csstools/postcss-exponential-functions@2.0.9(postcss@8.5.26)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.25)': + '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.26)': dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - '@csstools/postcss-gamut-mapping@2.0.11(postcss@8.5.25)': + '@csstools/postcss-gamut-mapping@2.0.11(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-gradients-interpolation-method@5.0.12(postcss@8.5.25)': + '@csstools/postcss-gradients-interpolation-method@5.0.12(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-hwb-function@4.0.12(postcss@8.5.25)': + '@csstools/postcss-hwb-function@4.0.12(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-ic-unit@4.0.4(postcss@8.5.25)': + '@csstools/postcss-ic-unit@4.0.4(postcss@8.5.26)': dependencies: - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - '@csstools/postcss-initial@2.0.1(postcss@8.5.25)': + '@csstools/postcss-initial@2.0.1(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-is-pseudo-class@5.0.3(postcss@8.5.25)': + '@csstools/postcss-is-pseudo-class@5.0.3(postcss@8.5.26)': dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - '@csstools/postcss-light-dark-function@2.0.11(postcss@8.5.25)': + '@csstools/postcss-light-dark-function@2.0.11(postcss@8.5.26)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-logical-float-and-clear@3.0.0(postcss@8.5.25)': + '@csstools/postcss-logical-float-and-clear@3.0.0(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.25)': + '@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.25)': + '@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-logical-resize@3.0.0(postcss@8.5.25)': + '@csstools/postcss-logical-resize@3.0.0(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - '@csstools/postcss-logical-viewport-units@3.0.4(postcss@8.5.25)': + '@csstools/postcss-logical-viewport-units@3.0.4(postcss@8.5.26)': dependencies: '@csstools/css-tokenizer': 3.0.4 - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-media-minmax@2.0.9(postcss@8.5.25)': + '@csstools/postcss-media-minmax@2.0.9(postcss@8.5.26)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5(postcss@8.5.25)': + '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5(postcss@8.5.26)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-nested-calc@4.0.0(postcss@8.5.25)': + '@csstools/postcss-nested-calc@4.0.0(postcss@8.5.26)': dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - '@csstools/postcss-normalize-display-values@4.0.1(postcss@8.5.25)': + '@csstools/postcss-normalize-display-values@4.0.1(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - '@csstools/postcss-oklab-function@4.0.12(postcss@8.5.25)': + '@csstools/postcss-oklab-function@4.0.12(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-position-area-property@1.0.0(postcss@8.5.25)': + '@csstools/postcss-position-area-property@1.0.0(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-progressive-custom-properties@4.2.1(postcss@8.5.25)': + '@csstools/postcss-progressive-custom-properties@4.2.1(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - '@csstools/postcss-property-rule-prelude-list@1.0.0(postcss@8.5.25)': + '@csstools/postcss-property-rule-prelude-list@1.0.0(postcss@8.5.26)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-random-function@2.0.1(postcss@8.5.25)': + '@csstools/postcss-random-function@2.0.1(postcss@8.5.26)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-relative-color-syntax@3.0.12(postcss@8.5.25)': + '@csstools/postcss-relative-color-syntax@3.0.12(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.5.25)': + '@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - '@csstools/postcss-sign-functions@1.1.4(postcss@8.5.25)': + '@csstools/postcss-sign-functions@1.1.4(postcss@8.5.26)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-stepped-value-functions@4.0.9(postcss@8.5.25)': + '@csstools/postcss-stepped-value-functions@4.0.9(postcss@8.5.26)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-syntax-descriptor-syntax-production@1.0.1(postcss@8.5.25)': + '@csstools/postcss-syntax-descriptor-syntax-production@1.0.1(postcss@8.5.26)': dependencies: '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-system-ui-font-family@1.0.0(postcss@8.5.25)': + '@csstools/postcss-system-ui-font-family@1.0.0(postcss@8.5.26)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-text-decoration-shorthand@4.0.3(postcss@8.5.25)': + '@csstools/postcss-text-decoration-shorthand@4.0.3(postcss@8.5.26)': dependencies: '@csstools/color-helpers': 5.1.0 - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - '@csstools/postcss-trigonometric-functions@4.0.9(postcss@8.5.25)': + '@csstools/postcss-trigonometric-functions@4.0.9(postcss@8.5.26)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-unset-value@4.0.0(postcss@8.5.25)': + '@csstools/postcss-unset-value@4.0.0(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 '@csstools/selector-resolve-nested@3.1.0(postcss-selector-parser@7.1.4)': dependencies: @@ -14412,27 +14395,27 @@ snapshots: dependencies: postcss-selector-parser: 7.1.4 - '@csstools/utilities@2.0.0(postcss@8.5.25)': + '@csstools/utilities@2.0.0(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 '@discoveryjs/json-ext@0.5.7': {} - '@docsearch/core@4.6.3(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@docsearch/core@4.6.3(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) '@docsearch/css@4.6.3': {} - '@docsearch/react@4.6.3(@algolia/client-search@5.56.0)(@types/react@19.2.17)(algoliasearch@5.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)': + '@docsearch/react@4.6.3(@algolia/client-search@5.56.0)(@types/react@19.2.18)(algoliasearch@5.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)': dependencies: '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.56.0)(algoliasearch@5.56.0)(search-insights@2.17.3) - '@docsearch/core': 4.6.3(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@docsearch/core': 4.6.3(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@docsearch/css': 4.6.3 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) search-insights: 2.17.3 @@ -14440,7 +14423,7 @@ snapshots: - '@algolia/client-search' - algoliasearch - '@docusaurus/babel@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/babel@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/generator': 7.29.7 @@ -14452,7 +14435,7 @@ snapshots: '@babel/runtime': 7.29.7 '@babel/traverse': 7.29.7(supports-color@8.1.1) '@docusaurus/logger': 3.10.2 - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) babel-plugin-dynamic-import-node: 2.3.3 fs-extra: 11.3.6 tslib: 2.8.1 @@ -14474,32 +14457,32 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/bundler@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/bundler@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) - '@docusaurus/babel': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/babel': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/cssnano-preset': 3.10.2 '@docusaurus/logger': 3.10.2 - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - babel-loader: 9.2.1(@babel/core@7.29.7(supports-color@8.1.1))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + babel-loader: 9.2.1(@babel/core@7.29.7(supports-color@8.1.1))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) clean-css: 5.3.3 - copy-webpack-plugin: 11.0.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) - css-loader: 6.11.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) - css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) - cssnano: 6.1.2(postcss@8.5.25) - file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + copy-webpack-plugin: 11.0.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) + css-loader: 6.11.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) + cssnano: 6.1.2(postcss@8.5.26) + file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) html-minifier-terser: 7.2.0 - mini-css-extract-plugin: 2.10.2(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) - null-loader: 4.0.1(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) - postcss: 8.5.25 - postcss-loader: 7.3.4(@typescript/typescript6@6.0.2)(postcss@8.5.25)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) - postcss-preset-env: 10.6.1(postcss@8.5.25) - terser-webpack-plugin: 5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + mini-css-extract-plugin: 2.10.2(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) + null-loader: 4.0.1(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) + postcss: 8.5.26 + postcss-loader: 7.3.4(@typescript/typescript6@6.0.2)(postcss@8.5.26)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) + postcss-preset-env: 10.6.1(postcss@8.5.26) + terser-webpack-plugin: 5.6.1(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) - webpackbar: 7.0.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) + webpackbar: 7.0.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) transitivePeerDependencies: - '@minify-html/node' - '@parcel/css' @@ -14517,16 +14500,16 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/babel': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/bundler': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/babel': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/bundler': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.8) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@mdx-js/react': 3.1.1(@types/react@19.2.18)(react@19.2.8) boxen: 6.2.1 chalk: 4.1.2 chokidar: 3.6.0 @@ -14541,7 +14524,7 @@ snapshots: execa: 5.1.1 fs-extra: 11.3.6 html-tags: 3.3.1 - html-webpack-plugin: 5.6.7(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + html-webpack-plugin: 5.6.7(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) leven: 3.1.0 lodash: 4.18.1 open: 8.4.2 @@ -14551,7 +14534,7 @@ snapshots: react-dom: 19.2.8(react@19.2.8) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.8)' - react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.8))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.8))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) react-router: 5.3.4(react@19.2.8) react-router-config: 5.1.1(react-router@5.3.4(react@19.2.8))(react@19.2.8) react-router-dom: 5.3.4(react@19.2.8) @@ -14560,9 +14543,9 @@ snapshots: tinypool: 1.1.1 tslib: 2.8.1 update-notifier: 6.0.2 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) webpack-bundle-analyzer: 4.10.2 - webpack-dev-server: 5.2.6(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + webpack-dev-server: 5.2.6(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) webpack-merge: 6.0.1 transitivePeerDependencies: - '@minify-html/node' @@ -14588,9 +14571,9 @@ snapshots: '@docusaurus/cssnano-preset@3.10.2': dependencies: - cssnano-preset-advanced: 6.1.2(postcss@8.5.25) - postcss: 8.5.25 - postcss-sort-media-queries: 5.2.0(postcss@8.5.25) + cssnano-preset-advanced: 6.1.2(postcss@8.5.26) + postcss: 8.5.26 + postcss-sort-media-queries: 5.2.0(postcss@8.5.26) tslib: 2.8.1 '@docusaurus/logger@3.10.2': @@ -14598,16 +14581,16 @@ snapshots: chalk: 4.1.2 tslib: 2.8.1 - '@docusaurus/mdx-loader@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/mdx-loader@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@docusaurus/logger': 3.10.2 - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@mdx-js/mdx': 3.1.1(supports-color@8.1.1) '@slorber/remark-comment': 1.0.0 escape-html: 1.0.3 estree-util-value-to-estree: 3.5.0 - file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) fs-extra: 11.3.6 image-size: 2.0.2 mdast-util-mdx: 3.0.0(supports-color@8.1.1) @@ -14623,9 +14606,9 @@ snapshots: tslib: 2.8.1 unified: 11.0.5 unist-util-visit: 5.1.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) vfile: 6.0.3 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - '@minify-html/node' - '@swc/core' @@ -14642,11 +14625,11 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/module-type-aliases@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/module-type-aliases@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@types/history': 4.7.11 - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@types/react-router-config': 5.0.11 '@types/react-router-dom': 5.3.3 react: 19.2.8 @@ -14669,17 +14652,17 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/plugin-content-blog@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-content-blog@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) cheerio: 1.0.0-rc.12 combine-promises: 1.2.0 feed: 4.2.2 @@ -14692,7 +14675,7 @@ snapshots: tslib: 2.8.1 unist-util-visit: 5.1.0 utility-types: 3.11.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14717,17 +14700,17 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@types/react-router-config': 5.0.11 combine-promises: 1.2.0 fs-extra: 11.3.6 @@ -14738,7 +14721,7 @@ snapshots: schema-dts: 1.1.5 tslib: 2.8.1 utility-types: 3.11.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14763,18 +14746,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-pages@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-content-pages@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) fs-extra: 11.3.6 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14799,12 +14782,12 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-css-cascade-layers@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-css-cascade-layers@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -14832,11 +14815,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-debug@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-debug@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) fs-extra: 11.3.6 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -14866,11 +14849,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-analytics@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-google-analytics@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 @@ -14898,11 +14881,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-gtag@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-google-gtag@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 @@ -14930,11 +14913,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-tag-manager@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-google-tag-manager@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 @@ -14962,14 +14945,14 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-sitemap@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-sitemap@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) fs-extra: 11.3.6 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -14999,18 +14982,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-svgr@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-svgr@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) '@svgr/webpack': 8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -15035,23 +15018,23 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/preset-classic@3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/preset-classic@3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-blog': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-pages': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-css-cascade-layers': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-debug': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-google-analytics': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-google-gtag': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-google-tag-manager': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-sitemap': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-svgr': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-classic': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-search-algolia': 3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-blog': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-pages': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-css-cascade-layers': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-debug': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-google-analytics': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-google-gtag': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-google-tag-manager': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-sitemap': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-svgr': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-classic': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-search-algolia': 3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) transitivePeerDependencies: @@ -15083,31 +15066,31 @@ snapshots: '@docusaurus/react-loadable@6.0.0(react@19.2.8)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 react: 19.2.8 - '@docusaurus/theme-classic@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/theme-classic@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-blog': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-pages': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-blog': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-pages': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/theme-translations': 3.10.2 - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.8) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@mdx-js/react': 3.1.1(@types/react@19.2.18)(react@19.2.8) clsx: 2.1.1 copy-text-to-clipboard: 3.2.2 infima: 0.2.0-alpha.45 lodash: 4.18.1 nprogress: 0.2.0 - postcss: 8.5.25 + postcss: 8.5.26 prism-react-renderer: 2.4.1(react@19.2.8) prismjs: 1.30.0 react: 19.2.8 @@ -15139,15 +15122,15 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/theme-common@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@types/history': 4.7.11 - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@types/react-router-config': 5.0.11 clsx: 2.1.1 parse-numeric-range: 1.3.0 @@ -15172,13 +15155,13 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/theme-mermaid@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/theme-mermaid@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) mermaid: 11.16.0 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -15208,17 +15191,17 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-search-algolia@3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/theme-search-algolia@3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@algolia/autocomplete-core': 1.19.9(@algolia/client-search@5.56.0)(algoliasearch@5.56.0)(search-insights@2.17.3) - '@docsearch/react': 4.6.3(@algolia/client-search@5.56.0)(@types/react@19.2.17)(algoliasearch@5.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3) - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docsearch/react': 4.6.3(@algolia/client-search@5.56.0)(@types/react@19.2.18)(algoliasearch@5.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/theme-translations': 3.10.2 - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) algoliasearch: 5.56.0 algoliasearch-helper: 3.29.2(algoliasearch@5.56.0) clsx: 2.1.1 @@ -15263,19 +15246,19 @@ snapshots: '@docusaurus/tsconfig@3.10.2': {} - '@docusaurus/types@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/types@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@mdx-js/mdx': 3.1.1(supports-color@8.1.1) '@types/history': 4.7.11 '@types/mdast': 4.0.4 - '@types/react': 19.2.17 + '@types/react': 19.2.18 commander: 5.1.0 joi: 17.13.4 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' utility-types: 3.11.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) webpack-merge: 5.10.0 transitivePeerDependencies: - '@minify-html/node' @@ -15293,9 +15276,9 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-common@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/utils-common@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) tslib: 2.8.1 transitivePeerDependencies: - '@minify-html/node' @@ -15315,11 +15298,11 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-validation@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/utils-validation@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@docusaurus/logger': 3.10.2 - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) fs-extra: 11.3.6 joi: 17.13.4 js-yaml: 4.3.0 @@ -15343,15 +15326,15 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/utils@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@11ty/gray-matter': 1.0.0 '@docusaurus/logger': 3.10.2 - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) escape-string-regexp: 4.0.0 execa: 5.1.1 - file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) fs-extra: 11.3.6 github-slugger: 1.5.0 globby: 11.1.0 @@ -15363,9 +15346,9 @@ snapshots: prompts: 2.4.2 resolve-pathname: 3.0.0 tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) utility-types: 3.11.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - '@minify-html/node' - '@swc/core' @@ -15384,29 +15367,18 @@ snapshots: - uglify-js - webpack-cli - '@emnapi/core@1.11.1': - dependencies: - '@emnapi/wasi-threads': 1.2.2 - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.2': - dependencies: - tslib: 2.8.1 - optional: true - '@esbuild/aix-ppc64@0.19.12': optional: true '@esbuild/aix-ppc64@0.27.4': optional: true - '@esbuild/aix-ppc64@0.28.1': + '@esbuild/aix-ppc64@0.28.2': optional: true '@esbuild/android-arm64@0.19.12': @@ -15415,7 +15387,7 @@ snapshots: '@esbuild/android-arm64@0.27.4': optional: true - '@esbuild/android-arm64@0.28.1': + '@esbuild/android-arm64@0.28.2': optional: true '@esbuild/android-arm@0.19.12': @@ -15424,7 +15396,7 @@ snapshots: '@esbuild/android-arm@0.27.4': optional: true - '@esbuild/android-arm@0.28.1': + '@esbuild/android-arm@0.28.2': optional: true '@esbuild/android-x64@0.19.12': @@ -15433,7 +15405,7 @@ snapshots: '@esbuild/android-x64@0.27.4': optional: true - '@esbuild/android-x64@0.28.1': + '@esbuild/android-x64@0.28.2': optional: true '@esbuild/darwin-arm64@0.19.12': @@ -15442,7 +15414,7 @@ snapshots: '@esbuild/darwin-arm64@0.27.4': optional: true - '@esbuild/darwin-arm64@0.28.1': + '@esbuild/darwin-arm64@0.28.2': optional: true '@esbuild/darwin-x64@0.19.12': @@ -15451,7 +15423,7 @@ snapshots: '@esbuild/darwin-x64@0.27.4': optional: true - '@esbuild/darwin-x64@0.28.1': + '@esbuild/darwin-x64@0.28.2': optional: true '@esbuild/freebsd-arm64@0.19.12': @@ -15460,7 +15432,7 @@ snapshots: '@esbuild/freebsd-arm64@0.27.4': optional: true - '@esbuild/freebsd-arm64@0.28.1': + '@esbuild/freebsd-arm64@0.28.2': optional: true '@esbuild/freebsd-x64@0.19.12': @@ -15469,7 +15441,7 @@ snapshots: '@esbuild/freebsd-x64@0.27.4': optional: true - '@esbuild/freebsd-x64@0.28.1': + '@esbuild/freebsd-x64@0.28.2': optional: true '@esbuild/linux-arm64@0.19.12': @@ -15478,7 +15450,7 @@ snapshots: '@esbuild/linux-arm64@0.27.4': optional: true - '@esbuild/linux-arm64@0.28.1': + '@esbuild/linux-arm64@0.28.2': optional: true '@esbuild/linux-arm@0.19.12': @@ -15487,7 +15459,7 @@ snapshots: '@esbuild/linux-arm@0.27.4': optional: true - '@esbuild/linux-arm@0.28.1': + '@esbuild/linux-arm@0.28.2': optional: true '@esbuild/linux-ia32@0.19.12': @@ -15496,7 +15468,7 @@ snapshots: '@esbuild/linux-ia32@0.27.4': optional: true - '@esbuild/linux-ia32@0.28.1': + '@esbuild/linux-ia32@0.28.2': optional: true '@esbuild/linux-loong64@0.19.12': @@ -15505,7 +15477,7 @@ snapshots: '@esbuild/linux-loong64@0.27.4': optional: true - '@esbuild/linux-loong64@0.28.1': + '@esbuild/linux-loong64@0.28.2': optional: true '@esbuild/linux-mips64el@0.19.12': @@ -15514,7 +15486,7 @@ snapshots: '@esbuild/linux-mips64el@0.27.4': optional: true - '@esbuild/linux-mips64el@0.28.1': + '@esbuild/linux-mips64el@0.28.2': optional: true '@esbuild/linux-ppc64@0.19.12': @@ -15523,7 +15495,7 @@ snapshots: '@esbuild/linux-ppc64@0.27.4': optional: true - '@esbuild/linux-ppc64@0.28.1': + '@esbuild/linux-ppc64@0.28.2': optional: true '@esbuild/linux-riscv64@0.19.12': @@ -15532,7 +15504,7 @@ snapshots: '@esbuild/linux-riscv64@0.27.4': optional: true - '@esbuild/linux-riscv64@0.28.1': + '@esbuild/linux-riscv64@0.28.2': optional: true '@esbuild/linux-s390x@0.19.12': @@ -15541,7 +15513,7 @@ snapshots: '@esbuild/linux-s390x@0.27.4': optional: true - '@esbuild/linux-s390x@0.28.1': + '@esbuild/linux-s390x@0.28.2': optional: true '@esbuild/linux-x64@0.19.12': @@ -15550,13 +15522,13 @@ snapshots: '@esbuild/linux-x64@0.27.4': optional: true - '@esbuild/linux-x64@0.28.1': + '@esbuild/linux-x64@0.28.2': optional: true '@esbuild/netbsd-arm64@0.27.4': optional: true - '@esbuild/netbsd-arm64@0.28.1': + '@esbuild/netbsd-arm64@0.28.2': optional: true '@esbuild/netbsd-x64@0.19.12': @@ -15565,13 +15537,13 @@ snapshots: '@esbuild/netbsd-x64@0.27.4': optional: true - '@esbuild/netbsd-x64@0.28.1': + '@esbuild/netbsd-x64@0.28.2': optional: true '@esbuild/openbsd-arm64@0.27.4': optional: true - '@esbuild/openbsd-arm64@0.28.1': + '@esbuild/openbsd-arm64@0.28.2': optional: true '@esbuild/openbsd-x64@0.19.12': @@ -15580,13 +15552,13 @@ snapshots: '@esbuild/openbsd-x64@0.27.4': optional: true - '@esbuild/openbsd-x64@0.28.1': + '@esbuild/openbsd-x64@0.28.2': optional: true '@esbuild/openharmony-arm64@0.27.4': optional: true - '@esbuild/openharmony-arm64@0.28.1': + '@esbuild/openharmony-arm64@0.28.2': optional: true '@esbuild/sunos-x64@0.19.12': @@ -15595,7 +15567,7 @@ snapshots: '@esbuild/sunos-x64@0.27.4': optional: true - '@esbuild/sunos-x64@0.28.1': + '@esbuild/sunos-x64@0.28.2': optional: true '@esbuild/win32-arm64@0.19.12': @@ -15604,7 +15576,7 @@ snapshots: '@esbuild/win32-arm64@0.27.4': optional: true - '@esbuild/win32-arm64@0.28.1': + '@esbuild/win32-arm64@0.28.2': optional: true '@esbuild/win32-ia32@0.19.12': @@ -15613,7 +15585,7 @@ snapshots: '@esbuild/win32-ia32@0.27.4': optional: true - '@esbuild/win32-ia32@0.28.1': + '@esbuild/win32-ia32@0.28.2': optional: true '@esbuild/win32-x64@0.19.12': @@ -15622,7 +15594,7 @@ snapshots: '@esbuild/win32-x64@0.27.4': optional: true - '@esbuild/win32-x64@0.28.1': + '@esbuild/win32-x64@0.28.2': optional: true '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))': @@ -15707,7 +15679,7 @@ snapshots: '@formatjs/icu-skeleton-parser': 1.8.16 tslib: 2.8.1 - '@formatjs/icu-messageformat-parser@3.5.15': + '@formatjs/icu-messageformat-parser@3.5.16': dependencies: '@formatjs/icu-skeleton-parser': 2.1.11 @@ -15894,18 +15866,18 @@ snapshots: pg-connection-string: 2.14.0 postgres: 3.4.9 - '@immich/ui@0.83.0(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))': + '@immich/ui@0.83.0(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))': dependencies: '@internationalized/date': 3.12.2 '@mdi/js': 7.4.47 - '@sveltejs/kit': 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) - bits-ui: 2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + '@sveltejs/kit': 2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) + bits-ui: 2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) luxon: 3.7.2 - simple-icons: 16.27.1 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + simple-icons: 16.28.0 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) svelte-highlight: 7.11.0 tailwind-merge: 3.6.0 - tailwind-variants: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.3) + tailwind-variants: 3.3.1(tailwind-merge@3.6.0)(tailwindcss@4.3.3) tailwindcss: 4.3.3 '@inquirer/ansi@1.0.2': {} @@ -16261,8 +16233,8 @@ snapshots: '@koddsson/eslint-plugin-tscompat@0.2.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@mdn/browser-compat-data': 6.1.5 - '@typescript-eslint/type-utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/type-utils': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/utils': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) browserslist: 4.28.6 transitivePeerDependencies: - eslint @@ -16395,10 +16367,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8)': + '@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8)': dependencies: '@types/mdx': 2.0.14 - '@types/react': 19.2.17 + '@types/react': 19.2.18 react: 19.2.8 '@mermaid-js/parser@1.2.0': @@ -16427,28 +16399,21 @@ snapshots: '@namnode/store@0.1.0': {} - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 - optional: true - '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)': dependencies: '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(bullmq@5.81.2(supports-color@8.1.1))': + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(bullmq@5.81.3(supports-color@8.1.1))': dependencies: '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28) '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) - bullmq: 5.81.2(supports-color@8.1.1) + bullmq: 5.81.3(supports-color@8.1.1) tslib: 2.8.1 - '@nestjs/cli@11.0.24(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/node@24.13.3)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(prettier@3.9.6)(uglify-js@3.19.3)': + '@nestjs/cli@11.0.24(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/node@24.13.3)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(prettier@3.9.6)(uglify-js@3.19.3)': dependencies: '@angular-devkit/core': 19.2.27(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) @@ -16459,17 +16424,17 @@ snapshots: chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) glob: 13.0.6 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) webpack-node-externals: 3.0.0 optionalDependencies: - '@swc/core': 1.15.46(@swc/helpers@0.5.23) + '@swc/core': 1.15.47(@swc/helpers@0.5.23) transitivePeerDependencies: - '@minify-html/node' - '@swc/css' @@ -16610,7 +16575,7 @@ snapshots: '@noble/hashes@1.8.0': {} - '@noble/hashes@2.2.0': {} + '@noble/hashes@2.3.0': {} '@nodelib/fs.scandir@2.1.5': dependencies: @@ -16901,7 +16866,7 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) - '@oxc-project/types@0.139.0': {} + '@oxc-project/types@0.143.0': {} '@paralleldrive/cuid2@2.3.1': dependencies: @@ -17062,32 +17027,32 @@ snapshots: tslib: 2.8.1 tsyringe: 4.10.0 - '@photo-sphere-viewer/core@5.15.0': + '@photo-sphere-viewer/core@5.15.1': dependencies: three: 0.185.1 - '@photo-sphere-viewer/equirectangular-video-adapter@5.15.0(@photo-sphere-viewer/core@5.15.0)(@photo-sphere-viewer/video-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0))': + '@photo-sphere-viewer/equirectangular-video-adapter@5.15.1(@photo-sphere-viewer/core@5.15.1)(@photo-sphere-viewer/video-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1))': dependencies: - '@photo-sphere-viewer/core': 5.15.0 - '@photo-sphere-viewer/video-plugin': 5.15.0(@photo-sphere-viewer/core@5.15.0) + '@photo-sphere-viewer/core': 5.15.1 + '@photo-sphere-viewer/video-plugin': 5.15.1(@photo-sphere-viewer/core@5.15.1) three: 0.185.1 - '@photo-sphere-viewer/markers-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)': + '@photo-sphere-viewer/markers-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1)': dependencies: - '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 - '@photo-sphere-viewer/resolution-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)(@photo-sphere-viewer/settings-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0))': + '@photo-sphere-viewer/resolution-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1)(@photo-sphere-viewer/settings-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1))': dependencies: - '@photo-sphere-viewer/core': 5.15.0 - '@photo-sphere-viewer/settings-plugin': 5.15.0(@photo-sphere-viewer/core@5.15.0) + '@photo-sphere-viewer/core': 5.15.1 + '@photo-sphere-viewer/settings-plugin': 5.15.1(@photo-sphere-viewer/core@5.15.1) - '@photo-sphere-viewer/settings-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)': + '@photo-sphere-viewer/settings-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1)': dependencies: - '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 - '@photo-sphere-viewer/video-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)': + '@photo-sphere-viewer/video-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1)': dependencies: - '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 three: 0.185.1 '@photostructure/tz-lookup@11.5.0': {} @@ -17097,9 +17062,9 @@ snapshots: '@pkgr/core@0.3.6': {} - '@playwright/test@1.62.0': + '@playwright/test@1.62.1': dependencies: - playwright: 1.62.0 + playwright: 1.62.1 '@pnpm/config.env-replace@1.1.0': {} @@ -17274,53 +17239,46 @@ snapshots: '@codemirror/state': 6.7.1 '@codemirror/view': 6.43.7 - '@rolldown/binding-android-arm64@1.1.5': + '@rolldown/binding-android-arm64@1.2.3': optional: true - '@rolldown/binding-darwin-arm64@1.1.5': + '@rolldown/binding-darwin-arm64@1.2.3': optional: true - '@rolldown/binding-darwin-x64@1.1.5': + '@rolldown/binding-darwin-x64@1.2.3': optional: true - '@rolldown/binding-freebsd-x64@1.1.5': + '@rolldown/binding-freebsd-x64@1.2.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.5': + '@rolldown/binding-linux-arm64-gnu@1.2.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.5': + '@rolldown/binding-linux-arm64-musl@1.2.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.5': + '@rolldown/binding-linux-ppc64-gnu@1.2.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.5': + '@rolldown/binding-linux-s390x-gnu@1.2.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.5': + '@rolldown/binding-linux-x64-gnu@1.2.3': optional: true - '@rolldown/binding-linux-x64-musl@1.1.5': + '@rolldown/binding-linux-x64-musl@1.2.3': optional: true - '@rolldown/binding-openharmony-arm64@1.1.5': + '@rolldown/binding-openharmony-arm64@1.2.3': optional: true - '@rolldown/binding-wasm32-wasi@1.1.5': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@rolldown/binding-win32-arm64-msvc@1.2.3': optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.5': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.1.5': + '@rolldown/binding-win32-x64-msvc@1.2.3': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -17460,58 +17418,58 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.11(acorn@8.18.0)': + '@sveltejs/acorn-typescript@1.0.12(acorn@8.18.0)': dependencies: acorn: 8.18.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))': dependencies: - '@sveltejs/kit': 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@sveltejs/kit': 2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) - '@sveltejs/enhanced-img@0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(rollup@4.62.0)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@sveltejs/enhanced-img@0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(rollup@4.62.0)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) magic-string: 0.30.21 sharp: 0.34.5 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) - svelte-parse-markup: 0.1.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)) - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + svelte-parse-markup: 0.1.5(svelte@5.56.8(@typescript-eslint/types@8.66.0)) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) vite-imagetools: 9.0.3(rollup@4.62.0) zimmerframe: 1.1.4 transitivePeerDependencies: - rollup - '@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))': dependencies: '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.11(acorn@8.18.0) - '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@sveltejs/acorn-typescript': 1.0.12(acorn@8.18.0) + '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@types/cookie': 0.6.0 acorn: 8.18.0 cookie: 0.6.0 - devalue: 5.8.2 + devalue: 5.9.0 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 mrmime: 2.0.1 set-cookie-parser: 3.1.2 sirv: 3.0.2 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) optionalDependencies: '@opentelemetry/api': 1.9.1 typescript: '@typescript/typescript6@6.0.2' - '@sveltejs/load-config@0.2.0': {} + '@sveltejs/load-config@0.2.2': {} - '@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))': dependencies: deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.3 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) - vitefu: 1.1.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: @@ -17606,59 +17564,59 @@ snapshots: - supports-color - typescript - '@swc/core-darwin-arm64@1.15.46': + '@swc/core-darwin-arm64@1.15.47': optional: true - '@swc/core-darwin-x64@1.15.46': + '@swc/core-darwin-x64@1.15.47': optional: true - '@swc/core-linux-arm-gnueabihf@1.15.46': + '@swc/core-linux-arm-gnueabihf@1.15.47': optional: true - '@swc/core-linux-arm64-gnu@1.15.46': + '@swc/core-linux-arm64-gnu@1.15.47': optional: true - '@swc/core-linux-arm64-musl@1.15.46': + '@swc/core-linux-arm64-musl@1.15.47': optional: true - '@swc/core-linux-ppc64-gnu@1.15.46': + '@swc/core-linux-ppc64-gnu@1.15.47': optional: true - '@swc/core-linux-s390x-gnu@1.15.46': + '@swc/core-linux-s390x-gnu@1.15.47': optional: true - '@swc/core-linux-x64-gnu@1.15.46': + '@swc/core-linux-x64-gnu@1.15.47': optional: true - '@swc/core-linux-x64-musl@1.15.46': + '@swc/core-linux-x64-musl@1.15.47': optional: true - '@swc/core-win32-arm64-msvc@1.15.46': + '@swc/core-win32-arm64-msvc@1.15.47': optional: true - '@swc/core-win32-ia32-msvc@1.15.46': + '@swc/core-win32-ia32-msvc@1.15.47': optional: true - '@swc/core-win32-x64-msvc@1.15.46': + '@swc/core-win32-x64-msvc@1.15.47': optional: true - '@swc/core@1.15.46(@swc/helpers@0.5.23)': + '@swc/core@1.15.47(@swc/helpers@0.5.23)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.27 optionalDependencies: - '@swc/core-darwin-arm64': 1.15.46 - '@swc/core-darwin-x64': 1.15.46 - '@swc/core-linux-arm-gnueabihf': 1.15.46 - '@swc/core-linux-arm64-gnu': 1.15.46 - '@swc/core-linux-arm64-musl': 1.15.46 - '@swc/core-linux-ppc64-gnu': 1.15.46 - '@swc/core-linux-s390x-gnu': 1.15.46 - '@swc/core-linux-x64-gnu': 1.15.46 - '@swc/core-linux-x64-musl': 1.15.46 - '@swc/core-win32-arm64-msvc': 1.15.46 - '@swc/core-win32-ia32-msvc': 1.15.46 - '@swc/core-win32-x64-msvc': 1.15.46 + '@swc/core-darwin-arm64': 1.15.47 + '@swc/core-darwin-x64': 1.15.47 + '@swc/core-linux-arm-gnueabihf': 1.15.47 + '@swc/core-linux-arm64-gnu': 1.15.47 + '@swc/core-linux-arm64-musl': 1.15.47 + '@swc/core-linux-ppc64-gnu': 1.15.47 + '@swc/core-linux-s390x-gnu': 1.15.47 + '@swc/core-linux-x64-gnu': 1.15.47 + '@swc/core-linux-x64-musl': 1.15.47 + '@swc/core-win32-arm64-msvc': 1.15.47 + '@swc/core-win32-ia32-msvc': 1.15.47 + '@swc/core-win32-x64-msvc': 1.15.47 '@swc/helpers': 0.5.23 '@swc/counter@0.1.3': {} @@ -17736,12 +17694,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.3(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) '@testing-library/dom@10.4.1': dependencies: @@ -17764,20 +17722,20 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/svelte-core@1.1.3(svelte@5.56.8(@typescript-eslint/types@8.65.0))': + '@testing-library/svelte-core@1.1.3(svelte@5.56.8(@typescript-eslint/types@8.66.0))': dependencies: - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - '@testing-library/svelte@5.4.2(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))(vitest@4.1.10)': + '@testing-library/svelte@5.4.2(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@testing-library/dom': 10.4.1 - '@testing-library/svelte-core': 1.1.3(svelte@5.56.8(@typescript-eslint/types@8.65.0)) - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + '@testing-library/svelte-core': 1.1.3(svelte@5.56.8(@typescript-eslint/types@8.66.0)) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) optionalDependencies: - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) - '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + '@testing-library/user-event@14.6.3(@testing-library/dom@10.4.1)': dependencies: '@testing-library/dom': 10.4.1 @@ -17790,7 +17748,7 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@trivago/prettier-plugin-sort-imports@6.0.2(prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)))(prettier@3.9.6)(supports-color@8.1.1)(svelte@5.56.8(@typescript-eslint/types@8.65.0))': + '@trivago/prettier-plugin-sort-imports@6.0.2(prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.66.0)))(prettier@3.9.6)(supports-color@8.1.1)(svelte@5.56.8(@typescript-eslint/types@8.66.0))': dependencies: '@babel/generator': 7.29.7 '@babel/parser': 7.29.7 @@ -17802,8 +17760,8 @@ snapshots: parse-imports-exports: 0.2.4 prettier: 3.9.6 optionalDependencies: - prettier-plugin-svelte: 4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)) - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + prettier-plugin-svelte: 4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.66.0)) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) transitivePeerDependencies: - supports-color @@ -17826,11 +17784,6 @@ snapshots: '@types/geojson': 7946.0.16 tslib: 2.8.1 - '@tybys/wasm-util@0.10.3': - dependencies: - tslib: 2.8.1 - optional: true - '@types/accepts@1.3.7': dependencies: '@types/node': 24.13.3 @@ -18179,11 +18132,11 @@ snapshots: '@types/lodash-es@4.17.12': dependencies: - '@types/lodash': 4.17.24 + '@types/lodash': 4.17.25 - '@types/lodash@4.17.24': {} + '@types/lodash@4.17.25': {} - '@types/luxon@3.7.2': {} + '@types/luxon@3.7.3': {} '@types/mdast@4.0.4': dependencies: @@ -18223,7 +18176,7 @@ snapshots: dependencies: '@types/node': 24.13.3 - '@types/oidc-provider@9.5.0': + '@types/oidc-provider@9.11.0': dependencies: '@types/keygrip': 1.0.6 '@types/koa': 3.0.3 @@ -18233,18 +18186,18 @@ snapshots: '@types/pg-pool@2.0.7': dependencies: - '@types/pg': 8.20.0 + '@types/pg': 8.20.4 '@types/pg@8.15.6': dependencies: '@types/node': 24.13.3 - pg-protocol: 1.15.0 + pg-protocol: 1.16.0 pg-types: 2.2.0 - '@types/pg@8.20.0': + '@types/pg@8.20.4': dependencies: '@types/node': 24.13.3 - pg-protocol: 1.15.0 + pg-protocol: 1.16.0 pg-types: 2.2.0 '@types/picomatch@4.0.3': {} @@ -18266,21 +18219,21 @@ snapshots: '@types/react-router-config@5.0.11': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@types/react-router': 5.1.20 '@types/react-router-dom@5.3.3': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@types/react-router': 5.1.20 '@types/react-router@5.1.20': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@types/react@19.2.17': + '@types/react@19.2.18': dependencies: csstype: 3.2.3 @@ -18294,7 +18247,7 @@ snapshots: dependencies: '@types/node': 24.13.3 - '@types/semver@7.7.1': {} + '@types/semver@7.8.0': {} '@types/send@0.17.6': dependencies: @@ -18375,14 +18328,14 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': + '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/type-utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/parser': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/type-utils': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/utils': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/visitor-keys': 8.66.0 eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) ignore: 7.0.6 natural-compare: 1.4.0 @@ -18391,41 +18344,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': + '@typescript-eslint/parser@8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/visitor-keys': 8.66.0 debug: 4.4.3(supports-color@8.1.1) eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': + '@typescript-eslint/project-service@8.66.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.65.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/tsconfig-utils': 8.66.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.66.0 debug: 4.4.3(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.65.0': + '@typescript-eslint/scope-manager@8.66.0': dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/visitor-keys': 8.66.0 - '@typescript-eslint/tsconfig-utils@8.65.0(@typescript/typescript6@6.0.2)': + '@typescript-eslint/tsconfig-utils@8.66.0(@typescript/typescript6@6.0.2)': dependencies: typescript: '@typescript/typescript6@6.0.2' - '@typescript-eslint/type-utils@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': + '@typescript-eslint/type-utils@8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/utils': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) debug: 4.4.3(supports-color@8.1.1) eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) @@ -18433,14 +18386,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.65.0': {} + '@typescript-eslint/types@8.66.0': {} - '@typescript-eslint/typescript-estree@8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': + '@typescript-eslint/typescript-estree@8.66.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@typescript-eslint/project-service': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - '@typescript-eslint/tsconfig-utils': 8.65.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/project-service': 8.66.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/tsconfig-utils': 8.66.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/visitor-keys': 8.66.0 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.6 semver: 7.8.5 @@ -18450,20 +18403,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': + '@typescript-eslint/utils@8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.65.0': + '@typescript-eslint/visitor-keys@8.66.0': dependencies: - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/types': 8.66.0 eslint-visitor-keys: 5.0.1 '@typescript/typescript-aix-ppc64@7.0.2': @@ -18541,7 +18494,7 @@ snapshots: dependencies: valibot: 1.4.2(@typescript/typescript6@6.0.2) - '@vitest/coverage-v8@4.1.10(vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@vitest/coverage-v8@4.1.10(vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.10 @@ -18553,7 +18506,7 @@ snapshots: obug: 2.1.3 std-env: 4.2.0 tinyrainbow: 3.1.0 - vitest: 3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vitest: 3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': dependencies: @@ -18567,7 +18520,7 @@ snapshots: obug: 2.1.3 std-env: 4.2.0 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@vitest/expect@3.2.7': dependencies: @@ -18586,21 +18539,21 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) - '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) '@vitest/pretty-format@3.2.7': dependencies: @@ -18736,10 +18689,10 @@ snapshots: dependencies: '@namnode/store': 0.1.0 - '@zoom-image/svelte@0.3.9(svelte@5.56.8(@typescript-eslint/types@8.65.0))': + '@zoom-image/svelte@0.3.9(svelte@5.56.8(@typescript-eslint/types@8.66.0))': dependencies: '@zoom-image/core': 0.42.0 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) abbrev@1.1.1: {} @@ -18986,25 +18939,25 @@ snapshots: dependencies: immediate: 3.3.0 - autoprefixer@10.5.4(postcss@8.5.25): + autoprefixer@10.5.4(postcss@8.5.26): dependencies: browserslist: 4.28.6 caniuse-lite: 1.0.30001806 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 axobject-query@4.1.0: {} b4a@1.8.1: {} - babel-loader@9.2.1(@babel/core@7.29.7(supports-color@8.1.1))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + babel-loader@9.2.1(@babel/core@7.29.7(supports-color@8.1.1))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) find-cache-dir: 4.0.0 schema-utils: 4.3.3 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) babel-plugin-dynamic-import-node@2.3.3: dependencies: @@ -19052,12 +19005,12 @@ snapshots: bare-events@2.9.1: {} - bare-fs@4.7.4: + bare-fs@4.8.0: dependencies: bare-events: 2.9.1 bare-path: 3.1.1 bare-stream: 2.13.3(bare-events@2.9.1) - bare-url: 2.4.5 + bare-url: 2.5.1 fast-fifo: 1.3.2 transitivePeerDependencies: - bare-abort-controller @@ -19075,7 +19028,7 @@ snapshots: transitivePeerDependencies: - react-native-b4a - bare-url@2.4.5: + bare-url@2.5.1: dependencies: bare-path: 3.1.1 @@ -19105,15 +19058,15 @@ snapshots: binary-extensions@2.3.0: {} - bits-ui@2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + bits-ui@2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: '@floating-ui/core': 1.8.0 '@floating-ui/dom': 1.8.0 '@internationalized/date': 3.12.2 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) - svelte: 5.56.8(@typescript-eslint/types@8.65.0) - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + runed: 0.35.1(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) tabbable: 6.5.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -19234,11 +19187,11 @@ snapshots: builtin-modules@5.2.0: {} - bullmq@5.81.2(supports-color@8.1.1): + bullmq@5.81.3(supports-color@8.1.1): dependencies: cron-parser: 4.9.0 ioredis: 5.11.1(supports-color@8.1.1) - msgpackr: 2.0.4 + msgpackr: 2.0.5 node-abort-controller: 3.1.1 semver: 7.8.5 tslib: 2.8.1 @@ -19657,7 +19610,7 @@ snapshots: copy-text-to-clipboard@3.2.2: {} - copy-webpack-plugin@11.0.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + copy-webpack-plugin@11.0.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -19665,7 +19618,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) core-js-compat@3.49.0: dependencies: @@ -19727,7 +19680,7 @@ snapshots: cron@4.4.0: dependencies: - '@types/luxon': 3.7.2 + '@types/luxon': 3.7.3 luxon: 3.7.2 cross-spawn@7.0.6: @@ -19740,53 +19693,53 @@ snapshots: dependencies: type-fest: 1.4.0 - css-blank-pseudo@7.0.1(postcss@8.5.25): + css-blank-pseudo@7.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - css-declaration-sorter@7.4.0(postcss@8.5.25): + css-declaration-sorter@7.4.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - css-has-pseudo@7.0.3(postcss@8.5.25): + css-has-pseudo@7.0.3(postcss@8.5.26): dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 postcss-value-parser: 4.2.0 - css-loader@6.11.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + css-loader@6.11.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: - icss-utils: 5.1.0(postcss@8.5.25) - postcss: 8.5.25 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.25) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.25) - postcss-modules-scope: 3.2.1(postcss@8.5.25) - postcss-modules-values: 4.0.0(postcss@8.5.25) + icss-utils: 5.1.0(postcss@8.5.26) + postcss: 8.5.26 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.26) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.26) + postcss-modules-scope: 3.2.1(postcss@8.5.26) + postcss-modules-values: 4.0.0(postcss@8.5.26) postcss-value-parser: 4.2.0 semver: 7.8.5 optionalDependencies: - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) - css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 - cssnano: 6.1.2(postcss@8.5.25) + cssnano: 6.1.2(postcss@8.5.26) jest-worker: 29.7.0 - postcss: 8.5.25 + postcss: 8.5.26 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) optionalDependencies: clean-css: 5.3.3 csso: 5.0.5 - esbuild: 0.28.1 + esbuild: 0.28.2 lightningcss: 1.33.0 - css-prefers-color-scheme@10.0.0(postcss@8.5.25): + css-prefers-color-scheme@10.0.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 css-select@4.3.0: dependencies: @@ -19824,60 +19777,60 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-advanced@6.1.2(postcss@8.5.25): + cssnano-preset-advanced@6.1.2(postcss@8.5.26): dependencies: - autoprefixer: 10.5.4(postcss@8.5.25) + autoprefixer: 10.5.4(postcss@8.5.26) browserslist: 4.28.6 - cssnano-preset-default: 6.1.2(postcss@8.5.25) - postcss: 8.5.25 - postcss-discard-unused: 6.0.5(postcss@8.5.25) - postcss-merge-idents: 6.0.3(postcss@8.5.25) - postcss-reduce-idents: 6.0.3(postcss@8.5.25) - postcss-zindex: 6.0.2(postcss@8.5.25) + cssnano-preset-default: 6.1.2(postcss@8.5.26) + postcss: 8.5.26 + postcss-discard-unused: 6.0.5(postcss@8.5.26) + postcss-merge-idents: 6.0.3(postcss@8.5.26) + postcss-reduce-idents: 6.0.3(postcss@8.5.26) + postcss-zindex: 6.0.2(postcss@8.5.26) - cssnano-preset-default@6.1.2(postcss@8.5.25): + cssnano-preset-default@6.1.2(postcss@8.5.26): dependencies: browserslist: 4.28.6 - css-declaration-sorter: 7.4.0(postcss@8.5.25) - cssnano-utils: 4.0.2(postcss@8.5.25) - postcss: 8.5.25 - postcss-calc: 9.0.1(postcss@8.5.25) - postcss-colormin: 6.1.0(postcss@8.5.25) - postcss-convert-values: 6.1.0(postcss@8.5.25) - postcss-discard-comments: 6.0.2(postcss@8.5.25) - postcss-discard-duplicates: 6.0.3(postcss@8.5.25) - postcss-discard-empty: 6.0.3(postcss@8.5.25) - postcss-discard-overridden: 6.0.2(postcss@8.5.25) - postcss-merge-longhand: 6.0.5(postcss@8.5.25) - postcss-merge-rules: 6.1.1(postcss@8.5.25) - postcss-minify-font-values: 6.1.0(postcss@8.5.25) - postcss-minify-gradients: 6.0.3(postcss@8.5.25) - postcss-minify-params: 6.1.0(postcss@8.5.25) - postcss-minify-selectors: 6.0.4(postcss@8.5.25) - postcss-normalize-charset: 6.0.2(postcss@8.5.25) - postcss-normalize-display-values: 6.0.2(postcss@8.5.25) - postcss-normalize-positions: 6.0.2(postcss@8.5.25) - postcss-normalize-repeat-style: 6.0.2(postcss@8.5.25) - postcss-normalize-string: 6.0.2(postcss@8.5.25) - postcss-normalize-timing-functions: 6.0.2(postcss@8.5.25) - postcss-normalize-unicode: 6.1.0(postcss@8.5.25) - postcss-normalize-url: 6.0.2(postcss@8.5.25) - postcss-normalize-whitespace: 6.0.2(postcss@8.5.25) - postcss-ordered-values: 6.0.2(postcss@8.5.25) - postcss-reduce-initial: 6.1.0(postcss@8.5.25) - postcss-reduce-transforms: 6.0.2(postcss@8.5.25) - postcss-svgo: 6.0.3(postcss@8.5.25) - postcss-unique-selectors: 6.0.4(postcss@8.5.25) + css-declaration-sorter: 7.4.0(postcss@8.5.26) + cssnano-utils: 4.0.2(postcss@8.5.26) + postcss: 8.5.26 + postcss-calc: 9.0.1(postcss@8.5.26) + postcss-colormin: 6.1.0(postcss@8.5.26) + postcss-convert-values: 6.1.0(postcss@8.5.26) + postcss-discard-comments: 6.0.2(postcss@8.5.26) + postcss-discard-duplicates: 6.0.3(postcss@8.5.26) + postcss-discard-empty: 6.0.3(postcss@8.5.26) + postcss-discard-overridden: 6.0.2(postcss@8.5.26) + postcss-merge-longhand: 6.0.5(postcss@8.5.26) + postcss-merge-rules: 6.1.1(postcss@8.5.26) + postcss-minify-font-values: 6.1.0(postcss@8.5.26) + postcss-minify-gradients: 6.0.3(postcss@8.5.26) + postcss-minify-params: 6.1.0(postcss@8.5.26) + postcss-minify-selectors: 6.0.4(postcss@8.5.26) + postcss-normalize-charset: 6.0.2(postcss@8.5.26) + postcss-normalize-display-values: 6.0.2(postcss@8.5.26) + postcss-normalize-positions: 6.0.2(postcss@8.5.26) + postcss-normalize-repeat-style: 6.0.2(postcss@8.5.26) + postcss-normalize-string: 6.0.2(postcss@8.5.26) + postcss-normalize-timing-functions: 6.0.2(postcss@8.5.26) + postcss-normalize-unicode: 6.1.0(postcss@8.5.26) + postcss-normalize-url: 6.0.2(postcss@8.5.26) + postcss-normalize-whitespace: 6.0.2(postcss@8.5.26) + postcss-ordered-values: 6.0.2(postcss@8.5.26) + postcss-reduce-initial: 6.1.0(postcss@8.5.26) + postcss-reduce-transforms: 6.0.2(postcss@8.5.26) + postcss-svgo: 6.0.3(postcss@8.5.26) + postcss-unique-selectors: 6.0.4(postcss@8.5.26) - cssnano-utils@4.0.2(postcss@8.5.25): + cssnano-utils@4.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - cssnano@6.1.2(postcss@8.5.25): + cssnano@6.1.2(postcss@8.5.26): dependencies: - cssnano-preset-default: 6.1.2(postcss@8.5.25) + cssnano-preset-default: 6.1.2(postcss@8.5.26) lilconfig: 3.1.3 - postcss: 8.5.25 + postcss: 8.5.26 csso@5.0.5: dependencies: @@ -20202,7 +20155,7 @@ snapshots: dependencies: address: 2.0.3 - devalue@5.8.2: {} + devalue@5.9.0: {} devlop@1.1.0: dependencies: @@ -20259,9 +20212,9 @@ snapshots: transitivePeerDependencies: - supports-color - docusaurus-lunr-search@3.6.0(@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + docusaurus-lunr-search@3.6.0(@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) autocomplete.js: 0.37.1 clsx: 2.1.1 gauge: 3.0.2 @@ -20547,34 +20500,34 @@ snapshots: '@esbuild/win32-ia32': 0.27.4 '@esbuild/win32-x64': 0.27.4 - esbuild@0.28.1: + esbuild@0.28.2: optionalDependencies: - '@esbuild/aix-ppc64': 0.28.1 - '@esbuild/android-arm': 0.28.1 - '@esbuild/android-arm64': 0.28.1 - '@esbuild/android-x64': 0.28.1 - '@esbuild/darwin-arm64': 0.28.1 - '@esbuild/darwin-x64': 0.28.1 - '@esbuild/freebsd-arm64': 0.28.1 - '@esbuild/freebsd-x64': 0.28.1 - '@esbuild/linux-arm': 0.28.1 - '@esbuild/linux-arm64': 0.28.1 - '@esbuild/linux-ia32': 0.28.1 - '@esbuild/linux-loong64': 0.28.1 - '@esbuild/linux-mips64el': 0.28.1 - '@esbuild/linux-ppc64': 0.28.1 - '@esbuild/linux-riscv64': 0.28.1 - '@esbuild/linux-s390x': 0.28.1 - '@esbuild/linux-x64': 0.28.1 - '@esbuild/netbsd-arm64': 0.28.1 - '@esbuild/netbsd-x64': 0.28.1 - '@esbuild/openbsd-arm64': 0.28.1 - '@esbuild/openbsd-x64': 0.28.1 - '@esbuild/openharmony-arm64': 0.28.1 - '@esbuild/sunos-x64': 0.28.1 - '@esbuild/win32-arm64': 0.28.1 - '@esbuild/win32-ia32': 0.28.1 - '@esbuild/win32-x64': 0.28.1 + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 escalade@3.2.0: {} @@ -20630,7 +20583,7 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) - eslint-plugin-svelte@3.22.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + eslint-plugin-svelte@3.22.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) '@jridgewell/sourcemap-codec': 1.5.5 @@ -20638,13 +20591,13 @@ snapshots: esutils: 2.0.3 globals: 16.5.0 known-css-properties: 0.37.0 - postcss: 8.5.25 - postcss-load-config: 3.1.4(postcss@8.5.25) - postcss-safe-parser: 7.0.1(postcss@8.5.25) + postcss: 8.5.26 + postcss-load-config: 3.1.4(postcss@8.5.26) + postcss-safe-parser: 7.0.1(postcss@8.5.26) semver: 7.8.5 - svelte-eslint-parser: 1.8.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + svelte-eslint-parser: 1.8.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) optionalDependencies: - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) transitivePeerDependencies: - ts-node @@ -20660,7 +20613,7 @@ snapshots: entities: 4.5.0 eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) find-up-simple: 1.0.1 - globals: 17.7.0 + globals: 17.9.0 indent-string: 5.0.0 is-builtin-module: 5.0.0 is-identifier: 1.1.0 @@ -20759,11 +20712,11 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.2.13(@typescript-eslint/types@8.65.0): + esrap@2.2.13(@typescript-eslint/types@8.66.0): dependencies: '@jridgewell/sourcemap-codec': 1.5.5 optionalDependencies: - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/types': 8.66.0 esrecurse@4.3.0: dependencies: @@ -20862,7 +20815,7 @@ snapshots: exiftool-vendored@35.21.0: dependencies: '@photostructure/tz-lookup': 11.5.0 - '@types/luxon': 3.7.2 + '@types/luxon': 3.7.3 batch-cluster: 17.3.1 exiftool-vendored.pl: 13.59.0 he: 1.2.0 @@ -21022,11 +20975,11 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) file-source@0.6.1: dependencies: @@ -21113,7 +21066,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@babel/code-frame': 7.29.7 chalk: 4.1.2 @@ -21128,7 +21081,7 @@ snapshots: semver: 7.8.5 tapable: 2.3.3 typescript: 5.9.3 - webpack: 5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) form-data-encoder@2.1.4: {} @@ -21310,7 +21263,7 @@ snapshots: globals@16.5.0: {} - globals@17.7.0: {} + globals@17.9.0: {} globalyzer@0.1.0: {} @@ -21567,10 +21520,10 @@ snapshots: hls-video-element@1.5.11: dependencies: custom-media-element: 1.4.6 - hls.js: 1.6.16 + hls.js: 1.6.17 media-tracks: 0.3.5 - hls.js@1.6.16: {} + hls.js@1.6.17: {} hogan.js@3.0.2: dependencies: @@ -21627,7 +21580,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.7(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + html-webpack-plugin@5.6.7(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -21635,7 +21588,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.3.3 optionalDependencies: - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) html5parser@3.0.0: {} @@ -21764,9 +21717,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.25): + icss-utils@5.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 identifier-regex@1.1.0: dependencies: @@ -21856,10 +21809,10 @@ snapshots: '@formatjs/icu-messageformat-parser': 2.11.4 tslib: 2.8.1 - intl-messageformat@11.2.12: + intl-messageformat@11.2.13: dependencies: '@formatjs/fast-memoize': 3.1.7 - '@formatjs/icu-messageformat-parser': 3.5.15 + '@formatjs/icu-messageformat-parser': 3.5.16 invariant@2.2.4: dependencies: @@ -22074,7 +22027,7 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 - jose@6.2.4: {} + jose@6.2.8: {} js-tokens@10.0.0: {} @@ -22090,7 +22043,7 @@ snapshots: dependencies: argparse: 2.0.1 - js-yaml@5.2.2: + js-yaml@5.2.3: dependencies: argparse: 2.0.1 @@ -23176,11 +23129,11 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.10.2(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + mini-css-extract-plugin@2.10.2(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: schema-utils: 4.3.3 tapable: 2.3.3 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) minimalistic-assert@1.0.1: {} @@ -23202,22 +23155,22 @@ snapshots: minimist@1.2.8: {} - minimizer-webpack-plugin@5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + minimizer-webpack-plugin@5.6.1(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.49.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) optionalDependencies: - '@swc/core': 1.15.46(@swc/helpers@0.5.23) + '@swc/core': 1.15.47(@swc/helpers@0.5.23) clean-css: 5.3.3 - cssnano: 6.1.2(postcss@8.5.25) + cssnano: 6.1.2(postcss@8.5.26) csso: 5.0.5 - esbuild: 0.28.1 + esbuild: 0.28.2 html-minifier-terser: 7.2.0 lightningcss: 1.33.0 - postcss: 8.5.25 + postcss: 8.5.26 uglify-js: 3.19.3 minipass@3.3.6: @@ -23275,7 +23228,7 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 optional: true - msgpackr@2.0.4: + msgpackr@2.0.5: optionalDependencies: msgpackr-extract: 3.0.4 @@ -23308,9 +23261,9 @@ snapshots: nan@2.28.0: optional: true - nanoid@3.3.16: {} + nanoid@3.3.18: {} - nanoid@6.0.0: {} + nanoid@6.0.1: {} napi-build-utils@2.0.0: optional: true @@ -23439,7 +23392,7 @@ snapshots: node-releases@2.0.51: {} - nodemailer@9.0.3: {} + nodemailer@9.0.4: {} nopt@1.0.10: dependencies: @@ -23478,11 +23431,11 @@ snapshots: dependencies: boolbase: 1.0.0 - null-loader@4.0.1(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + null-loader@4.0.1(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) nwsapi@2.2.24: optional: true @@ -23518,16 +23471,16 @@ snapshots: obug@2.1.3: {} - oidc-provider@9.10.0(supports-color@8.1.1): + oidc-provider@9.11.2(supports-color@8.1.1): dependencies: '@koa/cors': 5.0.0 '@koa/router': 15.7.0(koa@3.2.1)(supports-color@8.1.1) debug: 4.4.3(supports-color@8.1.1) eta: 4.6.0 - jose: 6.2.4 + jose: 6.2.8 jsesc: 3.1.0 koa: 3.2.1 - nanoid: 6.0.0 + nanoid: 6.0.1 quick-lru: 7.3.0 raw-body: 4.0.0 transitivePeerDependencies: @@ -23577,7 +23530,7 @@ snapshots: openid-client@6.8.4: dependencies: - jose: 6.2.4 + jose: 6.2.8 oauth4webapi: 3.8.6 optionator@0.9.4: @@ -23807,7 +23760,7 @@ snapshots: dependencies: pg: 8.22.0 - pg-protocol@1.15.0: {} + pg-protocol@1.16.0: {} pg-types@2.2.0: dependencies: @@ -23821,7 +23774,7 @@ snapshots: dependencies: pg-connection-string: 2.14.0 pg-pool: 3.14.0(pg@8.22.0) - pg-protocol: 1.15.0 + pg-protocol: 1.16.0 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: @@ -23856,11 +23809,11 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 - playwright-core@1.62.0: {} + playwright-core@1.62.1: {} - playwright@1.62.0: + playwright@1.62.1: dependencies: - playwright-core: 1.62.0 + playwright-core: 1.62.1 optionalDependencies: fsevents: 2.3.2 @@ -23894,448 +23847,448 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 - postcss-attribute-case-insensitive@7.0.1(postcss@8.5.25): + postcss-attribute-case-insensitive@7.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-calc@9.0.1(postcss@8.5.25): + postcss-calc@9.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 6.1.4 postcss-value-parser: 4.2.0 - postcss-clamp@4.1.0(postcss@8.5.25): + postcss-clamp@4.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-color-functional-notation@7.0.12(postcss@8.5.25): + postcss-color-functional-notation@7.0.12(postcss@8.5.26): dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - postcss-color-hex-alpha@10.0.0(postcss@8.5.25): + postcss-color-hex-alpha@10.0.0(postcss@8.5.26): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-color-rebeccapurple@10.0.0(postcss@8.5.25): + postcss-color-rebeccapurple@10.0.0(postcss@8.5.26): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-colormin@6.1.0(postcss@8.5.25): + postcss-colormin@6.1.0(postcss@8.5.26): dependencies: browserslist: 4.28.6 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-convert-values@6.1.0(postcss@8.5.25): + postcss-convert-values@6.1.0(postcss@8.5.26): dependencies: browserslist: 4.28.6 - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-custom-media@11.0.6(postcss@8.5.25): + postcss-custom-media@11.0.6(postcss@8.5.26): dependencies: '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - postcss: 8.5.25 + postcss: 8.5.26 - postcss-custom-properties@14.0.6(postcss@8.5.25): + postcss-custom-properties@14.0.6(postcss@8.5.26): dependencies: '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-custom-selectors@8.0.5(postcss@8.5.25): + postcss-custom-selectors@8.0.5(postcss@8.5.26): dependencies: '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-dir-pseudo-class@9.0.1(postcss@8.5.25): + postcss-dir-pseudo-class@9.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-discard-comments@6.0.2(postcss@8.5.25): + postcss-discard-comments@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-discard-duplicates@6.0.3(postcss@8.5.25): + postcss-discard-duplicates@6.0.3(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-discard-empty@6.0.3(postcss@8.5.25): + postcss-discard-empty@6.0.3(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-discard-overridden@6.0.2(postcss@8.5.25): + postcss-discard-overridden@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-discard-unused@6.0.5(postcss@8.5.25): + postcss-discard-unused@6.0.5(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 6.1.4 - postcss-double-position-gradients@6.0.4(postcss@8.5.25): + postcss-double-position-gradients@6.0.4(postcss@8.5.26): dependencies: - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-focus-visible@10.0.1(postcss@8.5.25): + postcss-focus-visible@10.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-focus-within@9.0.1(postcss@8.5.25): + postcss-focus-within@9.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-font-variant@5.0.0(postcss@8.5.25): + postcss-font-variant@5.0.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-gap-properties@6.0.0(postcss@8.5.25): + postcss-gap-properties@6.0.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-image-set-function@7.0.0(postcss@8.5.25): + postcss-image-set-function@7.0.0(postcss@8.5.26): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-import@15.1.0(postcss@8.5.25): + postcss-import@15.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.12 - postcss-js@4.1.0(postcss@8.5.25): + postcss-js@4.1.0(postcss@8.5.26): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.25 + postcss: 8.5.26 - postcss-lab-function@7.0.12(postcss@8.5.25): + postcss-lab-function@7.0.12(postcss@8.5.26): dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - postcss-load-config@3.1.4(postcss@8.5.25): + postcss-load-config@3.1.4(postcss@8.5.26): dependencies: lilconfig: 2.1.0 yaml: 1.10.3 optionalDependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.25)(tsx@4.23.1)(yaml@2.9.0): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.26)(tsx@4.23.9)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 - postcss: 8.5.25 - tsx: 4.23.1 + postcss: 8.5.26 + tsx: 4.23.9 yaml: 2.9.0 - postcss-loader@7.3.4(@typescript/typescript6@6.0.2)(postcss@8.5.25)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + postcss-loader@7.3.4(@typescript/typescript6@6.0.2)(postcss@8.5.26)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: cosmiconfig: 8.3.6(@typescript/typescript6@6.0.2) jiti: 1.21.7 - postcss: 8.5.25 + postcss: 8.5.26 semver: 7.8.5 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - typescript - postcss-logical@8.1.0(postcss@8.5.25): + postcss-logical@8.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-merge-idents@6.0.3(postcss@8.5.25): + postcss-merge-idents@6.0.3(postcss@8.5.26): dependencies: - cssnano-utils: 4.0.2(postcss@8.5.25) - postcss: 8.5.25 + cssnano-utils: 4.0.2(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-merge-longhand@6.0.5(postcss@8.5.25): + postcss-merge-longhand@6.0.5(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - stylehacks: 6.1.1(postcss@8.5.25) + stylehacks: 6.1.1(postcss@8.5.26) - postcss-merge-rules@6.1.1(postcss@8.5.25): + postcss-merge-rules@6.1.1(postcss@8.5.26): dependencies: browserslist: 4.28.6 caniuse-api: 3.0.0 - cssnano-utils: 4.0.2(postcss@8.5.25) - postcss: 8.5.25 + cssnano-utils: 4.0.2(postcss@8.5.26) + postcss: 8.5.26 postcss-selector-parser: 6.1.4 - postcss-minify-font-values@6.1.0(postcss@8.5.25): + postcss-minify-font-values@6.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-minify-gradients@6.0.3(postcss@8.5.25): + postcss-minify-gradients@6.0.3(postcss@8.5.26): dependencies: colord: 2.9.3 - cssnano-utils: 4.0.2(postcss@8.5.25) - postcss: 8.5.25 + cssnano-utils: 4.0.2(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-minify-params@6.1.0(postcss@8.5.25): + postcss-minify-params@6.1.0(postcss@8.5.26): dependencies: browserslist: 4.28.6 - cssnano-utils: 4.0.2(postcss@8.5.25) - postcss: 8.5.25 + cssnano-utils: 4.0.2(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-minify-selectors@6.0.4(postcss@8.5.25): + postcss-minify-selectors@6.0.4(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 6.1.4 - postcss-modules-extract-imports@3.1.0(postcss@8.5.25): + postcss-modules-extract-imports@3.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-modules-local-by-default@4.2.0(postcss@8.5.25): + postcss-modules-local-by-default@4.2.0(postcss@8.5.26): dependencies: - icss-utils: 5.1.0(postcss@8.5.25) - postcss: 8.5.25 + icss-utils: 5.1.0(postcss@8.5.26) + postcss: 8.5.26 postcss-selector-parser: 7.1.4 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.5.25): + postcss-modules-scope@3.2.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-modules-values@4.0.0(postcss@8.5.25): + postcss-modules-values@4.0.0(postcss@8.5.26): dependencies: - icss-utils: 5.1.0(postcss@8.5.25) - postcss: 8.5.25 + icss-utils: 5.1.0(postcss@8.5.26) + postcss: 8.5.26 - postcss-nested@6.2.0(postcss@8.5.25): + postcss-nested@6.2.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 6.1.4 - postcss-nesting@13.0.2(postcss@8.5.25): + postcss-nesting@13.0.2(postcss@8.5.26): dependencies: '@csstools/selector-resolve-nested': 3.1.0(postcss-selector-parser@7.1.4) '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-normalize-charset@6.0.2(postcss@8.5.25): + postcss-normalize-charset@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-normalize-display-values@6.0.2(postcss@8.5.25): + postcss-normalize-display-values@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-positions@6.0.2(postcss@8.5.25): + postcss-normalize-positions@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@6.0.2(postcss@8.5.25): + postcss-normalize-repeat-style@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-string@6.0.2(postcss@8.5.25): + postcss-normalize-string@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@6.0.2(postcss@8.5.25): + postcss-normalize-timing-functions@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@6.1.0(postcss@8.5.25): + postcss-normalize-unicode@6.1.0(postcss@8.5.26): dependencies: browserslist: 4.28.6 - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-url@6.0.2(postcss@8.5.25): + postcss-normalize-url@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@6.0.2(postcss@8.5.25): + postcss-normalize-whitespace@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-opacity-percentage@3.0.0(postcss@8.5.25): + postcss-opacity-percentage@3.0.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-ordered-values@6.0.2(postcss@8.5.25): + postcss-ordered-values@6.0.2(postcss@8.5.26): dependencies: - cssnano-utils: 4.0.2(postcss@8.5.25) - postcss: 8.5.25 + cssnano-utils: 4.0.2(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-overflow-shorthand@6.0.0(postcss@8.5.25): + postcss-overflow-shorthand@6.0.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-page-break@3.0.4(postcss@8.5.25): + postcss-page-break@3.0.4(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-place@10.0.0(postcss@8.5.25): + postcss-place@10.0.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-preset-env@10.6.1(postcss@8.5.25): + postcss-preset-env@10.6.1(postcss@8.5.26): dependencies: - '@csstools/postcss-alpha-function': 1.0.1(postcss@8.5.25) - '@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.25) - '@csstools/postcss-color-function': 4.0.12(postcss@8.5.25) - '@csstools/postcss-color-function-display-p3-linear': 1.0.1(postcss@8.5.25) - '@csstools/postcss-color-mix-function': 3.0.12(postcss@8.5.25) - '@csstools/postcss-color-mix-variadic-function-arguments': 1.0.2(postcss@8.5.25) - '@csstools/postcss-content-alt-text': 2.0.8(postcss@8.5.25) - '@csstools/postcss-contrast-color-function': 2.0.12(postcss@8.5.25) - '@csstools/postcss-exponential-functions': 2.0.9(postcss@8.5.25) - '@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.25) - '@csstools/postcss-gamut-mapping': 2.0.11(postcss@8.5.25) - '@csstools/postcss-gradients-interpolation-method': 5.0.12(postcss@8.5.25) - '@csstools/postcss-hwb-function': 4.0.12(postcss@8.5.25) - '@csstools/postcss-ic-unit': 4.0.4(postcss@8.5.25) - '@csstools/postcss-initial': 2.0.1(postcss@8.5.25) - '@csstools/postcss-is-pseudo-class': 5.0.3(postcss@8.5.25) - '@csstools/postcss-light-dark-function': 2.0.11(postcss@8.5.25) - '@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.25) - '@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.25) - '@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.25) - '@csstools/postcss-logical-resize': 3.0.0(postcss@8.5.25) - '@csstools/postcss-logical-viewport-units': 3.0.4(postcss@8.5.25) - '@csstools/postcss-media-minmax': 2.0.9(postcss@8.5.25) - '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.5(postcss@8.5.25) - '@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.25) - '@csstools/postcss-normalize-display-values': 4.0.1(postcss@8.5.25) - '@csstools/postcss-oklab-function': 4.0.12(postcss@8.5.25) - '@csstools/postcss-position-area-property': 1.0.0(postcss@8.5.25) - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/postcss-property-rule-prelude-list': 1.0.0(postcss@8.5.25) - '@csstools/postcss-random-function': 2.0.1(postcss@8.5.25) - '@csstools/postcss-relative-color-syntax': 3.0.12(postcss@8.5.25) - '@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.25) - '@csstools/postcss-sign-functions': 1.1.4(postcss@8.5.25) - '@csstools/postcss-stepped-value-functions': 4.0.9(postcss@8.5.25) - '@csstools/postcss-syntax-descriptor-syntax-production': 1.0.1(postcss@8.5.25) - '@csstools/postcss-system-ui-font-family': 1.0.0(postcss@8.5.25) - '@csstools/postcss-text-decoration-shorthand': 4.0.3(postcss@8.5.25) - '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.25) - '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.25) - autoprefixer: 10.5.4(postcss@8.5.25) + '@csstools/postcss-alpha-function': 1.0.1(postcss@8.5.26) + '@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.26) + '@csstools/postcss-color-function': 4.0.12(postcss@8.5.26) + '@csstools/postcss-color-function-display-p3-linear': 1.0.1(postcss@8.5.26) + '@csstools/postcss-color-mix-function': 3.0.12(postcss@8.5.26) + '@csstools/postcss-color-mix-variadic-function-arguments': 1.0.2(postcss@8.5.26) + '@csstools/postcss-content-alt-text': 2.0.8(postcss@8.5.26) + '@csstools/postcss-contrast-color-function': 2.0.12(postcss@8.5.26) + '@csstools/postcss-exponential-functions': 2.0.9(postcss@8.5.26) + '@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.26) + '@csstools/postcss-gamut-mapping': 2.0.11(postcss@8.5.26) + '@csstools/postcss-gradients-interpolation-method': 5.0.12(postcss@8.5.26) + '@csstools/postcss-hwb-function': 4.0.12(postcss@8.5.26) + '@csstools/postcss-ic-unit': 4.0.4(postcss@8.5.26) + '@csstools/postcss-initial': 2.0.1(postcss@8.5.26) + '@csstools/postcss-is-pseudo-class': 5.0.3(postcss@8.5.26) + '@csstools/postcss-light-dark-function': 2.0.11(postcss@8.5.26) + '@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.26) + '@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.26) + '@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.26) + '@csstools/postcss-logical-resize': 3.0.0(postcss@8.5.26) + '@csstools/postcss-logical-viewport-units': 3.0.4(postcss@8.5.26) + '@csstools/postcss-media-minmax': 2.0.9(postcss@8.5.26) + '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.5(postcss@8.5.26) + '@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.26) + '@csstools/postcss-normalize-display-values': 4.0.1(postcss@8.5.26) + '@csstools/postcss-oklab-function': 4.0.12(postcss@8.5.26) + '@csstools/postcss-position-area-property': 1.0.0(postcss@8.5.26) + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/postcss-property-rule-prelude-list': 1.0.0(postcss@8.5.26) + '@csstools/postcss-random-function': 2.0.1(postcss@8.5.26) + '@csstools/postcss-relative-color-syntax': 3.0.12(postcss@8.5.26) + '@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.26) + '@csstools/postcss-sign-functions': 1.1.4(postcss@8.5.26) + '@csstools/postcss-stepped-value-functions': 4.0.9(postcss@8.5.26) + '@csstools/postcss-syntax-descriptor-syntax-production': 1.0.1(postcss@8.5.26) + '@csstools/postcss-system-ui-font-family': 1.0.0(postcss@8.5.26) + '@csstools/postcss-text-decoration-shorthand': 4.0.3(postcss@8.5.26) + '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.26) + '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.26) + autoprefixer: 10.5.4(postcss@8.5.26) browserslist: 4.28.6 - css-blank-pseudo: 7.0.1(postcss@8.5.25) - css-has-pseudo: 7.0.3(postcss@8.5.25) - css-prefers-color-scheme: 10.0.0(postcss@8.5.25) + css-blank-pseudo: 7.0.1(postcss@8.5.26) + css-has-pseudo: 7.0.3(postcss@8.5.26) + css-prefers-color-scheme: 10.0.0(postcss@8.5.26) cssdb: 8.9.0 - postcss: 8.5.25 - postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.25) - postcss-clamp: 4.1.0(postcss@8.5.25) - postcss-color-functional-notation: 7.0.12(postcss@8.5.25) - postcss-color-hex-alpha: 10.0.0(postcss@8.5.25) - postcss-color-rebeccapurple: 10.0.0(postcss@8.5.25) - postcss-custom-media: 11.0.6(postcss@8.5.25) - postcss-custom-properties: 14.0.6(postcss@8.5.25) - postcss-custom-selectors: 8.0.5(postcss@8.5.25) - postcss-dir-pseudo-class: 9.0.1(postcss@8.5.25) - postcss-double-position-gradients: 6.0.4(postcss@8.5.25) - postcss-focus-visible: 10.0.1(postcss@8.5.25) - postcss-focus-within: 9.0.1(postcss@8.5.25) - postcss-font-variant: 5.0.0(postcss@8.5.25) - postcss-gap-properties: 6.0.0(postcss@8.5.25) - postcss-image-set-function: 7.0.0(postcss@8.5.25) - postcss-lab-function: 7.0.12(postcss@8.5.25) - postcss-logical: 8.1.0(postcss@8.5.25) - postcss-nesting: 13.0.2(postcss@8.5.25) - postcss-opacity-percentage: 3.0.0(postcss@8.5.25) - postcss-overflow-shorthand: 6.0.0(postcss@8.5.25) - postcss-page-break: 3.0.4(postcss@8.5.25) - postcss-place: 10.0.0(postcss@8.5.25) - postcss-pseudo-class-any-link: 10.0.1(postcss@8.5.25) - postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.25) - postcss-selector-not: 8.0.1(postcss@8.5.25) + postcss: 8.5.26 + postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.26) + postcss-clamp: 4.1.0(postcss@8.5.26) + postcss-color-functional-notation: 7.0.12(postcss@8.5.26) + postcss-color-hex-alpha: 10.0.0(postcss@8.5.26) + postcss-color-rebeccapurple: 10.0.0(postcss@8.5.26) + postcss-custom-media: 11.0.6(postcss@8.5.26) + postcss-custom-properties: 14.0.6(postcss@8.5.26) + postcss-custom-selectors: 8.0.5(postcss@8.5.26) + postcss-dir-pseudo-class: 9.0.1(postcss@8.5.26) + postcss-double-position-gradients: 6.0.4(postcss@8.5.26) + postcss-focus-visible: 10.0.1(postcss@8.5.26) + postcss-focus-within: 9.0.1(postcss@8.5.26) + postcss-font-variant: 5.0.0(postcss@8.5.26) + postcss-gap-properties: 6.0.0(postcss@8.5.26) + postcss-image-set-function: 7.0.0(postcss@8.5.26) + postcss-lab-function: 7.0.12(postcss@8.5.26) + postcss-logical: 8.1.0(postcss@8.5.26) + postcss-nesting: 13.0.2(postcss@8.5.26) + postcss-opacity-percentage: 3.0.0(postcss@8.5.26) + postcss-overflow-shorthand: 6.0.0(postcss@8.5.26) + postcss-page-break: 3.0.4(postcss@8.5.26) + postcss-place: 10.0.0(postcss@8.5.26) + postcss-pseudo-class-any-link: 10.0.1(postcss@8.5.26) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.26) + postcss-selector-not: 8.0.1(postcss@8.5.26) - postcss-pseudo-class-any-link@10.0.1(postcss@8.5.25): + postcss-pseudo-class-any-link@10.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-reduce-idents@6.0.3(postcss@8.5.25): + postcss-reduce-idents@6.0.3(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-reduce-initial@6.1.0(postcss@8.5.25): + postcss-reduce-initial@6.1.0(postcss@8.5.26): dependencies: browserslist: 4.28.6 caniuse-api: 3.0.0 - postcss: 8.5.25 + postcss: 8.5.26 - postcss-reduce-transforms@6.0.2(postcss@8.5.25): + postcss-reduce-transforms@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-replace-overflow-wrap@4.0.0(postcss@8.5.25): + postcss-replace-overflow-wrap@4.0.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-safe-parser@7.0.1(postcss@8.5.25): + postcss-safe-parser@7.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-scss@4.0.9(postcss@8.5.25): + postcss-scss@4.0.9(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-selector-not@8.0.1(postcss@8.5.25): + postcss-selector-not@8.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 postcss-selector-parser@6.1.4: @@ -24348,31 +24301,31 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-sort-media-queries@5.2.0(postcss@8.5.25): + postcss-sort-media-queries@5.2.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 sort-css-media-queries: 2.2.0 - postcss-svgo@6.0.3(postcss@8.5.25): + postcss-svgo@6.0.3(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 svgo: 3.3.4 - postcss-unique-selectors@6.0.4(postcss@8.5.25): + postcss-unique-selectors@6.0.4(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 6.1.4 postcss-value-parser@4.2.0: {} - postcss-zindex@6.0.2(postcss@8.5.25): + postcss-zindex@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss@8.5.25: + postcss@8.5.26: dependencies: - nanoid: 3.3.16 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -24423,10 +24376,10 @@ snapshots: dependencies: prettier: 3.9.6 - prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: prettier: 3.9.6 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) prettier@3.9.6: {} @@ -24588,11 +24541,11 @@ snapshots: bytes: 3.1.2 http-errors: 2.0.1 - raw-loader@4.0.2(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + raw-loader@4.0.2(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) rc@1.2.8: dependencies: @@ -24640,11 +24593,11 @@ snapshots: dependencies: react: 19.2.8 - react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.8))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.8))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@babel/runtime': 7.29.7 react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.8)' - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) react-router-config@5.1.1(react-router@5.3.4(react@19.2.8))(react@19.2.8): dependencies: @@ -24953,35 +24906,34 @@ snapshots: robust-predicates@3.0.3: {} - rolldown@1.1.5: + rolldown@1.2.3: dependencies: - '@oxc-project/types': 0.139.0 + '@oxc-project/types': 0.143.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.5 - '@rolldown/binding-darwin-arm64': 1.1.5 - '@rolldown/binding-darwin-x64': 1.1.5 - '@rolldown/binding-freebsd-x64': 1.1.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 - '@rolldown/binding-linux-arm64-gnu': 1.1.5 - '@rolldown/binding-linux-arm64-musl': 1.1.5 - '@rolldown/binding-linux-ppc64-gnu': 1.1.5 - '@rolldown/binding-linux-s390x-gnu': 1.1.5 - '@rolldown/binding-linux-x64-gnu': 1.1.5 - '@rolldown/binding-linux-x64-musl': 1.1.5 - '@rolldown/binding-openharmony-arm64': 1.1.5 - '@rolldown/binding-wasm32-wasi': 1.1.5 - '@rolldown/binding-win32-arm64-msvc': 1.1.5 - '@rolldown/binding-win32-x64-msvc': 1.1.5 + '@rolldown/binding-android-arm64': 1.2.3 + '@rolldown/binding-darwin-arm64': 1.2.3 + '@rolldown/binding-darwin-x64': 1.2.3 + '@rolldown/binding-freebsd-x64': 1.2.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.3 + '@rolldown/binding-linux-arm64-gnu': 1.2.3 + '@rolldown/binding-linux-arm64-musl': 1.2.3 + '@rolldown/binding-linux-ppc64-gnu': 1.2.3 + '@rolldown/binding-linux-s390x-gnu': 1.2.3 + '@rolldown/binding-linux-x64-gnu': 1.2.3 + '@rolldown/binding-linux-x64-musl': 1.2.3 + '@rolldown/binding-openharmony-arm64': 1.2.3 + '@rolldown/binding-win32-arm64-msvc': 1.2.3 + '@rolldown/binding-win32-x64-msvc': 1.2.3 - rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.0): + rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.0): dependencies: open: 11.0.0 picomatch: 4.0.5 source-map: 0.7.6 yargs: 18.0.0 optionalDependencies: - rolldown: 1.1.5 + rolldown: 1.2.3 rollup: 4.62.0 rollup@4.62.0: @@ -25039,7 +24991,7 @@ snapshots: dependencies: escalade: 3.2.0 picocolors: 1.1.1 - postcss: 8.5.25 + postcss: 8.5.26 strip-json-comments: 3.1.1 run-applescript@7.1.0: {} @@ -25050,14 +25002,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 - runed@0.35.1(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + runed@0.35.1(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) optionalDependencies: - '@sveltejs/kit': 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@sveltejs/kit': 2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) rw@1.3.3: {} @@ -25334,7 +25286,7 @@ snapshots: simple-concat: 1.0.1 optional: true - simple-icons@16.27.1: {} + simple-icons@16.28.0: {} sirv@2.0.4: dependencies: @@ -25595,10 +25547,10 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - stylehacks@6.1.1(postcss@8.5.25): + stylehacks@6.1.1(postcss@8.5.26): dependencies: browserslist: 4.28.6 - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 6.1.4 stylis@4.4.0: {} @@ -25651,34 +25603,34 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-awesome@3.3.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-awesome@3.3.5(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - svelte-check@4.7.3(@typescript/typescript6@6.0.2)(picomatch@4.0.5)(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-check@4.7.4(@typescript/typescript6@6.0.2)(picomatch@4.0.5)(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: '@jridgewell/trace-mapping': 0.3.31 - '@sveltejs/load-config': 0.2.0 + '@sveltejs/load-config': 0.2.2 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.5) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.8.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-eslint-parser@1.8.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 - postcss: 8.5.25 - postcss-scss: 4.0.9(postcss@8.5.25) + postcss: 8.5.26 + postcss-scss: 4.0.9(postcss@8.5.26) postcss-selector-parser: 7.1.4 semver: 7.8.5 optionalDependencies: - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) svelte-floating-ui@1.5.8: dependencies: @@ -25691,7 +25643,7 @@ snapshots: dependencies: highlight.js: 11.11.1 - svelte-i18n@4.0.1(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-i18n@4.0.1(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: cli-color: 2.0.4 deepmerge: 4.3.1 @@ -25699,10 +25651,10 @@ snapshots: estree-walker: 2.0.2 intl-messageformat: 10.7.18 sade: 1.8.1 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) tiny-glob: 0.2.9 - svelte-jsoneditor@3.13.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-jsoneditor@3.13.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: '@codemirror/autocomplete': 6.20.3 '@codemirror/commands': 6.10.4 @@ -25729,55 +25681,55 @@ snapshots: memoize-one: 6.0.0 natural-compare-lite: 1.4.0 sass: 1.102.0 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) - svelte-awesome: 3.3.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + svelte-awesome: 3.3.5(svelte@5.56.8(@typescript-eslint/types@8.66.0)) svelte-select: 5.8.3 vanilla-picker: 2.12.3 - svelte-maplibre@1.3.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-maplibre@1.3.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: d3-geo: 3.1.1 dequal: 2.0.3 just-compare: 2.3.0 maplibre-gl: 5.24.0 pmtiles: 3.2.1 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - svelte-parse-markup@0.1.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-parse-markup@0.1.5(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - svelte-persisted-store@0.12.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-persisted-store@0.12.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) svelte-select@5.8.3: dependencies: svelte-floating-ui: 1.5.8 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-toolbelt@0.10.6(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + runed: 0.35.1(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) style-to-object: 1.0.14 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) transitivePeerDependencies: - '@sveltejs/kit' - svelte@5.56.8(@typescript-eslint/types@8.65.0): + svelte@5.56.8(@typescript-eslint/types@8.66.0): dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.11(acorn@8.18.0) + '@sveltejs/acorn-typescript': 1.0.12(acorn@8.18.0) '@types/estree': 1.0.9 '@types/trusted-types': 2.0.7 acorn: 8.18.0 aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.8.2 + devalue: 5.9.0 esm-env: 1.2.2 - esrap: 2.2.13(@typescript-eslint/types@8.65.0) + esrap: 2.2.13(@typescript-eslint/types@8.66.0) is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 @@ -25820,27 +25772,26 @@ snapshots: tailwind-merge@3.6.0: {} - tailwind-variants@3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.3): - dependencies: - tailwindcss: 4.3.3 + tailwind-variants@3.3.1(tailwind-merge@3.6.0)(tailwindcss@4.3.3): optionalDependencies: tailwind-merge: 3.6.0 + tailwindcss: 4.3.3 - tailwindcss-email-variants@3.0.5(tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0)): + tailwindcss-email-variants@3.0.5(tailwindcss@3.4.19(tsx@4.23.9)(yaml@2.9.0)): dependencies: - tailwindcss: 3.4.19(tsx@4.23.1)(yaml@2.9.0) + tailwindcss: 3.4.19(tsx@4.23.9)(yaml@2.9.0) - tailwindcss-mso@2.0.3(tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0)): + tailwindcss-mso@2.0.3(tailwindcss@3.4.19(tsx@4.23.9)(yaml@2.9.0)): dependencies: - tailwindcss: 3.4.19(tsx@4.23.1)(yaml@2.9.0) + tailwindcss: 3.4.19(tsx@4.23.9)(yaml@2.9.0) - tailwindcss-preset-email@1.4.1(tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0)): + tailwindcss-preset-email@1.4.1(tailwindcss@3.4.19(tsx@4.23.9)(yaml@2.9.0)): dependencies: - tailwindcss: 3.4.19(tsx@4.23.1)(yaml@2.9.0) - tailwindcss-email-variants: 3.0.5(tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0)) - tailwindcss-mso: 2.0.3(tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0)) + tailwindcss: 3.4.19(tsx@4.23.9)(yaml@2.9.0) + tailwindcss-email-variants: 3.0.5(tailwindcss@3.4.19(tsx@4.23.9)(yaml@2.9.0)) + tailwindcss-mso: 2.0.3(tailwindcss@3.4.19(tsx@4.23.9)(yaml@2.9.0)) - tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0): + tailwindcss@3.4.19(tsx@4.23.9)(yaml@2.9.0): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -25856,11 +25807,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.25 - postcss-import: 15.1.0(postcss@8.5.25) - postcss-js: 4.1.0(postcss@8.5.25) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.25)(tsx@4.23.1)(yaml@2.9.0) - postcss-nested: 6.2.0(postcss@8.5.25) + postcss: 8.5.26 + postcss-import: 15.1.0(postcss@8.5.26) + postcss-js: 4.1.0(postcss@8.5.26) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.26)(tsx@4.23.9)(yaml@2.9.0) + postcss-nested: 6.2.0(postcss@8.5.26) postcss-selector-parser: 6.1.4 resolve: 1.22.12 sucrase: 3.35.1 @@ -25884,7 +25835,7 @@ snapshots: pump: 3.0.4 tar-stream: 3.2.0 optionalDependencies: - bare-fs: 4.7.4 + bare-fs: 4.8.0 bare-path: 3.1.1 transitivePeerDependencies: - bare-abort-controller @@ -25902,7 +25853,7 @@ snapshots: tar-stream@3.2.0: dependencies: b4a: 1.8.1 - bare-fs: 4.7.4 + bare-fs: 4.8.0 fast-fifo: 1.3.2 streamx: 2.28.0 transitivePeerDependencies: @@ -25934,40 +25885,40 @@ snapshots: - bare-abort-controller - react-native-b4a - terser-webpack-plugin@5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + terser-webpack-plugin@5.6.1(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)(webpack@5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.49.0 - webpack: 5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) optionalDependencies: - '@swc/core': 1.15.46(@swc/helpers@0.5.23) + '@swc/core': 1.15.47(@swc/helpers@0.5.23) clean-css: 5.3.3 - cssnano: 6.1.2(postcss@8.5.25) + cssnano: 6.1.2(postcss@8.5.26) csso: 5.0.5 - esbuild: 0.28.1 + esbuild: 0.28.2 html-minifier-terser: 7.2.0 lightningcss: 1.33.0 - postcss: 8.5.25 + postcss: 8.5.26 uglify-js: 3.19.3 - terser-webpack-plugin@5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + terser-webpack-plugin@5.6.1(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.49.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) optionalDependencies: - '@swc/core': 1.15.46(@swc/helpers@0.5.23) + '@swc/core': 1.15.47(@swc/helpers@0.5.23) clean-css: 5.3.3 - cssnano: 6.1.2(postcss@8.5.25) + cssnano: 6.1.2(postcss@8.5.26) csso: 5.0.5 - esbuild: 0.28.1 + esbuild: 0.28.2 html-minifier-terser: 7.2.0 lightningcss: 1.33.0 - postcss: 8.5.25 + postcss: 8.5.26 uglify-js: 3.19.3 terser@5.49.0: @@ -25977,7 +25928,7 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 - testcontainers@12.0.4(supports-color@8.1.1): + testcontainers@12.1.0(supports-color@8.1.1): dependencies: '@balena/dockerignore': 1.0.2 '@types/dockerode': 4.0.1 @@ -25993,7 +25944,7 @@ snapshots: ssh-remote-port-forward: 1.0.4 tar-fs: 3.1.3 tmp: 0.2.7 - undici: 8.7.0 + undici: 8.10.0 transitivePeerDependencies: - bare-abort-controller - bare-buffer @@ -26165,9 +26116,9 @@ snapshots: tsscmp@1.0.6: {} - tsx@4.23.1: + tsx@4.23.9: dependencies: - esbuild: 0.28.1 + esbuild: 0.28.2 optionalDependencies: fsevents: 2.3.3 @@ -26217,12 +26168,12 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): + typescript-eslint@8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/parser': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/parser': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/typescript-estree': 8.66.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/utils': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: @@ -26280,7 +26231,7 @@ snapshots: undici@6.26.0: {} - undici@8.7.0: {} + undici@8.10.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -26371,10 +26322,10 @@ snapshots: unpipe@1.0.0: {} - unplugin-swc@1.5.9(@swc/core@1.15.46(@swc/helpers@0.5.23))(rollup@4.62.0): + unplugin-swc@1.5.10(@swc/core@1.15.47(@swc/helpers@0.5.23))(rollup@4.62.0): dependencies: '@rollup/pluginutils': 5.4.0(rollup@4.62.0) - '@swc/core': 1.15.46(@swc/helpers@0.5.23) + '@swc/core': 1.15.47(@swc/helpers@0.5.23) load-tsconfig: 0.2.5 unplugin: 2.3.11 transitivePeerDependencies: @@ -26416,14 +26367,14 @@ snapshots: dependencies: punycode: 2.3.1 - url-loader@4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) optionalDependencies: - file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) url@0.11.4: dependencies: @@ -26505,13 +26456,13 @@ snapshots: transitivePeerDependencies: - rollup - vite-node@3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): + vite-node@3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0): dependencies: cac: 6.7.14 debug: 4.4.3(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -26526,22 +26477,22 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@6.1.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): + vite-tsconfig-paths@6.1.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1)(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)): dependencies: debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.1.6(@typescript/typescript6@6.0.2) - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) transitivePeerDependencies: - supports-color - typescript - vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): + vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0): dependencies: - esbuild: 0.28.1 + esbuild: 0.28.2 fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 - postcss: 8.5.25 + postcss: 8.5.26 rollup: 4.62.0 tinyglobby: 0.2.17 optionalDependencies: @@ -26551,39 +26502,39 @@ snapshots: lightningcss: 1.33.0 sass: 1.102.0 terser: 5.49.0 - tsx: 4.23.1 + tsx: 4.23.9 yaml: 2.9.0 - vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): + vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.25 - rolldown: 1.1.5 + postcss: 8.5.26 + rolldown: 1.2.3 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.3 - esbuild: 0.28.1 + esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.7.0 sass: 1.102.0 terser: 5.49.0 - tsx: 4.23.1 + tsx: 4.23.9 yaml: 2.9.0 - vitefu@1.1.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): + vitefu@1.1.3(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)): optionalDependencies: - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) vitest-fetch-mock@0.4.5(vitest@4.1.10): dependencies: - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) - vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): + vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 - '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.7 '@vitest/runner': 3.2.7 '@vitest/snapshot': 3.2.7 @@ -26601,8 +26552,8 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) - vite-node: 3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.13 @@ -26623,10 +26574,10 @@ snapshots: - tsx - yaml - vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -26643,7 +26594,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -26654,10 +26605,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -26674,7 +26625,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -26733,7 +26684,7 @@ snapshots: - bufferutil - utf-8-validate - webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: colorette: 2.0.20 memfs: 4.64.0(tslib@2.8.1) @@ -26742,11 +26693,11 @@ snapshots: range-parser: 1.3.0 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - tslib - webpack-dev-server@5.2.6(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + webpack-dev-server@5.2.6(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -26774,10 +26725,10 @@ snapshots: serve-index: 1.9.2(supports-color@8.1.1) sockjs: 0.3.24 spdy: 4.0.2(supports-color@8.1.1) - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) ws: 8.21.1 optionalDependencies: - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - bufferutil - debug @@ -26803,7 +26754,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3): + webpack@5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.9 @@ -26826,7 +26777,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 - terser-webpack-plugin: 5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + terser-webpack-plugin: 5.6.1(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)(webpack@5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) watchpack: 2.5.2 webpack-sources: 3.5.1 transitivePeerDependencies: @@ -26843,7 +26794,7 @@ snapshots: - postcss - uglify-js - webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3): + webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3): dependencies: '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 @@ -26861,7 +26812,7 @@ snapshots: graceful-fs: 4.2.11 loader-runner: 4.3.2 mime-db: 1.54.0 - minimizer-webpack-plugin: 5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + minimizer-webpack-plugin: 5.6.1(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 @@ -26881,14 +26832,14 @@ snapshots: - postcss - uglify-js - webpackbar@7.0.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + webpackbar@7.0.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: ansis: 3.17.0 consola: 3.4.2 pretty-time: 1.1.0 std-env: 3.10.0 optionalDependencies: - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) websocket-driver@0.7.5: dependencies: From 7fea9f20664f3c554f3cadd77220079089dda825 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 12:02:06 +0200 Subject: [PATCH 23/49] fix(deps): update dependency @immich/ui to ^0.85.0 (#30705) --- pnpm-lock.yaml | 17 ++++++++++++----- web/package.json | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54faa1fa2d..1e147c2502 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -798,8 +798,8 @@ importers: specifier: workspace:* version: link:../packages/sdk '@immich/ui': - specifier: ^0.83.0 - version: 0.83.0(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) + specifier: ^0.85.0 + version: 0.85.0(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) '@mapbox/mapbox-gl-rtl-text': specifier: 0.4.0 version: 0.4.0 @@ -3104,8 +3104,8 @@ packages: resolution: {integrity: sha512-O1SJ+BbeFVsUTF4af1MfagJZM+lPgLjI8lQ3SZNjpo8SGJReSbUl2ii03OKuGni/G0yp2GnRLpOTNSHYGtVrcg==} hasBin: true - '@immich/ui@0.83.0': - resolution: {integrity: sha512-Xh3R3yhn8/Qyq8lGWIiVmturA+dHXtmW/h++AGb2UAwS/58qu7ssocO/HWIamaSpkgjPGotlWpPIpUJq1zh9FQ==} + '@immich/ui@0.85.0': + resolution: {integrity: sha512-KCAEtVGexZ1G+nuT25bv1QS6Rc2wOlLymp4qDBZVqMxu3o3RWpUzafRHvZt5g4FnlAgfHZdXAIHVVrhJzkrlDA==} peerDependencies: '@sveltejs/kit': ^2.13.0 svelte: ^5.0.0 @@ -7996,6 +7996,10 @@ packages: resolution: {integrity: sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==} engines: {node: '>=18'} + fuse.js@7.5.0: + resolution: {integrity: sha512-sQtrEfA+ez/3G0cCZecF70oqpCRttCexYUG4mUrtWL49ULUzUyxokt5kyqwtKzj1270RaKih+hcP3qLcumccow==} + engines: {node: '>=10'} + gauge@3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} @@ -15866,12 +15870,13 @@ snapshots: pg-connection-string: 2.14.0 postgres: 3.4.9 - '@immich/ui@0.83.0(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))': + '@immich/ui@0.85.0(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))': dependencies: '@internationalized/date': 3.12.2 '@mdi/js': 7.4.47 '@sveltejs/kit': 2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) bits-ui: 2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) + fuse.js: 7.5.0 luxon: 3.7.2 simple-icons: 16.28.0 svelte: 5.56.8(@typescript-eslint/types@8.66.0) @@ -21143,6 +21148,8 @@ snapshots: function-timeout@1.0.2: {} + fuse.js@7.5.0: {} + gauge@3.0.2: dependencies: aproba: 2.1.0 diff --git a/web/package.json b/web/package.json index 338e2f5abe..d709ca0de6 100644 --- a/web/package.json +++ b/web/package.json @@ -27,7 +27,7 @@ "@formatjs/icu-messageformat-parser": "^3.0.0", "@immich/justified-layout-wasm": "^0.4.3", "@immich/sdk": "workspace:*", - "@immich/ui": "^0.83.0", + "@immich/ui": "^0.85.0", "@mapbox/mapbox-gl-rtl-text": "0.4.0", "@mdi/js": "^7.4.47", "@noble/hashes": "^2.2.0", From c89104d144c568a52a36d9eebf83bb378ce81bea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 12:21:04 +0200 Subject: [PATCH 24/49] chore(deps): update ghcr.io/jdx/mise docker tag to v2026.8.3 (#30699) --- server/Dockerfile | 2 +- server/Dockerfile.dev | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Dockerfile b/server/Dockerfile index 802877421d..9d5eb1cccb 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -56,7 +56,7 @@ FROM builder AS plugins ARG TARGETPLATFORM -COPY --from=ghcr.io/jdx/mise:2026.8.1@sha256:b2297770273f71e685b8056e3b07bfda4ffc35f0fb62e0339b3cdc1e5766e2fe /usr/local/bin/mise /usr/local/bin/mise +COPY --from=ghcr.io/jdx/mise:2026.8.3@sha256:92dbc3f2573926d8974e4641ad8449f16c323130b9f41c39aff19b7b2f500ef6 /usr/local/bin/mise /usr/local/bin/mise WORKDIR /app COPY ./mise.toml ./mise.toml diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev index 23a565b4bb..9a46f41b4e 100644 --- a/server/Dockerfile.dev +++ b/server/Dockerfile.dev @@ -2,7 +2,7 @@ FROM ghcr.io/immich-app/base-server-dev:202607211135@sha256:83c9ff3f7390111596a2dcd24a746c3f8618d58259dbc7aae36618d153462f18 AS dev -COPY --from=ghcr.io/jdx/mise:2026.8.1@sha256:b2297770273f71e685b8056e3b07bfda4ffc35f0fb62e0339b3cdc1e5766e2fe /usr/local/bin/mise /usr/local/bin/mise +COPY --from=ghcr.io/jdx/mise:2026.8.3@sha256:92dbc3f2573926d8974e4641ad8449f16c323130b9f41c39aff19b7b2f500ef6 /usr/local/bin/mise /usr/local/bin/mise RUN echo "devdir=/buildcache/node-gyp" >> /usr/local/etc/npmrc && \ echo "store-dir=/buildcache/pnpm-store" >> /usr/local/etc/npmrc && \ From 23636382c49d7670dc73aa6a11ce3c3225de7714 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 12:27:42 +0200 Subject: [PATCH 25/49] chore(deps): update node.js to v24.19.0 (#30703) --- packages/cli/Dockerfile | 2 +- packages/e2e-auth-server/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/Dockerfile b/packages/cli/Dockerfile index 716522ecd7..c1c320fc11 100644 --- a/packages/cli/Dockerfile +++ b/packages/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.18.0-trixie-slim@sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573 AS core +FROM node:24.19.0-trixie-slim@sha256:0711b541c1c33a8a530ac4f0d391baa9a15b3d804695b1b24a47daa5fb60e74d AS core WORKDIR /usr/src/app COPY package* pnpm* .pnpmfile.cjs ./ diff --git a/packages/e2e-auth-server/Dockerfile b/packages/e2e-auth-server/Dockerfile index bfc939c084..087fa45d23 100644 --- a/packages/e2e-auth-server/Dockerfile +++ b/packages/e2e-auth-server/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.18.0-trixie-slim@sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573 +FROM node:24.19.0-trixie-slim@sha256:0711b541c1c33a8a530ac4f0d391baa9a15b3d804695b1b24a47daa5fb60e74d WORKDIR /usr/src/app COPY package* pnpm* .pnpmfile.cjs ./ COPY ./packages ./packages/ From 427591fb9e0ce846817d9297b4b3e02f7e45a163 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 12:28:01 +0200 Subject: [PATCH 26/49] chore(deps): update grafana/grafana docker tag to v12.4.7-ubuntu (#30700) --- 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 779065e327..b717de02f9 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -97,7 +97,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:12.4.6-ubuntu@sha256:124ddf024f95e61b562a02112b47bc64de75973370cf440b1356d31cd8178844 + image: grafana/grafana:12.4.7-ubuntu@sha256:d327f509400334fbea74a282d287f20c3187222741ec99e76f253cac16c90129 volumes: - grafana-data:/var/lib/grafana From b4c27ee0b46b2ebc7785ec6cc67734b36ef09b9c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 12:28:58 +0200 Subject: [PATCH 27/49] chore(deps): update github-actions (#30702) --- .github/workflows/build-mobile.yml | 4 ++-- .github/workflows/check-openapi.yml | 2 +- .github/workflows/close-duplicates.yml | 2 +- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/docker.yml | 2 +- .github/workflows/docs-build.yml | 2 +- .github/workflows/static_analysis.yml | 2 +- .github/workflows/test.yml | 6 +++--- .github/workflows/weblate-lock.yml | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 2c6d889d7b..602ffb3c18 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -59,7 +59,7 @@ jobs: - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4 + uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -106,7 +106,7 @@ jobs: working-directory: ./mobile run: printf "%s" $KEY_JKS | base64 -d > android/key.jks - - uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: 'zulu' java-version: '17' diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml index a38f131a71..522444db97 100644 --- a/.github/workflows/check-openapi.yml +++ b/.github/workflows/check-openapi.yml @@ -25,7 +25,7 @@ jobs: persist-credentials: false - name: Check for breaking API changes - uses: oasdiff/oasdiff-action/breaking@0ab8ad204b00d25acc5ae87106281433e288d0c1 # v0.1.10 + uses: oasdiff/oasdiff-action/breaking@b7c3adeb54330db1903d27c61db520e5661ad55b # v0.1.11 with: base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json revision: open-api/immich-openapi-specs.json diff --git a/.github/workflows/close-duplicates.yml b/.github/workflows/close-duplicates.yml index cf932dac8d..a1ec4e80c8 100644 --- a/.github/workflows/close-duplicates.yml +++ b/.github/workflows/close-duplicates.yml @@ -39,7 +39,7 @@ jobs: needs: [get_body, should_run] if: ${{ needs.should_run.outputs.should_run == 'true' }} container: - image: ghcr.io/immich-app/mdq:main@sha256:d603f5618c1fc9f2b4d506ffed54ba6bcfec83d56b893b5fa8b3f899a402fa0a + image: ghcr.io/immich-app/mdq:main@sha256:74b93dfbf13f8ee73eb35d8d5195ff667f93f9bc55df5ae453dd43a461c821fa outputs: checked: ${{ steps.get_checkbox.outputs.checked }} steps: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7e14227a51..522de204ea 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -71,7 +71,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 + uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -84,6 +84,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 657d61d647..dc61607cc6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -31,7 +31,7 @@ jobs: - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4 + uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0 with: github-token: ${{ steps.token.outputs.token }} filters: | diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 6d7bd497d7..e0c5de3fab 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -29,7 +29,7 @@ jobs: - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4 + uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0 with: github-token: ${{ steps.token.outputs.token }} filters: | diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 3004f0323f..0a3c61668c 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -28,7 +28,7 @@ jobs: - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4 + uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0 with: github-token: ${{ steps.token.outputs.token }} filters: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index acf3a8d7f1..cb3448e870 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4 + uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -419,7 +419,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - name: Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -498,7 +498,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - name: Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index 9447f4dea0..94a81f8f5d 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -32,7 +32,7 @@ jobs: - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4 + uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0 with: github-token: ${{ steps.token.outputs.token }} filters: | From 9273a4ca2ad7d75dcc7d8377aa19f75b5cca6f75 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Tue, 11 Aug 2026 14:40:12 +0200 Subject: [PATCH 28/49] chore: rework docker build workflow (#30626) --- .github/workflows/docker.yml | 144 ++++++++++++++++++++++++----------- 1 file changed, 100 insertions(+), 44 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index dc61607cc6..90ba3f40d6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -14,6 +14,10 @@ concurrency: permissions: {} +env: + GHCR_NAMESPACE: ghcr.io/${{ github.repository_owner }} + DOCKERHUB_NAMESPACE: docker.io/altran1502 + jobs: pre-job: runs-on: ubuntu-latest @@ -44,8 +48,6 @@ jobs: - 'machine-learning/**' force-filters: | - '.github/workflows/docker.yml' - - '.github/workflows/multi-runner-build.yml' - - '.github/actions/image-build' force-events: 'workflow_dispatch,release' retag_ml: @@ -58,7 +60,30 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn'] + include: &ml-variants + - image: immich-machine-learning + device: cpu + - image: immich-machine-learning + device: cuda + suffixes: '-cuda' + platforms: linux/amd64 + - image: immich-machine-learning + device: openvino + suffixes: '-openvino' + platforms: linux/amd64 + - image: immich-machine-learning + device: armnn + suffixes: '-armnn' + platforms: linux/arm64 + - image: immich-machine-learning + device: rknn + suffixes: '-rknn' + platforms: linux/arm64 + - image: immich-machine-learning + device: rocm + suffixes: '-rocm' + platforms: linux/amd64 + runner-mapping: '{"linux/amd64": "pokedex-large"}' steps: - name: Login to GitHub Container Registry uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 @@ -70,10 +95,10 @@ jobs: - name: Re-tag image env: REGISTRY_NAME: 'ghcr.io' - REPOSITORY: ${{ github.repository_owner }}/immich-machine-learning - TAG_OLD: main${{ matrix.suffix }} - TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }} - TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }} + REPOSITORY: ${{ github.repository_owner }}/${{ matrix.image }} + TAG_OLD: main${{ matrix.suffixes }} + TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffixes }} + TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffixes }} run: | docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" @@ -86,9 +111,6 @@ jobs: packages: write if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == false && !github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest - strategy: - matrix: - suffix: [''] steps: - name: Login to GitHub Container Registry uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 @@ -101,9 +123,9 @@ jobs: env: REGISTRY_NAME: 'ghcr.io' REPOSITORY: ${{ github.repository_owner }}/immich-server - TAG_OLD: main${{ matrix.suffix }} - TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }} - TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }} + TAG_OLD: main + TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }} + TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }} run: | docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" @@ -115,40 +137,27 @@ jobs: strategy: fail-fast: false matrix: - include: - - device: cpu - - device: cuda - suffixes: '-cuda' - platforms: linux/amd64 - - device: openvino - suffixes: '-openvino' - platforms: linux/amd64 - - device: armnn - suffixes: '-armnn' - platforms: linux/arm64 - - device: rknn - suffixes: '-rknn' - platforms: linux/arm64 - - device: rocm - suffixes: '-rocm' - platforms: linux/amd64 - runner-mapping: '{"linux/amd64": "pokedex-large"}' - uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@50dc3a14f0606ecd8fc28d78d3a3c655115ab695 # multi-runner-build-workflow-v3.1.0 + include: *ml-variants + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@4d420cf8e5ea499f99b2c2849ff9a219d5f7b178 # multi-runner-build-workflow-v4.0.0 permissions: contents: read actions: read packages: write - secrets: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} with: - image: immich-machine-learning + image: ${{ matrix.image }} context: machine-learning dockerfile: machine-learning/Dockerfile platforms: ${{ matrix.platforms }} runner-mapping: ${{ matrix.runner-mapping }} suffixes: ${{ matrix.suffixes }} - dockerhub-push: ${{ github.event_name == 'release' && !github.event.release.prerelease }} + tags: &tag-rules | + type=ref,event=branch + type=ref,event=pr + type=sha,format=long,prefix=commit- + type=semver,pattern={{version}},prefix=v + type=semver,pattern={{major}},prefix=v + type=match,pattern=v(\d+),group=1,prefix=v,suffix=-rc,enable=${{ github.event.release.prerelease == true }} + type=raw,value=release,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }} build-args: | DEVICE=${{ matrix.device }} @@ -156,25 +165,72 @@ jobs: name: Build and Push Server needs: pre-job if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }} - uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@50dc3a14f0606ecd8fc28d78d3a3c655115ab695 # multi-runner-build-workflow-v3.1.0 + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@4d420cf8e5ea499f99b2c2849ff9a219d5f7b178 # multi-runner-build-workflow-v4.0.0 permissions: contents: read actions: read packages: write - secrets: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} with: image: immich-server context: . dockerfile: server/Dockerfile - dockerhub-push: ${{ github.event_name == 'release' && !github.event.release.prerelease }} + tags: *tag-rules build-args: | DEVICE=cpu + mirror: + name: Mirror to Docker Hub + needs: [machine-learning, server] + if: ${{ github.event_name == 'release' && github.event.release.prerelease == false }} + permissions: + packages: read + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Slightly hacky; we reuse the ml matrix but tack on immich-server too + image: [immich-server] + include: *ml-variants + env: + IMAGE: ${{ matrix.image }} + SUFFIX: ${{ matrix.suffixes }} + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Docker Hub + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Generate docker image tags + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + flavor: | + latest=false + images: ${{ env.GHCR_NAMESPACE }}/${{ env.IMAGE }} + tags: *tag-rules + + - name: Mirror tags to Docker Hub + env: + TAGS: ${{ steps.meta.outputs.tags }} + run: | + for tag in ${TAGS}; do + version="${tag##*:}" + docker buildx imagetools create \ + -t "${DOCKERHUB_NAMESPACE}/${IMAGE}:${version}${SUFFIX}" \ + "${GHCR_NAMESPACE}/${IMAGE}:${version}${SUFFIX}" + done + success-check-server: name: Docker Build & Push Server Success - needs: [server, retag_server] + needs: [server, retag_server, mirror] permissions: {} runs-on: ubuntu-latest if: always() @@ -185,7 +241,7 @@ jobs: success-check-ml: name: Docker Build & Push ML Success - needs: [machine-learning, retag_ml] + needs: [machine-learning, retag_ml, mirror] permissions: {} runs-on: ubuntu-latest if: always() From cf17dcbfee1366a2f19bf82d385cf4e85df880d3 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Tue, 11 Aug 2026 14:45:32 +0200 Subject: [PATCH 29/49] feat: derive mobile build number from semver (#30569) --- .github/workflows/prepare-release.yml | 7 +-- packages/scripts/src/cli.ts | 43 ++++++-------- packages/scripts/src/commands/release.spec.ts | 58 +++++++++++++++++++ packages/scripts/src/commands/release.ts | 35 ++++++++--- packages/scripts/src/types.ts | 2 +- 5 files changed, 104 insertions(+), 41 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 1bf9a61e8c..4a405d8ccb 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -17,10 +17,6 @@ on: - prepatch - prerelease - release - mobileBump: - description: 'Bump mobile build number' - required: false - type: boolean skipTranslations: description: 'Skip translations' required: false @@ -79,8 +75,7 @@ jobs: - name: Bump version env: RELEASE_TYPE: ${{ inputs.releaseType }} - MOBILE_BUMP: ${{ inputs.mobileBump }} - run: mise //:release --type "${RELEASE_TYPE}" --mobile "${MOBILE_BUMP}" + run: mise //:release --type "${RELEASE_TYPE}" - id: output run: | diff --git a/packages/scripts/src/cli.ts b/packages/scripts/src/cli.ts index c89a0f142c..95e50e9770 100644 --- a/packages/scripts/src/cli.ts +++ b/packages/scripts/src/cli.ts @@ -23,32 +23,25 @@ export const cli = (argv: string[]) => { .choices(RELEASE_TYPES) .makeOptionMandatory(), ) - .addOption( - new Option('-m, --mobile ', 'pump mobile build number') - .choices(['true', 'false']) - .default('false'), - ) - .action( - ({ type, mobile }: { type: ReleaseType; mobile: 'true' | 'false' }) => { - try { - console.log(handleRelease({ type, mobile: mobile === 'true' })); - } catch (error) { - if (error instanceof ReleaseInputError) { - console.log(program.usage()); - process.exit(1); - } - - if (error instanceof ReleaseError) { - console.log( - `Invalid pump: ${type}. Pumping from ${error.version} to ${error.newVersion} is not allowed.`, - ); - process.exit(1); - } - - throw error; + .action(({ type }: { type: ReleaseType }) => { + try { + console.log(handleRelease({ type })); + } catch (error) { + if (error instanceof ReleaseInputError) { + console.log(program.usage()); + process.exit(1); } - }, - ); + + if (error instanceof ReleaseError) { + console.log( + `Invalid pump: ${type}. Pumping from ${error.version} to ${error.newVersion} is not allowed.`, + ); + process.exit(1); + } + + throw error; + } + }); return program.parse(argv); }; diff --git a/packages/scripts/src/commands/release.spec.ts b/packages/scripts/src/commands/release.spec.ts index de769ebdc6..0b1be22302 100644 --- a/packages/scripts/src/commands/release.spec.ts +++ b/packages/scripts/src/commands/release.spec.ts @@ -1,11 +1,16 @@ +import semver from 'semver'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { resolveArchivedVersions, ArchivedVersion, + getMobileBuild, getNewVersion, ReleaseError, } from './release'; +const mobileBuild = (version: string) => + getMobileBuild(semver.parse(version) as semver.SemVer); + const archived = (label: string): ArchivedVersion => ({ label, url: `https://docs.${label}.archive.immich.app`, @@ -137,3 +142,56 @@ describe(resolveArchivedVersions.name, () => { ]); }); }); + +describe(getMobileBuild.name, () => { + it('should encode the version', () => { + expect(mobileBuild('3.1.0')).toBe(3_010_099); + expect(mobileBuild('3.1.4')).toBe(3_010_499); + expect(mobileBuild('3.2.0-rc.0')).toBe(3_020_000); + expect(mobileBuild('3.2.0-rc.3')).toBe(3_020_003); + }); + + it('should be above the last hand-maintained build number', () => { + expect(mobileBuild('3.1.0')).toBeGreaterThan(3057); + }); + + it('should stay under the play store limit', () => { + expect(mobileBuild('99.99.99')).toBeLessThan(2_100_000_000); + }); + + it('should increase across a release cycle', () => { + const versions = [ + '3.1.0', + '3.1.1', + '3.2.0-rc.0', + '3.2.0-rc.1', + '3.2.0', + '3.2.1', + '3.3.0-rc.0', + '4.0.0-rc.0', + '4.0.0', + ]; + + const builds = versions.map((version) => mobileBuild(version)); + expect(builds).toEqual([...builds].sort((a, b) => a - b)); + expect(new Set(builds).size).toBe(builds.length); + }); + + it('should rank a release above its own candidates', () => { + expect(mobileBuild('3.2.0')).toBeGreaterThan(mobileBuild('3.2.0-rc.98')); + }); + + it('should rank a patch on an older line below the newer line', () => { + expect(mobileBuild('3.1.5')).toBeLessThan(mobileBuild('3.2.0')); + }); + + it('should refuse versions it cannot encode', () => { + expect(() => mobileBuild('100.0.0')).toThrow('Cannot derive'); + expect(() => mobileBuild('3.100.0')).toThrow('Cannot derive'); + expect(() => mobileBuild('3.1.100')).toThrow('Cannot derive'); + expect(() => mobileBuild('3.1.0-rc.99')).toThrow('Cannot derive'); + expect(() => mobileBuild('3.1.0-beta')).toThrow('Cannot derive'); + expect(() => mobileBuild('3.1.0-rc.-1')).toThrow('Cannot derive'); + expect(() => mobileBuild('3.1.0-rc.x')).toThrow('Cannot derive'); + }); +}); diff --git a/packages/scripts/src/commands/release.ts b/packages/scripts/src/commands/release.ts index 68ef1dbf0e..307b66165a 100644 --- a/packages/scripts/src/commands/release.ts +++ b/packages/scripts/src/commands/release.ts @@ -35,7 +35,7 @@ const Files = { }, }; -export const handleRelease = ({ type, mobile }: ReleaseOptions) => { +export const handleRelease = ({ type }: ReleaseOptions) => { const versionRaw = getVersion(); const newVersionRaw = getNewVersion(versionRaw, type); const newVersion = semver.parse(normalize(newVersionRaw)); @@ -43,8 +43,7 @@ export const handleRelease = ({ type, mobile }: ReleaseOptions) => { throw new ReleaseInputError(); } const newVersionNoRc = `${newVersion.major}.${newVersion.minor}.${newVersion.patch}`; - const mobileBuild = getMobileBuild(); - const newMobileBuild = mobile ? mobileBuild + 1 : mobileBuild; + const newMobileBuild = getMobileBuild(newVersion); // pump versions everywhere @@ -189,14 +188,32 @@ export const getNewVersion = (versionRaw: string, type: string) => { return newVersionRaw; }; -const getMobileBuild = () => { - const pubspec = new TextFile(Files.Mobile.Pubspec).read(); - const match = pubspec.match(/^version: .*\+(\d+)$/m); - if (!match) { - throw new Error('Could not find mobile build number in pubspec.yaml'); +const RADIX = 100; +const STABLE = RADIX - 1; + +export const getMobileBuild = (version: SemVer) => { + const { major, minor, patch, prerelease } = version; + const candidate = prerelease[1]; + const digit = typeof candidate === 'number' ? candidate : STABLE; + + const valid = + major < RADIX && + minor < RADIX && + patch < RADIX && + (prerelease.length === 0 || digit < STABLE); + + if (!valid) { + throw new Error( + `Cannot derive a mobile build number from ${version.format()}`, + ); } - return Number(match[1]); + return ( + major * Math.pow(RADIX, 3) + + minor * Math.pow(RADIX, 2) + + patch * RADIX + + digit + ); }; const pump = (path: string, pattern: RegExp, replacement: string) => { diff --git a/packages/scripts/src/types.ts b/packages/scripts/src/types.ts index 9bc23f87b5..586fdb8763 100644 --- a/packages/scripts/src/types.ts +++ b/packages/scripts/src/types.ts @@ -11,7 +11,7 @@ export const RELEASE_TYPES = [ ] as const; export type ReleaseType = (typeof RELEASE_TYPES)[number]; -export type ReleaseOptions = { type: string; mobile: boolean }; +export type ReleaseOptions = { type: string }; export class TextFile { constructor(private file: string) {} From 9773d72428e279eadbd1c2a81cda7ef092215f05 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Tue, 11 Aug 2026 14:55:27 +0200 Subject: [PATCH 30/49] chore: use gh release create instead of softprops action (#30577) --- .github/workflows/prepare-release.yml | 34 +++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 4a405d8ccb..98a3daa3b2 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -147,19 +147,23 @@ jobs: github-token: ${{ steps.generate-token.outputs.token }} - name: Create draft release - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 - with: - draft: true - prerelease: ${{ needs.bump_version.outputs.rc }} - tag_name: ${{ needs.bump_version.outputs.version }} - token: ${{ steps.generate-token.outputs.token }} - generate_release_notes: true - body_path: misc/release/notes.tmpl - files: | - docker/docker-compose.yml - docker/docker-compose.rootless.yml - docker/example.env - docker/hwaccel.ml.yml - docker/hwaccel.transcoding.yml - docker/prometheus.yml + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + REPO: ${{ github.repository }} + TAG: ${{ needs.bump_version.outputs.version }} + PRERELEASE: ${{ needs.bump_version.outputs.rc }} + run: | + gh release create "${TAG}" \ + --repo "${REPO}" \ + --draft \ + --verify-tag \ + --prerelease="${PRERELEASE}" \ + --notes-file misc/release/notes.tmpl \ + --generate-notes \ + docker/docker-compose.yml \ + docker/docker-compose.rootless.yml \ + docker/example.env \ + docker/hwaccel.ml.yml \ + docker/hwaccel.transcoding.yml \ + docker/prometheus.yml \ *.apk From 927b4d0ea287470e669bee6004e302a6e2bf1b6c Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 11 Aug 2026 09:27:35 -0400 Subject: [PATCH 31/49] refactor: find or fail (#30697) --- server/src/services/album.service.ts | 9 +++------ server/src/services/asset.service.ts | 9 +++------ server/src/services/library.service.ts | 10 +++------- server/src/services/memory.service.ts | 11 ++++------- server/src/services/person.service.ts | 10 +++------- server/src/services/shared-link.service.ts | 10 +++------- server/src/services/stack.service.spec.ts | 6 ++++-- server/src/services/stack.service.ts | 10 +++------- server/src/services/tag.service.ts | 9 +++------ server/src/services/user-admin.service.ts | 9 +++------ server/src/services/user.service.ts | 9 +++------ server/src/services/workflow.service.ts | 9 +++------ server/src/utils/misc.ts | 11 ++++++++++- 13 files changed, 48 insertions(+), 74 deletions(-) diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 071c5d5aec..a94049bbd2 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -19,6 +19,7 @@ import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.reposi import { BaseService } from 'src/services/base.service'; import { addAssets, removeAssets } from 'src/utils/asset.util'; import { asDateTimeString } from 'src/utils/date'; +import { findOrFail } from 'src/utils/misc'; import { getPreferences } from 'src/utils/preferences'; @Injectable() @@ -351,11 +352,7 @@ export class AlbumService extends BaseService { await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role }); } - private async findOrFail(id: string, authUserId: string, options: AlbumInfoOptions) { - const album = await this.albumRepository.getById(id, options, authUserId); - if (!album) { - throw new BadRequestException('Album not found'); - } - return album; + private findOrFail(id: string, authUserId: string, options: AlbumInfoOptions) { + return findOrFail(() => this.albumRepository.getById(id, options, authUserId), 'Album'); } } diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 1edccf9483..72b5ad997b 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -46,6 +46,7 @@ import { } from 'src/utils/asset.util'; import { updateLockedColumns } from 'src/utils/database'; import { extractTimeZone } from 'src/utils/date'; +import { findOrFail } from 'src/utils/misc'; import { transformOcrBoundingBox } from 'src/utils/transform'; @Injectable() @@ -496,12 +497,8 @@ export class AssetService extends BaseService { await this.jobRepository.queueAll(jobs); } - private async findOrFail(id: string) { - const asset = await this.assetRepository.getById(id); - if (!asset) { - throw new BadRequestException('Asset not found'); - } - return asset; + private findOrFail(id: string) { + return findOrFail(() => this.assetRepository.getById(id), 'Asset'); } private async updateExif(dto: { diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index f13a440f84..56e5fa6be8 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -34,7 +34,7 @@ import { AssetTable } from 'src/schema/tables/asset.table'; import { BaseService } from 'src/services/base.service'; import { JobOf } from 'src/types'; import { mimeTypes } from 'src/utils/mime-types'; -import { handlePromiseError } from 'src/utils/misc'; +import { findOrFail, handlePromiseError } from 'src/utils/misc'; @Injectable() export class LibraryService extends BaseService { @@ -793,11 +793,7 @@ export class LibraryService extends BaseService { return JobStatus.Success; } - private async findOrFail(id: string) { - const library = await this.libraryRepository.get(id); - if (!library) { - throw new BadRequestException('Library not found'); - } - return library; + private findOrFail(id: string) { + return findOrFail(() => this.libraryRepository.get(id), 'Library'); } } diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 41510056ff..1175a11af4 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { Memory } from 'src/database'; import { OnJob } from 'src/decorators'; @@ -8,6 +8,7 @@ import { MemoryCreateDto, MemoryResponseDto, MemorySearchDto, MemoryUpdateDto, m import { DatabaseLock, JobName, MemoryType, Permission, QueueName, SystemMetadataKey } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { addAssets, removeAssets } from 'src/utils/asset.util'; +import { findOrFail } from 'src/utils/misc'; const DAYS = 3; @@ -162,11 +163,7 @@ export class MemoryService extends BaseService { return results; } - private async findOrFail(id: string) { - const memory = await this.memoryRepository.get(id); - if (!memory) { - throw new BadRequestException('Memory not found'); - } - return memory; + private findOrFail(id: string) { + return findOrFail(() => this.memoryRepository.get(id), 'Memory'); } } diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 8e0cd2ff01..ff381ce736 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -43,7 +43,7 @@ import { JobItem, JobOf } from 'src/types'; import { getDimensions } from 'src/utils/asset.util'; import { ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; -import { isFacialRecognitionEnabled } from 'src/utils/misc'; +import { findOrFail, isFacialRecognitionEnabled } from 'src/utils/misc'; import { Point, transformPoints } from 'src/utils/transform'; @Injectable() @@ -614,12 +614,8 @@ export class PersonService extends BaseService { return results; } - private async findOrFail(id: string) { - const person = await this.personRepository.getById(id); - if (!person) { - throw new BadRequestException('Person not found'); - } - return person; + private findOrFail(id: string) { + return findOrFail(() => this.personRepository.getById(id), 'Person'); } // TODO return a asset face response diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 41b1d5403c..31ca32f552 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -13,7 +13,7 @@ import { } from 'src/dtos/shared-link.dto'; import { Permission, SharedLinkType } from 'src/enum'; import { BaseService } from 'src/services/base.service'; -import { getExternalDomain, OpenGraphTags } from 'src/utils/misc'; +import { findOrFail, getExternalDomain, OpenGraphTags } from 'src/utils/misc'; @Injectable() export class SharedLinkService extends BaseService { @@ -143,12 +143,8 @@ export class SharedLinkService extends BaseService { } // TODO: replace `userId` with permissions and access control checks - private async findOrFail(userId: string, id: string) { - const sharedLink = await this.sharedLinkRepository.get(userId, id); - if (!sharedLink) { - throw new BadRequestException('Shared link not found'); - } - return sharedLink; + private findOrFail(userId: string, id: string) { + return findOrFail(() => this.sharedLinkRepository.get(userId, id), 'Shared link'); } async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise { diff --git a/server/src/services/stack.service.spec.ts b/server/src/services/stack.service.spec.ts index d47d634f4f..933c7f39c5 100644 --- a/server/src/services/stack.service.spec.ts +++ b/server/src/services/stack.service.spec.ts @@ -85,8 +85,9 @@ describe(StackService.name, () => { it('should fail if stack could not be found', async () => { mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); + mocks.stack.getById.mockResolvedValue(void 0); - await expect(sut.get(authStub.admin, 'stack-id')).rejects.toBeInstanceOf(Error); + await expect(sut.get(authStub.admin, 'stack-id')).rejects.toBeInstanceOf(BadRequestException); expect(mocks.access.stack.checkOwnerAccess).toHaveBeenCalled(); expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id'); @@ -124,8 +125,9 @@ describe(StackService.name, () => { it('should fail if stack could not be found', async () => { mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); + mocks.stack.getById.mockResolvedValue(void 0); - await expect(sut.update(AuthFactory.create(), 'stack-id', {})).rejects.toBeInstanceOf(Error); + await expect(sut.update(AuthFactory.create(), 'stack-id', {})).rejects.toBeInstanceOf(BadRequestException); expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id'); expect(mocks.stack.update).not.toHaveBeenCalled(); diff --git a/server/src/services/stack.service.ts b/server/src/services/stack.service.ts index 2dc9f1547e..9dc47a97bf 100644 --- a/server/src/services/stack.service.ts +++ b/server/src/services/stack.service.ts @@ -4,6 +4,7 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto'; import { Permission } from 'src/enum'; import { BaseService } from 'src/services/base.service'; +import { findOrFail } from 'src/utils/misc'; import { UUIDAssetIDParamDto } from 'src/validation'; @Injectable() @@ -77,12 +78,7 @@ export class StackService extends BaseService { await this.eventRepository.emit('StackUpdate', { stackId, userId: auth.user.id }); } - private async findOrFail(id: string) { - const stack = await this.stackRepository.getById(id); - if (!stack) { - throw new Error('Asset stack not found'); - } - - return stack; + private findOrFail(id: string) { + return findOrFail(() => this.stackRepository.getById(id), 'Asset stack'); } } diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts index 1c7195e4c3..3cd442b6a7 100644 --- a/server/src/services/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -17,6 +17,7 @@ import { TagAssetTable } from 'src/schema/tables/tag-asset.table'; import { BaseService } from 'src/services/base.service'; import { addAssets, removeAssets } from 'src/utils/asset.util'; import { updateLockedColumns } from 'src/utils/database'; +import { findOrFail } from 'src/utils/misc'; import { upsertTags } from 'src/utils/tag'; @Injectable() @@ -146,12 +147,8 @@ export class TagService extends BaseService { return JobStatus.Success; } - private async findOrFail(id: string) { - const tag = await this.tagRepository.get(id); - if (!tag) { - throw new BadRequestException('Tag not found'); - } - return tag; + private findOrFail(id: string) { + return findOrFail(() => this.tagRepository.get(id), 'Tag'); } private async updateTags(assetId: string) { diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index 15ea8c0598..f80a810616 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -17,6 +17,7 @@ import { JobName, UserMetadataKey, UserStatus } from 'src/enum'; import { UserFindOptions } from 'src/repositories/user.repository'; import { BaseService } from 'src/services/base.service'; import { getCalendarHeatmap } from 'src/services/shared/user-methods'; +import { findOrFail } from 'src/utils/misc'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; @Injectable() @@ -158,11 +159,7 @@ export class UserAdminService extends BaseService { return mapPreferences(newPreferences); } - private async findOrFail(id: string, options: UserFindOptions) { - const user = await this.userRepository.get(id, options); - if (!user) { - throw new BadRequestException('User not found'); - } - return user; + private findOrFail(id: string, options: UserFindOptions) { + return findOrFail(() => this.userRepository.get(id, options), 'User'); } } diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index c7a5bc4e02..1630fd9dd9 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -20,6 +20,7 @@ import { getCalendarHeatmap } from 'src/services/shared/user-methods'; import { JobOf, UserMetadataItem } from 'src/types'; import { ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; +import { findOrFail } from 'src/utils/misc'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; import { generateProfileImage } from 'src/utils/profile-image'; @@ -302,11 +303,7 @@ export class UserService extends BaseService { return DateTime.now().minus({ days: delayUntilDeletion }) > DateTime.fromJSDate(user.deletedAt); } - private async findOrFail(id: string, options: UserFindOptions) { - const user = await this.userRepository.get(id, options); - if (!user) { - throw new BadRequestException('User not found'); - } - return user; + private findOrFail(id: string, options: UserFindOptions) { + return findOrFail(() => this.userRepository.get(id, options), 'User'); } } diff --git a/server/src/services/workflow.service.ts b/server/src/services/workflow.service.ts index 850b4ac086..1d4ecb89a0 100644 --- a/server/src/services/workflow.service.ts +++ b/server/src/services/workflow.service.ts @@ -14,6 +14,7 @@ import { import { Permission } from 'src/enum'; import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository'; import { BaseService } from 'src/services/base.service'; +import { findOrFail } from 'src/utils/misc'; import { getWorkflowTriggers, isMethodCompatible, resolveMethod } from 'src/utils/workflow'; @Injectable() @@ -104,11 +105,7 @@ export class WorkflowService extends BaseService { return results; } - private async findOrFail(id: string) { - const workflow = await this.workflowRepository.get(id); - if (!workflow) { - throw new BadRequestException('Workflow not found'); - } - return workflow; + private findOrFail(id: string) { + return findOrFail(() => this.workflowRepository.get(id), 'Workflow'); } } diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 0e7a9e6cdc..4db4140c1b 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -1,4 +1,4 @@ -import { INestApplication } from '@nestjs/common'; +import { BadRequestException, INestApplication } from '@nestjs/common'; import { ApiBodyOptions, DocumentBuilder, @@ -111,6 +111,15 @@ export const handlePromiseError = (promise: Promise, logger: LoggingReposi promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack)); }; +export const findOrFail = async (find: () => Promise, entity: string): Promise> => { + const value = await find(); + if (!value) { + throw new BadRequestException(`${entity} not found`); + } + + return value; +}; + export interface OpenGraphTags { title: string; description: string; From d1662fb2b6f112606238440b5457511b6c30f594 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:46:26 +0200 Subject: [PATCH 32/49] fix: face label clipping (#30712) --- web/src/lib/components/asset-viewer/PhotoViewer.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/lib/components/asset-viewer/PhotoViewer.svelte b/web/src/lib/components/asset-viewer/PhotoViewer.svelte index c0af733f3f..94da4bafc4 100644 --- a/web/src/lib/components/asset-viewer/PhotoViewer.svelte +++ b/web/src/lib/components/asset-viewer/PhotoViewer.svelte @@ -273,7 +273,9 @@ From ee525f159b19705604249f0553b67d9e6b680228 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 11 Aug 2026 10:01:42 -0400 Subject: [PATCH 33/49] refactor: queue in batches (#30698) --- server/src/services/asset.service.ts | 26 +---- server/src/services/duplicate.service.ts | 23 ++--- server/src/services/integrity.service.ts | 28 ++---- server/src/services/library.service.ts | 67 ++++--------- server/src/services/media.service.spec.ts | 8 +- server/src/services/media.service.ts | 112 ++++++++-------------- server/src/services/metadata.service.ts | 37 +++---- server/src/services/ocr.service.ts | 19 +--- server/src/services/person.service.ts | 36 +++---- server/src/services/smart-info.service.ts | 18 +--- server/src/services/trash.service.ts | 35 ++----- server/src/utils/misc.ts | 19 +++- 12 files changed, 136 insertions(+), 292 deletions(-) diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 72b5ad997b..15385df0e0 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import _ from 'lodash'; import { DateTime, Duration } from 'luxon'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { AssetFile } from 'src/database'; import { OnJob } from 'src/decorators'; import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; @@ -46,7 +45,7 @@ import { } from 'src/utils/asset.util'; import { updateLockedColumns } from 'src/utils/database'; import { extractTimeZone } from 'src/utils/date'; -import { findOrFail } from 'src/utils/misc'; +import { batched, findOrFail } from 'src/utils/misc'; import { transformOcrBoundingBox } from 'src/utils/transform'; @Injectable() @@ -279,31 +278,12 @@ export class AssetService extends BaseService { .minus(Duration.fromObject({ days: trashedDays })) .toJSDate(); - let chunk: Array<{ id: string; isOffline: boolean }> = []; - const queueChunk = async () => { - if (chunk.length === 0) { - return; - } - + for await (const assets of batched(this.assetJobRepository.streamForDeletedJob(trashedBefore))) { await this.jobRepository.queueAll( - chunk.map(({ id, isOffline }) => ({ - name: JobName.AssetDelete, - data: { id, deleteOnDisk: !isOffline }, - })), + assets.map(({ id, isOffline }) => ({ name: JobName.AssetDelete, data: { id, deleteOnDisk: !isOffline } })), ); - chunk = []; - }; - - const assets = this.assetJobRepository.streamForDeletedJob(trashedBefore); - for await (const asset of assets) { - chunk.push(asset); - if (chunk.length >= JOBS_ASSET_PAGINATION_SIZE) { - await queueChunk(); - } } - await queueChunk(); - return JobStatus.Success; } diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index 8cb04e29ba..08c7630f91 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { OnJob } from 'src/decorators'; import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; @@ -8,9 +7,9 @@ import { DuplicateResolveDto, DuplicateResolveGroupDto, DuplicateResponseDto } f import { AssetStatus, AssetVisibility, JobName, JobStatus, Permission, QueueName } from 'src/enum'; import { AssetDuplicateResult } from 'src/repositories/search.repository'; import { BaseService } from 'src/services/base.service'; -import { JobItem, JobOf } from 'src/types'; +import { JobOf } from 'src/types'; import { suggestDuplicateKeepAssetIds } from 'src/utils/duplicate'; -import { isDuplicateDetectionEnabled } from 'src/utils/misc'; +import { batched, isDuplicateDetectionEnabled } from 'src/utils/misc'; type ResolveRequest = { assetUpdate: { @@ -307,22 +306,12 @@ export class DuplicateService extends BaseService { return JobStatus.Skipped; } - let jobs: JobItem[] = []; - const queueAll = async () => { - await this.jobRepository.queueAll(jobs); - jobs = []; - }; - - const assets = this.assetJobRepository.streamForSearchDuplicates(force); - for await (const asset of assets) { - jobs.push({ name: JobName.AssetDetectDuplicates, data: { id: asset.id } }); - if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { - await queueAll(); - } + for await (const assets of batched(this.assetJobRepository.streamForSearchDuplicates(force))) { + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.AssetDetectDuplicates, data: { id: asset.id } })), + ); } - await queueAll(); - return JobStatus.Success; } diff --git a/server/src/services/integrity.service.ts b/server/src/services/integrity.service.ts index a7a194b25f..6c7a505812 100644 --- a/server/src/services/integrity.service.ts +++ b/server/src/services/integrity.service.ts @@ -35,7 +35,7 @@ import { IIntegrityUntrackedFilesJob, } from 'src/types'; import { ImmichFileResponse } from 'src/utils/file'; -import { handlePromiseError } from 'src/utils/misc'; +import { batched, handlePromiseError } from 'src/utils/misc'; /** * Untracked Files: @@ -201,7 +201,7 @@ export class IntegrityService extends BaseService { const reports = this.integrityRepository.streamIntegrityReportsWithAssetChecksum(IntegrityReport.UntrackedFile); let total = 0; - for await (const batchReports of chunk(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { + for await (const batchReports of batched(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { await this.jobRepository.queue({ name: JobName.IntegrityUntrackedFilesRefresh, data: { @@ -338,7 +338,7 @@ export class IntegrityService extends BaseService { const reports = this.integrityRepository.streamIntegrityReportsWithAssetChecksum(IntegrityReport.MissingFile); let total = 0; - for await (const batchReports of chunk(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { + for await (const batchReports of batched(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { await this.jobRepository.queue({ name: JobName.IntegrityMissingFilesRefresh, data: { @@ -365,7 +365,7 @@ export class IntegrityService extends BaseService { const assetPaths = this.integrityRepository.streamAssetPathsForMissingFiles(); let total = 0; - for await (const batchPaths of chunk(assetPaths, JOBS_LIBRARY_PAGINATION_SIZE)) { + for await (const batchPaths of batched(assetPaths, JOBS_LIBRARY_PAGINATION_SIZE)) { await this.jobRepository.queue({ name: JobName.IntegrityMissingFiles, data: { @@ -450,7 +450,7 @@ export class IntegrityService extends BaseService { const reports = this.integrityRepository.streamIntegrityReportsWithAssetChecksum(IntegrityReport.ChecksumFail); let total = 0; - for await (const batchReports of chunk(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { + for await (const batchReports of batched(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { await this.jobRepository.queue({ name: JobName.IntegrityChecksumFilesRefresh, data: { @@ -656,7 +656,7 @@ export class IntegrityService extends BaseService { for (const property of properties) { const reports = this.integrityRepository.streamIntegrityReportsByProperty(property, type); - for await (const batch of chunk(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { + for await (const batch of batched(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { await this.jobRepository.queue({ name: JobName.IntegrityDeleteReports, data: { @@ -705,19 +705,3 @@ export class IntegrityService extends BaseService { return JobStatus.Success; } } - -async function* chunk(generator: AsyncIterableIterator, n: number) { - let chunk: T[] = []; - for await (const item of generator) { - chunk.push(item); - - if (chunk.length === n) { - yield chunk; - chunk = []; - } - } - - if (chunk.length > 0) { - yield chunk; - } -} diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 56e5fa6be8..9e5e0cf4d1 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -34,7 +34,7 @@ import { AssetTable } from 'src/schema/tables/asset.table'; import { BaseService } from 'src/services/base.service'; import { JobOf } from 'src/types'; import { mimeTypes } from 'src/utils/mime-types'; -import { findOrFail, handlePromiseError } from 'src/utils/misc'; +import { batched, findOrFail, handlePromiseError } from 'src/utils/misc'; @Injectable() export class LibraryService extends BaseService { @@ -375,35 +375,20 @@ export class LibraryService extends BaseService { await this.assetRepository.updateByLibraryId(libraryId, { deletedAt: new Date() }); - let isAssetsFound = false; - let chunk: string[] = []; - - const queueChunk = async () => { - if (chunk.length === 0) { - return; - } - - isAssetsFound = true; - this.logger.debug(`Queueing deletion of ${chunk.length} asset(s) in library ${libraryId}`); - await this.jobRepository.queueAll( - chunk.map((id) => ({ name: JobName.AssetDelete, data: { id, deleteOnDisk: false } })), - ); - chunk = []; - }; - this.logger.debug(`Will delete all assets in library ${libraryId}`); - const assets = this.libraryRepository.streamAssetIds(libraryId); - for await (const asset of assets) { - chunk.push(asset.id); - - if (chunk.length >= JOBS_LIBRARY_PAGINATION_SIZE) { - await queueChunk(); - } + let hasAssets = false; + for await (const assets of batched( + this.libraryRepository.streamAssetIds(libraryId), + JOBS_LIBRARY_PAGINATION_SIZE, + )) { + this.logger.debug(`Queueing deletion of ${assets.length} asset(s) in library ${libraryId}`); + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.AssetDelete, data: { id: asset.id, deleteOnDisk: false } })), + ); + hasAssets = true; } - await queueChunk(); - - if (!isAssetsFound) { + if (!hasAssets) { this.logger.log(`Deleting library ${libraryId}`); await this.libraryRepository.delete(libraryId); } @@ -746,15 +731,12 @@ export class LibraryService extends BaseService { return JobStatus.Success; } - let chunk: string[] = []; + this.logger.log(`Scanning library ${library.id} for assets missing from disk...`); + let count = 0; - - const queueChunk = async () => { - if (chunk.length === 0) { - return; - } - - count += chunk.length; + const existingAssets = this.libraryRepository.streamAssetIds(library.id); + for await (const assets of batched(existingAssets, JOBS_LIBRARY_PAGINATION_SIZE)) { + count += assets.length; await this.jobRepository.queue({ name: JobName.LibrarySyncAssets, @@ -762,32 +744,19 @@ export class LibraryService extends BaseService { libraryId: library.id, importPaths: library.importPaths, exclusionPatterns: library.exclusionPatterns, - assetIds: chunk.map((id) => id), + assetIds: assets.map(({ id }) => id), progressCounter: count, totalAssets: assetCount, }, }); - chunk = []; const completePercentage = ((100 * count) / assetCount).toFixed(1); this.logger.log( `Queued check of ${count} of ${assetCount} (${completePercentage} %) existing asset(s) so far in library ${library.id}`, ); - }; - - this.logger.log(`Scanning library ${library.id} for assets missing from disk...`); - const existingAssets = this.libraryRepository.streamAssetIds(library.id); - - for await (const asset of existingAssets) { - chunk.push(asset.id); - if (chunk.length === JOBS_LIBRARY_PAGINATION_SIZE) { - await queueChunk(); - } } - await queueChunk(); - this.logger.log(`Finished queuing ${count} asset check(s) for library ${library.id}`); return JobStatus.Success; diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 290bf947a1..d64e0088eb 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -207,8 +207,7 @@ describe(MediaService.name, () => { await sut.handleQueueGenerateThumbnails({ force: false }); expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([]); - + expect(mocks.job.queueAll).toHaveBeenCalledTimes(1); expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' }); }); @@ -237,7 +236,10 @@ describe(MediaService.name, () => { await sut.handleQueueGenerateThumbnails({ force: false }); expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([]); + expect(mocks.job.queueAll).toHaveBeenCalledTimes(1); + expect(mocks.job.queueAll).toHaveBeenCalledWith([ + { name: JobName.AssetEditThumbnailGeneration, data: { id: asset.id } }, + ]); expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' }); }); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 956636dd51..19b1688e88 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { SystemConfig } from 'src/config'; -import { FACE_THUMBNAIL_SIZE, JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; +import { FACE_THUMBNAIL_SIZE } from 'src/constants'; import { ImagePathOptions, StorageCore, ThumbnailPathEntity } from 'src/cores/storage.core'; import { AssetFile } from 'src/database'; import { OnEvent, OnJob } from 'src/decorators'; @@ -43,7 +43,7 @@ import { getAssetFile, getDimensions } from 'src/utils/asset.util'; import { checkFaceVisibility, checkOcrVisibility } from 'src/utils/editor'; import { BaseConfig, ThumbnailConfig } from 'src/utils/media'; import { mimeTypes } from 'src/utils/mime-types'; -import { clamp } from 'src/utils/misc'; +import { batched, clamp } from 'src/utils/misc'; import { getOutputDimensions } from 'src/utils/transform'; interface UpsertFileOptions { @@ -69,52 +69,42 @@ export class MediaService extends BaseService { @OnJob({ name: JobName.AssetGenerateThumbnailsQueueAll, queue: QueueName.ThumbnailGeneration }) async handleQueueGenerateThumbnails({ force }: JobOf): Promise { const config = await this.getConfig({ withCache: true }); - let jobs: JobItem[] = []; - - const queueAll = async () => { - await this.jobRepository.queueAll(jobs); - jobs = []; - }; const isFullsizeEnabled = config.image.fullsize.enabled; - for await (const asset of this.assetJobRepository.streamForThumbnailJob({ - force, - fullsizeEnabled: isFullsizeEnabled, - })) { - if (force || !asset.isEdited) { - jobs.push({ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } }); - } - - if (asset.isEdited) { - jobs.push({ name: JobName.AssetEditThumbnailGeneration, data: { id: asset.id } }); - } - - if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { - await queueAll(); - } - } - - await queueAll(); - - const people = this.personRepository.getAll(force ? undefined : { thumbnailPath: '' }); - - for await (const person of people) { - if (!person.faceAssetId) { - const face = await this.personRepository.getRandomFace(person.id); - if (!face) { - continue; + for await (const assets of batched( + this.assetJobRepository.streamForThumbnailJob({ force, fullsizeEnabled: isFullsizeEnabled }), + )) { + const jobs: JobItem[] = []; + for (const asset of assets) { + if (force || !asset.isEdited) { + jobs.push({ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } }); } - await this.personRepository.update({ id: person.id, faceAssetId: face.id }); + if (asset.isEdited) { + jobs.push({ name: JobName.AssetEditThumbnailGeneration, data: { id: asset.id } }); + } } - jobs.push({ name: JobName.PersonGenerateThumbnail, data: { id: person.id } }); - if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { - await queueAll(); - } + await this.jobRepository.queueAll(jobs); } - await queueAll(); + for await (const people of batched(this.personRepository.getAll(force ? undefined : { thumbnailPath: '' }))) { + const jobs: JobItem[] = []; + for (const person of people) { + if (!person.faceAssetId) { + const face = await this.personRepository.getRandomFace(person.id); + if (!face) { + continue; + } + + await this.personRepository.update({ id: person.id, faceAssetId: face.id }); + } + + jobs.push({ name: JobName.PersonGenerateThumbnail, data: { id: person.id } }); + } + + await this.jobRepository.queueAll(jobs); + } return JobStatus.Success; } @@ -127,30 +117,18 @@ export class MediaService extends BaseService { await this.storageCore.removeEmptyDirs(StorageFolder.EncodedVideo); } - let jobs: JobItem[] = []; - const assets = this.assetJobRepository.streamForMigrationJob(); - for await (const asset of assets) { - jobs.push({ name: JobName.AssetFileMigration, data: { id: asset.id } }); - if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(jobs); - jobs = []; - } + for await (const assets of batched(this.assetJobRepository.streamForMigrationJob())) { + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.AssetFileMigration, data: { id: asset.id } })), + ); } - await this.jobRepository.queueAll(jobs); - jobs = []; - - for await (const person of this.personRepository.getAll()) { - jobs.push({ name: JobName.PersonFileMigration, data: { id: person.id } }); - - if (jobs.length === JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(jobs); - jobs = []; - } + for await (const people of batched(this.personRepository.getAll())) { + await this.jobRepository.queueAll( + people.map((person) => ({ name: JobName.PersonFileMigration, data: { id: person.id } })), + ); } - await this.jobRepository.queueAll(jobs); - return JobStatus.Success; } @@ -551,18 +529,12 @@ export class MediaService extends BaseService { async handleQueueVideoConversion(job: JobOf): Promise { const { force } = job; - let queue: { name: JobName.AssetEncodeVideo; data: { id: string } }[] = []; - for await (const asset of this.assetJobRepository.streamForVideoConversion(force)) { - queue.push({ name: JobName.AssetEncodeVideo, data: { id: asset.id } }); - - if (queue.length >= JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(queue); - queue = []; - } + for await (const assets of batched(this.assetJobRepository.streamForVideoConversion(force))) { + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.AssetEncodeVideo, data: { id: asset.id } })), + ); } - await this.jobRepository.queueAll(queue); - return JobStatus.Success; } diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 37dd92e27d..53573549eb 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -6,7 +6,7 @@ import { DateTime, Duration } from 'luxon'; import { Stats } from 'node:fs'; import { constants } from 'node:fs/promises'; import { join, parse } from 'node:path'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; + import { StorageCore } from 'src/cores/storage.core'; import { Asset, AssetFile } from 'src/database'; import { OnEvent, OnJob } from 'src/decorators'; @@ -30,12 +30,12 @@ import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { PersonTable } from 'src/schema/tables/person.table'; import { BaseService } from 'src/services/base.service'; -import { JobItem, JobOf } from 'src/types'; +import { JobOf } from 'src/types'; import { getAssetFiles } from 'src/utils/asset.util'; import { isAssetChecksumConstraint } from 'src/utils/database'; import { mergeTimeZone } from 'src/utils/date'; import { mimeTypes } from 'src/utils/mime-types'; -import { isFaceImportEnabled } from 'src/utils/misc'; +import { batched, isFaceImportEnabled } from 'src/utils/misc'; import { upsertTags } from 'src/utils/tag'; import { Tasks } from 'src/utils/tasks'; @@ -218,17 +218,12 @@ export class MetadataService extends BaseService { async handleQueueMetadataExtraction(job: JobOf): Promise { const { force } = job; - let queue: { name: JobName.AssetExtractMetadata; data: { id: string } }[] = []; - for await (const asset of this.assetJobRepository.streamForMetadataExtraction(force)) { - queue.push({ name: JobName.AssetExtractMetadata, data: { id: asset.id } }); - - if (queue.length >= JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(queue); - queue = []; - } + for await (const assets of batched(this.assetJobRepository.streamForMetadataExtraction(force))) { + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.AssetExtractMetadata, data: { id: asset.id } })), + ); } - await this.jobRepository.queueAll(queue); return JobStatus.Success; } @@ -417,22 +412,12 @@ export class MetadataService extends BaseService { @OnJob({ name: JobName.SidecarQueueAll, queue: QueueName.Sidecar }) async handleQueueSidecar({ force }: JobOf): Promise { - let jobs: JobItem[] = []; - const queueAll = async () => { - await this.jobRepository.queueAll(jobs); - jobs = []; - }; - - const assets = this.assetJobRepository.streamForSidecar(force); - for await (const asset of assets) { - jobs.push({ name: JobName.SidecarCheck, data: { id: asset.id } }); - if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { - await queueAll(); - } + for await (const assets of batched(this.assetJobRepository.streamForSidecar(force))) { + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.SidecarCheck, data: { id: asset.id } })), + ); } - await queueAll(); - return JobStatus.Success; } diff --git a/server/src/services/ocr.service.ts b/server/src/services/ocr.service.ts index d92d399dba..8e32a44376 100644 --- a/server/src/services/ocr.service.ts +++ b/server/src/services/ocr.service.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; + import { OnJob } from 'src/decorators'; import { AssetVisibility, JobName, JobStatus, QueueName } from 'src/enum'; import { OCR } from 'src/repositories/machine-learning.repository'; import { BaseService } from 'src/services/base.service'; -import { JobItem, JobOf } from 'src/types'; +import { JobOf } from 'src/types'; import { tokenizeForSearch } from 'src/utils/database'; -import { isOcrEnabled } from 'src/utils/misc'; +import { batched, isOcrEnabled } from 'src/utils/misc'; @Injectable() export class OcrService extends BaseService { @@ -21,19 +21,10 @@ export class OcrService extends BaseService { await this.ocrRepository.deleteAll(); } - let jobs: JobItem[] = []; - const assets = this.assetJobRepository.streamForOcrJob(force); - - for await (const asset of assets) { - jobs.push({ name: JobName.Ocr, data: { id: asset.id } }); - - if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(jobs); - jobs = []; - } + for await (const assets of batched(this.assetJobRepository.streamForOcrJob(force))) { + await this.jobRepository.queueAll(assets.map((asset) => ({ name: JobName.Ocr, data: { id: asset.id } }))); } - await this.jobRepository.queueAll(jobs); return JobStatus.Success; } diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index ff381ce736..7c08c36e17 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { Insertable, Updateable } from 'kysely'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { Person } from 'src/database'; import { Chunked, OnJob } from 'src/decorators'; import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; @@ -43,7 +42,7 @@ import { JobItem, JobOf } from 'src/types'; import { getDimensions } from 'src/utils/asset.util'; import { ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; -import { findOrFail, isFacialRecognitionEnabled } from 'src/utils/misc'; +import { batched, findOrFail, isFacialRecognitionEnabled } from 'src/utils/misc'; import { Point, transformPoints } from 'src/utils/transform'; @Injectable() @@ -277,19 +276,12 @@ export class PersonService extends BaseService { await this.personRepository.vacuum({ reindexVectors: true }); } - let jobs: JobItem[] = []; - const assets = this.assetJobRepository.streamForDetectFacesJob(force); - for await (const asset of assets) { - jobs.push({ name: JobName.AssetDetectFaces, data: { id: asset.id } }); - - if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(jobs); - jobs = []; - } + for await (const assets of batched(this.assetJobRepository.streamForDetectFacesJob(force))) { + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.AssetDetectFaces, data: { id: asset.id } })), + ); } - await this.jobRepository.queueAll(jobs); - if (force === undefined) { await this.jobRepository.queue({ name: JobName.PersonCleanup }); } @@ -435,22 +427,16 @@ export class PersonService extends BaseService { await this.databaseRepository.prewarm(VectorIndex.Face); const lastRun = new Date().toISOString(); - const facePagination = this.personRepository.getAllFaces( + + const faces = this.personRepository.getAllFaces( force ? undefined : { personId: null, sourceType: SourceType.MachineLearning }, ); - - let jobs: { name: JobName.FacialRecognition; data: { id: string; deferred: false } }[] = []; - for await (const face of facePagination) { - jobs.push({ name: JobName.FacialRecognition, data: { id: face.id, deferred: false } }); - - if (jobs.length === JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(jobs); - jobs = []; - } + for await (const batch of batched(faces)) { + await this.jobRepository.queueAll( + batch.map((face) => ({ name: JobName.FacialRecognition, data: { id: face.id, deferred: false } })), + ); } - await this.jobRepository.queueAll(jobs); - await this.systemMetadataRepository.set(SystemMetadataKey.FacialRecognitionState, { lastRun }); return JobStatus.Success; diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 93b8944099..19a1774492 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; import { SystemConfig } from 'src/config'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; + import { OnEvent, OnJob } from 'src/decorators'; import { AssetVisibility, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; -import { JobItem, JobOf } from 'src/types'; -import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc'; +import { JobOf } from 'src/types'; +import { batched, getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc'; @Injectable() export class SmartInfoService extends BaseService { @@ -77,18 +77,10 @@ export class SmartInfoService extends BaseService { await this.databaseRepository.setDimensionSize(dimSize); } - let queue: JobItem[] = []; - const assets = this.assetJobRepository.streamForEncodeClip(force); - for await (const asset of assets) { - queue.push({ name: JobName.SmartSearch, data: { id: asset.id } }); - if (queue.length >= JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(queue); - queue = []; - } + for await (const assets of batched(this.assetJobRepository.streamForEncodeClip(force))) { + await this.jobRepository.queueAll(assets.map((asset) => ({ name: JobName.SmartSearch, data: { id: asset.id } }))); } - await this.jobRepository.queueAll(queue); - return JobStatus.Success; } diff --git a/server/src/services/trash.service.ts b/server/src/services/trash.service.ts index f1d368111e..368e16beb5 100644 --- a/server/src/services/trash.service.ts +++ b/server/src/services/trash.service.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { OnEvent, OnJob } from 'src/decorators'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { TrashResponseDto } from 'src/dtos/trash.dto'; import { JobName, JobStatus, Permission, QueueName } from 'src/enum'; import { BaseService } from 'src/services/base.service'; +import { batched } from 'src/utils/misc'; @Injectable() export class TrashService extends BaseService { @@ -47,39 +47,16 @@ export class TrashService extends BaseService { @OnJob({ name: JobName.AssetEmptyTrash, queue: QueueName.BackgroundTask }) async handleEmptyTrash() { - const assets = this.trashRepository.getDeletedIds(); - let count = 0; - const batch: string[] = []; - for await (const { id } of assets) { - batch.push(id); - - if (batch.length === JOBS_ASSET_PAGINATION_SIZE) { - await this.handleBatch(batch); - count += batch.length; - batch.length = 0; - } + for await (const assets of batched(this.trashRepository.getDeletedIds())) { + await this.jobRepository.queueAll( + assets.map(({ id }) => ({ name: JobName.AssetDelete, data: { id, deleteOnDisk: true } })), + ); + count += assets.length; } - 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.AssetDelete, - data: { - id: assetId, - deleteOnDisk: true, - }, - })), - ); - } } diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 4db4140c1b..a514007987 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -14,7 +14,7 @@ import path from 'node:path'; import picomatch from 'picomatch'; import parse from 'picomatch/lib/parse'; import { SystemConfig } from 'src/config'; -import { CLIP_MODEL_INFO, endpointTags, serverVersion } from 'src/constants'; +import { CLIP_MODEL_INFO, JOBS_ASSET_PAGINATION_SIZE, endpointTags, serverVersion } from 'src/constants'; import { extraModels } from 'src/decorators'; import { ApiCustomExtension, ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -120,6 +120,23 @@ export const findOrFail = async (find: () => Promise, entity: string): Pro return value; }; +export async function* batched(items: AsyncIterable, size = JOBS_ASSET_PAGINATION_SIZE): AsyncGenerator { + let batch: T[] = []; + + for await (const item of items) { + batch.push(item); + + if (batch.length >= size) { + yield batch; + batch = []; + } + } + + if (batch.length > 0) { + yield batch; + } +} + export interface OpenGraphTags { title: string; description: string; From 1b8cbaab9762dc53fa9b15366f22b65dc91a5e81 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 11 Aug 2026 11:02:14 -0400 Subject: [PATCH 34/49] refactor(server): migrate e2e auth/validation tests to controller specs (#30715) Co-authored-by: Claude (thor) --- e2e/src/specs/server/api/partner.e2e-spec.ts | 29 ------- e2e/src/specs/server/api/server.e2e-spec.ts | 36 -------- e2e/src/specs/server/api/session.e2e-spec.ts | 12 --- .../specs/server/api/shared-link.e2e-spec.ts | 56 ------------- e2e/src/specs/server/api/stack.e2e-spec.ts | 38 +-------- .../server/api/system-metadata.e2e-spec.ts | 12 --- e2e/src/specs/server/api/tag.e2e-spec.ts | 78 ------------------ .../specs/server/api/user-admin.e2e-spec.ts | 75 ----------------- e2e/src/specs/server/api/user.e2e-spec.ts | 33 -------- .../controllers/activity.controller.spec.ts | 15 ---- .../src/controllers/album.controller.spec.ts | 61 -------------- .../controllers/api-key.controller.spec.ts | 36 -------- .../asset-media.controller.spec.ts | 16 ---- .../src/controllers/asset.controller.spec.ts | 71 ---------------- .../src/controllers/auth.controller.spec.ts | 42 ---------- .../database-backup.controller.spec.ts | 7 -- .../controllers/download.controller.spec.ts | 48 ----------- .../controllers/duplicate.controller.spec.ts | 19 ----- .../maintenance.controller.spec.ts | 5 -- .../src/controllers/memory.controller.spec.ts | 44 ---------- .../notification-admin.controller.spec.ts | 5 -- .../notification.controller.spec.ts | 20 ----- .../controllers/partner.controller.spec.ts | 21 ----- .../src/controllers/person.controller.spec.ts | 48 ----------- .../src/controllers/plugin.controller.spec.ts | 11 --- .../src/controllers/search.controller.spec.ts | 46 ----------- .../src/controllers/server.controller.spec.ts | 35 -------- .../shared-link.controller.spec.ts | 21 ++++- .../src/controllers/stack.controller.spec.ts | 46 +++++++++++ .../src/controllers/sync.controller.spec.ts | 22 ----- .../system-config.controller.spec.ts | 19 ----- server/src/controllers/tag.controller.spec.ts | 34 ++------ .../controllers/timeline.controller.spec.ts | 10 --- .../controllers/user-admin.controller.spec.ts | 82 ++++++++----------- .../src/controllers/user.controller.spec.ts | 59 ++++++------- .../controllers/workflow.controller.spec.ts | 21 ----- 36 files changed, 125 insertions(+), 1108 deletions(-) delete mode 100644 server/src/controllers/download.controller.spec.ts delete mode 100644 server/src/controllers/server.controller.spec.ts create mode 100644 server/src/controllers/stack.controller.spec.ts diff --git a/e2e/src/specs/server/api/partner.e2e-spec.ts b/e2e/src/specs/server/api/partner.e2e-spec.ts index 9047a97055..0eb7d6db3d 100644 --- a/e2e/src/specs/server/api/partner.e2e-spec.ts +++ b/e2e/src/specs/server/api/partner.e2e-spec.ts @@ -1,6 +1,5 @@ import { LoginResponseDto, createPartner } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; -import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -29,13 +28,6 @@ describe('/partners', () => { }); describe('GET /partners', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/partners'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get all partners shared by user', async () => { const { status, body } = await request(app) .get('/partners') @@ -58,13 +50,6 @@ describe('/partners', () => { }); describe('POST /partners/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post(`/partners/${user3.userId}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should share with new partner', async () => { const { status, body } = await request(app) .post(`/partners/${user3.userId}`) @@ -85,13 +70,6 @@ describe('/partners', () => { }); describe('PUT /partners/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/partners/${user2.userId}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should update partner', async () => { const { status, body } = await request(app) .put(`/partners/${user2.userId}`) @@ -104,13 +82,6 @@ describe('/partners', () => { }); describe('DELETE /partners/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/partners/${user3.userId}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should delete partner', async () => { const { status } = await request(app) .delete(`/partners/${user3.userId}`) diff --git a/e2e/src/specs/server/api/server.e2e-spec.ts b/e2e/src/specs/server/api/server.e2e-spec.ts index 53857a31fa..f0eed82f7a 100644 --- a/e2e/src/specs/server/api/server.e2e-spec.ts +++ b/e2e/src/specs/server/api/server.e2e-spec.ts @@ -22,12 +22,6 @@ describe('/server', () => { }); describe('GET /server/about', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/server/about'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should return about information', async () => { const { status, body } = await request(app) .get('/server/about') @@ -56,12 +50,6 @@ describe('/server', () => { }); describe('GET /server/storage', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/server/storage'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should return the disk information', async () => { const { status, body } = await request(app) .get('/server/storage') @@ -149,12 +137,6 @@ describe('/server', () => { }); describe('GET /server/statistics', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/server/statistics'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should only work for admins', async () => { const { status, body } = await request(app) .get('/server/statistics') @@ -213,12 +195,6 @@ describe('/server', () => { }); describe('GET /server/license', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/server/license'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should only work for admins', async () => { const { status, body } = await request(app) .get('/server/license') @@ -241,12 +217,6 @@ describe('/server', () => { }); describe('DELETE /server/license', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete('/server/license'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should only work for admins', async () => { const { status, body } = await request(app) .delete('/server/license') @@ -266,12 +236,6 @@ describe('/server', () => { }); describe('PUT /server/license', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put('/server/license'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should only work for admins', async () => { const { status, body } = await request(app) .put('/server/license') diff --git a/e2e/src/specs/server/api/session.e2e-spec.ts b/e2e/src/specs/server/api/session.e2e-spec.ts index 0b632f78ba..1766b732cd 100644 --- a/e2e/src/specs/server/api/session.e2e-spec.ts +++ b/e2e/src/specs/server/api/session.e2e-spec.ts @@ -15,12 +15,6 @@ describe('/sessions', () => { }); describe('GET /sessions', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/sessions'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get a list of authorized devices', async () => { const { status, body } = await request(app).get('/sessions').set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(200); @@ -29,12 +23,6 @@ describe('/sessions', () => { }); describe('DELETE /sessions', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/sessions`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should logout all devices (except the current one)', async () => { for (let i = 0; i < 5; i++) { await login({ loginCredentialDto: loginDto.admin }); diff --git a/e2e/src/specs/server/api/shared-link.e2e-spec.ts b/e2e/src/specs/server/api/shared-link.e2e-spec.ts index 8cdf2dc03c..b9f695b48e 100644 --- a/e2e/src/specs/server/api/shared-link.e2e-spec.ts +++ b/e2e/src/specs/server/api/shared-link.e2e-spec.ts @@ -137,13 +137,6 @@ describe('/shared-links', () => { }); describe('GET /shared-links', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/shared-links'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get all shared links created by user', async () => { const { status, body } = await request(app) .get('/shared-links') @@ -200,12 +193,6 @@ describe('/shared-links', () => { }); describe('GET /shared-links/me', () => { - it('should not require admin authentication', async () => { - const { status } = await request(app).get('/shared-links/me').set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(403); - }); - it('should get data for correct shared link', async () => { const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key }); @@ -293,13 +280,6 @@ describe('/shared-links', () => { }); describe('GET /shared-links/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/shared-links/${linkWithAlbum.id}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get shared link by id', async () => { const { status, body } = await request(app) .get(`/shared-links/${linkWithAlbum.id}`) @@ -326,26 +306,6 @@ describe('/shared-links', () => { }); describe('POST /shared-links', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .post('/shared-links') - .send({ type: SharedLinkType.Album, albumId: uuidDto.notFound }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a type and the correspondent asset/album id', async () => { - const { status, body } = await request(app) - .post('/shared-links') - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(400); - expect(body).toEqual( - errorDto.validationError([{ path: [], message: 'Invalid input: expected object, received undefined' }]), - ); - }); - it('should require an asset/album id', async () => { const { status, body } = await request(app) .post('/shared-links') @@ -383,15 +343,6 @@ describe('/shared-links', () => { }); describe('PATCH /shared-links/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .patch(`/shared-links/${linkWithAlbum.id}`) - .send({ description: 'foo' }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should fail if invalid link', async () => { const { status, body } = await request(app) .patch(`/shared-links/${uuidDto.notFound}`) @@ -474,13 +425,6 @@ describe('/shared-links', () => { }); describe('DELETE /shared-links/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/shared-links/${linkWithAlbum.id}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should fail if invalid link', async () => { const { status, body } = await request(app) .delete(`/shared-links/${uuidDto.notFound}`) diff --git a/e2e/src/specs/server/api/stack.e2e-spec.ts b/e2e/src/specs/server/api/stack.e2e-spec.ts index 76bf514dc8..3b8f642334 100644 --- a/e2e/src/specs/server/api/stack.e2e-spec.ts +++ b/e2e/src/specs/server/api/stack.e2e-spec.ts @@ -1,5 +1,5 @@ import { AssetMediaResponseDto, LoginResponseDto, searchStacks } from '@immich/sdk'; -import { createUserDto, uuidDto } from 'src/fixtures'; +import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; @@ -25,42 +25,6 @@ describe('/stacks', () => { }); describe('POST /stacks', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .post('/stacks') - .send({ assetIds: [asset.id] }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require at least two assets', async () => { - const { status, body } = await request(app) - .post('/stacks') - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ assetIds: [asset.id] }); - - expect(status).toBe(400); - expect(body).toEqual( - errorDto.validationError([{ path: ['assetIds'], message: 'Too small: expected array to have >=2 items' }]), - ); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .post('/stacks') - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ assetIds: [uuidDto.invalid, uuidDto.invalid] }); - - expect(status).toBe(400); - expect(body).toEqual( - errorDto.validationError([ - { path: ['assetIds', 0], message: 'Invalid UUID' }, - { path: ['assetIds', 1], message: 'Invalid UUID' }, - ]), - ); - }); - it('should require access', async () => { const user2Asset = await utils.createAsset(user2.accessToken); const { status, body } = await request(app) diff --git a/e2e/src/specs/server/api/system-metadata.e2e-spec.ts b/e2e/src/specs/server/api/system-metadata.e2e-spec.ts index bd17bf2524..114ed2f995 100644 --- a/e2e/src/specs/server/api/system-metadata.e2e-spec.ts +++ b/e2e/src/specs/server/api/system-metadata.e2e-spec.ts @@ -16,12 +16,6 @@ describe('/server-info', () => { }); describe('POST /system-metadata/admin-onboarding', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/system-metadata/admin-onboarding').send({ isOnboarded: true }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should only work for admins', async () => { const { status, body } = await request(app) .post('/system-metadata/admin-onboarding') @@ -47,12 +41,6 @@ describe('/server-info', () => { }); describe('GET /system-metadata/reverse-geocoding-state', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/system-metadata/reverse-geocoding-state'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should only work for admins', async () => { const { status, body } = await request(app) .get('/system-metadata/reverse-geocoding-state') diff --git a/e2e/src/specs/server/api/tag.e2e-spec.ts b/e2e/src/specs/server/api/tag.e2e-spec.ts index d303a1e98d..98d66f6350 100644 --- a/e2e/src/specs/server/api/tag.e2e-spec.ts +++ b/e2e/src/specs/server/api/tag.e2e-spec.ts @@ -41,12 +41,6 @@ describe('/tags', () => { }); describe('POST /tags', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/tags').send({ name: 'TagA' }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization (api key)', async () => { const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); const { status, body } = await request(app).post('/tags').set('x-api-key', secret).send({ name: 'TagA' }); @@ -117,12 +111,6 @@ describe('/tags', () => { }); describe('GET /tags', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/tags'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization (api key)', async () => { const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); const { status, body } = await request(app).get('/tags').set('x-api-key', secret); @@ -169,12 +157,6 @@ describe('/tags', () => { }); describe('PUT /tags', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/tags`).send({ name: 'TagA/TagB' }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization (api key)', async () => { const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); const { status, body } = await request(app).put('/tags').set('x-api-key', secret).send({ name: 'TagA' }); @@ -213,12 +195,6 @@ describe('/tags', () => { }); describe('PUT /tags/assets', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/tags/assets`).send({ tagIds: [], assetIds: [] }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization (api key)', async () => { const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); const { status, body } = await request(app) @@ -279,12 +255,6 @@ describe('/tags', () => { }); describe('GET /tags/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/tags/${uuidDto.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const tag = await create(user.accessToken, { name: 'TagA' }); const { status, body } = await request(app) @@ -304,14 +274,6 @@ describe('/tags', () => { expect(body).toEqual(errorDto.missingPermission('tag.read')); }); - it('should require a valid uuid', async () => { - const { status, body } = await request(app) - .get(`/tags/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }])); - }); - it('should get tag details', async () => { const tag = await create(user.accessToken, { name: 'TagA' }); const { status, body } = await request(app) @@ -349,13 +311,6 @@ describe('/tags', () => { }); describe('PUT /tags/:id', () => { - it('should require authentication', async () => { - const tag = await create(user.accessToken, { name: 'TagA' }); - const { status, body } = await request(app).put(`/tags/${tag.id}`).send({ color: '#000000' }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const tag = await create(admin.accessToken, { name: 'tagA' }); const { status, body } = await request(app) @@ -399,12 +354,6 @@ describe('/tags', () => { }); describe('DELETE /tags/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/tags/${uuidDto.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const tag = await create(user.accessToken, { name: 'TagA' }); const { status, body } = await request(app) @@ -422,14 +371,6 @@ describe('/tags', () => { expect(body).toEqual(errorDto.missingPermission('tag.delete')); }); - it('should require a valid uuid', async () => { - const { status, body } = await request(app) - .delete(`/tags/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }])); - }); - it('should delete a tag', async () => { const tag = await create(user.accessToken, { name: 'TagA' }); const { status } = await request(app) @@ -463,15 +404,6 @@ describe('/tags', () => { }); describe('PUT /tags/:id/assets', () => { - it('should require authentication', async () => { - const tagA = await create(user.accessToken, { name: 'TagA' }); - const { status, body } = await request(app) - .put(`/tags/${tagA.id}/assets`) - .send({ ids: [userAsset.id] }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const tag = await create(user.accessToken, { name: 'TagA' }); const { status, body } = await request(app) @@ -531,16 +463,6 @@ describe('/tags', () => { }); describe('DELETE /tags/:id/assets', () => { - it('should require authentication', async () => { - const tagA = await create(admin.accessToken, { name: 'TagA' }); - const { status, body } = await request(app) - .delete(`/tags/${tagA}/assets`) - .send({ ids: [userAsset.id] }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const tagA = await create(user.accessToken, { name: 'TagA' }); await tagAssets( diff --git a/e2e/src/specs/server/api/user-admin.e2e-spec.ts b/e2e/src/specs/server/api/user-admin.e2e-spec.ts index f442d4249d..160c929591 100644 --- a/e2e/src/specs/server/api/user-admin.e2e-spec.ts +++ b/e2e/src/specs/server/api/user-admin.e2e-spec.ts @@ -46,12 +46,6 @@ describe('/admin/users', () => { }); describe('GET /admin/users', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/admin/users`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { status, body } = await request(app) .get(`/admin/users`) @@ -94,12 +88,6 @@ describe('/admin/users', () => { }); describe('POST /admin/users', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post(`/admin/users`).send(createUserDto.user1); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { status, body } = await request(app) .post(`/admin/users`) @@ -109,23 +97,6 @@ describe('/admin/users', () => { expect(body).toEqual(errorDto.forbidden); }); - for (const [key, message] of [ - ['password', 'Invalid input: expected string, received null'], - ['email', 'Invalid input: expected email, received object'], - ['name', 'Invalid input: expected string, received null'], - ['shouldChangePassword', 'Invalid input: expected boolean, received null'], - ['notify', 'Invalid input: expected boolean, received null'], - ] as const) { - it(`should not allow null ${key}`, async () => { - const { status, body } = await request(app) - .post(`/admin/users`) - .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ ...createUserDto.user1, [key]: null }); - expect(status).toBe(400); - expect(body).toEqual(errorDto.validationError([{ path: [key], message }])); - }); - } - it('should accept `isAdmin`', async () => { const { status, body } = await request(app) .post(`/admin/users`) @@ -146,12 +117,6 @@ describe('/admin/users', () => { }); describe('PUT /admin/users/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/admin/users/${uuidDto.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { status, body } = await request(app) .put(`/admin/users/${uuidDto.notFound}`) @@ -160,22 +125,6 @@ describe('/admin/users', () => { expect(body).toEqual(errorDto.forbidden); }); - for (const [key, message] of [ - ['password', 'Invalid input: expected string, received null'], - ['email', 'Invalid input: expected email, received object'], - ['name', 'Invalid input: expected string, received null'], - ['shouldChangePassword', 'Invalid input: expected boolean, received null'], - ] as const) { - it(`should not allow null ${key}`, async () => { - const { status, body } = await request(app) - .put(`/admin/users/${uuidDto.notFound}`) - .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ [key]: null }); - expect(status).toBe(400); - expect(body).toEqual(errorDto.validationError([{ path: [key], message }])); - }); - } - it('should allow a non-admin to become an admin', async () => { const user = await utils.userSetup(admin.accessToken, createUserDto.create('admin2')); const { status, body } = await request(app) @@ -245,12 +194,6 @@ describe('/admin/users', () => { }); describe('PUT /admin/users/:id/preferences', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/admin/users/${userToDelete.userId}/preferences`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should update memories enabled', async () => { const before = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) }); expect(before).toMatchObject({ memories: { enabled: true } }); @@ -282,12 +225,6 @@ describe('/admin/users', () => { }); describe('DELETE /admin/users/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/admin/users/${userToDelete.userId}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { status, body } = await request(app) .delete(`/admin/users/${userToDelete.userId}`) @@ -360,12 +297,6 @@ describe('/admin/users', () => { }); describe('GET /admin/users/:id/calendar-heatmap', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/admin/users/${nonAdmin.userId}/calendar-heatmap`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require admin permissions', async () => { const { status, body } = await request(app) .get(`/admin/users/${nonAdmin.userId}/calendar-heatmap`) @@ -385,12 +316,6 @@ describe('/admin/users', () => { }); describe('POST /admin/users/:id/restore', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post(`/admin/users/${userToDelete.userId}/restore`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { status, body } = await request(app) .post(`/admin/users/${userToDelete.userId}/restore`) diff --git a/e2e/src/specs/server/api/user.e2e-spec.ts b/e2e/src/specs/server/api/user.e2e-spec.ts index 2dc789a91b..2f270ca7ca 100644 --- a/e2e/src/specs/server/api/user.e2e-spec.ts +++ b/e2e/src/specs/server/api/user.e2e-spec.ts @@ -171,20 +171,6 @@ describe('/users', () => { expect(after).toMatchObject({ memories: { enabled: false } }); }); - it('should require an integer for download archive size', async () => { - const { status, body } = await request(app) - .put(`/users/me/preferences`) - .send({ download: { archiveSize: 1_234_567.89 } }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(400); - expect(body).toEqual( - errorDto.validationError([ - { path: ['download', 'archiveSize'], message: 'Invalid input: expected int, received number' }, - ]), - ); - }); - it('should update download archive size', async () => { const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); expect(before).toMatchObject({ download: { archiveSize: 4 * 2 ** 30 } }); @@ -201,20 +187,6 @@ describe('/users', () => { expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } }); }); - it('should require a boolean for download include embedded videos', async () => { - const { status, body } = await request(app) - .put(`/users/me/preferences`) - .send({ download: { includeEmbeddedVideos: 1_234_567.89 } }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(400); - expect(body).toEqual( - errorDto.validationError([ - { path: ['download', 'includeEmbeddedVideos'], message: 'Invalid input: expected boolean, received number' }, - ]), - ); - }); - it('should update download include embedded videos', async () => { const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); expect(before).toMatchObject({ download: { includeEmbeddedVideos: false } }); @@ -318,11 +290,6 @@ describe('/users', () => { }); describe('DELETE /users/me/license', () => { - it('should require authentication', async () => { - const { status } = await request(app).put(`/users/me/license`); - expect(status).toEqual(401); - }); - it('should delete the user license', async () => { const { status } = await request(app) .delete(`/users/me/license`) diff --git a/server/src/controllers/activity.controller.spec.ts b/server/src/controllers/activity.controller.spec.ts index 0b677b83fa..899c7257d6 100644 --- a/server/src/controllers/activity.controller.spec.ts +++ b/server/src/controllers/activity.controller.spec.ts @@ -19,11 +19,6 @@ describe(ActivityController.name, () => { }); describe('GET /activities', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/activities'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require an albumId', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/activities'); expect(status).toEqual(400); @@ -50,11 +45,6 @@ describe(ActivityController.name, () => { }); describe('POST /activities', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/activities'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require an albumId', async () => { const { status, body } = await request(ctx.getHttpServer()) .post('/activities') @@ -77,11 +67,6 @@ describe(ActivityController.name, () => { }); describe('DELETE /activities/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/activities/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/activities/123`); expect(status).toBe(400); diff --git a/server/src/controllers/album.controller.spec.ts b/server/src/controllers/album.controller.spec.ts index 2ab7b08ceb..239a602100 100644 --- a/server/src/controllers/album.controller.spec.ts +++ b/server/src/controllers/album.controller.spec.ts @@ -19,11 +19,6 @@ describe(AlbumController.name, () => { }); describe('GET /albums', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/albums'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should reject an invalid shared param', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/albums?isShared=invalid'); expect(status).toEqual(400); @@ -40,60 +35,4 @@ describe(AlbumController.name, () => { expect(body).toEqual(factory.responses.validationError([{ path: ['assetId'], message: 'Invalid UUID' }])); }); }); - - describe('GET /albums/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/albums/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /albums/statistics', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/albums/statistics'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('POST /albums', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/albums').send({ albumName: 'New album' }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('PUT /albums/:id/assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/albums/${factory.uuid()}/assets`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('PUT /albums/assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/albums/assets`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('PATCH /albums/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).patch(`/albums/${factory.uuid()}`).send({ albumName: 'New album name' }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('DELETE /albums/:id/assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/albums/${factory.uuid()}/assets`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('PUT :id/users', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/albums/${factory.uuid()}/users`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); }); diff --git a/server/src/controllers/api-key.controller.spec.ts b/server/src/controllers/api-key.controller.spec.ts index 91a7c43a2d..08963362b6 100644 --- a/server/src/controllers/api-key.controller.spec.ts +++ b/server/src/controllers/api-key.controller.spec.ts @@ -19,33 +19,7 @@ describe(ApiKeyController.name, () => { ctx.reset(); }); - describe('POST /api-keys', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/api-keys').send({ name: 'API Key' }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /api-keys', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/api-keys'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /api-keys/me', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/api-keys/me`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('GET /api-keys/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/api-keys/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/api-keys/123`); expect(status).toBe(400); @@ -54,11 +28,6 @@ describe(ApiKeyController.name, () => { }); describe('PUT /api-keys/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/api-keys/${factory.uuid()}`).send({ name: 'new name' }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()) .put(`/api-keys/123`) @@ -76,11 +45,6 @@ describe(ApiKeyController.name, () => { }); describe('DELETE /api-keys/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/api-keys/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/api-keys/123`); expect(status).toBe(400); diff --git a/server/src/controllers/asset-media.controller.spec.ts b/server/src/controllers/asset-media.controller.spec.ts index 668d88acbf..43d9a97780 100644 --- a/server/src/controllers/asset-media.controller.spec.ts +++ b/server/src/controllers/asset-media.controller.spec.ts @@ -44,11 +44,6 @@ describe(AssetMediaController.name, () => { }); describe('POST /assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post(`/assets`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should accept metadata', async () => { const mobileMetadata = { key: AssetMetadataKey.MobileApp, value: { iCloudId: '123' } }; const { status } = await request(ctx.getHttpServer()) @@ -171,20 +166,9 @@ describe(AssetMediaController.name, () => { }); // TODO figure out how to deal with `sendFile` - describe.skip('GET /assets/:id/original', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/original`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); // TODO figure out how to deal with `sendFile` describe('GET /assets/:id/thumbnail', () => { - it.skip('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should redirect if size=original is requested', async () => { const { status } = await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail?size=original`); expect(status).toBe(302); diff --git a/server/src/controllers/asset.controller.spec.ts b/server/src/controllers/asset.controller.spec.ts index 8ee6995b1f..92710d6ffc 100644 --- a/server/src/controllers/asset.controller.spec.ts +++ b/server/src/controllers/asset.controller.spec.ts @@ -20,11 +20,6 @@ describe(AssetController.name, () => { }); describe('PUT /assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/assets`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()) .put(`/assets`) @@ -59,13 +54,6 @@ describe(AssetController.name, () => { }); describe('DELETE /assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()) - .delete(`/assets`) - .send({ ids: [factory.uuid()] }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()) .delete(`/assets`) @@ -77,11 +65,6 @@ describe(AssetController.name, () => { }); describe('GET /assets/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123`); expect(status).toBe(400); @@ -90,11 +73,6 @@ describe(AssetController.name, () => { }); describe('PUT /assets/copy', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/copy`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require target and source id', async () => { const { status, body } = await request(ctx.getHttpServer()).put('/assets/copy').send({}); expect(status).toBe(400); @@ -115,11 +93,6 @@ describe(AssetController.name, () => { }); describe('PUT /assets/metadata', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/assets/metadata`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid assetId', async () => { const { status, body } = await request(ctx.getHttpServer()) .put('/assets/metadata') @@ -151,11 +124,6 @@ describe(AssetController.name, () => { }); describe('DELETE /assets/metadata', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/assets/metadata`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid assetId', async () => { const { status, body } = await request(ctx.getHttpServer()) .delete('/assets/metadata') @@ -187,11 +155,6 @@ describe(AssetController.name, () => { }); describe('PUT /assets/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/123`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123`); expect(status).toBe(400); @@ -267,26 +230,7 @@ describe(AssetController.name, () => { }); }); - describe('GET /assets/statistics', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/statistics`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /assets/:id/metadata', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('PUT /assets/:id/metadata', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/metadata`).send({ items: [] }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123/metadata`).send({ items: [] }); expect(status).toBe(400); @@ -353,11 +297,6 @@ describe(AssetController.name, () => { }); describe('GET /assets/:id/metadata/:key', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata/mobile-app`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123/metadata/mobile-app`); expect(status).toBe(400); @@ -366,11 +305,6 @@ describe(AssetController.name, () => { }); describe('PUT /assets/:id/edits', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/edits`).send({ edits: [] }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should accept valid edits and pass to service correctly', async () => { const edits = [ { @@ -456,11 +390,6 @@ describe(AssetController.name, () => { }); describe('DELETE /assets/:id/metadata/:key', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/assets/${factory.uuid()}/metadata/mobile-app`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/123/metadata/mobile-app`); expect(status).toBe(400); diff --git a/server/src/controllers/auth.controller.spec.ts b/server/src/controllers/auth.controller.spec.ts index 3bf1d59f8b..832547776d 100644 --- a/server/src/controllers/auth.controller.spec.ts +++ b/server/src/controllers/auth.controller.spec.ts @@ -199,28 +199,7 @@ describe(AuthController.name, () => { }); }); - describe('POST /auth/logout', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/auth/logout'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('POST /auth/change-password', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()) - .post('/auth/change-password') - .send({ password: 'password', newPassword: 'Password1234', invalidateSessions: false }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('POST /auth/pin-code', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '123456' }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should reject 5 digits', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '12345' }); expect(status).toEqual(400); @@ -251,25 +230,4 @@ describe(AuthController.name, () => { ); }); }); - - describe('PUT /auth/pin-code', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put('/auth/pin-code').send({ pinCode: '123456', newPinCode: '654321' }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('DELETE /auth/pin-code', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete('/auth/pin-code').send({ pinCode: '123456' }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /auth/status', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/auth/status'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); }); diff --git a/server/src/controllers/database-backup.controller.spec.ts b/server/src/controllers/database-backup.controller.spec.ts index 43fa779e8d..57a7ecc310 100644 --- a/server/src/controllers/database-backup.controller.spec.ts +++ b/server/src/controllers/database-backup.controller.spec.ts @@ -25,13 +25,6 @@ describe(DatabaseBackupController.name, () => { ctx.reset(); }); - describe('GET /admin/database-backups', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/admin/database-backups').send(); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('POST /admin/database-backups/start-restore', () => { it('should not be an authenticated route', async () => { maintenanceService.startRestoreFlow.mockResolvedValue({ jwt: 'jwt' }); diff --git a/server/src/controllers/download.controller.spec.ts b/server/src/controllers/download.controller.spec.ts deleted file mode 100644 index f42afe8c50..0000000000 --- a/server/src/controllers/download.controller.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Readable } from 'node:stream'; -import { DownloadController } from 'src/controllers/download.controller'; -import { DownloadService } from 'src/services/download.service'; -import request from 'supertest'; -import { factory } from 'test/small.factory'; -import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; - -describe(DownloadController.name, () => { - let ctx: ControllerContext; - const service = mockBaseService(DownloadService); - - beforeAll(async () => { - ctx = await controllerSetup(DownloadController, [{ provide: DownloadService, useValue: service }]); - return () => ctx.close(); - }); - - beforeEach(() => { - service.resetAllMocks(); - ctx.reset(); - }); - - describe('POST /download/info', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()) - .post('/download/info') - .send({ assetIds: [factory.uuid()] }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('POST /download/archive', () => { - it('should be an authenticated route', async () => { - const stream = new Readable({ - read() { - // eslint-disable-next-line unicorn/no-this-outside-of-class - this.push('test'); - // eslint-disable-next-line unicorn/no-this-outside-of-class - this.push(null); - }, - }); - service.downloadArchive.mockResolvedValue({ stream }); - await request(ctx.getHttpServer()) - .post('/download/archive') - .send({ assetIds: [factory.uuid()] }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); -}); diff --git a/server/src/controllers/duplicate.controller.spec.ts b/server/src/controllers/duplicate.controller.spec.ts index 7bbafb4665..c8ca0e5793 100644 --- a/server/src/controllers/duplicate.controller.spec.ts +++ b/server/src/controllers/duplicate.controller.spec.ts @@ -18,26 +18,7 @@ describe(DuplicateController.name, () => { ctx.reset(); }); - describe('GET /duplicates', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/duplicates'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('DELETE /duplicates', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete('/duplicates'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('DELETE /duplicates/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/duplicates/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/duplicates/123`); expect(status).toBe(400); diff --git a/server/src/controllers/maintenance.controller.spec.ts b/server/src/controllers/maintenance.controller.spec.ts index 630bb7c8b8..59b00b8aec 100644 --- a/server/src/controllers/maintenance.controller.spec.ts +++ b/server/src/controllers/maintenance.controller.spec.ts @@ -20,11 +20,6 @@ describe(MaintenanceController.name, () => { }); describe('POST /admin/maintenance', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/admin/maintenance').send(); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a backup file when action is restore', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/admin/maintenance').send({ action: MaintenanceAction.RestoreDatabase, diff --git a/server/src/controllers/memory.controller.spec.ts b/server/src/controllers/memory.controller.spec.ts index a3839793eb..b717ee966c 100644 --- a/server/src/controllers/memory.controller.spec.ts +++ b/server/src/controllers/memory.controller.spec.ts @@ -20,11 +20,6 @@ describe(MemoryController.name, () => { }); describe('GET /memories', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/memories'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should not require any parameters', async () => { await request(ctx.getHttpServer()).get('/memories').query({}); expect(service.search).toHaveBeenCalled(); @@ -32,11 +27,6 @@ describe(MemoryController.name, () => { }); describe('POST /memories', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/memories'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should validate data when type is on this day', async () => { const { status, body } = await request(ctx.getHttpServer()) .post('/memories') @@ -69,19 +59,7 @@ describe(MemoryController.name, () => { }); }); - describe('GET /memories/statistics', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/memories/statistics'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('GET /memories/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/memories/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/memories/invalid`); expect(status).toBe(400); @@ -90,11 +68,6 @@ describe(MemoryController.name, () => { }); describe('PUT /memories/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid`); expect(status).toBe(400); @@ -114,19 +87,7 @@ describe(MemoryController.name, () => { }); }); - describe('DELETE /memories/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/memories/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('PUT /memories/:id/assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}/assets`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid/assets`).send({ ids: [] }); expect(status).toBe(400); @@ -143,11 +104,6 @@ describe(MemoryController.name, () => { }); describe('DELETE /memories/:id/assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/memories/${factory.uuid()}/assets`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/memories/invalid/assets`); expect(status).toBe(400); diff --git a/server/src/controllers/notification-admin.controller.spec.ts b/server/src/controllers/notification-admin.controller.spec.ts index b93726eb32..e440f5bcf6 100644 --- a/server/src/controllers/notification-admin.controller.spec.ts +++ b/server/src/controllers/notification-admin.controller.spec.ts @@ -21,11 +21,6 @@ describe(NotificationAdminController.name, () => { }); describe('POST /admin/notifications', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/admin/notifications'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should accept a null readAt', async () => { await request(ctx.getHttpServer()) .post(`/admin/notifications`) diff --git a/server/src/controllers/notification.controller.spec.ts b/server/src/controllers/notification.controller.spec.ts index 1759e13404..f65537cbba 100644 --- a/server/src/controllers/notification.controller.spec.ts +++ b/server/src/controllers/notification.controller.spec.ts @@ -20,11 +20,6 @@ describe(NotificationController.name, () => { }); describe('GET /notifications', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/notifications'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should reject an invalid notification level`, async () => { const { status, body } = await request(ctx.getHttpServer()) .get(`/notifications`) @@ -40,11 +35,6 @@ describe(NotificationController.name, () => { }); describe('PUT /notifications', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put('/notifications'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - describe('ids', () => { it('should require a list', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/notifications`).send({ ids: true }); @@ -75,11 +65,6 @@ describe(NotificationController.name, () => { }); describe('GET /notifications/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/notifications/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/notifications/123`); expect(status).toBe(400); @@ -88,11 +73,6 @@ describe(NotificationController.name, () => { }); describe('PUT /notifications/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/notifications/${factory.uuid()}`).send({ readAt: factory.date() }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should accept a null readAt', async () => { const id = factory.uuid(); await request(ctx.getHttpServer()).put(`/notifications/${id}`).send({ readAt: null }); diff --git a/server/src/controllers/partner.controller.spec.ts b/server/src/controllers/partner.controller.spec.ts index d6541411b8..38cb717bea 100644 --- a/server/src/controllers/partner.controller.spec.ts +++ b/server/src/controllers/partner.controller.spec.ts @@ -3,7 +3,6 @@ import { LoggingRepository } from 'src/repositories/logging.repository'; import { PartnerService } from 'src/services/partner.service'; import request from 'supertest'; import { errorDto } from 'test/medium/responses'; -import { factory } from 'test/small.factory'; import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; describe(PartnerController.name, () => { @@ -24,11 +23,6 @@ describe(PartnerController.name, () => { }); describe('GET /partners', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/partners'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require a direction`, async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/partners`).set('Authorization', `Bearer token`); expect(status).toBe(400); @@ -54,11 +48,6 @@ describe(PartnerController.name, () => { }); describe('POST /partners', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/partners'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require sharedWithId to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .post(`/partners`) @@ -70,11 +59,6 @@ describe(PartnerController.name, () => { }); describe('PUT /partners/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/partners/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require id to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .put(`/partners/invalid`) @@ -86,11 +70,6 @@ describe(PartnerController.name, () => { }); describe('DELETE /partners/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/partners/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require id to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .delete(`/partners/invalid`) diff --git a/server/src/controllers/person.controller.spec.ts b/server/src/controllers/person.controller.spec.ts index 64fe4f3554..0d3d3eea4a 100644 --- a/server/src/controllers/person.controller.spec.ts +++ b/server/src/controllers/person.controller.spec.ts @@ -24,11 +24,6 @@ describe(PersonController.name, () => { }); describe('GET /people', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/people'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require closestPersonId to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .get(`/people`) @@ -48,19 +43,7 @@ describe(PersonController.name, () => { }); }); - describe('POST /people', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/people'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('DELETE /people', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete('/people'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require uuids in the body', async () => { const { status, body } = await request(ctx.getHttpServer()) .delete('/people') @@ -78,19 +61,7 @@ describe(PersonController.name, () => { }); }); - describe('GET /people/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('PUT /people/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/people/123`); expect(status).toBe(400); @@ -177,11 +148,6 @@ describe(PersonController.name, () => { }); describe('DELETE /people/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/people/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/people/invalid`); expect(status).toBe(400); @@ -194,18 +160,4 @@ describe(PersonController.name, () => { expect(service.delete).toHaveBeenCalled(); }); }); - - describe('POST /people/:id/merge', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post(`/people/${factory.uuid()}/merge`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /people/:id/statistics', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}/statistics`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); }); diff --git a/server/src/controllers/plugin.controller.spec.ts b/server/src/controllers/plugin.controller.spec.ts index 881a7dd953..bc42a4f75b 100644 --- a/server/src/controllers/plugin.controller.spec.ts +++ b/server/src/controllers/plugin.controller.spec.ts @@ -3,7 +3,6 @@ import { LoggingRepository } from 'src/repositories/logging.repository'; import { PluginService } from 'src/services/plugin.service'; import request from 'supertest'; import { errorDto } from 'test/medium/responses'; -import { factory } from 'test/small.factory'; import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; describe(PluginController.name, () => { @@ -24,11 +23,6 @@ describe(PluginController.name, () => { }); describe('GET /plugins', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/plugins'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require id to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .get(`/plugins`) @@ -40,11 +34,6 @@ describe(PluginController.name, () => { }); describe('GET /plugins/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/plugins/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require id to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .get(`/plugins/invalid`) diff --git a/server/src/controllers/search.controller.spec.ts b/server/src/controllers/search.controller.spec.ts index a1fed4c7ae..f1ac01ed41 100644 --- a/server/src/controllers/search.controller.spec.ts +++ b/server/src/controllers/search.controller.spec.ts @@ -19,11 +19,6 @@ describe(SearchController.name, () => { }); describe('POST /search/metadata', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/search/metadata'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should reject page as a string', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: 'abc' }); expect(status).toBe(400); @@ -121,11 +116,6 @@ describe(SearchController.name, () => { }); describe('POST /search/random', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/search/random'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should reject if withStacked is not a boolean', async () => { const { status, body } = await request(ctx.getHttpServer()) .post('/search/random') @@ -151,26 +141,7 @@ describe(SearchController.name, () => { }); }); - describe('POST /search/smart', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/search/smart'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /search/explore', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/search/explore'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('POST /search/person', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/search/person'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a name', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({}); expect(status).toBe(400); @@ -181,11 +152,6 @@ describe(SearchController.name, () => { }); describe('GET /search/places', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/search/places'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a name', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({}); expect(status).toBe(400); @@ -195,19 +161,7 @@ describe(SearchController.name, () => { }); }); - describe('GET /search/cities', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/search/cities'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('GET /search/suggestions', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/search/suggestions'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a type', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/search/suggestions').send({}); expect(status).toBe(400); diff --git a/server/src/controllers/server.controller.spec.ts b/server/src/controllers/server.controller.spec.ts deleted file mode 100644 index 6b00490d28..0000000000 --- a/server/src/controllers/server.controller.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ServerController } from 'src/controllers/server.controller'; -import { ServerService } from 'src/services/server.service'; -import { SystemMetadataService } from 'src/services/system-metadata.service'; -import { VersionService } from 'src/services/version.service'; -import request from 'supertest'; -import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; - -describe(ServerController.name, () => { - let ctx: ControllerContext; - const serverService = mockBaseService(ServerService); - const systemMetadataService = mockBaseService(SystemMetadataService); - const versionService = mockBaseService(VersionService); - - beforeAll(async () => { - ctx = await controllerSetup(ServerController, [ - { provide: ServerService, useValue: serverService }, - { provide: SystemMetadataService, useValue: systemMetadataService }, - { provide: VersionService, useValue: versionService }, - ]); - return () => ctx.close(); - }); - - beforeEach(() => { - serverService.resetAllMocks(); - versionService.resetAllMocks(); - ctx.reset(); - }); - - describe('GET /server/license', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/server/license'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); -}); diff --git a/server/src/controllers/shared-link.controller.spec.ts b/server/src/controllers/shared-link.controller.spec.ts index d8b89d0029..dc069ad267 100644 --- a/server/src/controllers/shared-link.controller.spec.ts +++ b/server/src/controllers/shared-link.controller.spec.ts @@ -2,6 +2,7 @@ import { SharedLinkController } from 'src/controllers/shared-link.controller'; import { Permission, SharedLinkType } from 'src/enum'; import { SharedLinkService } from 'src/services/shared-link.service'; import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; import { factory } from 'test/small.factory'; import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; @@ -19,10 +20,24 @@ describe(SharedLinkController.name, () => { ctx.reset(); }); + describe('GET /shared-links/me', () => { + it('should be a shared link route', async () => { + await request(ctx.getHttpServer()).get('/shared-links/me'); + expect(ctx.authenticate).toHaveBeenCalledWith( + expect.objectContaining({ metadata: expect.objectContaining({ sharedLinkRoute: true }) }), + ); + }); + }); + describe('POST /shared-links', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/shared-links'); - expect(ctx.authenticate).toHaveBeenCalled(); + it('should require a type and the correspondent asset/album id', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/shared-links') + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([{ path: [], message: 'Invalid input: expected object, received undefined' }]), + ); }); it('should allow an null expiresAt', async () => { diff --git a/server/src/controllers/stack.controller.spec.ts b/server/src/controllers/stack.controller.spec.ts new file mode 100644 index 0000000000..bc9c87da14 --- /dev/null +++ b/server/src/controllers/stack.controller.spec.ts @@ -0,0 +1,46 @@ +import { StackController } from 'src/controllers/stack.controller'; +import { StackService } from 'src/services/stack.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { factory } from 'test/small.factory'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(StackController.name, () => { + let ctx: ControllerContext; + const service = mockBaseService(StackService); + + beforeAll(async () => { + ctx = await controllerSetup(StackController, [{ provide: StackService, useValue: service }]); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + ctx.reset(); + }); + + describe('POST /stacks', () => { + it('should require at least two assets', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/stacks') + .send({ assetIds: [factory.uuid()] }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([{ path: ['assetIds'], message: 'Too small: expected array to have >=2 items' }]), + ); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/stacks') + .send({ assetIds: ['invalid', 'invalid'] }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([ + { path: ['assetIds', 0], message: 'Invalid UUID' }, + { path: ['assetIds', 1], message: 'Invalid UUID' }, + ]), + ); + }); + }); +}); diff --git a/server/src/controllers/sync.controller.spec.ts b/server/src/controllers/sync.controller.spec.ts index cae7650d9a..1f2c0f137a 100644 --- a/server/src/controllers/sync.controller.spec.ts +++ b/server/src/controllers/sync.controller.spec.ts @@ -25,11 +25,6 @@ describe(SyncController.name, () => { }); describe('POST /sync/stream', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/sync/stream'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require sync request type enums', async () => { const { status, body } = await request(ctx.getHttpServer()) .post('/sync/stream') @@ -44,19 +39,7 @@ describe(SyncController.name, () => { }); }); - describe('GET /sync/ack', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/sync/ack'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('POST /sync/ack', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/sync/ack'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should not allow more than 1,000 entries', async () => { const acks = Array.from({ length: 1001 }, (_, i) => `ack-${i}`); const { status, body } = await request(ctx.getHttpServer()).post('/sync/ack').send({ acks }); @@ -69,11 +52,6 @@ describe(SyncController.name, () => { }); describe('DELETE /sync/ack', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete('/sync/ack'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require sync response type enums', async () => { const { status, body } = await request(ctx.getHttpServer()) .delete('/sync/ack') diff --git a/server/src/controllers/system-config.controller.spec.ts b/server/src/controllers/system-config.controller.spec.ts index 439b5ddec1..7d40f12583 100644 --- a/server/src/controllers/system-config.controller.spec.ts +++ b/server/src/controllers/system-config.controller.spec.ts @@ -40,26 +40,7 @@ describe(SystemConfigController.name, () => { ctx.reset(); }); - describe('GET /system-config', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/system-config'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /system-config/defaults', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/system-config/defaults'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('PUT /system-config', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put('/system-config'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - describe('nightlyTasks', () => { it('should validate nightly jobs start time', async () => { const config = validConfig(); diff --git a/server/src/controllers/tag.controller.spec.ts b/server/src/controllers/tag.controller.spec.ts index c2a2de95bd..a89ce14dda 100644 --- a/server/src/controllers/tag.controller.spec.ts +++ b/server/src/controllers/tag.controller.spec.ts @@ -2,7 +2,6 @@ import { TagController } from 'src/controllers/tag.controller'; import { TagService } from 'src/services/tag.service'; import request from 'supertest'; import { errorDto } from 'test/medium/responses'; -import { factory } from 'test/small.factory'; import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; describe(TagController.name, () => { @@ -19,38 +18,14 @@ describe(TagController.name, () => { ctx.reset(); }); - describe('GET /tags', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/tags'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('POST /tags', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/tags'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should a null parentId', async () => { await request(ctx.getHttpServer()).post(`/tags`).send({ name: 'tag', parentId: null }); expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ parentId: null })); }); }); - describe('PUT /tags', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put('/tags'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('GET /tags/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/tags/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/tags/123`); expect(status).toBe(400); @@ -58,10 +33,11 @@ describe(TagController.name, () => { }); }); - describe('PUT /tags/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/tags/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); + describe('DELETE /tags/:id', () => { + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).delete(`/tags/123`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }])); }); }); }); diff --git a/server/src/controllers/timeline.controller.spec.ts b/server/src/controllers/timeline.controller.spec.ts index ff18c020c1..288945ee97 100644 --- a/server/src/controllers/timeline.controller.spec.ts +++ b/server/src/controllers/timeline.controller.spec.ts @@ -19,11 +19,6 @@ describe(TimelineController.name, () => { }); describe('GET /timeline/buckets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/timeline/buckets'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should parse bbox query string into an object', async () => { const { status } = await request(ctx.getHttpServer()) .get('/timeline/buckets') @@ -58,11 +53,6 @@ describe(TimelineController.name, () => { }); describe('GET /timeline/bucket', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/timeline/bucket?timeBucket=1900-01-01'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - // TODO enable date string validation while still accepting 5 digit years it.fails('should fail if time bucket is invalid', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/timeline/bucket').query({ timeBucket: 'foo' }); diff --git a/server/src/controllers/user-admin.controller.spec.ts b/server/src/controllers/user-admin.controller.spec.ts index b5840a33e1..3fa423f9b3 100644 --- a/server/src/controllers/user-admin.controller.spec.ts +++ b/server/src/controllers/user-admin.controller.spec.ts @@ -24,26 +24,7 @@ describe(UserAdminController.name, () => { ctx.reset(); }); - describe('GET /admin/users', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/admin/users'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('PUT /admin/users/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/admin/users/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('POST /admin/users', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/admin/users'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should allow a null pinCode', async () => { await request(ctx.getHttpServer()).post(`/admin/users`).send({ name: 'Test user', @@ -64,25 +45,22 @@ describe(UserAdminController.name, () => { expect(service.create).toHaveBeenCalledWith(expect.objectContaining({ avatarColor: null })); }); - it(`should `, async () => { - const dto: UserAdminCreateDto = { - email: 'user@immich.app', - password: 'test', - name: 'Test User', - quotaSizeInBytes: 1.2, - }; - - const { status, body } = await request(ctx.getHttpServer()) - .post(`/admin/users`) - .set('Authorization', `Bearer token`) - .send(dto); - expect(status).toBe(400); - expect(body).toEqual( - errorDto.validationError([ - { path: ['quotaSizeInBytes'], message: 'Invalid input: expected int, received number' }, - ]), - ); - }); + for (const [key, message] of [ + ['password', 'Invalid input: expected string, received null'], + ['email', 'Invalid input: expected email, received object'], + ['name', 'Invalid input: expected string, received null'], + ['shouldChangePassword', 'Invalid input: expected boolean, received null'], + ['notify', 'Invalid input: expected boolean, received null'], + ] as const) { + it(`should not allow null ${key}`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post(`/admin/users`) + .set('Authorization', `Bearer token`) + .send({ email: 'user@immich.app', password: 'test', name: 'Test User', [key]: null }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.validationError([{ path: [key], message }])); + }); + } it(`should not allow decimal quota`, async () => { const dto: UserAdminCreateDto = { @@ -105,19 +83,7 @@ describe(UserAdminController.name, () => { }); }); - describe('GET /admin/users/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/admin/users/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('PUT /admin/users/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/admin/users/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should not allow decimal quota`, async () => { const { status, body } = await request(ctx.getHttpServer()) .put(`/admin/users/${factory.uuid()}`) @@ -142,5 +108,21 @@ describe(UserAdminController.name, () => { await request(ctx.getHttpServer()).put(`/admin/users/${id}`).send({ avatarColor: null }); expect(service.update).toHaveBeenCalledWith(undefined, id, expect.objectContaining({ avatarColor: null })); }); + + for (const [key, message] of [ + ['password', 'Invalid input: expected string, received null'], + ['email', 'Invalid input: expected email, received object'], + ['name', 'Invalid input: expected string, received null'], + ['shouldChangePassword', 'Invalid input: expected boolean, received null'], + ] as const) { + it(`should not allow null ${key}`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/admin/users/${factory.uuid()}`) + .set('Authorization', `Bearer token`) + .send({ [key]: null }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.validationError([{ path: [key], message }])); + }); + } }); }); diff --git a/server/src/controllers/user.controller.spec.ts b/server/src/controllers/user.controller.spec.ts index f512e2de39..d15eda31dc 100644 --- a/server/src/controllers/user.controller.spec.ts +++ b/server/src/controllers/user.controller.spec.ts @@ -3,7 +3,6 @@ import { LoggingRepository } from 'src/repositories/logging.repository'; import { UserService } from 'src/services/user.service'; import request from 'supertest'; import { errorDto } from 'test/medium/responses'; -import { factory } from 'test/small.factory'; import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; describe(UserController.name, () => { @@ -23,26 +22,7 @@ describe(UserController.name, () => { ctx.reset(); }); - describe('GET /users', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/users'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /users/me', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/users/me'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('PUT /users/me', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put('/users/me'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - for (const [key, message] of [ ['email', 'Invalid input: expected email, received object'], ['name', 'Invalid input: expected string, received null'], @@ -66,24 +46,31 @@ describe(UserController.name, () => { }); }); - describe('GET /users/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/users/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); + describe('PUT /users/me/preferences', () => { + it('should require an integer for download archive size', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/users/me/preferences`) + .set('Authorization', `Bearer token`) + .send({ download: { archiveSize: 1_234_567.89 } }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([ + { path: ['download', 'archiveSize'], message: 'Invalid input: expected int, received number' }, + ]), + ); }); - }); - describe('PUT /users/me/license', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put('/users/me/license'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('DELETE /users/me/license', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete('/users/me/license'); - expect(ctx.authenticate).toHaveBeenCalled(); + it('should require a boolean for download include embedded videos', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/users/me/preferences`) + .set('Authorization', `Bearer token`) + .send({ download: { includeEmbeddedVideos: 1_234_567.89 } }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([ + { path: ['download', 'includeEmbeddedVideos'], message: 'Invalid input: expected boolean, received number' }, + ]), + ); }); }); }); diff --git a/server/src/controllers/workflow.controller.spec.ts b/server/src/controllers/workflow.controller.spec.ts index ad0dc4982d..0b91667671 100644 --- a/server/src/controllers/workflow.controller.spec.ts +++ b/server/src/controllers/workflow.controller.spec.ts @@ -4,7 +4,6 @@ import { LoggingRepository } from 'src/repositories/logging.repository'; import { WorkflowService } from 'src/services/workflow.service'; import request from 'supertest'; import { errorDto } from 'test/medium/responses'; -import { factory } from 'test/small.factory'; import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; describe(WorkflowController.name, () => { @@ -25,11 +24,6 @@ describe(WorkflowController.name, () => { }); describe('POST /workflows', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/workflows').send({}); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require a valid trigger`, async () => { const { status, body } = await request(ctx.getHttpServer()) .post(`/workflows`) @@ -65,11 +59,6 @@ describe(WorkflowController.name, () => { }); describe('GET /workflows', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/workflows'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require id to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .get(`/workflows`) @@ -81,11 +70,6 @@ describe(WorkflowController.name, () => { }); describe('GET /workflows/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/workflows/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require id to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .get(`/workflows/invalid`) @@ -96,11 +80,6 @@ describe(WorkflowController.name, () => { }); describe('PATCH /workflows/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).patch(`/workflows/${factory.uuid()}`).send({}); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require id to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .patch(`/workflows/invalid`) From 00d10dab6391542419a0e362f51b409821d98541 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 11 Aug 2026 12:39:25 -0400 Subject: [PATCH 35/49] refactor: e2e user tests (#30716) --- e2e/src/specs/server/api/user.e2e-spec.ts | 250 +----------------- .../specs/services/user.service.spec.ts | 208 ++++++++++++++- 2 files changed, 198 insertions(+), 260 deletions(-) diff --git a/e2e/src/specs/server/api/user.e2e-spec.ts b/e2e/src/specs/server/api/user.e2e-spec.ts index 2f270ca7ca..0e62672f36 100644 --- a/e2e/src/specs/server/api/user.e2e-spec.ts +++ b/e2e/src/specs/server/api/user.e2e-spec.ts @@ -1,34 +1,18 @@ -import { LoginResponseDto, SharedLinkType, deleteUserAdmin, getMyPreferences, getMyUser, login } from '@immich/sdk'; +import { LoginResponseDto, SharedLinkType, getMyUser, login } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; -const userLicense = { - licenseKey: 'IMCL-FF69-TUK1-RWZU-V9Q8-QGQS-S5GC-X4R2-UFK4', - activationKey: - 'KuX8KsktrBSiXpQMAH0zLgA5SpijXVr_PDkzLdWUlAogCTMBZ0I3KCHXK0eE9EEd7harxup8_EHMeqAWeHo5VQzol6LGECpFv585U9asXD4Zc-UXt3mhJr2uhazqipBIBwJA2YhmUCDy8hiyiGsukDQNu9Rg9C77UeoKuZBWVjWUBWG0mc1iRqfvF0faVM20w53czAzlhaMxzVGc3Oimbd7xi_CAMSujF_2y8QpA3X2fOVkQkzdcH9lV0COejl7IyH27zQQ9HrlrXv3Lai5Hw67kNkaSjmunVBxC5PS0TpKoc9SfBJMaAGWnaDbjhjYUrm-8nIDQnoeEAidDXVAdPw', -}; - describe('/users', () => { let admin: LoginResponseDto; - let deletedUser: LoginResponseDto; let nonAdmin: LoginResponseDto; beforeAll(async () => { await utils.resetDatabase(); admin = await utils.adminSetup({ onboarding: false }); - - [deletedUser, nonAdmin] = await Promise.all([ - utils.userSetup(admin.accessToken, createUserDto.user1), - utils.userSetup(admin.accessToken, createUserDto.user2), - ]); - - await deleteUserAdmin( - { id: deletedUser.userId, userAdminDeleteDto: {} }, - { headers: asBearerAuth(admin.accessToken) }, - ); + nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user2); }); describe('GET /users/me', () => { @@ -42,55 +26,9 @@ describe('/users', () => { expect(status).toBe(403); expect(body).toEqual(errorDto.forbidden); }); - - it('should get my user', async () => { - const { status, body } = await request(app).get(`/users/me`).set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(200); - expect(body).toMatchObject({ - id: admin.userId, - email: 'admin@immich.cloud', - quotaUsageInBytes: 0, - }); - }); - - it('should get my user with license info', async () => { - const { status: licenseStatus } = await request(app) - .put(`/users/me/license`) - .send(userLicense) - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - expect(licenseStatus).toBe(200); - const { status, body } = await request(app) - .get(`/users/me`) - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - expect(status).toBe(200); - expect(body).toMatchObject({ - id: nonAdmin.userId, - email: nonAdmin.userEmail, - quotaUsageInBytes: 0, - license: userLicense, - }); - }); }); describe('PUT /users/me', () => { - it('should update first and last name', async () => { - const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) }); - - const { status, body } = await request(app) - .put(`/users/me`) - .send({ name: 'Name' }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual({ - ...before, - updatedAt: expect.any(String), - profileChangedAt: expect.any(String), - createdAt: expect.any(String), - name: 'Name', - }); - }); - /** @deprecated */ it('should allow a user to change their password (deprecated)', async () => { const user = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) }); @@ -112,189 +50,5 @@ describe('/users', () => { expect(token.accessToken).toBeDefined(); }); - - it('should not allow user to change to a taken email', async () => { - const { status, body } = await request(app) - .put(`/users/me`) - .send({ email: 'admin@immich.cloud' }) - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - - expect(status).toBe(400); - expect(body).toMatchObject(errorDto.badRequest('Email is not available')); - }); - - it('should update my email', async () => { - const before = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) }); - const { status, body } = await request(app) - .put(`/users/me`) - .send({ email: 'non-admin@immich.cloud' }) - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ - ...before, - email: 'non-admin@immich.cloud', - updatedAt: expect.anything(), - createdAt: expect.anything(), - profileChangedAt: expect.anything(), - }); - }); - - it('should update avatar color', async () => { - const { status, body } = await request(app) - .put(`/users/me`) - .send({ avatarColor: 'blue' }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ avatarColor: 'blue' }); - - const after = await getMyUser({ headers: asBearerAuth(admin.accessToken) }); - expect(after).toMatchObject({ avatarColor: 'blue' }); - }); - }); - - describe('PUT /users/me/preferences', () => { - it('should update memories enabled', async () => { - const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(before).toMatchObject({ memories: { enabled: true } }); - - const { status, body } = await request(app) - .put(`/users/me/preferences`) - .send({ memories: { enabled: false } }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ memories: { enabled: false } }); - - const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(after).toMatchObject({ memories: { enabled: false } }); - }); - - it('should update download archive size', async () => { - const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(before).toMatchObject({ download: { archiveSize: 4 * 2 ** 30 } }); - - const { status, body } = await request(app) - .put(`/users/me/preferences`) - .send({ download: { archiveSize: 1_234_567 } }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ download: { archiveSize: 1_234_567 } }); - - const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } }); - }); - - it('should update download include embedded videos', async () => { - const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(before).toMatchObject({ download: { includeEmbeddedVideos: false } }); - - const { status, body } = await request(app) - .put(`/users/me/preferences`) - .send({ download: { includeEmbeddedVideos: true } }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ download: { includeEmbeddedVideos: true } }); - - const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(after).toMatchObject({ download: { includeEmbeddedVideos: true } }); - }); - - it('should update minimum face count to display people', async () => { - const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(before).toMatchObject({ people: { minimumFaces: 3 } }); - - const { status, body } = await request(app) - .put('/users/me/preferences') - .send({ people: { minimumFaces: 2 } }) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(200); - expect(body).toMatchObject({ people: { minimumFaces: 2 } }); - - const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(after).toMatchObject({ people: { minimumFaces: 2 } }); - }); - }); - - describe('GET /users/:id', () => { - it('should get the user', async () => { - const { status, body } = await request(app) - .get(`/users/${admin.userId}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(200); - expect(body).toMatchObject({ - id: admin.userId, - email: 'admin@immich.cloud', - }); - - expect(body).not.toMatchObject({ - shouldChangePassword: expect.anything(), - storageLabel: expect.anything(), - }); - }); - }); - - describe('GET /server/license', () => { - it('should return the user license', async () => { - await request(app) - .put('/users/me/license') - .set('Authorization', `Bearer ${nonAdmin.accessToken}`) - .send(userLicense); - const { status, body } = await request(app) - .get('/users/me/license') - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - expect(status).toBe(200); - expect(body).toEqual({ - ...userLicense, - activatedAt: expect.any(String), - }); - }); - }); - - describe('PUT /users/me/license', () => { - it('should set the user license', async () => { - const { status, body } = await request(app) - .put(`/users/me/license`) - .send(userLicense) - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - expect(status).toBe(200); - expect(body).toMatchObject({ ...userLicense, activatedAt: expect.any(String) }); - expect(status).toBe(200); - expect(body).toEqual({ ...userLicense, activatedAt: expect.any(String) }); - const { body: licenseBody } = await request(app) - .get('/users/me/license') - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - expect(licenseBody).toEqual({ ...userLicense, activatedAt: expect.any(String) }); - }); - - it('should reject license not starting with IMCL-', async () => { - const { status, body } = await request(app) - .put('/users/me/license') - .set('Authorization', `Bearer ${nonAdmin.accessToken}`) - .send({ licenseKey: 'IMSV-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD', activationKey: 'activationKey' }); - expect(status).toBe(400); - expect(body.message).toBe('Invalid license key'); - }); - - it('should reject license with invalid activation key', async () => { - const { status, body } = await request(app) - .put('/users/me/license') - .set('Authorization', `Bearer ${nonAdmin.accessToken}`) - .send({ licenseKey: userLicense.licenseKey, activationKey: `invalid${userLicense.activationKey}` }); - expect(status).toBe(400); - expect(body.message).toBe('Invalid license key'); - }); - }); - - describe('DELETE /users/me/license', () => { - it('should delete the user license', async () => { - const { status } = await request(app) - .delete(`/users/me/license`) - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - expect(status).toBe(204); - }); }); }); diff --git a/server/test/medium/specs/services/user.service.spec.ts b/server/test/medium/specs/services/user.service.spec.ts index 2eac06d674..85ab9a8c0d 100644 --- a/server/test/medium/specs/services/user.service.spec.ts +++ b/server/test/medium/specs/services/user.service.spec.ts @@ -1,6 +1,6 @@ import { Kysely } from 'kysely'; import { DateTime } from 'luxon'; -import { ImmichEnvironment, JobName, JobStatus } from 'src/enum'; +import { ImmichEnvironment, JobName, JobStatus, UserAvatarColor } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { EventRepository } from 'src/repositories/event.repository'; @@ -10,10 +10,17 @@ import { SystemMetadataRepository } from 'src/repositories/system-metadata.repos import { UserRepository } from 'src/repositories/user.repository'; import { DB } from 'src/schema'; import { UserService } from 'src/services/user.service'; +import { HumanReadableSize } from 'src/utils/bytes'; import { mediumFactory, newMediumService } from 'test/medium.factory'; import { factory } from 'test/small.factory'; import { getKyselyDB } from 'test/utils'; +const userLicense = { + licenseKey: 'IMCL-FF69-TUK1-RWZU-V9Q8-QGQS-S5GC-X4R2-UFK4', + activationKey: + 'KuX8KsktrBSiXpQMAH0zLgA5SpijXVr_PDkzLdWUlAogCTMBZ0I3KCHXK0eE9EEd7harxup8_EHMeqAWeHo5VQzol6LGECpFv585U9asXD4Zc-UXt3mhJr2uhazqipBIBwJA2YhmUCDy8hiyiGsukDQNu9Rg9C77UeoKuZBWVjWUBWG0mc1iRqfvF0faVM20w53czAzlhaMxzVGc3Oimbd7xi_CAMSujF_2y8QpA3X2fOVkQkzdcH9lV0COejl7IyH27zQQ9HrlrXv3Lai5Hw67kNkaSjmunVBxC5PS0TpKoc9SfBJMaAGWnaDbjhjYUrm-8nIDQnoeEAidDXVAdPw', +}; + let defaultDatabase: Kysely; const setup = (db?: Kysely) => { @@ -38,9 +45,10 @@ describe(UserService.name, () => { const { sut, ctx } = setup(); ctx.getMock(EventRepository).emit.mockResolvedValue(); const user = mediumFactory.userInsert(); - await expect(sut.createUser({ name: user.name, email: user.email })).resolves.toEqual( - expect.objectContaining({ name: user.name, email: user.email }), - ); + const created = await sut.createUser({ name: user.name, email: user.email }); + expect(created).toEqual(expect.objectContaining({ name: user.name, email: user.email })); + + await expect(sut.get(created.id)).resolves.toMatchObject({ name: user.name, email: user.email }); }); it('should reject user with duplicate email', async () => { @@ -97,6 +105,38 @@ describe(UserService.name, () => { expect((result as any).password).toBeUndefined(); }); + + it('should not expose private fields', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + + await expect(sut.get(user.id)).resolves.not.toMatchObject({ + shouldChangePassword: expect.anything(), + storageLabel: expect.anything(), + }); + }); + }); + + describe('getMe', () => { + it('should get my user', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + await expect(sut.getMe(auth)).resolves.toEqual( + expect.objectContaining({ id: user.id, email: user.email, quotaUsageInBytes: 0 }), + ); + }); + + it('should include license info', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + await sut.setLicense(auth, userLicense); + + await expect(sut.getMe(auth)).resolves.toMatchObject({ license: userLicense }); + }); }); describe('updateMe', () => { @@ -109,25 +149,169 @@ describe(UserService.name, () => { expect(before.updatedAt).toBeDefined(); expect(after.updatedAt).toBeDefined(); expect(before.updatedAt).not.toEqual(after.updatedAt); + + await expect(sut.getMe(auth)).resolves.toMatchObject({ + name: `${before.name} Updated`, + updatedAt: after.updatedAt, + }); + }); + + it('should update the name', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + const dto = { name: 'Name' }; + + await expect(sut.updateMe(auth, dto)).resolves.toMatchObject(dto); + await expect(sut.getMe(auth)).resolves.toMatchObject(dto); + }); + + it('should update the email', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + const dto = { email: 'updated@immich.cloud' }; + + await expect(sut.updateMe(auth, dto)).resolves.toMatchObject(dto); + await expect(sut.getMe(auth)).resolves.toMatchObject(dto); + }); + + it('should not allow an email that is already taken', async () => { + const { sut, ctx } = setup(); + const { user: user1 } = await ctx.newUser(); + const { user: user2 } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user2.id } }); + + await expect(sut.updateMe(auth, { email: user1.email })).rejects.toThrow('Email is not available'); + }); + + it('should update the avatar color', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + const dto = { avatarColor: UserAvatarColor.Blue }; + + await expect(sut.updateMe(auth, dto)).resolves.toMatchObject(dto); + await expect(sut.getMe(auth)).resolves.toMatchObject(dto); + }); + + it('should clear shouldChangePassword when the password is updated', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser({ shouldChangePassword: true }); + const auth = factory.auth({ user: { id: user.id } }); + + await expect(sut.updateMe(auth, { password: 'super-secret' })).resolves.toMatchObject({ + shouldChangePassword: false, + }); + await expect(sut.getMe(auth)).resolves.toMatchObject({ shouldChangePassword: false }); + }); + }); + + describe('updateMyPreferences', () => { + it('should update memories enabled', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + const dto = { memories: { enabled: false } }; + + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject({ memories: { enabled: true } }); + await expect(sut.updateMyPreferences(auth, dto)).resolves.toMatchObject(dto); + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject(dto); + }); + + it('should update the download archive size', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + const dto = { download: { archiveSize: 1_234_567 } }; + + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject({ + download: { archiveSize: 4 * HumanReadableSize.GiB }, + }); + await expect(sut.updateMyPreferences(auth, dto)).resolves.toMatchObject(dto); + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject(dto); + }); + + it('should update download include embedded videos', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + const dto = { download: { includeEmbeddedVideos: true } }; + + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject({ + download: { includeEmbeddedVideos: false }, + }); + await expect(sut.updateMyPreferences(auth, dto)).resolves.toMatchObject(dto); + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject(dto); + }); + + it('should update the minimum face count', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + const dto = { people: { minimumFaces: 2 } }; + + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject({ people: { minimumFaces: 3 } }); + await expect(sut.updateMyPreferences(auth, dto)).resolves.toMatchObject(dto); + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject(dto); }); }); describe('setLicense', () => { it('should set a license', async () => { - const license = { - licenseKey: 'IMCL-FF69-TUK1-RWZU-V9Q8-QGQS-S5GC-X4R2-UFK4', - activationKey: - 'KuX8KsktrBSiXpQMAH0zLgA5SpijXVr_PDkzLdWUlAogCTMBZ0I3KCHXK0eE9EEd7harxup8_EHMeqAWeHo5VQzol6LGECpFv585U9asXD4Zc-UXt3mhJr2uhazqipBIBwJA2YhmUCDy8hiyiGsukDQNu9Rg9C77UeoKuZBWVjWUBWG0mc1iRqfvF0faVM20w53czAzlhaMxzVGc3Oimbd7xi_CAMSujF_2y8QpA3X2fOVkQkzdcH9lV0COejl7IyH27zQQ9HrlrXv3Lai5Hw67kNkaSjmunVBxC5PS0TpKoc9SfBJMaAGWnaDbjhjYUrm-8nIDQnoeEAidDXVAdPw', - }; const { sut, ctx } = setup(); const { user } = await ctx.newUser(); const auth = factory.auth({ user: { id: user.id } }); await expect(sut.getLicense(auth)).rejects.toThrowError(); - const after = await sut.setLicense(auth, license); - expect(after.licenseKey).toEqual(license.licenseKey); - expect(after.activationKey).toEqual(license.activationKey); + const after = await sut.setLicense(auth, userLicense); + expect(after.licenseKey).toEqual(userLicense.licenseKey); + expect(after.activationKey).toEqual(userLicense.activationKey); const response = await sut.getLicense(auth); expect(response).toEqual(after); + await expect(sut.getMe(auth)).resolves.toMatchObject({ license: after }); + }); + + it('should reject a license key that does not start with IMCL-', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + await expect( + sut.setLicense(auth, { + licenseKey: 'IMSV-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD', + activationKey: 'activationKey', + }), + ).rejects.toThrow('Invalid license key'); + }); + + it('should reject an invalid activation key', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + await expect( + sut.setLicense(auth, { ...userLicense, activationKey: `invalid${userLicense.activationKey}` }), + ).rejects.toThrow('Invalid license key'); + }); + }); + + describe('deleteLicense', () => { + it('should delete the license', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + await sut.setLicense(auth, userLicense); + await sut.deleteLicense(auth); + + await expect(sut.getLicense(auth)).rejects.toThrowError(); }); }); From a55dc80a5685925be16db81085c37b50078e65d6 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:15:51 +0200 Subject: [PATCH 36/49] chore: enforce strict equality checks (#30718) --- server/eslint.config.mjs | 1 + server/src/repositories/asset.repository.ts | 4 ++-- server/src/repositories/telemetry.repository.ts | 2 +- server/src/repositories/user.repository.ts | 2 +- server/src/services/auth.service.ts | 2 +- server/src/services/metadata.service.ts | 8 ++++---- server/src/services/storage-template.service.ts | 4 ++-- server/src/utils/bytes.ts | 2 +- server/src/utils/transform.ts | 2 +- web/eslint.config.js | 2 ++ web/src/lib/actions/use-actions.ts | 2 +- web/src/lib/components/asset-viewer/ActivityViewer.svelte | 6 +++--- web/src/lib/components/asset-viewer/AssetViewer.svelte | 2 +- .../lib/components/asset-viewer/VideoRemoteViewer.svelte | 6 +++--- web/src/lib/components/faces-page/PersonSidePanel.svelte | 2 +- .../components/shared-components/CoordinatesInput.svelte | 2 +- web/src/lib/components/shared-components/map/Map.svelte | 2 +- web/src/lib/elements/FormatMessage.svelte | 2 +- web/src/lib/i18n.spec.ts | 2 +- .../VirtualScrollManager/VirtualScrollManager.svelte.ts | 6 +++--- web/src/lib/modals/GeolocationUpdateConfirmModal.svelte | 1 + web/src/lib/modals/timezone-utils.ts | 2 +- web/src/lib/stores/upload.ts | 4 ++-- web/src/lib/utils/actions.ts | 2 +- web/src/lib/utils/duplicate-utils.ts | 5 +++++ web/src/lib/utils/navigation.ts | 2 +- web/src/lib/utils/timeline-util.ts | 4 ++-- web/src/routes/(user)/people/+page.svelte | 2 +- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 4 ++-- .../search/[[photos=photos]]/[[assetId=id]]/+page.svelte | 2 +- web/src/routes/(user)/user-settings/AppSettings.svelte | 2 +- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 2 +- web/src/routes/admin/system-settings/JobSettings.svelte | 2 +- web/src/routes/auth/onboarding/+page.svelte | 2 +- web/src/routes/auth/onboarding/OnboardingHello.svelte | 2 +- 35 files changed, 54 insertions(+), 45 deletions(-) diff --git a/server/eslint.config.mjs b/server/eslint.config.mjs index 2cca2cab6d..2c11b7d3cb 100644 --- a/server/eslint.config.mjs +++ b/server/eslint.config.mjs @@ -76,6 +76,7 @@ export default typescriptEslint.config([ curly: 2, 'prettier/prettier': 0, 'object-shorthand': ['error', 'always'], + eqeqeq: 'error', 'no-restricted-imports': [ 'error', diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index ca00245c27..add3f0a24d 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -838,7 +838,7 @@ export class AssetRepository { ) .$if(!!options.withCoordinates, (qb) => qb.select(['asset_exif.latitude', 'asset_exif.longitude'])) .where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null) - .$if(options.visibility == undefined, withDefaultVisibility) + .$if(options.visibility === undefined, withDefaultVisibility) .$if(!!options.visibility, (qb) => qb.where('asset.visibility', '=', options.visibility!)) .$if(!!options.bbox, (qb) => { const bbox = options.bbox!; @@ -899,7 +899,7 @@ export class AssetRepository { .$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted)) .$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)) .orderBy( - options.orderBy == AssetOrderBy.CreatedAt + options.orderBy === AssetOrderBy.CreatedAt ? sql`"createdAt"` : sql`(asset."localDateTime" AT TIME ZONE 'UTC')::date`, order, diff --git a/server/src/repositories/telemetry.repository.ts b/server/src/repositories/telemetry.repository.ts index 9a486005b5..146eaa30d7 100644 --- a/server/src/repositories/telemetry.repository.ts +++ b/server/src/repositories/telemetry.repository.ts @@ -149,7 +149,7 @@ export class TelemetryRepository { const unit = 'ms'; for (const [propName, descriptor] of Object.entries(descriptors)) { - const isMethod = typeof descriptor.value == 'function' && propName !== 'constructor'; + const isMethod = typeof descriptor.value === 'function' && propName !== 'constructor'; if (!isMethod) { continue; } diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index 11a6e532aa..afe14fb499 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -321,7 +321,7 @@ export class UserRepository { updatedAt: new Date(), }) .where('user.deletedAt', 'is', null) - .$if(id != undefined, (eb) => eb.where('user.id', '=', asUuid(id!))); + .$if(id !== undefined, (eb) => eb.where('user.id', '=', asUuid(id!))); await query.execute(); } diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 59e20276af..5603819212 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -549,7 +549,7 @@ export class AuthService extends BaseService { const now = DateTime.now(); const updatedAt = DateTime.fromJSDate(session.updatedAt); const diff = now.diff(updatedAt, ['hours']); - if (diff.hours > 1 || appVersion != session.appVersion) { + if (diff.hours > 1 || appVersion !== session.appVersion) { await this.sessionRepository.update(session.id, { id: session.id, updatedAt: new Date(), diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 53573549eb..a95d1f1497 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -109,7 +109,7 @@ const validateRange = (value: number | undefined, min: number, max: number): Non const val = validate(value); // check if the value is within the range - if (val == null || val < min || val > max) { + if (val === null || val < min || val > max) { return null; } @@ -372,8 +372,8 @@ export class MetadataService extends BaseService { fileModifiedAt: stats.mtime, // Keep unedited assets in sync with the file on disk, but don't overwrite edited dimensions. - width: !asset.isEdited || asset.width == null ? assetWidth : undefined, - height: !asset.isEdited || asset.height == null ? assetHeight : undefined, + width: !asset.isEdited || asset.width === null ? assetWidth : undefined, + height: !asset.isEdited || asset.height === null ? assetHeight : undefined, }), async () => { await this.assetRepository.upsertExif({ @@ -988,7 +988,7 @@ export class MetadataService extends BaseService { // timezone let timeZone = exifTags.zone ?? null; - if (timeZone == null && (dateTime?.rawValue?.endsWith('Z') || dateTime?.rawValue?.endsWith('+00:00'))) { + if (timeZone === null && (dateTime?.rawValue?.endsWith('Z') || dateTime?.rawValue?.endsWith('+00:00'))) { // exiftool-vendored returns "no timezone" information even though "+00:00" might be set explicitly // https://github.com/photostructure/exiftool-vendored.js/issues/203 timeZone = 'UTC+0'; diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index e4f2dcacf7..de731f46c6 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -402,8 +402,8 @@ export class StorageTemplateService extends BaseService { const substitutions: Record = { filename, ext: extension, - filetype: asset.type == AssetType.Image ? 'IMG' : 'VID', - filetypefull: asset.type == AssetType.Image ? 'IMAGE' : 'VIDEO', + filetype: asset.type === AssetType.Image ? 'IMG' : 'VID', + filetypefull: asset.type === AssetType.Image ? 'IMAGE' : 'VIDEO', assetId: asset.id, assetIdShort: asset.id.slice(-12), //just throw into the root if it doesn't belong to an album diff --git a/server/src/utils/bytes.ts b/server/src/utils/bytes.ts index 5e476f4dea..67e94c7aca 100644 --- a/server/src/utils/bytes.ts +++ b/server/src/utils/bytes.ts @@ -20,7 +20,7 @@ export function asHumanReadable(bytes: number, precision = 1): string { } } - return `${remainder.toFixed(magnitude == 0 ? 0 : precision)} ${units[magnitude]}`; + return `${remainder.toFixed(magnitude === 0 ? 0 : precision)} ${units[magnitude]}`; } // if an asset is jsonified in the DB before being returned, its buffer fields will be hex-encoded strings diff --git a/server/src/utils/transform.ts b/server/src/utils/transform.ts index aa1fe8b7fc..76d5247de3 100644 --- a/server/src/utils/transform.ts +++ b/server/src/utils/transform.ts @@ -227,7 +227,7 @@ export const transformOcrBoundingBox = ( const { points: transformedPoints, currentWidth, currentHeight } = transformPoints(points, edits, imageDimensions); // Reorder points to maintain semantic ordering (topLeft, topRight, bottomRight, bottomLeft) - const netRotation = edits.find((e) => e.action == AssetEditAction.Rotate)?.parameters.angle ?? 0 % 360; + const netRotation = edits.find((e) => e.action === AssetEditAction.Rotate)?.parameters.angle ?? 0 % 360; const reorderedPoints = reorderQuadPointsForRotation(transformedPoints, netRotation); const [p1, p2, p3, p4] = reorderedPoints; diff --git a/web/eslint.config.js b/web/eslint.config.js index de378d11c7..5974eaf68f 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -19,6 +19,7 @@ export default typescriptEslint.config( ...eslintPluginSvelte.configs.recommended, eslintPluginUnicorn.configs.recommended, js.configs.recommended, + prettier, { plugins: { tscompat: tslintPluginCompat, @@ -158,6 +159,7 @@ export default typescriptEslint.config( 'svelte/button-has-type': 'error', 'object-shorthand': ['error', 'always'], 'svelte/no-navigation-without-resolve': 'off', + eqeqeq: 'error', }, }, { diff --git a/web/src/lib/actions/use-actions.ts b/web/src/lib/actions/use-actions.ts index 622615358d..0b09549a03 100644 --- a/web/src/lib/actions/use-actions.ts +++ b/web/src/lib/actions/use-actions.ts @@ -40,7 +40,7 @@ export function useActions(node: HTMLElement | SVGElement, actions: ActionArray) return { update(actions: ActionArray) { - if ((actions?.length || 0) != actionReturns.length) { + if ((actions?.length || 0) !== actionReturns.length) { throw new Error('You must not change the length of an actions array.'); } diff --git a/web/src/lib/components/asset-viewer/ActivityViewer.svelte b/web/src/lib/components/asset-viewer/ActivityViewer.svelte index 803495c8bb..01c970e627 100644 --- a/web/src/lib/components/asset-viewer/ActivityViewer.svelte +++ b/web/src/lib/components/asset-viewer/ActivityViewer.svelte @@ -99,7 +99,7 @@ }; $effect(() => { - if (assetId && previousAssetId != assetId) { + if (assetId && previousAssetId !== assetId) { previousAssetId = assetId; } }); @@ -168,7 +168,7 @@ {/if} - {#if (index != activityManager.activities.length - 1 && !shouldGroup(activityManager.activities[index].createdAt, activityManager.activities[index + 1].createdAt)) || index === activityManager.activities.length - 1} + {#if (index !== activityManager.activities.length - 1 && !shouldGroup(activityManager.activities[index].createdAt, activityManager.activities[index + 1].createdAt)) || index === activityManager.activities.length - 1}
{/if}
- {#if (index != activityManager.activities.length - 1 && isTenMinutesApart(activityManager.activities[index].createdAt, activityManager.activities[index + 1].createdAt)) || index === activityManager.activities.length - 1} + {#if (index !== activityManager.activities.length - 1 && isTenMinutesApart(activityManager.activities[index].createdAt, activityManager.activities[index + 1].createdAt)) || index === activityManager.activities.length - 1}
{/if} - {#if $slideshowState != SlideshowState.None} + {#if $slideshowState !== SlideshowState.None}
- {#if castManager.castState == CastState.BUFFERING} + {#if castManager.castState === CastState.BUFFERING}
@@ -85,9 +85,9 @@ color="primary" shape="round" variant="ghost" - icon={castManager.castState == CastState.PLAYING ? mdiPause : mdiPlay} + icon={castManager.castState === CastState.PLAYING ? mdiPause : mdiPlay} onclick={() => handlePlayPauseButton()} - aria-label={castManager.castState == CastState.PLAYING ? 'Pause' : 'Play'} + aria-label={castManager.castState === CastState.PLAYING ? 'Pause' : 'Play'} /> {/if} diff --git a/web/src/lib/components/faces-page/PersonSidePanel.svelte b/web/src/lib/components/faces-page/PersonSidePanel.svelte index eaeabcc921..759bd03bce 100644 --- a/web/src/lib/components/faces-page/PersonSidePanel.svelte +++ b/web/src/lib/components/faces-page/PersonSidePanel.svelte @@ -357,7 +357,7 @@
{/if}
- {#if face.person != null} + {#if face.person !== null}
{ - if (lat != null && lng != null) { + if (lat !== undefined && lng !== undefined) { onUpdate(lat, lng); } }; diff --git a/web/src/lib/components/shared-components/map/Map.svelte b/web/src/lib/components/shared-components/map/Map.svelte index 52805d9b96..9811a8a7be 100644 --- a/web/src/lib/components/shared-components/map/Map.svelte +++ b/web/src/lib/components/shared-components/map/Map.svelte @@ -287,7 +287,7 @@ if (previousStyle) { // Preserves the custom map markers from the previous style when the theme is switched // Required until https://github.com/dimfeld/svelte-maplibre/issues/146 is fixed - const customLayers = previousStyle.layers.filter((l) => l.type == 'fill' && l.source == 'geojson'); + const customLayers = previousStyle.layers.filter((l) => l.type === 'fill' && l.source === 'geojson'); const layers = nextStyle.layers.concat(customLayers); const sources = nextStyle.sources; diff --git a/web/src/lib/elements/FormatMessage.svelte b/web/src/lib/elements/FormatMessage.svelte index 53f72451cc..cc097d783f 100644 --- a/web/src/lib/elements/FormatMessage.svelte +++ b/web/src/lib/elements/FormatMessage.svelte @@ -23,7 +23,7 @@ let { key, values = {}, children }: Props = $props(); const getLocale = (locale?: string | null) => { - if (locale == null) { + if (!locale) { throw new Error('Cannot format a message without first setting the initial locale.'); } diff --git a/web/src/lib/i18n.spec.ts b/web/src/lib/i18n.spec.ts index ebf2fdfa3b..bac5dba4b7 100644 --- a/web/src/lib/i18n.spec.ts +++ b/web/src/lib/i18n.spec.ts @@ -6,7 +6,7 @@ describe('i18n', () => { const languageFiles = readdirSync('../i18n').sort(); for (const filename of languageFiles) { test(`${filename} should have a loader`, async () => { - if (!filename.endsWith('.json') || filename == 'package.json') { + if (!filename.endsWith('.json') || filename === 'package.json') { return; } diff --git a/web/src/lib/managers/VirtualScrollManager/VirtualScrollManager.svelte.ts b/web/src/lib/managers/VirtualScrollManager/VirtualScrollManager.svelte.ts index fdbc86a7db..e3e72aec82 100644 --- a/web/src/lib/managers/VirtualScrollManager/VirtualScrollManager.svelte.ts +++ b/web/src/lib/managers/VirtualScrollManager/VirtualScrollManager.svelte.ts @@ -55,7 +55,7 @@ export abstract class VirtualScrollManager { } #setHeaderHeight(value: number) { - if (this.#headerHeight == value) { + if (this.#headerHeight === value) { return false; } this.#headerHeight = value; @@ -67,7 +67,7 @@ export abstract class VirtualScrollManager { } #setGap(value: number) { - if (this.#gap == value) { + if (this.#gap === value) { return false; } this.#gap = value; @@ -79,7 +79,7 @@ export abstract class VirtualScrollManager { } #setRowHeight(value: number) { - if (this.#rowHeight == value) { + if (this.#rowHeight === value) { return false; } this.#rowHeight = value; diff --git a/web/src/lib/modals/GeolocationUpdateConfirmModal.svelte b/web/src/lib/modals/GeolocationUpdateConfirmModal.svelte index 8230f62c73..a57dd1db6e 100644 --- a/web/src/lib/modals/GeolocationUpdateConfirmModal.svelte +++ b/web/src/lib/modals/GeolocationUpdateConfirmModal.svelte @@ -13,6 +13,7 @@ const { point, assetCount, onClose }: Props = $props(); const hasExistingLocations = $derived( + // eslint-disable-next-line eqeqeq assetMultiSelectManager.assets.some((asset) => asset.latitude != null || asset.longitude != null), ); diff --git a/web/src/lib/modals/timezone-utils.ts b/web/src/lib/modals/timezone-utils.ts index 2dfbb3fd5f..ae86e46321 100644 --- a/web/src/lib/modals/timezone-utils.ts +++ b/web/src/lib/modals/timezone-utils.ts @@ -95,7 +95,7 @@ function zoneOptionForDate(zone: string, date: string) { function sortTwoZones(zoneA: ZoneOption, zoneB: ZoneOption) { const offsetDifference = zoneA.offsetMinutes - zoneB.offsetMinutes; - if (offsetDifference != 0) { + if (offsetDifference !== 0) { return offsetDifference; } return zoneA.value.localeCompare(zoneB.value, undefined, { sensitivity: 'base' }); diff --git a/web/src/lib/stores/upload.ts b/web/src/lib/stores/upload.ts index 04a8a45bee..21929ab1e4 100644 --- a/web/src/lib/stores/upload.ts +++ b/web/src/lib/stores/upload.ts @@ -67,7 +67,7 @@ function createUploadStore() { const updateAssetMap = (id: string, mapper: (assets: UploadAsset) => UploadAsset) => { uploadAssets.update((uploadingAssets) => { return uploadingAssets.map((asset) => { - if (asset.id == id) { + if (asset.id === id) { return mapper(asset); } return asset; @@ -111,7 +111,7 @@ function createUploadStore() { }); } - return uploadingAsset.filter((a) => a.id != id); + return uploadingAsset.filter((a) => a.id !== id); }); }; diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts index 3624b34f93..f5733fd5b2 100644 --- a/web/src/lib/utils/actions.ts +++ b/web/src/lib/utils/actions.ts @@ -68,7 +68,7 @@ const undoDeleteAssets = async (onUndoDelete: OnUndoDelete, assets: TimelineAsse * @param {StackResponse} stackResponse - The stack response containing the stack and assets to delete. */ export function updateStackedAssetInTimeline(timelineManager: TimelineManager, { stack, toDeleteIds }: StackResponse) { - if (stack == undefined) { + if (stack === undefined) { return; } diff --git a/web/src/lib/utils/duplicate-utils.ts b/web/src/lib/utils/duplicate-utils.ts index 86b221906f..53a52ddcf9 100644 --- a/web/src/lib/utils/duplicate-utils.ts +++ b/web/src/lib/utils/duplicate-utils.ts @@ -147,6 +147,7 @@ const metadataFields = [ titleKey: 'gps', keys: ['latitude', 'longitude'], render: (asset, $t) => + // eslint-disable-next-line eqeqeq asset.exifInfo?.latitude != null && asset.exifInfo?.longitude != null ? `${asset.exifInfo.latitude.toFixed(4)}, ${asset.exifInfo.longitude.toFixed(4)}` : $t('unknown'), @@ -173,18 +174,21 @@ const metadataFields = [ icon: mdiCameraIris, titleKey: 'f_number', keys: ['fNumber'], + // eslint-disable-next-line eqeqeq render: (asset, $t) => (asset.exifInfo?.fNumber == null ? $t('unknown') : `f/${asset.exifInfo.fNumber.toFixed(1)}`), }, { icon: mdiRayStartArrow, titleKey: 'focal_length', keys: ['focalLength'], + // eslint-disable-next-line eqeqeq render: (asset, $t) => (asset.exifInfo?.focalLength == null ? $t('unknown') : `${asset.exifInfo.focalLength} mm`), }, { icon: mdiBrightness6, titleKey: 'iso', keys: ['iso'], + // eslint-disable-next-line eqeqeq render: (asset, $t) => (asset.exifInfo?.iso == null ? $t('unknown') : `ISO ${asset.exifInfo.iso}`), }, { @@ -203,6 +207,7 @@ const metadataFields = [ icon: mdiStarOutline, titleKey: 'rating', keys: ['rating'], + // eslint-disable-next-line eqeqeq render: (asset, $t) => (asset.exifInfo?.rating == null ? $t('unknown') : `${asset.exifInfo.rating} stars`), }, { diff --git a/web/src/lib/utils/navigation.ts b/web/src/lib/utils/navigation.ts index 91513911f1..2f88da1f43 100644 --- a/web/src/lib/utils/navigation.ts +++ b/web/src/lib/utils/navigation.ts @@ -44,7 +44,7 @@ export function currentUrlReplaceAssetId(assetId: string) { // always remove the assetGridScrollTargetParams params.delete('at'); const paramsString = params.toString(); - const searchparams = paramsString == '' ? '' : '?' + params.toString(); + const searchparams = paramsString === '' ? '' : '?' + params.toString(); // this contains special casing for the /photos/:assetId photos route, which hangs directly // off / instead of a subpath, unlike every other asset-containing route. return isPhotosRoute(page.route.id) diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index 018d7d34ef..e1e717f45a 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -183,8 +183,8 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset): isFavorite: assetResponse.isFavorite, visibility: assetResponse.visibility, isTrashed: assetResponse.isTrashed, - isVideo: assetResponse.type == AssetTypeEnum.Video, - isImage: assetResponse.type == AssetTypeEnum.Image, + isVideo: assetResponse.type === AssetTypeEnum.Video, + isImage: assetResponse.type === AssetTypeEnum.Image, stack: assetResponse.stack || null, duration: assetResponse.duration || null, projectionType: assetResponse.exifInfo?.projectionType || null, diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index 733c1f556a..c1028ad658 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -222,7 +222,7 @@ const onNameChangeSubmit = async (name: string, targetPerson: PersonResponseDto) => { try { - if (name == targetPerson.name) { + if (name === targetPerson.name) { return; } diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3fe707c6df..e4389f09dc 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -98,7 +98,7 @@ } else if ($page.params.assetId) { previousRoute = Route.viewPerson(data.person); } - if (action == 'merge') { + if (action === 'merge') { viewMode = PersonPageViewMode.MERGE_PEOPLE; } @@ -180,7 +180,7 @@ const [, personToBeMergedInto] = result; - if (personToBeMergedInto.name != personName && person.id === personToBeMergedInto.id) { + if (personToBeMergedInto.name !== personName && person.id === personToBeMergedInto.id) { await updateAssetCount(); return { merged: true }; } diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index e7a18f1d0f..6683dbfe2e 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -201,7 +201,7 @@ personIds.map(async (personId) => { const person = await getPerson({ id: personId }); - if (person.name == '') { + if (person.name === '') { return $t('no_name'); } diff --git a/web/src/routes/(user)/user-settings/AppSettings.svelte b/web/src/routes/(user)/user-settings/AppSettings.svelte index ddfac02bfc..04c8b6af06 100644 --- a/web/src/routes/(user)/user-settings/AppSettings.svelte +++ b/web/src/routes/(user)/user-settings/AppSettings.svelte @@ -74,7 +74,7 @@ - + {selectedDate} diff --git a/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte index f4c0a93655..338c463375 100644 --- a/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -38,7 +38,7 @@ }; const preAction = async (payload: Action) => { - if (payload.type == 'trash') { + if (payload.type === 'trash') { // eslint-disable-next-line @typescript-eslint/no-unused-expressions (await navigateToAsset(assetCursor?.nextAsset)) || (await navigateToAsset(assetCursor?.previousAsset)) || diff --git a/web/src/routes/admin/system-settings/JobSettings.svelte b/web/src/routes/admin/system-settings/JobSettings.svelte index a43be5e9f4..c229a788b4 100644 --- a/web/src/routes/admin/system-settings/JobSettings.svelte +++ b/web/src/routes/admin/system-settings/JobSettings.svelte @@ -66,7 +66,7 @@ description="" bind:value={configToEdit.job[queueName].concurrency} required={true} - isEdited={configToEdit.job[queueName].concurrency != config.job[queueName].concurrency} + isEdited={configToEdit.job[queueName].concurrency !== config.job[queueName].concurrency} /> {:else} { - if (nextStepIndex == -1) { + if (nextStepIndex === -1) { if (authManager.user.isAdmin) { await updateAdminOnboarding({ adminOnboardingUpdateDto: { isOnboarded: true } }); await serverConfigManager.loadServerConfig(); diff --git a/web/src/routes/auth/onboarding/OnboardingHello.svelte b/web/src/routes/auth/onboarding/OnboardingHello.svelte index 533de49f81..1b5b0d2e88 100644 --- a/web/src/routes/auth/onboarding/OnboardingHello.svelte +++ b/web/src/routes/auth/onboarding/OnboardingHello.svelte @@ -16,7 +16,7 @@ {$t('onboarding_welcome_user', { values: { user: authManager.user.name } })}

- {userRole == OnboardingRole.SERVER + {userRole === OnboardingRole.SERVER ? $t('onboarding_server_welcome_description') : $t('onboarding_user_welcome_description')}

From c7d7889ed0101ff2f49e0365459e9943d1dcac08 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Tue, 11 Aug 2026 20:41:40 +0200 Subject: [PATCH 37/49] fix: pass secrets to build-mobile (#29031) --- .github/workflows/build-mobile.yml | 4 ++++ .github/workflows/prepare-release.yml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 602ffb3c18..b2ba244ab9 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -12,6 +12,10 @@ on: default: 'development' type: string secrets: + PUSH_O_MATIC_APP_CLIENT_ID: + required: true + PUSH_O_MATIC_APP_KEY: + required: true KEY_JKS: required: true ALIAS: diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 98a3daa3b2..abc84c7cdb 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -102,6 +102,8 @@ jobs: contents: read pull-requests: write secrets: + PUSH_O_MATIC_APP_CLIENT_ID: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} + PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }} KEY_JKS: ${{ secrets.KEY_JKS }} ALIAS: ${{ secrets.ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} From 199723261c6ffa897fec8ccdaea6359e39c37cc3 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:05:18 +0200 Subject: [PATCH 38/49] fix: owner cascade delete album (#30692) --- server/src/schema/functions.ts | 14 +++++++ server/src/schema/index.ts | 2 + .../1786385711807-AlbumOwnerDeleteTrigger.ts | 31 ++++++++++++++++ server/src/schema/tables/album-user.table.ts | 3 +- .../specs/services/album.service.spec.ts | 37 +++++++++++++++++++ 5 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 server/src/schema/migrations/1786385711807-AlbumOwnerDeleteTrigger.ts create mode 100644 server/test/medium/specs/services/album.service.spec.ts diff --git a/server/src/schema/functions.ts b/server/src/schema/functions.ts index 230f22478d..2ee61d82dd 100644 --- a/server/src/schema/functions.ts +++ b/server/src/schema/functions.ts @@ -133,6 +133,20 @@ export const album_asset_delete_audit = registerFunction({ END`, }); +export const album_user_delete = registerFunction({ + name: 'album_user_delete', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + DELETE FROM "album" + WHERE "album"."id" = OLD."albumId" + AND NOT EXISTS (SELECT "albumId" FROM "album_user" WHERE "album_user"."albumId" = "album"."id" AND "album_user"."role" = 'owner'); + + RETURN NULL; + END`, +}); + export const album_user_delete_audit = registerFunction({ name: 'album_user_delete_audit', returnType: 'TRIGGER', diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index 4e55ff4cf6..171fefb50f 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -7,6 +7,7 @@ import { } from 'src/schema/enums'; import { album_user_after_insert, + album_user_delete, album_user_delete_audit, asset_delete_audit, asset_face_audit, @@ -174,6 +175,7 @@ export class ImmichDatabase { asset_metadata_audit, asset_face_audit, asset_ocr_delete_audit, + album_user_delete, ]; enum = [album_user_role_enum, assets_status_enum, asset_face_source_type, asset_visibility_enum]; diff --git a/server/src/schema/migrations/1786385711807-AlbumOwnerDeleteTrigger.ts b/server/src/schema/migrations/1786385711807-AlbumOwnerDeleteTrigger.ts new file mode 100644 index 0000000000..082e43ff23 --- /dev/null +++ b/server/src/schema/migrations/1786385711807-AlbumOwnerDeleteTrigger.ts @@ -0,0 +1,31 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`DELETE FROM "album" WHERE NOT EXISTS (SELECT * FROM "album_user" WHERE "album_user"."albumId" = "album"."id" AND "album_user"."role" = 'owner');`.execute(db); + await sql`CREATE OR REPLACE FUNCTION album_user_delete() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + DELETE FROM "album" + WHERE "album"."id" = OLD."albumId" + AND NOT EXISTS (SELECT "albumId" FROM "album_user" WHERE "album_user"."albumId" = "album"."id" AND "album_user"."role" = 'owner'); + + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "album_user_delete" + AFTER DELETE ON "album_user" + REFERENCING OLD TABLE AS "old" + FOR EACH ROW + EXECUTE FUNCTION album_user_delete();`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_album_user_delete', '{"type":"function","name":"album_user_delete","sql":"CREATE OR REPLACE FUNCTION album_user_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n DELETE FROM \\"album\\"\\n WHERE \\"album\\".\\"id\\" = OLD.\\"albumId\\"\\n AND NOT EXISTS (SELECT \\"albumId\\" FROM \\"album_user\\" WHERE \\"album_user\\".\\"albumId\\" = \\"album\\".\\"id\\" AND \\"album_user\\".\\"role\\" = ''owner'');\\n\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_user_delete', '{"type":"trigger","name":"album_user_delete","sql":"CREATE OR REPLACE TRIGGER \\"album_user_delete\\"\\n AFTER DELETE ON \\"album_user\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION album_user_delete();"}'::jsonb);`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP TRIGGER "album_user_delete" ON "album_user";`.execute(db); + await sql`DROP FUNCTION album_user_delete;`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_album_user_delete';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_user_delete';`.execute(db); +} diff --git a/server/src/schema/tables/album-user.table.ts b/server/src/schema/tables/album-user.table.ts index 677d6ca2f2..c015c1354d 100644 --- a/server/src/schema/tables/album-user.table.ts +++ b/server/src/schema/tables/album-user.table.ts @@ -13,7 +13,7 @@ import { import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { AlbumUserRole } from 'src/enum'; import { album_user_role_enum } from 'src/schema/enums'; -import { album_user_after_insert, album_user_delete_audit } from 'src/schema/functions'; +import { album_user_after_insert, album_user_delete, album_user_delete_audit } from 'src/schema/functions'; import { AlbumTable } from 'src/schema/tables/album.table'; import { UserTable } from 'src/schema/tables/user.table'; @@ -38,6 +38,7 @@ import { UserTable } from 'src/schema/tables/user.table'; referencingOldTableAs: 'old', when: 'pg_trigger_depth() <= 1', }) +@AfterDeleteTrigger({ scope: 'row', function: album_user_delete, referencingOldTableAs: 'old' }) export class AlbumUserTable { @ForeignKeyColumn(() => AlbumTable, { onDelete: 'CASCADE', diff --git a/server/test/medium/specs/services/album.service.spec.ts b/server/test/medium/specs/services/album.service.spec.ts new file mode 100644 index 0000000000..bc73007593 --- /dev/null +++ b/server/test/medium/specs/services/album.service.spec.ts @@ -0,0 +1,37 @@ +import { Kysely } from 'kysely'; +import { AlbumRepository } from 'src/repositories/album.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { UserRepository } from 'src/repositories/user.repository'; +import { DB } from 'src/schema'; +import { AlbumService } from 'src/services/album.service'; +import { newMediumService } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + return newMediumService(AlbumService, { + database: db || defaultDatabase, + real: [AlbumRepository, UserRepository], + mock: [LoggingRepository], + }); +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(AlbumService.name, () => { + describe('database triggers', () => { + it('should cascade delete an album when the owner is deleted', async () => { + const { ctx } = setup(); + const { user } = await ctx.newUser(); + await ctx.newAlbum({ ownerId: user.id }); + + await ctx.get(UserRepository).delete({ id: user.id }, true); + + await expect(ctx.database.selectFrom('album').selectAll().execute()).resolves.toEqual([]); + await expect(ctx.database.selectFrom('album_user').selectAll().execute()).resolves.toEqual([]); + }); + }); +}); From 0344c61e4cee9cb2e8ee3ce2292177cd2b0c4129 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Wed, 12 Aug 2026 13:36:33 +0200 Subject: [PATCH 39/49] feat: use version service for docs archive switcher (#30675) --- docs/src/components/version-switcher.tsx | 9 +- docs/static/_redirects | 1 + docs/static/archived-versions.json | 265 ------------------ packages/scripts/src/commands/release.spec.ts | 78 +----- packages/scripts/src/commands/release.ts | 52 ---- 5 files changed, 6 insertions(+), 399 deletions(-) delete mode 100644 docs/static/archived-versions.json diff --git a/docs/src/components/version-switcher.tsx b/docs/src/components/version-switcher.tsx index 739d7bd001..967c85c780 100644 --- a/docs/src/components/version-switcher.tsx +++ b/docs/src/components/version-switcher.tsx @@ -2,6 +2,8 @@ import { useWindowSize } from '@docusaurus/theme-common'; import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem'; import React, { useEffect, useState } from 'react'; +const ARCHIVED_VERSIONS_URL = 'https://version.immich.cloud/v1/docs/versions'; + export default function VersionSwitcher(): JSX.Element { const [versions, setVersions] = useState([]); const [activeLabel, setLabel] = useState('Versions'); @@ -11,12 +13,7 @@ export default function VersionSwitcher(): JSX.Element { useEffect(() => { async function getVersions() { try { - let baseUrl = 'https://docs.immich.app'; - if (window.location.origin === 'http://localhost:3005') { - baseUrl = window.location.origin; - } - - const response = await fetch(`${baseUrl}/archived-versions.json`); + const response = await fetch(ARCHIVED_VERSIONS_URL); const archiveVersions = await response.json(); diff --git a/docs/static/_redirects b/docs/static/_redirects index 633025ac17..e218124e59 100644 --- a/docs/static/_redirects +++ b/docs/static/_redirects @@ -1,3 +1,4 @@ +/archived-versions.json https://version.immich.cloud/v1/docs/versions 307 / /overview/quick-start 307 /mobile-app-beta-program /features/mobile-app 307 /contribution-guidelines /overview/support-the-project#contributing 307 diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json deleted file mode 100644 index dbeed76b73..0000000000 --- a/docs/static/archived-versions.json +++ /dev/null @@ -1,265 +0,0 @@ -[ - { - "label": "v3.1.0", - "url": "https://docs.v3.1.0.archive.immich.app" - }, - { - "label": "v3.0.3", - "url": "https://docs.v3.0.3.archive.immich.app" - }, - { - "label": "v2.7.5", - "url": "https://docs.v2.7.5.archive.immich.app" - }, - { - "label": "v2.6.3", - "url": "https://docs.v2.6.3.archive.immich.app" - }, - { - "label": "v2.5.6", - "url": "https://docs.v2.5.6.archive.immich.app" - }, - { - "label": "v2.4.1", - "url": "https://docs.v2.4.1.archive.immich.app" - }, - { - "label": "v2.3.1", - "url": "https://docs.v2.3.1.archive.immich.app" - }, - { - "label": "v2.2.3", - "url": "https://docs.v2.2.3.archive.immich.app" - }, - { - "label": "v2.1.0", - "url": "https://docs.v2.1.0.archive.immich.app" - }, - { - "label": "v2.0.1", - "url": "https://docs.v2.0.1.archive.immich.app" - }, - { - "label": "v1.144.1", - "url": "https://docs.v1.144.1.archive.immich.app" - }, - { - "label": "v1.143.1", - "url": "https://docs.v1.143.1.archive.immich.app" - }, - { - "label": "v1.142.1", - "url": "https://v1.142.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.141.1", - "url": "https://v1.141.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.140.1", - "url": "https://v1.140.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.139.4", - "url": "https://v1.139.4.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.138.1", - "url": "https://v1.138.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.137.3", - "url": "https://v1.137.3.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.136.0", - "url": "https://v1.136.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.135.3", - "url": "https://v1.135.3.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.134.0", - "url": "https://v1.134.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.133.1", - "url": "https://v1.133.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.132.3", - "url": "https://v1.132.3.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.131.3", - "url": "https://v1.131.3.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.130.3", - "url": "https://v1.130.3.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.129.0", - "url": "https://v1.129.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.128.0", - "url": "https://v1.128.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.127.0", - "url": "https://v1.127.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.126.1", - "url": "https://v1.126.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.125.7", - "url": "https://v1.125.7.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.124.2", - "url": "https://v1.124.2.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.123.0", - "url": "https://v1.123.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.122.3", - "url": "https://v1.122.3.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.121.0", - "url": "https://v1.121.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.120.2", - "url": "https://v1.120.2.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.119.1", - "url": "https://v1.119.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.118.2", - "url": "https://v1.118.2.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.117.0", - "url": "https://v1.117.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.116.2", - "url": "https://v1.116.2.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.115.0", - "url": "https://v1.115.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.114.0", - "url": "https://v1.114.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.113.1", - "url": "https://v1.113.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.112.1", - "url": "https://v1.112.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.111.0", - "url": "https://v1.111.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.110.0", - "url": "https://v1.110.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.109.2", - "url": "https://v1.109.2.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.108.0", - "url": "https://v1.108.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.107.2", - "url": "https://v1.107.2.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.106.4", - "url": "https://v1.106.4.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.105.1", - "url": "https://v1.105.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.104.0", - "url": "https://v1.104.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.103.1", - "url": "https://v1.103.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.102.3", - "url": "https://v1.102.3.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.101.0", - "url": "https://v1.101.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.100.0", - "url": "https://v1.100.0.archive.immich.app", - "rootPath": "/docs" - } -] diff --git a/packages/scripts/src/commands/release.spec.ts b/packages/scripts/src/commands/release.spec.ts index 0b1be22302..dc3f82517d 100644 --- a/packages/scripts/src/commands/release.spec.ts +++ b/packages/scripts/src/commands/release.spec.ts @@ -1,23 +1,10 @@ import semver from 'semver'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - resolveArchivedVersions, - ArchivedVersion, - getMobileBuild, - getNewVersion, - ReleaseError, -} from './release'; +import { describe, expect, it } from 'vitest'; +import { getMobileBuild, getNewVersion, ReleaseError } from './release'; const mobileBuild = (version: string) => getMobileBuild(semver.parse(version) as semver.SemVer); -const archived = (label: string): ArchivedVersion => ({ - label, - url: `https://docs.${label}.archive.immich.app`, -}); - -const labels = (versions: ArchivedVersion[]) => versions.map((v) => v.label); - describe(getNewVersion.name, () => { describe('transitions', () => { const valid = [ @@ -82,67 +69,6 @@ describe(getNewVersion.name, () => { }); }); -describe(resolveArchivedVersions.name, () => { - beforeEach(() => { - // silence the "Removed ..." progress logging - vi.spyOn(console, 'log').mockImplementation(() => {}); - }); - - it('should prepend the new version to the front', () => { - const result = resolveArchivedVersions([archived('v2.9.0')], '3.0.0'); - - expect(result[0]).toEqual({ - label: 'v3.0.0', - url: 'https://docs.v3.0.0.archive.immich.app', - }); - expect(labels(result)).toEqual(['v3.0.0', 'v2.9.0']); - }); - - it('should handle an empty list', () => { - expect(labels(resolveArchivedVersions([], '3.0.0'))).toEqual(['v3.0.0']); - }); - - it('should drop older patch releases of the new version minor', () => { - const versions = ['v3.0.4', 'v3.0.3', 'v3.0.2'].map(archived); - - expect(labels(resolveArchivedVersions(versions, '3.0.5'))).toEqual([ - 'v3.0.5', - ]); - }); - - it('should keep the latest patch of each older minor', () => { - const versions = ['v3.1.2', 'v3.1.1', 'v3.0.9', 'v2.0.0'].map(archived); - - expect(labels(resolveArchivedVersions(versions, '3.2.0'))).toEqual([ - 'v3.2.0', - 'v3.1.2', - 'v3.0.9', - 'v2.0.0', - ]); - }); - - it('should keep an older minor when bumping a patch', () => { - const versions = ['v3.0.4', 'v3.0.3', 'v2.9.1', 'v2.9.0', 'v1.5.0'].map( - archived, - ); - - expect(labels(resolveArchivedVersions(versions, '3.0.5'))).toEqual([ - 'v3.0.5', - 'v2.9.1', - 'v1.5.0', - ]); - }); - - it('should replace a prerelease with its release', () => { - const versions = ['v3.0.0-rc.2', 'v3.0.0-rc.1', 'v2.9.0'].map(archived); - - expect(labels(resolveArchivedVersions(versions, '3.0.0'))).toEqual([ - 'v3.0.0', - 'v2.9.0', - ]); - }); -}); - describe(getMobileBuild.name, () => { it('should encode the version', () => { expect(mobileBuild('3.1.0')).toBe(3_010_099); diff --git a/packages/scripts/src/commands/release.ts b/packages/scripts/src/commands/release.ts index 307b66165a..2c0d82d5f6 100644 --- a/packages/scripts/src/commands/release.ts +++ b/packages/scripts/src/commands/release.ts @@ -26,7 +26,6 @@ const Files = { Docs: { Env: join(root, 'docs/docs/install/environment-variables.md'), Upgrading: join(root, 'docs/docs/install/upgrading.md'), - ArchivedVersions: join(root, 'docs/static/archived-versions.json'), }, Mobile: { Pubspec: join(root, 'mobile/pubspec.yaml'), @@ -92,13 +91,6 @@ export const handleRelease = ({ type }: ReleaseOptions) => { pump(Files.Docs.Upgrading, /:v\d+/, `:${major}`); } - // update archived versions list - const archivedFile = new JsonFile( - Files.Docs.ArchivedVersions, - ); - const versions = archivedFile.read(); - archivedFile.write(resolveArchivedVersions(versions, newVersionRaw)); - if (process.env.GITHUB_ENV) { // make available for following steps appendFileSync( @@ -222,50 +214,6 @@ const pump = (path: string, pattern: RegExp, replacement: string) => { file.write(update); }; -export interface ArchivedVersion { - label: string; - url: string; -} - -export const resolveArchivedVersions = ( - versions: ArchivedVersion[], - nextVersion: string, -): ArchivedVersion[] => { - const newVersion: ArchivedVersion = { - label: `v${nextVersion}`, - url: `https://docs.v${nextVersion}.archive.immich.app`, - }; - - let result = versions; - let lastVersion = asVersion(newVersion); - for (const item of versions) { - const version = asVersion(item); - // only keep the latest patch version for each minor release - if ( - lastVersion.major === version.major && - lastVersion.minor === version.minor && - lastVersion.patch >= version.patch - ) { - result = result.filter((item) => item.label !== version.label); - console.log( - `Removed ${version.label} (replaced with ${lastVersion.label})`, - ); - continue; - } - - lastVersion = version; - } - - return [newVersion, ...result]; -}; - -const asVersion = (item: ArchivedVersion) => { - const { label, url } = item; - const [version] = label.substring(1).split('-'); - const [major, minor, patch] = version.split('.').map(Number); - return { major, minor, patch, label, url }; -}; - const isPrerelease = (version: SemVer) => version.prerelease.length > 0; /** From 610bcfa6d09e6bfdb2bbaf16df8e77b83fc34356 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 12 Aug 2026 04:53:14 -0700 Subject: [PATCH 40/49] chore(mobile): add Cocoapods to mise (#30722) * chore(mobile): add Cocoapods to mise * Use new mise setup in CI --- .github/workflows/build-mobile.yml | 19 +++++++------------ mobile/mise.toml | 21 +++++++++++++++++++-- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index b2ba244ab9..6a9a01d41d 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -225,6 +225,13 @@ jobs: github_token: ${{ steps.token.outputs.token }} working_directory: ./mobile + - name: Setup Ruby + uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 + with: + ruby-version: '3.3' + bundler-cache: true + working-directory: ./mobile/ios + - name: Install dependencies run: mise //mobile:install:ci @@ -235,18 +242,6 @@ jobs: working-directory: ./mobile run: flutter build ios --config-only --no-codesign - - name: Setup Ruby - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 - with: - ruby-version: '3.3' - bundler-cache: true - working-directory: ./mobile/ios - - - name: Install CocoaPods dependencies - working-directory: ./mobile/ios - run: | - pod install - - name: Create API Key env: API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} diff --git a/mobile/mise.toml b/mobile/mise.toml index 08a26c0c3b..f021cd8747 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -29,7 +29,7 @@ sources = [ ] outputs = { auto = true } run = [ - "dart run build_runner build", + "dart run build_runner build", "dart format lib/routing/router.gr.dart", ] depends = ["//:open-api-dart"] @@ -99,7 +99,11 @@ run = "dart run drift_dev make-migrations" alias = "install" description = "Install flutter dependencies" depends = ["//:open-api-dart"] -run = "flutter pub get" +run = [ + # This receives passed `--enforce-lockfile` args + "flutter pub get", + { task = "install:ios" }, +] [tasks."install:ci"] description = "Install flutter dependencies in CI" @@ -132,6 +136,19 @@ run = [ ] wait_for = ["//:i18n:format-fix"] +[tasks."install:ios"] +description = "Install CocoaPods dependencies" +hide = true +sources = ["ios/Podfile", "ios/Podfile.lock", ".flutter-plugin-dependencies"] +outputs = ["ios/Pods/Manifest.lock"] +run = ''' +echo "Running pod install" +if [ "$(uname)" != "Darwin" ]; then + exit 0 +fi +cd ios && pod install +''' + [tasks."analyze:dart"] description = "Run Dart analysis" hide = true From ce705dcc068146fca2c6ecb050ab94ec6b54cc85 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:30:06 +0530 Subject: [PATCH 41/49] fix: riverpod reactivity regression from lint fixes (#30734) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../lib/pages/backup/drift_backup.page.dart | 12 +-- .../pages/drift_album_options.page.dart | 10 +- .../pages/drift_slideshow.page.dart | 97 +++++++++++++------ .../pages/drift_user_selection.page.dart | 4 +- .../pages/search/drift_search.page.dart | 9 +- .../widgets/common/immich_sliver_app_bar.dart | 20 +++- 6 files changed, 100 insertions(+), 52 deletions(-) diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index ad2674b81b..a0e55e58de 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -316,12 +316,11 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final backupAlbums = ref.watch(backupAlbumProvider); + Widget buildSelectedAlbumName() { String text = context.t.backup_controller_page_backup_selected; - final albums = ref - .read(backupAlbumProvider) - .where((album) => album.backupSelection == BackupSelection.selected) - .toList(); + final albums = backupAlbums.where((album) => album.backupSelection == BackupSelection.selected).toList(); if (albums.isNotEmpty) { for (var album in albums) { @@ -352,10 +351,7 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { Widget buildExcludedAlbumName() { String text = context.t.backup_controller_page_excluded; - final albums = ref - .read(backupAlbumProvider) - .where((album) => album.backupSelection == BackupSelection.excluded) - .toList(); + final albums = backupAlbums.where((album) => album.backupSelection == BackupSelection.excluded).toList(); if (albums.isNotEmpty) { for (var album in albums) { diff --git a/mobile/lib/presentation/pages/drift_album_options.page.dart b/mobile/lib/presentation/pages/drift_album_options.page.dart index 59cabf8daf..078b9cc325 100644 --- a/mobile/lib/presentation/pages/drift_album_options.page.dart +++ b/mobile/lib/presentation/pages/drift_album_options.page.dart @@ -32,6 +32,8 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { final userId = ref.watch(authProvider).userId; final activityEnabled = useState(album.isActivityEnabled); final isOwner = album.ownerId == userId; + final owner = isOwner ? ref.watch(currentUserProvider) : null; + final allUsers = isOwner ? null : ref.watch(driftUsersProvider); void showErrorMessage() { ContextHelper(context).pop(); @@ -141,7 +143,6 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { Widget buildOwnerInfo() { if (isOwner) { - final owner = ref.read(currentUserProvider); return ListTile( leading: owner != null ? UserCircleAvatar(user: owner) : const SizedBox(), title: Text(album.ownerName, style: const TextStyle(fontWeight: FontWeight.w500)), @@ -149,8 +150,11 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { trailing: Text(context.t.owner, style: context.textTheme.labelLarge), ); } else { - final usersProvider = ref.read(driftUsersProvider); - return usersProvider.maybeWhen( + if (allUsers == null) { + return const SizedBox(); + } + + return allUsers.maybeWhen( data: (users) { final user = users.firstWhereOrNull((u) => u.id == album.ownerId); diff --git a/mobile/lib/presentation/pages/drift_slideshow.page.dart b/mobile/lib/presentation/pages/drift_slideshow.page.dart index 824d7d7c6b..2662c60a30 100644 --- a/mobile/lib/presentation/pages/drift_slideshow.page.dart +++ b/mobile/lib/presentation/pages/drift_slideshow.page.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/config/slideshow_config.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -295,14 +296,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si color: context.colorScheme.primary, ); } else { - return LinearProgressIndicator( - color: context.colorScheme.primary, - borderRadius: BorderRadius.zero, - minHeight: 5, - value: - ref.read(videoPlayerProvider(asset.id).select((s) => s.position)).inMilliseconds / - asset.duration.inMilliseconds, - ); + return _VideoProgressBar(asset: asset); } } @@ -374,25 +368,13 @@ class _DriftSlideshowPageState extends ConsumerState with Si builder: (context, value, _) => buildPhotoView(scale * (1.0 + value * _kenBurnsZoom)), ); } else { - final status = ref.read(videoPlayerProvider(asset.id).select((s) => s.status)); - final position = ref.read(videoPlayerProvider(asset.id)).position; - - if (status == VideoPlaybackStatus.completed && isCurrent && position.inMicroseconds > 0) { - unawaited(_nextPage()); - } else if (status == VideoPlaybackStatus.playing) { - unawaited(ref.read(videoPlayerProvider(asset.id).notifier).setLoop(false)); - } - - return PhotoView.customChild( - onTapUp: (_, _, _) => _onTapUp(), - disableScaleGestures: true, - filterQuality: FilterQuality.high, - initialScale: scale, - child: NativeVideoViewer( - asset: asset, - isCurrent: isCurrent, - image: Image(image: imageProvider, fit: BoxFit.contain, alignment: Alignment.center), - ), + return _VideoChild( + asset: asset, + isCurrent: isCurrent, + scale: scale, + imageProvider: imageProvider, + onTapUp: _onTapUp, + onCompleted: _nextPage, ); } } @@ -477,6 +459,67 @@ class _DriftSlideshowPageState extends ConsumerState with Si } } +class _VideoChild extends ConsumerWidget { + final BaseAsset asset; + final bool isCurrent; + final PhotoViewComputedScale scale; + final ImageProvider imageProvider; + final VoidCallback onTapUp; + final VoidCallback onCompleted; + + const _VideoChild({ + required this.asset, + required this.isCurrent, + required this.scale, + required this.imageProvider, + required this.onTapUp, + required this.onCompleted, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + ref.listen(videoPlayerProvider(asset.id).select((s) => s.status), (_, status) { + if (status == VideoPlaybackStatus.completed) { + if (isCurrent && ref.read(videoPlayerProvider(asset.id)).position.inMicroseconds > 0) { + onCompleted(); + } + } else if (status == VideoPlaybackStatus.playing) { + unawaited(ref.read(videoPlayerProvider(asset.id).notifier).setLoop(false)); + } + }); + + return PhotoView.customChild( + onTapUp: (_, _, _) => onTapUp(), + disableScaleGestures: true, + filterQuality: FilterQuality.high, + initialScale: scale, + child: NativeVideoViewer( + asset: asset, + isCurrent: isCurrent, + image: Image(image: imageProvider, fit: BoxFit.contain, alignment: Alignment.center), + ), + ); + } +} + +class _VideoProgressBar extends ConsumerWidget { + final BaseAsset asset; + + const _VideoProgressBar({required this.asset}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final position = ref.watch(videoPlayerProvider(asset.id).select((s) => s.position)); + + return LinearProgressIndicator( + color: context.colorScheme.primary, + borderRadius: BorderRadius.zero, + minHeight: 5, + value: position.inMilliseconds / asset.duration.inMilliseconds, + ); + } +} + /// Progress bar for image slides, driven by an explicit [AnimationController]. /// /// [TweenAnimationBuilder] creates its controller internally with the default diff --git a/mobile/lib/presentation/pages/drift_user_selection.page.dart b/mobile/lib/presentation/pages/drift_user_selection.page.dart index 215a5d1071..c11d5a1fd6 100644 --- a/mobile/lib/presentation/pages/drift_user_selection.page.dart +++ b/mobile/lib/presentation/pages/drift_user_selection.page.dart @@ -52,6 +52,7 @@ class DriftUserSelectionPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final AsyncValue> suggestedShareUsers = ref.watch(driftUsersProvider); + final sharedUsers = ref.watch(remoteAlbumSharedUsersProvider(album.id)); final sharedUsersList = useState>({}); void addNewUsersHandler() { @@ -136,9 +137,6 @@ class DriftUserSelectionPage extends HookConsumerWidget { ), body: suggestedShareUsers.widgetWhen( onData: (users) { - // Get shared users for this album from the database - final sharedUsers = ref.read(remoteAlbumSharedUsersProvider(album.id)); - return sharedUsers.when( data: (albumSharedUsers) { // Filter out users that are already shared with this album and the owner diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 0c6b0d8ae4..dfcb5abd4d 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -742,9 +742,7 @@ class _SearchResultGrid extends ConsumerWidget { return false; } - Widget? _bottomWidget(BuildContext context, WidgetRef ref) { - final isLoading = ref.read(paginatedSearchProvider.select((s) => s.isLoading)); - + Widget? _bottomWidget(BuildContext context, {required bool isLoading, required bool hasMore}) { if (isLoading) { return const SliverFillRemaining( hasScrollBody: false, @@ -755,8 +753,6 @@ class _SearchResultGrid extends ConsumerWidget { ); } - final hasMore = ref.read(paginatedSearchProvider.select((s) => s.nextPage != null)); - if (hasMore) { return null; } @@ -778,6 +774,7 @@ class _SearchResultGrid extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final hasAssets = ref.watch(paginatedSearchProvider.select((s) => s.assets.isNotEmpty)); final isLoading = ref.watch(paginatedSearchProvider.select((s) => s.isLoading)); + final hasMore = ref.watch(paginatedSearchProvider.select((s) => s.nextPage != null)); if (!hasAssets && !isLoading) { return const _SearchNoResults(); @@ -807,7 +804,7 @@ class _SearchResultGrid extends ConsumerWidget { bottomSheet: const GeneralBottomSheet(minChildSize: 0.20), snapToMonth: false, loadingWidget: const SizedBox.shrink(), - bottomSliverWidget: _bottomWidget(context, ref), + bottomSliverWidget: _bottomWidget(context, isLoading: isLoading, hasMore: hasMore), ), ), ), diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 4c6b2a7211..6a119f8ca3 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -177,7 +177,15 @@ class _BackupIndicator extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final indicatorIcon = _getBackupBadgeIcon(context, ref); + final backupEnabled = ref.watch(appConfigProvider.select((c) => c.backup.enabled)); + final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none)); + final isUploading = ref.watch(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty)); + final indicatorIcon = _getBackupBadgeIcon( + context, + backupEnabled: backupEnabled, + hasError: hasError, + isUploading: isUploading, + ); return IconButton( onPressed: () => context.pushRoute(const DriftBackupRoute()), @@ -192,12 +200,14 @@ class _BackupIndicator extends ConsumerWidget { ); } - Widget? _getBackupBadgeIcon(BuildContext context, WidgetRef ref) { - final backupEnabled = ref.read(appConfigProvider.select((c) => c.backup.enabled)); - final hasError = ref.read(driftBackupProvider.select((state) => state.error != BackupError.none)); + Widget? _getBackupBadgeIcon( + BuildContext context, { + required bool backupEnabled, + required bool hasError, + required bool isUploading, + }) { final isDarkTheme = context.isDarkTheme; final iconColor = isDarkTheme ? Colors.white : Colors.black; - final isUploading = ref.read(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty)); if (!backupEnabled) { return _BadgeLabel( From 943c11c0196e7424e5e488131a2fb7f7522037ed Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:32:08 +0530 Subject: [PATCH 42/49] chore: label local only delete as trash (#30726) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/presentation/actions/delete.action.dart | 3 ++- .../unit/presentation/actions/delete_action_test.dart | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/mobile/lib/presentation/actions/delete.action.dart b/mobile/lib/presentation/actions/delete.action.dart index ea7423354a..23a2ef2a2f 100644 --- a/mobile/lib/presentation/actions/delete.action.dart +++ b/mobile/lib/presentation/actions/delete.action.dart @@ -37,7 +37,8 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, final trashEnabled = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); // Assets already in the trash or in the locked folder are deleted outright, irrespective of the server setting. - final trash = trashEnabled && !ownedRemote.every((asset) => asset.isTrashed || asset.isLocked); + final trash = + ownedRemote.isEmpty || (trashEnabled && !ownedRemote.every((asset) => asset.isTrashed || asset.isLocked)); return (localIds: localIds, remoteIds: ownedRemote.map((asset) => asset.id).toList(growable: false), trash: trash); }, dependencies: [assetsActionProvider]); diff --git a/mobile/test/unit/presentation/actions/delete_action_test.dart b/mobile/test/unit/presentation/actions/delete_action_test.dart index 29d81a5c2c..4c1bf9306c 100644 --- a/mobile/test/unit/presentation/actions/delete_action_test.dart +++ b/mobile/test/unit/presentation/actions/delete_action_test.dart @@ -170,6 +170,17 @@ void main() { verifyNever(() => assetService.trash(any())); verifyNever(() => assetService.delete(any())); }); + + testWidgets('is labelled trash', (tester) async { + await tester.pumpTestWidget( + context, + const ActionButton(action: DeleteAction(source: .timeline)), + overrides: context.selected({LocalAssetFactory.create()}), + ); + + expect(find.text(StaticTranslations.instance.trash), findsOneWidget); + expect(find.text(StaticTranslations.instance.delete), findsNothing); + }); }); group('prompt handling', () { From 52edcc0c74c2430418f88fe94700b549dac84e90 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:12:11 +0530 Subject: [PATCH 43/49] refactor: unify person model (#30659) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/domain/models/person.model.dart | 29 ++----------------- .../lib/domain/services/people.service.dart | 6 ++-- .../repositories/people.repository.dart | 21 +++----------- .../models/search/search_filter.model.dart | 4 +-- .../presentation/pages/drift_person.page.dart | 4 +-- .../pages/search/drift_search.page.dart | 2 +- .../asset_details/people_details.widget.dart | 4 +-- .../person_edit_birthday_modal.widget.dart | 2 +- .../people/person_edit_name_modal.widget.dart | 2 +- .../infrastructure/people.provider.dart | 4 +-- .../lib/providers/search/people.provider.dart | 2 +- .../repositories/person_api.repository.dart | 14 +++------ mobile/lib/services/person.service.dart | 4 +-- mobile/lib/utils/people.utils.dart | 4 +-- .../widgets/common/person_sliver_app_bar.dart | 4 +-- .../search/search_filter/people_picker.dart | 6 ++-- 16 files changed, 34 insertions(+), 78 deletions(-) diff --git a/mobile/lib/domain/models/person.model.dart b/mobile/lib/domain/models/person.model.dart index 56e6d3990d..94ead6a2e9 100644 --- a/mobile/lib/domain/models/person.model.dart +++ b/mobile/lib/domain/models/person.model.dart @@ -3,32 +3,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'person.model.freezed.dart'; -// TODO: Remove PersonDto once Isar is removed @freezed -abstract class PersonDto with _$PersonDto { - const factory PersonDto({ - required String id, - DateTime? birthDate, - required bool isHidden, - required String name, - required String thumbnailPath, - DateTime? updatedAt, - }) = _PersonDto; -} - -// Model for a person stored in the server -@freezed -abstract class DriftPerson with _$DriftPerson { - const factory DriftPerson({ - required String id, - required DateTime createdAt, - required DateTime updatedAt, - required String ownerId, - required String name, - String? faceAssetId, - required bool isFavorite, - required bool isHidden, - required String? color, - DateTime? birthDate, - }) = _DriftPerson; +abstract class Person with _$Person { + const factory Person({required String id, required String name, DateTime? updatedAt, DateTime? birthDate}) = _Person; } diff --git a/mobile/lib/domain/services/people.service.dart b/mobile/lib/domain/services/people.service.dart index 73972dbdc0..e86d43b283 100644 --- a/mobile/lib/domain/services/people.service.dart +++ b/mobile/lib/domain/services/people.service.dart @@ -10,15 +10,15 @@ class DriftPeopleService { const DriftPeopleService(this._repository, this._personApiRepository); - Future get(String personId) { + Future get(String personId) { return _repository.get(personId); } - Future> getAssetPeople(String assetId) { + Future> getAssetPeople(String assetId) { return _repository.getAssetPeople(assetId); } - Future> getAllPeople({int minFaces = 3}) { + Future> getAllPeople({int minFaces = 3}) { return _repository.getAllPeople(minFaces: minFaces); } diff --git a/mobile/lib/infrastructure/repositories/people.repository.dart b/mobile/lib/infrastructure/repositories/people.repository.dart index 0dd73cc81d..b539c6b2fd 100644 --- a/mobile/lib/infrastructure/repositories/people.repository.dart +++ b/mobile/lib/infrastructure/repositories/people.repository.dart @@ -8,14 +8,14 @@ class DriftPeopleRepository extends DriftDatabaseRepository { final Drift _db; const DriftPeopleRepository(this._db) : super(_db); - Future get(String personId) async { + Future get(String personId) async { final query = _db.select(_db.personEntity)..where((row) => row.id.equals(personId)); final result = await query.getSingleOrNull(); return result?.toDto(); } - Future> getAssetPeople(String assetId) async { + Future> getAssetPeople(String assetId) async { // An asset can have multiple face records for the same person (e.g., metadata // imports alongside ML detections). Use a subquery instead of a join so each // person is returned once, regardless of how many of their faces are on the asset @@ -33,7 +33,7 @@ class DriftPeopleRepository extends DriftDatabaseRepository { return query.map((row) => row.toDto()).get(); } - Future> getAllPeople({int minFaces = 3}) async { + Future> getAllPeople({int minFaces = 3}) async { final people = _db.personEntity; final faces = _db.assetFaceEntity; final assets = _db.remoteAssetEntity; @@ -76,18 +76,5 @@ class DriftPeopleRepository extends DriftDatabaseRepository { } extension on PersonEntityData { - DriftPerson toDto() { - return DriftPerson( - id: id, - createdAt: createdAt, - updatedAt: updatedAt, - ownerId: ownerId, - name: name, - faceAssetId: faceAssetId, - isFavorite: isFavorite, - isHidden: isHidden, - color: color, - birthDate: birthDate, - ); - } + Person toDto() => Person(id: id, updatedAt: updatedAt, name: name, birthDate: birthDate); } diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 7e5e1b819e..a9cc13ba6a 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -194,7 +194,7 @@ class SearchFilter { String? language; String? assetId; List? tagIds; - Set people; + Set people; SearchLocationFilter location; SearchCameraFilter camera; SearchDateFilter date; @@ -250,7 +250,7 @@ class SearchFilter { String? language, String? ocr, String? assetId, - Set? people, + Set? people, List? tagIds, SearchLocationFilter? location, SearchCameraFilter? camera, diff --git a/mobile/lib/presentation/pages/drift_person.page.dart b/mobile/lib/presentation/pages/drift_person.page.dart index 7389f506d5..cad3b7b6a6 100644 --- a/mobile/lib/presentation/pages/drift_person.page.dart +++ b/mobile/lib/presentation/pages/drift_person.page.dart @@ -14,7 +14,7 @@ import 'package:immich_mobile/widgets/common/person_sliver_app_bar.dart'; @RoutePage() class DriftPersonPage extends ConsumerStatefulWidget { - final DriftPerson person; + final Person person; const DriftPersonPage({super.key, required this.person}); @@ -23,7 +23,7 @@ class DriftPersonPage extends ConsumerStatefulWidget { } class _DriftPersonPageState extends ConsumerState { - late DriftPerson _person; + late Person _person; @override void initState() { diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index dfcb5abd4d..e364e0d2f8 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -129,7 +129,7 @@ class DriftSearchPage extends HookConsumerWidget { void showPeoplePicker() { var people = filter.value.people; - void handleOnSelect(Set value) { + void handleOnSelect(Set value) { people = value; } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart index 739fe0a0d9..00c20f35bd 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart @@ -30,7 +30,7 @@ class PeopleDetails extends ConsumerWidget { final peopleFuture = ref.watch(driftPeopleAssetProvider(asset.id)); - Future showNameEditModal(DriftPerson person) async { + Future showNameEditModal(Person person) async { await showDialog( context: context, useRootNavigator: false, @@ -96,7 +96,7 @@ class PeopleDetails extends ConsumerWidget { } class _Avatar extends StatelessWidget { - final DriftPerson person; + final Person person; final DateTime assetFileCreatedAt; final VoidCallback? onTap; final VoidCallback? onNameTap; diff --git a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart index 0b8a08a5d8..a4903c7a29 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:scroll_date_picker/scroll_date_picker.dart'; class DriftPersonBirthdayEditForm extends ConsumerStatefulWidget { - final DriftPerson person; + final Person person; const DriftPersonBirthdayEditForm({super.key, required this.person}); diff --git a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart index 3b0e62ab1a..5e8c634181 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart @@ -9,7 +9,7 @@ import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class DriftPersonNameEditForm extends ConsumerStatefulWidget { - final DriftPerson person; + final Person person; const DriftPersonNameEditForm({super.key, required this.person}); diff --git a/mobile/lib/providers/infrastructure/people.provider.dart b/mobile/lib/providers/infrastructure/people.provider.dart index 1939940975..73a35958bd 100644 --- a/mobile/lib/providers/infrastructure/people.provider.dart +++ b/mobile/lib/providers/infrastructure/people.provider.dart @@ -14,12 +14,12 @@ final driftPeopleServiceProvider = Provider( (ref) => DriftPeopleService(ref.watch(driftPeopleRepositoryProvider), ref.watch(personApiRepositoryProvider)), ); -final driftPeopleAssetProvider = FutureProvider.family, String>((ref, assetId) async { +final driftPeopleAssetProvider = FutureProvider.family, String>((ref, assetId) async { final service = ref.watch(driftPeopleServiceProvider); return service.getAssetPeople(assetId); }); -final driftGetAllPeopleProvider = FutureProvider>((ref) async { +final driftGetAllPeopleProvider = FutureProvider>((ref) async { final service = ref.watch(driftPeopleServiceProvider); final prefs = await ref.watch(userMetadataPreferencesProvider.future); return service.getAllPeople(minFaces: prefs?.minimumFaces ?? 3); diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart index 1bd58509f5..f9fc1ec133 100644 --- a/mobile/lib/providers/search/people.provider.dart +++ b/mobile/lib/providers/search/people.provider.dart @@ -2,7 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/services/person.service.dart'; -final getAllPeopleProvider = FutureProvider.autoDispose>((ref) async { +final getAllPeopleProvider = FutureProvider.autoDispose>((ref) async { final PersonService personService = ref.read(personServiceProvider); final people = await personService.getAllPeople(); diff --git a/mobile/lib/repositories/person_api.repository.dart b/mobile/lib/repositories/person_api.repository.dart index 158e3ac333..5804e1da3b 100644 --- a/mobile/lib/repositories/person_api.repository.dart +++ b/mobile/lib/repositories/person_api.repository.dart @@ -11,12 +11,12 @@ class PersonApiRepository extends ApiRepository { PersonApiRepository(this._api); - Future> getAll() async { + Future> getAll() async { final dto = await checkNull(_api.getAllPeople()); return dto.people.map(_toPerson).toList(); } - Future update(String id, {String? name, DateTime? birthday}) async { + Future update(String id, {String? name, DateTime? birthday}) async { final birthdayUtc = birthday == null ? null : DateTime.utc(birthday.year, birthday.month, birthday.day); final dto = PersonUpdateDto( name: name == null ? const Optional.absent() : Optional.present(name), @@ -26,12 +26,6 @@ class PersonApiRepository extends ApiRepository { return _toPerson(response); } - static PersonDto _toPerson(PersonResponseDto dto) => PersonDto( - birthDate: dto.birthDate, - id: dto.id, - isHidden: dto.isHidden, - name: dto.name, - thumbnailPath: dto.thumbnailPath, - updatedAt: dto.updatedAt.orElse(null), - ); + static Person _toPerson(PersonResponseDto dto) => + .new(birthDate: dto.birthDate, id: dto.id, name: dto.name, updatedAt: dto.updatedAt.value); } diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart index 0d589ea71d..6a9226a893 100644 --- a/mobile/lib/services/person.service.dart +++ b/mobile/lib/services/person.service.dart @@ -12,7 +12,7 @@ class PersonService { final PersonApiRepository _personApiRepository; PersonService(this._personApiRepository); - Future> getAllPeople() async { + Future> getAllPeople() async { try { return await _personApiRepository.getAll(); } catch (error, stack) { @@ -21,7 +21,7 @@ class PersonService { } } - Future updateName(String id, String name) async { + Future updateName(String id, String name) async { try { return await _personApiRepository.update(id, name: name); } catch (error, stack) { diff --git a/mobile/lib/utils/people.utils.dart b/mobile/lib/utils/people.utils.dart index 8da5a5a3eb..bd2b607e0b 100644 --- a/mobile/lib/utils/people.utils.dart +++ b/mobile/lib/utils/people.utils.dart @@ -34,7 +34,7 @@ int _calculateAgeInMonths(DateTime birthDate, DateTime referenceDate) { (referenceDate.day < birthDate.day ? 1 : 0); } -Future showNameEditModal(BuildContext context, DriftPerson person) { +Future showNameEditModal(BuildContext context, Person person) { return showDialog( context: context, useRootNavigator: false, @@ -44,7 +44,7 @@ Future showNameEditModal(BuildContext context, DriftPerson person) { ); } -Future showBirthdayEditModal(BuildContext context, DriftPerson person) { +Future showBirthdayEditModal(BuildContext context, Person person) { return showDialog( context: context, useRootNavigator: false, diff --git a/mobile/lib/widgets/common/person_sliver_app_bar.dart b/mobile/lib/widgets/common/person_sliver_app_bar.dart index 4b159447e6..56eb3c6094 100644 --- a/mobile/lib/widgets/common/person_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/person_sliver_app_bar.dart @@ -28,7 +28,7 @@ class PersonSliverAppBar extends ConsumerStatefulWidget { required this.onBirthdayTap, }); - final DriftPerson person; + final Person person; final VoidCallback onNameTap; final VoidCallback onBirthdayTap; final VoidCallback onShowOptions; @@ -137,7 +137,7 @@ class _MesmerizingSliverAppBarState extends ConsumerState { class _ExpandedBackground extends ConsumerStatefulWidget { final double scrollProgress; - final DriftPerson person; + final Person person; final VoidCallback onNameTap; final VoidCallback onBirthdayTap; diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index b4e73e97f9..1995851904 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -15,8 +15,8 @@ import 'package:immich_mobile/widgets/common/search_field.dart'; class PeoplePicker extends HookConsumerWidget { const PeoplePicker({super.key, required this.onSelect, this.filter}); - final Function(Set) onSelect; - final Set? filter; + final Function(Set) onSelect; + final Set? filter; @override Widget build(BuildContext context, WidgetRef ref) { @@ -24,7 +24,7 @@ class PeoplePicker extends HookConsumerWidget { const imageSize = 60.0; final searchQuery = useState(''); final people = ref.watch(getAllPeopleProvider); - final selectedPeople = useState>(filter ?? {}); + final selectedPeople = useState>(filter ?? {}); return Column( children: [ From 303a9f15b1a78e2aeb3c4195f6a97608152eb799 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:12:12 +0530 Subject: [PATCH 44/49] refactor: reactive driftGetAllPeopleProvider (#30660) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/domain/services/people.service.dart | 6 +++--- .../infrastructure/repositories/people.repository.dart | 4 ++-- mobile/lib/pages/common/tab_shell.page.dart | 2 -- .../people/person_edit_birthday_modal.widget.dart | 9 ++------- .../widgets/people/person_edit_name_modal.widget.dart | 7 +------ mobile/lib/providers/infrastructure/people.provider.dart | 4 ++-- 6 files changed, 10 insertions(+), 22 deletions(-) diff --git a/mobile/lib/domain/services/people.service.dart b/mobile/lib/domain/services/people.service.dart index e86d43b283..196cf1edbb 100644 --- a/mobile/lib/domain/services/people.service.dart +++ b/mobile/lib/domain/services/people.service.dart @@ -18,8 +18,8 @@ class DriftPeopleService { return _repository.getAssetPeople(assetId); } - Future> getAllPeople({int minFaces = 3}) { - return _repository.getAllPeople(minFaces: minFaces); + Stream> watch({int minFaces = 3}) { + return _repository.watch(minFaces: minFaces); } Future updateName(String personId, String name) async { @@ -27,7 +27,7 @@ class DriftPeopleService { return _repository.updateName(personId, name); } - Future updateBrithday(String personId, DateTime birthday) async { + Future updateBirthday(String personId, DateTime birthday) async { await _personApiRepository.update(personId, birthday: birthday); return _repository.updateBirthday(personId, birthday); } diff --git a/mobile/lib/infrastructure/repositories/people.repository.dart b/mobile/lib/infrastructure/repositories/people.repository.dart index b539c6b2fd..c5c7bfdb9c 100644 --- a/mobile/lib/infrastructure/repositories/people.repository.dart +++ b/mobile/lib/infrastructure/repositories/people.repository.dart @@ -33,7 +33,7 @@ class DriftPeopleRepository extends DriftDatabaseRepository { return query.map((row) => row.toDto()).get(); } - Future> getAllPeople({int minFaces = 3}) async { + Stream> watch({int minFaces = 3}) { final people = _db.personEntity; final faces = _db.assetFaceEntity; final assets = _db.remoteAssetEntity; @@ -59,7 +59,7 @@ class DriftPeopleRepository extends DriftDatabaseRepository { return query.map((row) { final person = row.readTable(people); return person.toDto(); - }).get(); + }).watch(); } Future updateName(String personId, String name) { diff --git a/mobile/lib/pages/common/tab_shell.page.dart b/mobile/lib/pages/common/tab_shell.page.dart index 5b620e9c91..240c4abe7a 100644 --- a/mobile/lib/pages/common/tab_shell.page.dart +++ b/mobile/lib/pages/common/tab_shell.page.dart @@ -12,7 +12,6 @@ import 'package:immich_mobile/presentation/pages/search/paginated_search.provide import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; import 'package:immich_mobile/providers/tab.provider.dart'; @@ -132,7 +131,6 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) { // Library page if (index == kLibraryTabIndex) { ref.invalidate(localAlbumProvider); - ref.invalidate(driftGetAllPeopleProvider); } ref.read(hapticFeedbackProvider.notifier).selectionClick(); diff --git a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart index a4903c7a29..e56a21e021 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart @@ -30,14 +30,9 @@ class _DriftPersonNameEditFormState extends ConsumerState saveBirthday() async { try { - final result = await ref.read(driftPeopleServiceProvider).updateBrithday(widget.person.id, _selectedDate); - - if (result != 0) { - ref.invalidate(driftGetAllPeopleProvider); - if (!mounted) { - return; - } + final result = await ref.read(driftPeopleServiceProvider).updateBirthday(widget.person.id, _selectedDate); + if (result != 0 && mounted) { context.pop(_selectedDate); } } catch (error) { diff --git a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart index 5e8c634181..4ae41bc640 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart @@ -29,12 +29,7 @@ class _DriftPersonNameEditFormState extends ConsumerState onEdit(String personId, String newName) async { try { final result = await ref.read(driftPeopleServiceProvider).updateName(personId, newName); - if (result != 0) { - ref.invalidate(driftGetAllPeopleProvider); - if (!mounted) { - return; - } - + if (result != 0 && mounted) { context.pop(newName); } } catch (error) { diff --git a/mobile/lib/providers/infrastructure/people.provider.dart b/mobile/lib/providers/infrastructure/people.provider.dart index 73a35958bd..26b145255c 100644 --- a/mobile/lib/providers/infrastructure/people.provider.dart +++ b/mobile/lib/providers/infrastructure/people.provider.dart @@ -19,8 +19,8 @@ final driftPeopleAssetProvider = FutureProvider.family, String>((re return service.getAssetPeople(assetId); }); -final driftGetAllPeopleProvider = FutureProvider>((ref) async { +final driftGetAllPeopleProvider = StreamProvider>((ref) async* { final service = ref.watch(driftPeopleServiceProvider); final prefs = await ref.watch(userMetadataPreferencesProvider.future); - return service.getAllPeople(minFaces: prefs?.minimumFaces ?? 3); + yield* service.watch(minFaces: prefs?.minimumFaces ?? 3); }); From 1c3a5cf5087182a929b16d71460e093af5b844a6 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:12:12 +0530 Subject: [PATCH 45/49] chore: remove old people provider (#30662) * chore: remove old people provider * refactor: rename driftGetAllPeopleProvider * minor fixes --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../pages/drift_library.page.dart | 2 +- .../pages/drift_people_collection.page.dart | 2 +- .../infrastructure/people.provider.dart | 2 +- .../lib/providers/search/people.provider.dart | 24 -------------- .../repositories/person_api.repository.dart | 7 +--- mobile/lib/services/person.service.dart | 32 ------------------- .../search/search_filter/people_picker.dart | 4 +-- 7 files changed, 6 insertions(+), 67 deletions(-) delete mode 100644 mobile/lib/providers/search/people.provider.dart delete mode 100644 mobile/lib/services/person.service.dart diff --git a/mobile/lib/presentation/pages/drift_library.page.dart b/mobile/lib/presentation/pages/drift_library.page.dart index 3118f2c4f0..137f46659f 100644 --- a/mobile/lib/presentation/pages/drift_library.page.dart +++ b/mobile/lib/presentation/pages/drift_library.page.dart @@ -145,7 +145,7 @@ class _PeopleCollectionCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final people = ref.watch(driftGetAllPeopleProvider); + final people = ref.watch(getAllPeopleProvider); return LayoutBuilder( builder: (context, constraints) { diff --git a/mobile/lib/presentation/pages/drift_people_collection.page.dart b/mobile/lib/presentation/pages/drift_people_collection.page.dart index cb305395cc..569cb99b01 100644 --- a/mobile/lib/presentation/pages/drift_people_collection.page.dart +++ b/mobile/lib/presentation/pages/drift_people_collection.page.dart @@ -33,7 +33,7 @@ class _DriftPeopleCollectionPageState extends ConsumerState, String>((re return service.getAssetPeople(assetId); }); -final driftGetAllPeopleProvider = StreamProvider>((ref) async* { +final getAllPeopleProvider = StreamProvider>((ref) async* { final service = ref.watch(driftPeopleServiceProvider); final prefs = await ref.watch(userMetadataPreferencesProvider.future); yield* service.watch(minFaces: prefs?.minimumFaces ?? 3); diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart deleted file mode 100644 index f9fc1ec133..0000000000 --- a/mobile/lib/providers/search/people.provider.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/services/person.service.dart'; - -final getAllPeopleProvider = FutureProvider.autoDispose>((ref) async { - final PersonService personService = ref.read(personServiceProvider); - - final people = await personService.getAllPeople(); - - return people; -}); - -final updatePersonNameProvider = FutureProvider.autoDispose( - (ref) => (String personId, String updatedName) async { - final PersonService personService = ref.read(personServiceProvider); - final person = await personService.updateName(personId, updatedName); - - if (person != null && person.name == updatedName) { - ref.invalidate(getAllPeopleProvider); - return true; - } - return false; - }, -); diff --git a/mobile/lib/repositories/person_api.repository.dart b/mobile/lib/repositories/person_api.repository.dart index 5804e1da3b..262868eea1 100644 --- a/mobile/lib/repositories/person_api.repository.dart +++ b/mobile/lib/repositories/person_api.repository.dart @@ -11,11 +11,6 @@ class PersonApiRepository extends ApiRepository { PersonApiRepository(this._api); - Future> getAll() async { - final dto = await checkNull(_api.getAllPeople()); - return dto.people.map(_toPerson).toList(); - } - Future update(String id, {String? name, DateTime? birthday}) async { final birthdayUtc = birthday == null ? null : DateTime.utc(birthday.year, birthday.month, birthday.day); final dto = PersonUpdateDto( @@ -27,5 +22,5 @@ class PersonApiRepository extends ApiRepository { } static Person _toPerson(PersonResponseDto dto) => - .new(birthDate: dto.birthDate, id: dto.id, name: dto.name, updatedAt: dto.updatedAt.value); + .new(birthDate: dto.birthDate, id: dto.id, name: dto.name, updatedAt: dto.updatedAt.orElse(null)); } diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart deleted file mode 100644 index 6a9226a893..0000000000 --- a/mobile/lib/services/person.service.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/repositories/person_api.repository.dart'; -import 'package:logging/logging.dart'; - -final personServiceProvider = Provider.autoDispose( - (ref) => PersonService(ref.watch(personApiRepositoryProvider)), -); - -class PersonService { - final Logger _log = Logger("PersonService"); - final PersonApiRepository _personApiRepository; - PersonService(this._personApiRepository); - - Future> getAllPeople() async { - try { - return await _personApiRepository.getAll(); - } catch (error, stack) { - _log.severe("Error while fetching curated people", error, stack); - return []; - } - } - - Future updateName(String id, String name) async { - try { - return await _personApiRepository.update(id, name: name); - } catch (error, stack) { - _log.severe("Error while updating person name", error, stack); - } - return null; - } -} diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index 1995851904..ee738265b2 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -8,7 +8,7 @@ import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; @@ -65,7 +65,7 @@ class PeoplePicker extends HookConsumerWidget { padding: const EdgeInsets.only(bottom: 2.0), child: LargeLeadingTile( title: Text( - person.name, + person.name.nullIfEmpty ?? context.t.no_name, style: context.textTheme.bodyLarge?.copyWith( fontSize: 20, fontWeight: FontWeight.w500, From 2a1691868e71aec6bd66eee82d1a6eb2e7aec636 Mon Sep 17 00:00:00 2001 From: Mees Frensel <33722705+meesfrensel@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:01:30 +0200 Subject: [PATCH 46/49] chore: use import.meta.dirname instead of __dirname (#30738) --- e2e/eslint.config.mjs | 7 +------ packages/cli/eslint.config.mjs | 7 +------ server/eslint.config.mjs | 7 +------ web/eslint.config.js | 10 +++------- web/vite.config.ts | 4 ++-- 5 files changed, 8 insertions(+), 27 deletions(-) diff --git a/e2e/eslint.config.mjs b/e2e/eslint.config.mjs index d5da8f8445..3d69d1f503 100644 --- a/e2e/eslint.config.mjs +++ b/e2e/eslint.config.mjs @@ -2,13 +2,8 @@ import js from '@eslint/js'; import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; import eslintPluginUnicorn from 'eslint-plugin-unicorn'; import globals from 'globals'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import typescriptEslint from 'typescript-eslint'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - export default typescriptEslint.config([ eslintPluginUnicorn.configs.recommended, eslintPluginPrettierRecommended, @@ -29,7 +24,7 @@ export default typescriptEslint.config([ parserOptions: { project: 'tsconfig.json', - tsconfigRootDir: __dirname, + tsconfigRootDir: import.meta.dirname, }, }, diff --git a/packages/cli/eslint.config.mjs b/packages/cli/eslint.config.mjs index bcc74c654d..cd32bf6023 100644 --- a/packages/cli/eslint.config.mjs +++ b/packages/cli/eslint.config.mjs @@ -2,13 +2,8 @@ import js from '@eslint/js'; import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; import eslintPluginUnicorn from 'eslint-plugin-unicorn'; import globals from 'globals'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import typescriptEslint from 'typescript-eslint'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - export default typescriptEslint.config([ eslintPluginUnicorn.configs.recommended, eslintPluginPrettierRecommended, @@ -29,7 +24,7 @@ export default typescriptEslint.config([ parserOptions: { project: 'tsconfig.json', - tsconfigRootDir: __dirname, + tsconfigRootDir: import.meta.dirname, }, }, diff --git a/server/eslint.config.mjs b/server/eslint.config.mjs index 2c11b7d3cb..949acaf139 100644 --- a/server/eslint.config.mjs +++ b/server/eslint.config.mjs @@ -2,13 +2,8 @@ import js from '@eslint/js'; import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; import eslintPluginUnicorn from 'eslint-plugin-unicorn'; import globals from 'globals'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import typescriptEslint from 'typescript-eslint'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - export default typescriptEslint.config([ eslintPluginUnicorn.configs.recommended, eslintPluginPrettierRecommended, @@ -29,7 +24,7 @@ export default typescriptEslint.config([ parserOptions: { project: 'tsconfig.json', - tsconfigRootDir: __dirname, + tsconfigRootDir: import.meta.dirname, }, }, diff --git a/web/eslint.config.js b/web/eslint.config.js index 5974eaf68f..fc69385c93 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -10,10 +10,6 @@ import parser from 'svelte-eslint-parser'; import typescriptEslint from 'typescript-eslint'; import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); export default typescriptEslint.config( ...eslintPluginSvelte.configs.recommended, @@ -29,7 +25,7 @@ export default typescriptEslint.config( 'error', { browserslist: fs - .readFileSync(path.join(__dirname, '.browserslistrc'), 'utf8') + .readFileSync(path.join(import.meta.dirname, '.browserslistrc'), 'utf8') .split('\n') .map((line) => line.trim()) .filter((line) => line && !line.startsWith('#')), @@ -40,7 +36,7 @@ export default typescriptEslint.config( parser, parserOptions: { project: ['./tsconfig.json'], - tsconfigRootDir: __dirname, + tsconfigRootDir: import.meta.dirname, }, }, // ignores: ['**/service-worker/**'], @@ -96,7 +92,7 @@ export default typescriptEslint.config( parserOptions: { extraFileExtensions: ['.svelte'], - tsconfigRootDir: __dirname, + tsconfigRootDir: import.meta.dirname, project: ['./tsconfig.json'], }, }, diff --git a/web/vite.config.ts b/web/vite.config.ts index c2b2b5bf25..07e7efaf2a 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -28,8 +28,8 @@ export default defineConfig({ alias: { '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/packages/ui'), + '@test-data': path.resolve(import.meta.dirname, './src/test-data'), + // '@immich/ui': path.resolve(import.meta.dirname, '../../ui/packages/ui'), }, }, server: { From ff5da0f84fc79d3167fbb6a6a193ed53dc36c987 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:00:08 +0530 Subject: [PATCH 47/49] chore: immich_mobile path in openapi pubspec (#30643) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- open-api/patch/pubspec_immich_mobile.yaml.patch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/open-api/patch/pubspec_immich_mobile.yaml.patch b/open-api/patch/pubspec_immich_mobile.yaml.patch index ad3bbc2edd..be210f864f 100644 --- a/open-api/patch/pubspec_immich_mobile.yaml.patch +++ b/open-api/patch/pubspec_immich_mobile.yaml.patch @@ -6,4 +6,4 @@ -dev_dependencies: - test: '>=1.21.6 <1.22.0' + immich_mobile: -+ path: ../ ++ path: ../../ From db9e7c20d71043a5a5a438781e3d9bebcf35bf97 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 12 Aug 2026 11:30:05 -0700 Subject: [PATCH 48/49] chore(mobile): add a single mise checkout command for reoccuring tasks (#30737) * chore(mobile): add a single mise checkout command for reoccuring tasks * Fix .flutter-plugins-dependencies path Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> --------- Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> --- mobile/mise.toml | 14 ++++++++- mobile/scripts/checkout_ios.sh | 52 ++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 mobile/scripts/checkout_ios.sh diff --git a/mobile/mise.toml b/mobile/mise.toml index f021cd8747..d8ddc8ab21 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -111,6 +111,18 @@ run = [ { task = "install", args = ["--enforce-lockfile"] }, ] +[tasks.checkout] +description = "Bring a checkout up to date after switching branches" +run = [ + { task = "install" }, + { task = "checkout:ios" }, + { task = "codegen" }, +] + +[tasks."checkout:ios"] +description = "Bring the iOS project up to date after switching branches" +run = "bash ./scripts/checkout_ios.sh" + [tasks.start] alias = "start" description = "Start flutter app" @@ -139,7 +151,7 @@ wait_for = ["//:i18n:format-fix"] [tasks."install:ios"] description = "Install CocoaPods dependencies" hide = true -sources = ["ios/Podfile", "ios/Podfile.lock", ".flutter-plugin-dependencies"] +sources = ["ios/Podfile", "ios/Podfile.lock", ".flutter-plugins-dependencies"] outputs = ["ios/Pods/Manifest.lock"] run = ''' echo "Running pod install" diff --git a/mobile/scripts/checkout_ios.sh b/mobile/scripts/checkout_ios.sh new file mode 100644 index 0000000000..74706e0d23 --- /dev/null +++ b/mobile/scripts/checkout_ios.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Reconciles the iOS project with the current Flutter setup +# +# `flutter build ios --config-only` is relatively expensive, so we attempt to avoid running it when possible + +set -euo pipefail + +stamp="build/ios_checkout.stamp" + +# All tracked files that can invalidate the Xcode + Flutter build state +inputs_digest() { + git ls-files -z ios pubspec.yaml pubspec.lock mise.toml mise.lock \ + | xargs -0 shasum \ + | shasum \ + | cut -d ' ' -f 1 +} + +# Echoes why the iOS project needs reconciling, or returns 1 if it does not. +# The first three cases can happen with no tracked file having changed at all, +# so comparing the digest alone is not enough. +staleness() { + local flutter_root + flutter_root="$(sed -n 's/^FLUTTER_ROOT=//p' ios/Flutter/Generated.xcconfig 2>/dev/null || true)" + + if [[ -z $flutter_root ]]; then + echo "misconfigured FLUTTER_ROOT" + elif [[ ! -d $flutter_root ]]; then + echo "the Flutter SDK has changed" + elif ! cmp -s ios/Podfile.lock ios/Pods/Manifest.lock; then + # This is also done by Xcode's "Check Pods Manifest.lock" build phase + echo "Cocoapods out of sync" + elif [[ ! -f $stamp ]]; then + echo "first checkout" + elif [[ $(<"$stamp") != "$(inputs_digest)" ]]; then + echo "tracked build state has changed" + else + return 1 + fi +} + +[[ $(uname) == Darwin ]] || exit 0 + +if ! reason="$(staleness)"; then + exit 0 +fi + +echo "Reconciling iOS project: $reason" +flutter build ios --config-only --debug + +# Save tracked files manifest +mkdir -p "$(dirname "$stamp")" +inputs_digest > "$stamp" From b82d4805525f68dbb07e160163e2a582e9dfab91 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 12 Aug 2026 12:57:45 -0700 Subject: [PATCH 49/49] fix(mobile): prevent iOS status bar scroll to top during transitions (#30717) --- mobile/lib/main.dart | 2 +- .../widgets/timeline/timeline.widget.dart | 10 +++++++++- .../lib/routing/app_navigation_observer.dart | 19 +++++++++++++++++++ .../timeline/timeline_scroll_to_top_test.dart | 14 +++++++++++--- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index d2fd945cbf..2f1cfdaf6a 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -277,7 +277,7 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve ), routerConfig: router.config( deepLinkBuilder: _deepLinkBuilder, - navigatorObservers: () => [AppNavigationObserver(ref: ref)], + navigatorObservers: () => [AppNavigationObserver(ref: ref), TransitioningRouteObserver()], ), ), ); diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index 9e65dcb72c..c41f88fcc0 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -26,6 +26,7 @@ import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.da import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/routing/app_navigation_observer.dart'; import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart'; @@ -190,7 +191,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi // may be in a background tab. In either case, `handleStatusBarTap()` still fires // Make sure the timeline is the primary route before scrolling to the top final routeData = context.findAncestorWidgetOfExactType()?.routeData; - if (ModalRoute.of(context)?.isCurrent == true && routeData?.isActive == true) { + // The tap is generated async, so it can arrive after a route pop has started (due to a back button or similar) + // Check if route is alive and not exiting before taking action + final observers = Navigator.maybeOf(context)?.widget.observers ?? const []; + final isRouteTransitioning = observers.whereType().any( + (observer) => observer.hasTransitioningRoute, + ); + + if (ModalRoute.of(context)?.isCurrent == true && routeData?.isActive == true && !isRouteTransitioning) { _scrollToTop(); } } diff --git a/mobile/lib/routing/app_navigation_observer.dart b/mobile/lib/routing/app_navigation_observer.dart index a7f6d532e9..90381cf49c 100644 --- a/mobile/lib/routing/app_navigation_observer.dart +++ b/mobile/lib/routing/app_navigation_observer.dart @@ -30,3 +30,22 @@ class AppNavigationObserver extends AutoRouterObserver { ref.invalidate(isAssetViewerOpenProvider); } } + +/// Tracks routes that are undergoing a pop transition +class TransitioningRouteObserver extends NavigatorObserver { + int _transitioningRoutes = 0; + + /// Whether a "popping" route is still on screen + bool get hasTransitioningRoute => _transitioningRoutes > 0; + + @override + void didPop(Route route, Route? previousRoute) { + if (route is! TransitionRoute) { + return; + } + + _transitioningRoutes += 1; + // Transition completed and route disposed + unawaited(route.completed.whenComplete(() => _transitioningRoutes -= 1)); + } +} diff --git a/mobile/test/presentation/widgets/timeline/timeline_scroll_to_top_test.dart b/mobile/test/presentation/widgets/timeline/timeline_scroll_to_top_test.dart index 28644d6702..f72af61a0a 100644 --- a/mobile/test/presentation/widgets/timeline/timeline_scroll_to_top_test.dart +++ b/mobile/test/presentation/widgets/timeline/timeline_scroll_to_top_test.dart @@ -13,6 +13,7 @@ import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/routing/app_navigation_observer.dart'; import '../../../fixtures/asset.stub.dart'; @@ -62,7 +63,9 @@ void main() { timelineServiceProvider.overrideWithValue(service), appConfigProvider.overrideWithValue(const AppConfig()), ], - child: MaterialApp.router(routerConfig: router.config()), + child: MaterialApp.router( + routerConfig: router.config(navigatorObservers: () => [TransitioningRouteObserver()]), + ), ), ); // Segment stream resolves @@ -96,9 +99,14 @@ void main() { await tester.pumpAndSettle(); expect(position.pixels, initialScrolledPosition, reason: 'ignored while behind a pushed route'); - // Once the timeline is visible, taps on the status bar should scroll to the top - await router.maybePop(); + // The tap is async, so it can arrive while a pushed route is transitioning to popped + unawaited(router.maybePop()); + await tester.pump(); + await tapStatusBar(); await tester.pumpAndSettle(); + expect(position.pixels, initialScrolledPosition, reason: 'ignored while the pushed route pops'); + + // Once the timeline is visible, taps on the status bar should scroll to the top await tapStatusBar(); await tester.pumpAndSettle(); expect(position.pixels, 0, reason: 'scrolls the foreground timeline');