From 4be8e1a9fc04de44923632520140d4e2c38895d7 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:15:31 +0530 Subject: [PATCH 1/7] fix: asset viewer bottom bar action colors (#30582) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../widgets/asset_viewer/bottom_bar.widget.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 3333ac9933..0188179919 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -51,7 +51,7 @@ class ViewerBottomBar extends ConsumerWidget { UploadAction(source: .viewer, showProgress: true), EditAssetAction(source: .viewer), ]), - if (asset.hasRemote) AddActionButton(originalTheme: originalTheme), + if (asset.hasRemote) ImmichColorOverride(color: null, child: AddActionButton(originalTheme: originalTheme)), ], ..._actionColumnButtons(context, ref, const [DeleteAction(source: .viewer)]), ], @@ -94,7 +94,10 @@ class ViewerBottomBar extends ConsumerWidget { if (asset.isImage) OcrToggleButton(asset: asset), if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag), if (!isReadonlyModeEnabled) - Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions), + ImmichColorOverride( + color: Colors.white, + child: Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions), + ), ], ), ), From 0687c0d3f76a075cf86ae5e28f1ec3ee3b355942 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:15:31 +0530 Subject: [PATCH 2/7] fix: wire secondary action for bottom bar actions (#30583) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../presentation/widgets/asset_viewer/bottom_bar.widget.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 0188179919..4ff839881a 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -22,7 +22,9 @@ import 'package:immich_ui/immich_ui.dart'; List _actionColumnButtons(BuildContext context, WidgetRef ref, List actions) => actions .map((a) => a.create(context, ref)) .nonNulls - .map((item) => ImmichColumnButton(icon: item.icon, label: item.label, onPressed: item.onAction)) + .map( + (item) => .new(icon: item.icon, label: item.label, onPressed: item.onAction, onLongPress: item.onSecondaryAction), + ) .toList(growable: false); class ViewerBottomBar extends ConsumerWidget { From f361dbbb775b23198b8b158c6588d8c2fa2ddc87 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 6 Aug 2026 12:42:12 +0200 Subject: [PATCH 3/7] chore: clean up last mergify remnant (#30600) --- .github/workflows/pr-label-validation.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml index abbcfaebd0..5820a8b284 100644 --- a/.github/workflows/pr-label-validation.yml +++ b/.github/workflows/pr-label-validation.yml @@ -8,7 +8,6 @@ permissions: {} jobs: validate-release-label: - if: ${{ !startsWith(github.event.pull_request.head.ref, 'mergify/merge-queue/') }} runs-on: ubuntu-latest permissions: issues: write From 63b90ec43da97f6281963cd5a2319a989f30a056 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:26:12 +0530 Subject: [PATCH 4/7] fix: map not updating after viewing an asset (#30601) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/providers/routes.provider.dart | 3 --- mobile/lib/routing/app_navigation_observer.dart | 8 ++++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mobile/lib/providers/routes.provider.dart b/mobile/lib/providers/routes.provider.dart index 6cc3d08c5d..776389b330 100644 --- a/mobile/lib/providers/routes.provider.dart +++ b/mobile/lib/providers/routes.provider.dart @@ -5,9 +5,6 @@ import 'package:immich_mobile/routing/router.dart'; @visibleForTesting bool isRouteInStack(Ref ref, String routeName) { final router = ref.watch(appRouterProvider); - void onChange() => ref.invalidateSelf(); - router.addListener(onChange); - ref.onDispose(() => router.removeListener(onChange)); return router.stackData.any((route) => route.name == routeName); } diff --git a/mobile/lib/routing/app_navigation_observer.dart b/mobile/lib/routing/app_navigation_observer.dart index f126788008..a7f6d532e9 100644 --- a/mobile/lib/routing/app_navigation_observer.dart +++ b/mobile/lib/routing/app_navigation_observer.dart @@ -13,6 +13,8 @@ class AppNavigationObserver extends AutoRouterObserver { @override void didPush(Route route, Route? previousRoute) { + ref.invalidate(inLockedViewProvider); + ref.invalidate(isAssetViewerOpenProvider); unawaited( Future(() { ref.read(currentRouteNameProvider.notifier).state = route.settings.name; @@ -21,4 +23,10 @@ class AppNavigationObserver extends AutoRouterObserver { }), ); } + + @override + void didPop(Route route, Route? previousRoute) { + ref.invalidate(inLockedViewProvider); + ref.invalidate(isAssetViewerOpenProvider); + } } 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 5/7] 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 6/7] 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 7/7] 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,