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)} -
- + -

- {tag.value} -

-
- - -
+ 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 @@