From 0e987352bb1eaea6a3faaa8d99d69089b70636f3 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Wed, 17 Sep 2025 23:50:43 +0530 Subject: [PATCH 01/42] fix: do not migrate existing users (#22146) fix: do not migrate if already on 15+ Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/utils/migration.dart | 56 ++++---- mobile/test/modules/utils/migration_test.dart | 131 ++++++++++++++++++ 2 files changed, 163 insertions(+), 24 deletions(-) create mode 100644 mobile/test/modules/utils/migration_test.dart diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index f4f0564425..1be8647e3d 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -62,30 +62,7 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { await Store.populateCache(); } - // Handle migration only for this version - // TODO: remove when old timeline is removed - final needBetaMigration = Store.tryGet(StoreKey.needBetaMigration); - if (version >= 15 && needBetaMigration == null) { - // Check both databases directly instead of relying on cache - - final isBeta = Store.tryGet(StoreKey.betaTimeline); - final isNewInstallation = await _isNewInstallation(db, drift); - - // For new installations, no migration needed - // For existing installations, only migrate if beta timeline is not enabled (null or false) - if (isNewInstallation || isBeta == true || (version > 15 && isBeta == null)) { - await Store.put(StoreKey.needBetaMigration, false); - await Store.put(StoreKey.betaTimeline, true); - } else { - await drift.reset(); - await Store.put(StoreKey.needBetaMigration, true); - } - } - - if (version < 16) { - await SyncStreamRepository(drift).reset(); - await Store.put(StoreKey.shouldResetSync, true); - } + await handleBetaMigration(version, await _isNewInstallation(db, drift), SyncStreamRepository(drift)); if (targetVersion >= 12) { await Store.put(StoreKey.version, targetVersion); @@ -99,6 +76,37 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { } } +Future handleBetaMigration(int version, bool isNewInstallation, SyncStreamRepository syncStreamRepository) async { + // Handle migration only for this version + // TODO: remove when old timeline is removed + final isBeta = Store.tryGet(StoreKey.betaTimeline); + final needBetaMigration = Store.tryGet(StoreKey.needBetaMigration); + if (version <= 15 && needBetaMigration == null) { + // For new installations, no migration needed + // For existing installations, only migrate if beta timeline is not enabled (null or false) + if (isNewInstallation || isBeta == true) { + await Store.put(StoreKey.needBetaMigration, false); + await Store.put(StoreKey.betaTimeline, true); + } else { + await Store.put(StoreKey.needBetaMigration, true); + } + } + + if (version > 15) { + if (isBeta == null || isBeta) { + await Store.put(StoreKey.needBetaMigration, false); + await Store.put(StoreKey.betaTimeline, true); + } else { + await Store.put(StoreKey.needBetaMigration, false); + } + } + + if (version < 16) { + await syncStreamRepository.reset(); + await Store.put(StoreKey.shouldResetSync, true); + } +} + Future _isNewInstallation(Isar db, Drift drift) async { try { final isarUserCount = await db.users.count(); diff --git a/mobile/test/modules/utils/migration_test.dart b/mobile/test/modules/utils/migration_test.dart new file mode 100644 index 0000000000..08ab1204a6 --- /dev/null +++ b/mobile/test/modules/utils/migration_test.dart @@ -0,0 +1,131 @@ +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; +import 'package:immich_mobile/utils/migration.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../infrastructure/repository.mock.dart'; + +void main() { + late Drift db; + late SyncStreamRepository mockSyncStreamRepository; + + setUpAll(() async { + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); + mockSyncStreamRepository = MockSyncStreamRepository(); + when(() => mockSyncStreamRepository.reset()).thenAnswer((_) async => {}); + }); + + tearDown(() async { + await Store.clear(); + }); + + group('handleBetaMigration Tests', () { + group("version < 15", () { + test('already on new timeline', () async { + await Store.put(StoreKey.betaTimeline, true); + + await handleBetaMigration(14, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.betaTimeline), true); + expect(Store.tryGet(StoreKey.needBetaMigration), false); + }); + + test('already on old timeline', () async { + await Store.put(StoreKey.betaTimeline, false); + + await handleBetaMigration(14, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.needBetaMigration), true); + }); + + test('fresh install', () async { + await Store.delete(StoreKey.betaTimeline); + await handleBetaMigration(14, true, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.betaTimeline), true); + expect(Store.tryGet(StoreKey.needBetaMigration), false); + }); + }); + + group("version == 15", () { + test('already on new timeline', () async { + await Store.put(StoreKey.betaTimeline, true); + + await handleBetaMigration(15, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.betaTimeline), true); + expect(Store.tryGet(StoreKey.needBetaMigration), false); + }); + + test('already on old timeline', () async { + await Store.put(StoreKey.betaTimeline, false); + + await handleBetaMigration(15, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.needBetaMigration), true); + }); + + test('fresh install', () async { + await Store.delete(StoreKey.betaTimeline); + await handleBetaMigration(15, true, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.betaTimeline), true); + expect(Store.tryGet(StoreKey.needBetaMigration), false); + }); + }); + + group("version > 15", () { + test('already on new timeline', () async { + await Store.put(StoreKey.betaTimeline, true); + + await handleBetaMigration(16, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.betaTimeline), true); + expect(Store.tryGet(StoreKey.needBetaMigration), false); + }); + + test('already on old timeline', () async { + await Store.put(StoreKey.betaTimeline, false); + + await handleBetaMigration(16, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.betaTimeline), false); + expect(Store.tryGet(StoreKey.needBetaMigration), false); + }); + + test('fresh install', () async { + await Store.delete(StoreKey.betaTimeline); + await handleBetaMigration(16, true, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.betaTimeline), true); + expect(Store.tryGet(StoreKey.needBetaMigration), false); + }); + }); + }); + + group('sync reset tests', () { + test('version < 16', () async { + await Store.put(StoreKey.shouldResetSync, false); + + await handleBetaMigration(15, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.shouldResetSync), true); + }); + + test('version >= 16', () async { + await Store.put(StoreKey.shouldResetSync, false); + + await handleBetaMigration(16, false, mockSyncStreamRepository); + + expect(Store.tryGet(StoreKey.shouldResetSync), false); + }); + }); +} From edc0698e2ad2e0d75fe0c5b5cb369cc34fa04068 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 17 Sep 2025 16:34:12 -0400 Subject: [PATCH 02/42] refactor: album edit modal (#22151) --- pnpm-lock.yaml | 27 ++++++++----- web/package.json | 2 +- web/src/lib/modals/AlbumEditModal.svelte | 49 +++++++++--------------- 3 files changed, 36 insertions(+), 42 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46b9ac746a..c0f7b987ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -684,8 +684,8 @@ importers: specifier: file:../open-api/typescript-sdk version: link:../open-api/typescript-sdk '@immich/ui': - specifier: ^0.28.1 - version: 0.28.1(@internationalized/date@3.8.2)(svelte@5.35.5) + specifier: ^0.29.0 + version: 0.29.0(@internationalized/date@3.8.2)(svelte@5.35.5) '@mapbox/mapbox-gl-rtl-text': specifier: 0.2.3 version: 0.2.3(mapbox-gl@1.13.3) @@ -2717,8 +2717,8 @@ packages: cpu: [x64] os: [win32] - '@immich/ui@0.28.1': - resolution: {integrity: sha512-FAQBPsbPaLtTYsEH+/wQxPbUI19ZPuOEScSHgSqteI601qVZNQcfU7YuinKqK94iva2RNhvUgrNGHweIBentZg==} + '@immich/ui@0.29.0': + resolution: {integrity: sha512-An9cf1L4nMO6+C1Tkktd+qjGmZvyGz/Un33cGsKQa2I7IdZHd67KbbC2v3wN3bQMiTjxtFJ8YR9EONohJ8jDtQ==} peerDependencies: svelte: ^5.0.0 @@ -4570,6 +4570,9 @@ packages: '@types/node@22.18.4': resolution: {integrity: sha512-UJdblFqXymSBhmZf96BnbisoFIr8ooiiBRMolQgg77Ea+VM37jXw76C2LQr9n8wm9+i/OvlUlW6xSvqwzwqznw==} + '@types/node@22.18.5': + resolution: {integrity: sha512-g9BpPfJvxYBXUWI9bV37j6d6LTMNQ88hPwdWWUeYZnMhlo66FIg9gCc1/DZb15QylJSKwOZjwrckvOTWpOiChg==} + '@types/node@24.3.0': resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} @@ -5225,8 +5228,8 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - bits-ui@2.9.8: - resolution: {integrity: sha512-oVAqdhLSuGIgEiT0yu3ShSI7AxncCxX26Gv6Lul94BuKHV2uzHoKfIodtnMQSq+udJ54svuCIRqA58whsv7vaA==} + bits-ui@2.9.9: + resolution: {integrity: sha512-U8qsCQ/5rwXAzUBn8lCLFUeHBoXsA54Lb4uFuurXu6VEszVXLCedZRnhHOQsTc1I+2Js5l4iLiBtPUG7WjNbOA==} engines: {node: '>=20'} peerDependencies: '@internationalized/date': ^3.8.1 @@ -14395,10 +14398,10 @@ snapshots: '@img/sharp-win32-x64@0.34.3': optional: true - '@immich/ui@0.28.1(@internationalized/date@3.8.2)(svelte@5.35.5)': + '@immich/ui@0.29.0(@internationalized/date@3.8.2)(svelte@5.35.5)': dependencies: '@mdi/js': 7.4.47 - bits-ui: 2.9.8(@internationalized/date@3.8.2)(svelte@5.35.5) + bits-ui: 2.9.9(@internationalized/date@3.8.2)(svelte@5.35.5) simple-icons: 15.15.0 svelte: 5.35.5 tailwind-merge: 3.3.1 @@ -16524,6 +16527,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@22.18.5': + dependencies: + undici-types: 6.21.0 + '@types/node@24.3.0': dependencies: undici-types: 7.10.0 @@ -16670,7 +16677,7 @@ snapshots: '@types/through@0.0.33': dependencies: - '@types/node': 22.18.4 + '@types/node': 22.18.5 '@types/ua-parser-js@0.7.39': {} @@ -17339,7 +17346,7 @@ snapshots: binary-extensions@2.3.0: {} - bits-ui@2.9.8(@internationalized/date@3.8.2)(svelte@5.35.5): + bits-ui@2.9.9(@internationalized/date@3.8.2)(svelte@5.35.5): dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/dom': 1.7.4 diff --git a/web/package.json b/web/package.json index ee88d1dedd..6ce2640966 100644 --- a/web/package.json +++ b/web/package.json @@ -28,7 +28,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.28.1", + "@immich/ui": "^0.29.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", diff --git a/web/src/lib/modals/AlbumEditModal.svelte b/web/src/lib/modals/AlbumEditModal.svelte index cf08208130..a7e62d1031 100644 --- a/web/src/lib/modals/AlbumEditModal.svelte +++ b/web/src/lib/modals/AlbumEditModal.svelte @@ -2,32 +2,28 @@ import AlbumCover from '$lib/components/album-page/album-cover.svelte'; import { handleError } from '$lib/utils/handle-error'; import { updateAlbumInfo, type AlbumResponseDto } from '@immich/sdk'; - import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui'; + import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, Textarea } from '@immich/ui'; import { mdiRenameOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; - interface Props { + type Props = { album: AlbumResponseDto; onClose: (album?: AlbumResponseDto) => void; - } + }; let { album = $bindable(), onClose }: Props = $props(); let albumName = $state(album.albumName); let description = $state(album.description); - let isSubmitting = $state(false); - const handleUpdateAlbumInfo = async () => { + const handleSubmit = async (event: Event) => { + event.preventDefault(); + isSubmitting = true; + try { - await updateAlbumInfo({ - id: album.id, - updateAlbumDto: { - albumName, - description, - }, - }); + await updateAlbumInfo({ id: album.id, updateAlbumDto: { albumName, description } }); album.albumName = albumName; album.description = description; onClose(album); @@ -37,31 +33,22 @@ isSubmitting = false; } }; - - const onsubmit = async (event: Event) => { - event.preventDefault(); - await handleUpdateAlbumInfo(); - }; -
-
- + +
+