diff --git a/cli/package.json b/cli/package.json
index 23a2ec062d..acd20a8468 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
- "version": "2.5.2",
+ "version": "2.5.3",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json
index d2a25bf4b6..0230654530 100644
--- a/docs/static/archived-versions.json
+++ b/docs/static/archived-versions.json
@@ -1,7 +1,7 @@
[
{
- "label": "v2.5.2",
- "url": "https://docs.v2.5.2.archive.immich.app"
+ "label": "v2.5.3",
+ "url": "https://docs.v2.5.3.archive.immich.app"
},
{
"label": "v2.4.1",
diff --git a/e2e/docker-compose.dev.yml b/e2e/docker-compose.dev.yml
index cd1d3d4982..14e159ed50 100644
--- a/e2e/docker-compose.dev.yml
+++ b/e2e/docker-compose.dev.yml
@@ -70,7 +70,7 @@ services:
restart: unless-stopped
redis:
- image: redis:6.2-alpine@sha256:37e002448575b32a599109664107e374c8709546905c372a34d64919043b9ceb
+ image: redis:6.2-alpine@sha256:46884be93652d02a96a176ccf173d1040bef365c5706aa7b6a1931caec8bfeef
database:
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:6f3e9d2c2177af16c2988ff71425d79d89ca630ec2f9c8db03209ab716542338
diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml
index a33cb6573c..a98a7013a4 100644
--- a/e2e/docker-compose.yml
+++ b/e2e/docker-compose.yml
@@ -42,7 +42,7 @@ services:
- 2285:2285
redis:
- image: redis:6.2-alpine@sha256:37e002448575b32a599109664107e374c8709546905c372a34d64919043b9ceb
+ image: redis:6.2-alpine@sha256:46884be93652d02a96a176ccf173d1040bef365c5706aa7b6a1931caec8bfeef
database:
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:6f3e9d2c2177af16c2988ff71425d79d89ca630ec2f9c8db03209ab716542338
diff --git a/e2e/package.json b/e2e/package.json
index b3973eb8bf..44fbe9320f 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
- "version": "2.5.2",
+ "version": "2.5.3",
"description": "",
"main": "index.js",
"type": "module",
diff --git a/i18n/package.json b/i18n/package.json
index efb0458819..20bcc64d9a 100644
--- a/i18n/package.json
+++ b/i18n/package.json
@@ -1,6 +1,6 @@
{
"name": "immich-i18n",
- "version": "2.5.2",
+ "version": "2.5.3",
"private": true,
"scripts": {
"format": "prettier --check .",
diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml
index ae6d7c0e2c..b613e85682 100644
--- a/machine-learning/pyproject.toml
+++ b/machine-learning/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "immich-ml"
-version = "2.5.2"
+version = "2.5.3"
description = ""
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
requires-python = ">=3.11,<4.0"
diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock
index 4ab5dbff36..4b4a0fbb94 100644
--- a/machine-learning/uv.lock
+++ b/machine-learning/uv.lock
@@ -882,7 +882,7 @@ wheels = [
[[package]]
name = "immich-ml"
-version = "2.5.2"
+version = "2.5.3"
source = { editable = "." }
dependencies = [
{ name = "aiocache" },
diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile
index 006a75c139..347ec1b680 100644
--- a/mobile/android/fastlane/Fastfile
+++ b/mobile/android/fastlane/Fastfile
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
- "android.injected.version.code" => 3033,
- "android.injected.version.name" => "2.5.2",
+ "android.injected.version.code" => 3034,
+ "android.injected.version.name" => "2.5.3",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
diff --git a/mobile/ios/Runner/Images/LocalImagesImpl.swift b/mobile/ios/Runner/Images/LocalImagesImpl.swift
index 4f2090443a..3af524f424 100644
--- a/mobile/ios/Runner/Images/LocalImagesImpl.swift
+++ b/mobile/ios/Runner/Images/LocalImagesImpl.swift
@@ -133,7 +133,6 @@ class LocalImageApiImpl: LocalImageApi {
"height": Int64(buffer.height),
"rowBytes": Int64(buffer.rowBytes)
]))
- print("Successful response for \(requestId)")
Self.remove(requestId: requestId)
} catch {
Self.remove(requestId: requestId)
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index 48320afa3f..86c487bb64 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -80,7 +80,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 2.5.2
+ 2.5.3
CFBundleSignature
????
CFBundleURLTypes
diff --git a/mobile/lib/presentation/pages/drift_album.page.dart b/mobile/lib/presentation/pages/drift_album.page.dart
index fe2ab61a58..cde8c127db 100644
--- a/mobile/lib/presentation/pages/drift_album.page.dart
+++ b/mobile/lib/presentation/pages/drift_album.page.dart
@@ -33,7 +33,7 @@ class _DriftAlbumsPageState extends ConsumerState {
@override
Widget build(BuildContext context) {
final albumCount = ref.watch(remoteAlbumProvider.select((state) => state.albums.length));
- final showScrollbar = albumCount > 10;
+ final showScrollbar = albumCount > 20;
final scrollView = CustomScrollView(
controller: _scrollController,
diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart
index 4db297d658..e35fbf7433 100644
--- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart
+++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart
@@ -87,7 +87,7 @@ class _AlbumSelectorState extends ConsumerState {
}
void onSearch(String searchTerm, QuickFilterMode filterMode) {
- final userId = ref.watch(currentUserProvider)?.id;
+ final userId = ref.read(currentUserProvider)?.id;
filter = filter.copyWith(query: searchTerm, userId: userId, mode: filterMode);
filterAlbums();
@@ -186,7 +186,7 @@ class _AlbumSelectorState extends ConsumerState {
@override
Widget build(BuildContext context) {
- final userId = ref.watch(currentUserProvider)?.id;
+ final userId = ref.watch(currentUserProvider.select((user) => user?.id));
// refilter and sort when albums change
ref.listen(remoteAlbumProvider.select((state) => state.albums), (_, _) async {
diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart
index 2f067fdf67..624c21f158 100644
--- a/mobile/lib/providers/backup/drift_backup.provider.dart
+++ b/mobile/lib/providers/backup/drift_backup.provider.dart
@@ -259,6 +259,11 @@ class DriftBackupNotifier extends StateNotifier {
}
Future startForegroundBackup(String userId) async {
+ // Cancel any existing backup before starting a new one
+ if (state.cancelToken != null) {
+ await stopForegroundBackup();
+ }
+
state = state.copyWith(error: BackupError.none);
final cancelToken = CancellationToken();
@@ -375,21 +380,21 @@ class DriftBackupNotifier extends StateNotifier {
_logger.warning("Skip handleBackupResume (pre-call): notifier disposed");
return;
}
- _logger.info("Resuming backup tasks...");
+ _logger.info("Start background backup sequence");
state = state.copyWith(error: BackupError.none);
final tasks = await _backgroundUploadService.getActiveTasks(kBackupGroup);
if (!mounted) {
_logger.warning("Skip handleBackupResume (post-call): notifier disposed");
return;
}
- _logger.info("Found ${tasks.length} tasks");
+ _logger.info("Found ${tasks.length} pending tasks");
if (tasks.isEmpty) {
- _logger.info("Start backup with URLSession");
+ _logger.info("No pending tasks, starting new upload");
return _backgroundUploadService.uploadBackupCandidates(userId);
}
- _logger.info("Tasks to resume: ${tasks.length}");
+ _logger.info("Resuming upload ${tasks.length} assets");
return _backgroundUploadService.resume();
}
}
diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart
index 4eece142d2..d54a677c24 100644
--- a/mobile/lib/services/background_upload.service.dart
+++ b/mobile/lib/services/background_upload.service.dart
@@ -164,9 +164,12 @@ class BackgroundUploadService {
final candidates = await _backupRepository.getCandidates(userId);
if (candidates.isEmpty) {
+ _logger.info("No new backup candidates found, finishing background upload");
return;
}
+ _logger.info("Found ${candidates.length} backup candidates for background tasks");
+
const batchSize = 100;
final batch = candidates.take(batchSize).toList();
List tasks = [];
@@ -179,6 +182,7 @@ class BackgroundUploadService {
}
if (tasks.isNotEmpty && !shouldAbortQueuingTasks) {
+ _logger.info("Enqueuing ${tasks.length} background upload tasks");
await enqueueTasks(tasks);
}
}
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 5ca810fe48..88d698e55b 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
-- API version: 2.5.2
+- API version: 2.5.3
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart
index 713bcafee3..e2db95b9e0 100644
--- a/mobile/openapi/lib/api/albums_api.dart
+++ b/mobile/openapi/lib/api/albums_api.dart
@@ -473,7 +473,7 @@ class AlbumsApi {
/// Filter albums containing this asset ID (ignores shared parameter)
///
/// * [bool] shared:
- /// Filter by shared status: true = only shared, false = only own, undefined = all
+ /// Filter by shared status: true = only shared, false = not shared, undefined = all owned albums
Future getAllAlbumsWithHttpInfo({ String? assetId, bool? shared, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/albums';
@@ -516,7 +516,7 @@ class AlbumsApi {
/// Filter albums containing this asset ID (ignores shared parameter)
///
/// * [bool] shared:
- /// Filter by shared status: true = only shared, false = only own, undefined = all
+ /// Filter by shared status: true = only shared, false = not shared, undefined = all owned albums
Future?> getAllAlbums({ String? assetId, bool? shared, }) async {
final response = await getAllAlbumsWithHttpInfo( assetId: assetId, shared: shared, );
if (response.statusCode >= HttpStatus.badRequest) {
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index d237c02023..c8aa680e07 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -1249,10 +1249,10 @@ packages:
dependency: transitive
description:
name: meta
- sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
+ sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
- version: "1.16.0"
+ version: "1.17.0"
mime:
dependency: transitive
description:
@@ -1942,10 +1942,10 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
+ sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
- version: "0.7.6"
+ version: "0.7.7"
thumbhash:
dependency: "direct main"
description:
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index 198d3ad8f7..bea2261e81 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
-version: 2.5.2+3033
+version: 2.5.3+3034
environment:
sdk: '>=3.8.0 <4.0.0'
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index 7f85bbc1cf..498de43471 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -1618,7 +1618,7 @@
"name": "shared",
"required": false,
"in": "query",
- "description": "Filter by shared status: true = only shared, false = only own, undefined = all",
+ "description": "Filter by shared status: true = only shared, false = not shared, undefined = all owned albums",
"schema": {
"type": "boolean"
}
@@ -15057,7 +15057,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
- "version": "2.5.2",
+ "version": "2.5.3",
"contact": {}
},
"tags": [
diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json
index 2d30f4fbd8..2a919aa640 100644
--- a/open-api/typescript-sdk/package.json
+++ b/open-api/typescript-sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
- "version": "2.5.2",
+ "version": "2.5.3",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index d8c960a393..a08065c0e5 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -1,6 +1,6 @@
/**
* Immich
- * 2.5.2
+ * 2.5.3
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/
diff --git a/package.json b/package.json
index dadfba8bf0..a1dd14b940 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "immich-monorepo",
- "version": "2.5.2",
+ "version": "2.5.3",
"description": "Monorepo for Immich",
"private": true,
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48",
diff --git a/server/package.json b/server/package.json
index c9e2c2ac22..05d811b8a7 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
{
"name": "immich",
- "version": "2.5.2",
+ "version": "2.5.3",
"description": "",
"author": "",
"private": true,
diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts
index 0f46ebaa42..62013fbd92 100644
--- a/server/src/dtos/album.dto.ts
+++ b/server/src/dtos/album.dto.ts
@@ -102,7 +102,7 @@ export class UpdateAlbumDto {
export class GetAlbumsDto {
@ValidateBoolean({
optional: true,
- description: 'Filter by shared status: true = only shared, false = only own, undefined = all',
+ description: 'Filter by shared status: true = only shared, false = not shared, undefined = all owned albums',
})
shared?: boolean;
diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts
index f74f9f4cec..f5af444a22 100644
--- a/server/src/services/metadata.service.ts
+++ b/server/src/services/metadata.service.ts
@@ -307,7 +307,6 @@ export class MetadataService extends BaseService {
const assetHeight = isSidewards ? validate(width) : validate(height);
const promises: Promise[] = [
- this.assetRepository.upsertExif(exifData, { lockedPropertiesBehavior: 'skip' }),
this.assetRepository.update({
id: asset.id,
duration: this.getDuration(exifTags),
@@ -322,6 +321,7 @@ export class MetadataService extends BaseService {
}),
];
+ await this.assetRepository.upsertExif(exifData, { lockedPropertiesBehavior: 'skip' });
await this.applyTagList(asset);
if (this.isMotionPhoto(asset, exifTags)) {
diff --git a/web/package.json b/web/package.json
index 6433a33dcc..dd6924b675 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,6 +1,6 @@
{
"name": "immich-web",
- "version": "2.5.2",
+ "version": "2.5.3",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {
diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte
index 6284e207c6..06f4e5a0b2 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte
@@ -14,6 +14,7 @@
import { eventManager } from '$lib/managers/event-manager.svelte';
import { imageManager } from '$lib/managers/ImageManager.svelte';
import { Route } from '$lib/route';
+ import { getAssetActions } from '$lib/services/asset.service';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { ocrManager } from '$lib/stores/ocr.svelte';
import { alwaysLoadOriginalVideo } from '$lib/stores/preferences.store';
@@ -36,6 +37,7 @@
type PersonResponseDto,
type StackResponseDto,
} from '@immich/sdk';
+ import { CommandPaletteDefaultProvider } from '@immich/ui';
import { onDestroy, onMount, untrack } from 'svelte';
import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition';
@@ -426,8 +428,11 @@
!assetViewerManager.isShowEditor &&
ocrManager.hasOcrData,
);
+
+ const { Tag } = $derived(getAssetActions($t, asset));
+
diff --git a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte
index cd9b1a40d2..09c4432723 100644
--- a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte
+++ b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte
@@ -1,12 +1,13 @@
-
+
{#if isOwner && !authManager.isSharedLink}
@@ -42,36 +44,24 @@
{#each tags as tag (tag.id)}
-
+ size="tiny"
+ class="hover:bg-primary-400"
+ shape="round"
+ />
+
{/each}
-
+
{/if}
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte
index 0a44505f40..2101107f6e 100644
--- a/web/src/lib/components/asset-viewer/photo-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte
@@ -55,13 +55,10 @@
let loader = $state();
- assetViewerManager.zoomState = {
- currentRotation: 0,
- currentZoom: 1,
- enable: true,
- currentPositionX: 0,
- currentPositionY: 0,
- };
+ $effect.pre(() => {
+ void asset.id;
+ untrack(() => assetViewerManager.resetZoomState());
+ });
onDestroy(() => {
$boundingBoxesArray = [];
diff --git a/web/src/lib/components/timeline/actions/TagAction.svelte b/web/src/lib/components/timeline/actions/TagAction.svelte
index d2235d7c74..dfe24b476b 100644
--- a/web/src/lib/components/timeline/actions/TagAction.svelte
+++ b/web/src/lib/components/timeline/actions/TagAction.svelte
@@ -20,11 +20,8 @@
const handleTagAssets = async () => {
const assets = [...getOwnedAssets()];
- const success = await modalManager.show(AssetTagModal, { assetIds: assets.map(({ id }) => id) });
-
- if (success) {
- clearSelect();
- }
+ await modalManager.show(AssetTagModal, { assetIds: assets.map(({ id }) => id) });
+ clearSelect();
};
diff --git a/web/src/lib/managers/asset-viewer-manager.svelte.ts b/web/src/lib/managers/asset-viewer-manager.svelte.ts
index 6996d939d5..36047d4690 100644
--- a/web/src/lib/managers/asset-viewer-manager.svelte.ts
+++ b/web/src/lib/managers/asset-viewer-manager.svelte.ts
@@ -5,6 +5,14 @@ import type { ZoomImageWheelState } from '@zoom-image/core';
const isShowDetailPanel = new PersistedLocalStorage('asset-viewer-state', false);
+const createDefaultZoomState = (): ZoomImageWheelState => ({
+ currentRotation: 0,
+ currentZoom: 1,
+ enable: true,
+ currentPositionX: 0,
+ currentPositionY: 0,
+});
+
export type Events = {
Zoom: [];
ZoomChange: [ZoomImageWheelState];
@@ -12,13 +20,7 @@ export type Events = {
};
export class AssetViewerManager extends BaseEventManager {
- #zoomState = $state({
- currentRotation: 0,
- currentZoom: 1,
- enable: true,
- currentPositionX: 0,
- currentPositionY: 0,
- });
+ #zoomState = $state(createDefaultZoomState());
imgRef = $state();
isShowActivityPanel = $state(false);
@@ -67,6 +69,10 @@ export class AssetViewerManager extends BaseEventManager {
this.#zoomState = state;
}
+ resetZoomState() {
+ this.zoomState = createDefaultZoomState();
+ }
+
toggleActivityPanel() {
this.closeDetailPanel();
this.isShowActivityPanel = !this.isShowActivityPanel;
diff --git a/web/src/lib/managers/event-manager.svelte.ts b/web/src/lib/managers/event-manager.svelte.ts
index 4825dbc93b..4093413d1a 100644
--- a/web/src/lib/managers/event-manager.svelte.ts
+++ b/web/src/lib/managers/event-manager.svelte.ts
@@ -37,6 +37,7 @@ export type Events = {
AssetsArchive: [string[]];
AssetsDelete: [string[]];
AssetEditsApplied: [string];
+ AssetsTag: [string[]];
AlbumAddAssets: [];
AlbumUpdate: [AlbumResponseDto];
diff --git a/web/src/lib/modals/AssetTagModal.svelte b/web/src/lib/modals/AssetTagModal.svelte
index e541e24b60..00862ce1b1 100644
--- a/web/src/lib/modals/AssetTagModal.svelte
+++ b/web/src/lib/modals/AssetTagModal.svelte
@@ -1,4 +1,5 @@