diff --git a/docs/docs/developer/pr-checklist.md b/docs/docs/developer/pr-checklist.md index f855e854c4..e68567bc8f 100644 --- a/docs/docs/developer/pr-checklist.md +++ b/docs/docs/developer/pr-checklist.md @@ -14,15 +14,15 @@ When contributing code through a pull request, please check the following: - [ ] `pnpm run check:typescript` (check typescript) - [ ] `pnpm test` (unit tests) +:::tip AIO +Run all web checks with `pnpm run check:all` +::: + ## Documentation - [ ] `pnpm run format` (formatting via Prettier) - [ ] Update the `_redirects` file if you have renamed a page or removed it from the documentation. -:::tip AIO -Run all web checks with `pnpm run check:all` -::: - ## Server Checks - [ ] `pnpm run lint` (linting via ESLint) diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 55c226d507..8863a13ee7 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -93,7 +93,7 @@ Information on the current workers can be found [here](/administration/jobs-work All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`. `DB_URL` must be in the format `postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename`. -You can require SSL by adding `?sslmode=require` to the end of the `DB_URL` string, or require SSL and skip certificate verification by adding `?sslmode=require&sslmode=no-verify`. +You can require SSL by adding `?sslmode=require` to the end of the `DB_URL` string, or require SSL and skip certificate verification by adding `?sslmode=require&uselibpqcompat=true`. This allows both immich and `pg_dumpall` (the utility used for database backups) to [properly connect](https://github.com/brianc/node-postgres/tree/master/packages/pg-connection-string#tcp-connections) to your database. When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSWORD` and `DB_DATABASE_NAME` database variables are ignored. diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index 8d4636bbe1..cc408548d2 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -50,7 +50,7 @@ const double kUploadStatusCanceled = -2.0; const int kMinMonthsToEnableScrubberSnap = 12; -const String kImmichAppStoreLink = "https://apps.apple.com/app/immich/id6449244941"; +const String kImmichAppStoreLink = "https://apps.apple.com/app/immich/id1613945652"; const String kImmichPlayStoreLink = "https://play.google.com/store/apps/details?id=app.alextran.immich"; const String kImmichLatestRelease = "https://github.com/immich-app/immich/releases/latest"; diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index 33661105e4..3d8fddc9b7 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -75,6 +75,20 @@ class AssetService { isFlipped = false; } + if (width == null || height == null) { + if (asset.hasRemote) { + final id = asset is LocalAsset ? asset.remoteId! : (asset as RemoteAsset).id; + final remoteAsset = await _remoteAssetRepository.get(id); + width = remoteAsset?.width?.toDouble(); + height = remoteAsset?.height?.toDouble(); + } else { + final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; + final localAsset = await _localAssetRepository.get(id); + width = localAsset?.width?.toDouble(); + height = localAsset?.height?.toDouble(); + } + } + final orientedWidth = isFlipped ? height : width; final orientedHeight = isFlipped ? width : height; if (orientedWidth != null && orientedHeight != null && orientedHeight > 0) { diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index 5cbae9c5a1..04eaf04694 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -363,14 +363,14 @@ extension on Iterable { } } -extension on PlatformAsset { +extension PlatformToLocalAsset on PlatformAsset { LocalAsset toLocalAsset() => LocalAsset( id: id, name: name, checksum: null, type: AssetType.values.elementAtOrNull(type) ?? AssetType.other, createdAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(), - updatedAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(), + updatedAt: tryFromSecondsSinceEpoch(updatedAt, isUtc: true) ?? DateTime.timestamp(), width: width, height: height, durationInSeconds: durationInSeconds, diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index 63259bc62b..59546a4539 100644 --- a/mobile/lib/infrastructure/repositories/local_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -261,7 +261,6 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { durationInSeconds: Value(asset.durationInSeconds), id: asset.id, orientation: Value(asset.orientation), - checksum: const Value(null), isFavorite: Value(asset.isFavorite), ); batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>( diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 1fc0ee43e5..d21e1e905b 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -265,7 +265,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { row.deletedAt.isNull() & row.isFavorite.equals(true) & row.ownerId.equals(userId) & - row.visibility.equalsValue(AssetVisibility.timeline), + (row.visibility.equalsValue(AssetVisibility.timeline) | row.visibility.equalsValue(AssetVisibility.archive)), groupBy: groupBy, origin: TimelineOrigin.favorite, ); diff --git a/mobile/lib/presentation/pages/drift_memory.page.dart b/mobile/lib/presentation/pages/drift_memory.page.dart index 55e5d24ecb..9042f2f1f5 100644 --- a/mobile/lib/presentation/pages/drift_memory.page.dart +++ b/mobile/lib/presentation/pages/drift_memory.page.dart @@ -24,6 +24,16 @@ class DriftMemoryPage extends HookConsumerWidget { const DriftMemoryPage({required this.memories, required this.memoryIndex, super.key}); + static void setMemory(WidgetRef ref, DriftMemory memory) { + if (memory.assets.isNotEmpty) { + ref.read(currentAssetNotifier.notifier).setAsset(memory.assets.first); + + if (memory.assets.first.isVideo) { + ref.read(videoPlaybackValueProvider.notifier).reset(); + } + } + } + @override Widget build(BuildContext context, WidgetRef ref) { final currentMemory = useState(memories[memoryIndex]); @@ -202,6 +212,10 @@ class DriftMemoryPage extends HookConsumerWidget { if (pageNumber < memories.length) { currentMemoryIndex.value = pageNumber; currentMemory.value = memories[pageNumber]; + + WidgetsBinding.instance.addPostFrameCallback((_) { + DriftMemoryPage.setMemory(ref, memories[pageNumber]); + }); } currentAssetPage.value = 0; 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 9155d82753..71fedf1258 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 @@ -77,6 +77,7 @@ class AddActionButton extends ConsumerWidget { color: context.themeData.scaffoldBackgroundColor, position: _menuPosition(context), items: items, + popUpAnimationStyle: AnimationStyle.noAnimation, ); if (selected == null) { diff --git a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart index b2c61c7488..e85a6c05f8 100644 --- a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart @@ -3,10 +3,9 @@ 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/presentation/pages/drift_memory.page.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -31,16 +30,9 @@ class DriftMemoryLane extends ConsumerWidget { overlayColor: WidgetStateProperty.all(Colors.white.withValues(alpha: 0.1)), onTap: (index) { ref.read(hapticFeedbackProvider.notifier).heavyImpact(); - if (memories[index].assets.isNotEmpty) { - final asset = memories[index].assets[0]; - ref.read(currentAssetNotifier.notifier).setAsset(asset); - - if (asset.isVideo) { - ref.read(videoPlaybackValueProvider.notifier).reset(); - } + DriftMemoryPage.setMemory(ref, memories[index]); } - context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index)); }, children: memories diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index b0d7ea6013..552c9e356a 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -22,14 +22,16 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; +import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/datetime_helpers.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; // ignore: import_rule_photo_manager import 'package:photo_manager/photo_manager.dart'; -const int targetVersion = 18; +const int targetVersion = 19; Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { final hasVersion = Store.tryGet(StoreKey.version) != null; @@ -78,6 +80,12 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { await Store.put(StoreKey.shouldResetSync, true); } + if (version < 19 && Store.isBetaTimelineEnabled) { + if (!await _populateUpdatedAtTime(drift)) { + return; + } + } + if (targetVersion >= 12) { await Store.put(StoreKey.version, targetVersion); return; @@ -221,6 +229,32 @@ Future _migrateDeviceAsset(Isar db) async { }); } +Future _populateUpdatedAtTime(Drift db) async { + try { + final nativeApi = NativeSyncApi(); + final albums = await nativeApi.getAlbums(); + for (final album in albums) { + final assets = await nativeApi.getAssetsForAlbum(album.id); + await db.batch((batch) async { + for (final asset in assets) { + batch.update( + db.localAssetEntity, + LocalAssetEntityCompanion( + updatedAt: Value(tryFromSecondsSinceEpoch(asset.updatedAt, isUtc: true) ?? DateTime.timestamp()), + ), + where: (t) => t.id.equals(asset.id), + ); + } + }); + } + + return true; + } catch (error) { + dPrint(() => "[MIGRATION] Error while populating updatedAt time: $error"); + return false; + } +} + Future migrateDeviceAssetToSqlite(Isar db, Drift drift) async { try { final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll(); diff --git a/mobile/test/domain/services/asset.service_test.dart b/mobile/test/domain/services/asset.service_test.dart new file mode 100644 index 0000000000..5e7179ffa6 --- /dev/null +++ b/mobile/test/domain/services/asset.service_test.dart @@ -0,0 +1,165 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/domain/services/asset.service.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../infrastructure/repository.mock.dart'; +import '../../test_utils.dart'; + +void main() { + late AssetService sut; + late MockRemoteAssetRepository mockRemoteAssetRepository; + late MockDriftLocalAssetRepository mockLocalAssetRepository; + + setUp(() { + mockRemoteAssetRepository = MockRemoteAssetRepository(); + mockLocalAssetRepository = MockDriftLocalAssetRepository(); + sut = AssetService( + remoteAssetRepository: mockRemoteAssetRepository, + localAssetRepository: mockLocalAssetRepository, + ); + }); + + group('getAspectRatio', () { + test('flips dimensions on Android for 90° and 270° orientations', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + addTearDown(() => debugDefaultTargetPlatformOverride = null); + + for (final orientation in [90, 270]) { + final localAsset = TestUtils.createLocalAsset( + id: 'local-$orientation', + width: 1920, + height: 1080, + orientation: orientation, + ); + + final result = await sut.getAspectRatio(localAsset); + + expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip on Android'); + } + }); + + test('does not flip dimensions on iOS regardless of orientation', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + addTearDown(() => debugDefaultTargetPlatformOverride = null); + + for (final orientation in [0, 90, 270]) { + final localAsset = TestUtils.createLocalAsset( + id: 'local-$orientation', + width: 1920, + height: 1080, + orientation: orientation, + ); + + final result = await sut.getAspectRatio(localAsset); + + expect(result, 1920 / 1080, reason: 'iOS should never flip dimensions'); + } + }); + + test('fetches dimensions from remote repository when missing from asset', () async { + final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null); + + final exif = const ExifInfo(orientation: '1'); + + final fetchedAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 1080); + + when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); + when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => fetchedAsset); + + final result = await sut.getAspectRatio(remoteAsset); + + expect(result, 1920 / 1080); + verify(() => mockRemoteAssetRepository.get('remote-1')).called(1); + }); + + test('fetches dimensions from local repository when missing from local asset', () async { + final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0); + + final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 0); + + when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset); + + final result = await sut.getAspectRatio(localAsset); + + expect(result, 1920 / 1080); + verify(() => mockLocalAssetRepository.get('local-1')).called(1); + }); + + test('returns 1.0 when dimensions are still unavailable after fetching', () async { + final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null); + + final exif = const ExifInfo(orientation: '1'); + + when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); + when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => null); + + final result = await sut.getAspectRatio(remoteAsset); + + expect(result, 1.0); + }); + + test('returns 1.0 when height is zero', () async { + final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 0); + + final exif = const ExifInfo(orientation: '1'); + + when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); + + final result = await sut.getAspectRatio(remoteAsset); + + expect(result, 1.0); + }); + + test('handles local asset with remoteId and uses exif from remote', () async { + final localAsset = TestUtils.createLocalAsset( + id: 'local-1', + remoteId: 'remote-1', + width: 1920, + height: 1080, + orientation: 0, + ); + + final exif = const ExifInfo(orientation: '6'); + + when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); + + final result = await sut.getAspectRatio(localAsset); + + expect(result, 1080 / 1920); + }); + + test('handles various flipped EXIF orientations correctly', () async { + final flippedOrientations = ['5', '6', '7', '8', '90', '-90']; + + for (final orientation in flippedOrientations) { + final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080); + + final exif = ExifInfo(orientation: orientation); + + when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif); + + final result = await sut.getAspectRatio(remoteAsset); + + expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip dimensions'); + } + }); + + test('handles various non-flipped EXIF orientations correctly', () async { + final nonFlippedOrientations = ['1', '2', '3', '4']; + + for (final orientation in nonFlippedOrientations) { + final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080); + + final exif = ExifInfo(orientation: orientation); + + when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif); + + final result = await sut.getAspectRatio(remoteAsset); + + expect(result, 1920 / 1080, reason: 'Orientation $orientation should NOT flip dimensions'); + } + }); + }); +} diff --git a/mobile/test/domain/services/local_sync_service_test.dart b/mobile/test/domain/services/local_sync_service_test.dart index 2f236971e0..92ab01c7e0 100644 --- a/mobile/test/domain/services/local_sync_service_test.dart +++ b/mobile/test/domain/services/local_sync_service_test.dart @@ -54,12 +54,7 @@ void main() { when(() => mockNativeSyncApi.shouldFullSync()).thenAnswer((_) async => false); when(() => mockNativeSyncApi.getMediaChanges()).thenAnswer( - (_) async => SyncDelta( - hasChanges: false, - updates: const [], - deletes: const [], - assetAlbums: const {}, - ), + (_) async => SyncDelta(hasChanges: false, updates: const [], deletes: const [], assetAlbums: const {}), ); when(() => mockNativeSyncApi.getTrashedAssets()).thenAnswer((_) async => {}); when(() => mockTrashedLocalAssetRepository.processTrashSnapshot(any())).thenAnswer((_) async {}); @@ -144,13 +139,19 @@ void main() { }); final localAssetToTrash = LocalAssetStub.image2.copyWith(id: 'local-trash', checksum: 'checksum-trash'); - when(() => mockTrashedLocalAssetRepository.getToTrash()).thenAnswer((_) async => {'album-a': [localAssetToTrash]}); + when(() => mockTrashedLocalAssetRepository.getToTrash()).thenAnswer( + (_) async => { + 'album-a': [localAssetToTrash], + }, + ); final assetEntity = MockAssetEntity(); when(() => assetEntity.getMediaUrl()).thenAnswer((_) async => 'content://local-trash'); when(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).thenAnswer((_) async => assetEntity); - await sut.processTrashedAssets({'album-a': [platformAsset]}); + await sut.processTrashedAssets({ + 'album-a': [platformAsset], + }); verify(() => mockTrashedLocalAssetRepository.processTrashSnapshot(any())).called(1); verify(() => mockTrashedLocalAssetRepository.getToTrash()).called(1); @@ -159,8 +160,7 @@ void main() { verify(() => mockTrashedLocalAssetRepository.applyRestoredAssets(restoredIds)).called(1); verify(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).called(1); - final moveArgs = - verify(() => mockLocalFilesManager.moveToTrash(captureAny())).captured.single as List; + final moveArgs = verify(() => mockLocalFilesManager.moveToTrash(captureAny())).captured.single as List; expect(moveArgs, ['content://local-trash']); final trashArgs = verify(() => mockTrashedLocalAssetRepository.trashLocalAsset(captureAny())).captured.single @@ -187,4 +187,25 @@ void main() { verifyNever(() => mockTrashedLocalAssetRepository.trashLocalAsset(any())); }); }); + + group('LocalSyncService - PlatformAsset conversion', () { + test('toLocalAsset uses correct updatedAt timestamp', () { + final platformAsset = PlatformAsset( + id: 'test-id', + name: 'test.jpg', + type: AssetType.image.index, + durationInSeconds: 0, + orientation: 0, + isFavorite: false, + createdAt: 1700000000, + updatedAt: 1732000000, + ); + + final localAsset = platformAsset.toLocalAsset(); + + expect(localAsset.createdAt.millisecondsSinceEpoch ~/ 1000, 1700000000); + expect(localAsset.updatedAt.millisecondsSinceEpoch ~/ 1000, 1732000000); + expect(localAsset.updatedAt, isNot(localAsset.createdAt)); + }); + }); } diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index becfafe33d..aac384c29e 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -4,6 +4,7 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; @@ -35,6 +36,8 @@ class MockLocalAssetRepository extends Mock implements DriftLocalAssetRepository class MockDriftLocalAssetRepository extends Mock implements DriftLocalAssetRepository {} +class MockRemoteAssetRepository extends Mock implements RemoteAssetRepository {} + class MockTrashedLocalAssetRepository extends Mock implements DriftTrashedLocalAssetRepository {} class MockStorageRepository extends Mock implements StorageRepository {} diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart index 9b59773d3b..498607e3d2 100644 --- a/mobile/test/test_utils.dart +++ b/mobile/test/test_utils.dart @@ -5,6 +5,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:fake_async/fake_async.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as domain; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/android_device_asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; @@ -116,4 +117,43 @@ abstract final class TestUtils { } return result; } + + static domain.RemoteAsset createRemoteAsset({required String id, int? width, int? height, String? ownerId}) { + return domain.RemoteAsset( + id: id, + checksum: 'checksum1', + ownerId: ownerId ?? 'owner1', + name: 'test.jpg', + type: domain.AssetType.image, + createdAt: DateTime(2024, 1, 1), + updatedAt: DateTime(2024, 1, 1), + durationInSeconds: 0, + isFavorite: false, + width: width, + height: height, + ); + } + + static domain.LocalAsset createLocalAsset({ + required String id, + String? remoteId, + int? width, + int? height, + int orientation = 0, + }) { + return domain.LocalAsset( + id: id, + remoteId: remoteId, + checksum: 'checksum1', + name: 'test.jpg', + type: domain.AssetType.image, + createdAt: DateTime(2024, 1, 1), + updatedAt: DateTime(2024, 1, 1), + durationInSeconds: 0, + isFavorite: false, + width: width, + height: height, + orientation: orientation, + ); + } } diff --git a/plugins/package-lock.json b/plugins/package-lock.json index 3b0f0b34cb..231e298970 100644 --- a/plugins/package-lock.json +++ b/plugins/package-lock.json @@ -1,385 +1,459 @@ { - "name": "js-pdk-template", + "name": "plugins", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "js-pdk-template", + "name": "plugins", "version": "1.0.0", - "license": "BSD-3-Clause", + "license": "AGPL-3.0", "devDependencies": { "@extism/js-pdk": "^1.0.1", - "esbuild": "^0.19.6", + "esbuild": "^0.25.0", "typescript": "^5.3.2" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ - "x64" + "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@extism/js-pdk": { @@ -389,41 +463,45 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/typescript": { diff --git a/plugins/package.json b/plugins/package.json index ab6b2f8435..024b7bc8a9 100644 --- a/plugins/package.json +++ b/plugins/package.json @@ -13,7 +13,7 @@ "license": "AGPL-3.0", "devDependencies": { "@extism/js-pdk": "^1.0.1", - "esbuild": "^0.19.6", + "esbuild": "^0.25.0", "typescript": "^5.3.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a00b6755a..acdc2045d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -311,8 +311,8 @@ importers: specifier: ^1.0.1 version: 1.1.1 esbuild: - specifier: ^0.19.6 - version: 0.19.12 + specifier: ^0.25.0 + version: 0.25.12 typescript: specifier: ^5.3.2 version: 5.9.3 @@ -456,7 +456,7 @@ importers: version: 5.10.0 js-yaml: specifier: ^4.1.0 - version: 4.1.0 + version: 4.1.1 jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -726,19 +726,22 @@ importers: specifier: ^7.4.47 version: 7.4.47 '@photo-sphere-viewer/core': - specifier: ^5.11.5 + specifier: ^5.14.0 version: 5.14.0 '@photo-sphere-viewer/equirectangular-video-adapter': - specifier: ^5.11.5 + specifier: ^5.14.0 version: 5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/video-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)) + '@photo-sphere-viewer/markers-plugin': + specifier: ^5.14.0 + version: 5.14.0(@photo-sphere-viewer/core@5.14.0) '@photo-sphere-viewer/resolution-plugin': - specifier: ^5.11.5 + specifier: ^5.14.0 version: 5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/settings-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)) '@photo-sphere-viewer/settings-plugin': - specifier: ^5.11.5 + specifier: ^5.14.0 version: 5.14.0(@photo-sphere-viewer/core@5.14.0) '@photo-sphere-viewer/video-plugin': - specifier: ^5.11.5 + specifier: ^5.14.0 version: 5.14.0(@photo-sphere-viewer/core@5.14.0) '@types/geojson': specifier: ^7946.0.16 @@ -3573,6 +3576,11 @@ packages: '@photo-sphere-viewer/core': 5.14.0 '@photo-sphere-viewer/video-plugin': 5.14.0 + '@photo-sphere-viewer/markers-plugin@5.14.0': + resolution: {integrity: sha512-w7txVHtLxXMS61m0EbNjgvdNXQYRh6Aa0oatft5oruKgoXLg/UlCu1mG6Btg+zrNsG05W2zl4gRM3fcWoVdneA==} + peerDependencies: + '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/resolution-plugin@5.14.0': resolution: {integrity: sha512-PvDMX1h+8FzWdySxiorQ2bSmyBGTPsZjNNFRBqIfmb5C+01aWCIE7kuXodXGHwpXQNcOojsVX9IiX0Vz4CiW4A==} peerDependencies: @@ -7697,14 +7705,18 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + jsdom@20.0.3: resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} engines: {node: '>=14'} @@ -13545,7 +13557,7 @@ snapshots: '@types/react-router-config': 5.0.11 combine-promises: 1.2.0 fs-extra: 11.3.2 - js-yaml: 4.1.0 + js-yaml: 4.1.1 lodash: 4.17.21 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -14001,7 +14013,7 @@ snapshots: '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fs-extra: 11.3.2 joi: 17.13.3 - js-yaml: 4.1.0 + js-yaml: 4.1.1 lodash: 4.17.21 tslib: 2.8.1 transitivePeerDependencies: @@ -14026,7 +14038,7 @@ snapshots: globby: 11.1.0 gray-matter: 4.0.3 jiti: 1.21.7 - js-yaml: 4.1.0 + js-yaml: 4.1.1 lodash: 4.17.21 micromatch: 4.0.8 p-queue: 6.6.2 @@ -14232,7 +14244,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -15374,6 +15386,10 @@ snapshots: '@photo-sphere-viewer/video-plugin': 5.14.0(@photo-sphere-viewer/core@5.14.0) three: 0.180.0 + '@photo-sphere-viewer/markers-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)': + dependencies: + '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/resolution-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/settings-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0))': dependencies: '@photo-sphere-viewer/core': 5.14.0 @@ -17953,7 +17969,7 @@ snapshots: cosmiconfig@8.3.6(typescript@5.8.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: @@ -17962,7 +17978,7 @@ snapshots: cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: @@ -19413,7 +19429,7 @@ snapshots: gray-matter@4.0.3: dependencies: - js-yaml: 3.14.1 + js-yaml: 3.14.2 kind-of: 6.0.3 section-matter: 1.0.0 strip-bom-string: 1.0.0 @@ -20121,7 +20137,7 @@ snapshots: js-tokens@9.0.1: {} - js-yaml@3.14.1: + js-yaml@3.14.2: dependencies: argparse: 1.0.10 esprima: 4.0.1 @@ -20130,6 +20146,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + jsdom@20.0.3(canvas@2.11.2): dependencies: abab: 2.0.6 diff --git a/server/Dockerfile b/server/Dockerfile index 3b2d885149..765e24b365 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/immich-app/base-server-dev:202511041104@sha256:7558931a4a71989e7fd9fa3e1ba6c28da15891867310edda8c58236171839f2f AS builder +FROM ghcr.io/immich-app/base-server-dev:202511181104@sha256:fd445b91d4db131aae71b143b647d2262818dac80946078ce231c79cb9acecba AS builder ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ CI=1 \ COREPACK_HOME=/tmp \ @@ -69,7 +69,7 @@ RUN --mount=type=cache,id=pnpm-plugins,target=/buildcache/pnpm-store \ --mount=type=cache,id=mise-tools,target=/buildcache/mise \ cd plugins && mise run build -FROM ghcr.io/immich-app/base-server-prod:202511041104@sha256:57c0379977fd5521d83cdf661aecd1497c83a9a661ebafe0a5243a09fc1064cb +FROM ghcr.io/immich-app/base-server-prod:202511181104@sha256:1bc2b7cebc4fd3296dc33a5779411e1c7d854ea713066c1e024d54f45f176f89 WORKDIR /usr/src/app ENV NODE_ENV=production \ diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev index 133b8a835d..07837fd757 100644 --- a/server/Dockerfile.dev +++ b/server/Dockerfile.dev @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:202511041104@sha256:7558931a4a71989e7fd9fa3e1ba6c28da15891867310edda8c58236171839f2f AS dev +FROM ghcr.io/immich-app/base-server-dev:202511181104@sha256:fd445b91d4db131aae71b143b647d2262818dac80946078ce231c79cb9acecba AS dev ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ CI=1 \ diff --git a/server/src/services/backup.service.spec.ts b/server/src/services/backup.service.spec.ts index 33620f34e9..38678df021 100644 --- a/server/src/services/backup.service.spec.ts +++ b/server/src/services/backup.service.spec.ts @@ -154,6 +154,39 @@ describe(BackupService.name, () => { mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); }); + it('should sanitize DB_URL (remove uselibpqcompat) before calling pg_dumpall', async () => { + // create a service instance with a URL connection that includes libpqcompat + const dbUrl = 'postgresql://postgres:pwd@host:5432/immich?sslmode=require&uselibpqcompat=true'; + const configMock = { + getEnv: () => ({ database: { config: { connectionType: 'url', url: dbUrl }, skipMigrations: false } }), + getWorker: () => ImmichWorker.Api, + isDev: () => false, + } as unknown as any; + + ({ sut, mocks } = newTestService(BackupService, { config: configMock })); + + mocks.storage.readdir.mockResolvedValue([]); + mocks.process.createSpawnDuplexStream.mockImplementation(() => mockDuplex('command', 0, 'data', '')); + mocks.storage.rename.mockResolvedValue(); + mocks.storage.unlink.mockResolvedValue(); + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); + mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); + mocks.database.getPostgresVersion.mockResolvedValue('14.10'); + + await sut.handleBackupDatabase(); + + expect(mocks.process.createSpawnDuplexStream).toHaveBeenCalled(); + const call = mocks.process.createSpawnDuplexStream.mock.calls[0]; + const args = call[1] as string[]; + expect(args).toMatchInlineSnapshot(` + [ + "postgresql://postgres:pwd@host:5432/immich?sslmode=require", + "--clean", + "--if-exists", + ] + `); + }); + it('should run a database backup successfully', async () => { const result = await sut.handleBackupDatabase(); expect(result).toBe(JobStatus.Success); diff --git a/server/src/utils/backups.ts b/server/src/utils/backups.ts index a79e591371..890520dc6c 100644 --- a/server/src/utils/backups.ts +++ b/server/src/utils/backups.ts @@ -69,7 +69,11 @@ export async function buildPostgresLaunchArguments( args.push('--dbname'); } - args.push(databaseConfig.url); + const url = new URL(databaseConfig.url); + // remove known bad parameters + url.searchParams.delete('uselibpqcompat'); + + args.push(url.toString()); } else { args.push( '--username', diff --git a/web/package.json b/web/package.json index 27454f7e88..d157c9fde0 100644 --- a/web/package.json +++ b/web/package.json @@ -11,13 +11,13 @@ "preview": "vite preview", "check:svelte": "svelte-check --no-tsconfig --fail-on-warnings", "check:typescript": "tsc --noEmit", - "check:watch": "npm run check:svelte -- --watch", - "check:code": "npm run format && npm run lint:p && npm run check:svelte && npm run check:typescript", - "check:all": "npm run check:code && npm run test:cov", + "check:watch": "pnpm run check:svelte --watch", + "check:code": "pnpm run format && pnpm run lint && pnpm run check:svelte && pnpm run check:typescript", + "check:all": "pnpm run check:code && pnpm run test:cov", "lint": "eslint . --max-warnings 0 --concurrency 4", - "lint:fix": "npm run lint -- --fix", + "lint:fix": "pnpm run lint --fix", "format": "prettier --check .", - "format:fix": "prettier --write . && npm run format:i18n", + "format:fix": "prettier --write . && pnpm run format:i18n", "format:i18n": "pnpm dlx sort-json ../i18n/*.json", "test": "vitest --run", "test:cov": "vitest --coverage", @@ -31,11 +31,12 @@ "@immich/ui": "^0.43.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", - "@photo-sphere-viewer/core": "^5.11.5", - "@photo-sphere-viewer/equirectangular-video-adapter": "^5.11.5", - "@photo-sphere-viewer/resolution-plugin": "^5.11.5", - "@photo-sphere-viewer/settings-plugin": "^5.11.5", - "@photo-sphere-viewer/video-plugin": "^5.11.5", + "@photo-sphere-viewer/core": "^5.14.0", + "@photo-sphere-viewer/equirectangular-video-adapter": "^5.14.0", + "@photo-sphere-viewer/markers-plugin": "^5.14.0", + "@photo-sphere-viewer/resolution-plugin": "^5.14.0", + "@photo-sphere-viewer/settings-plugin": "^5.14.0", + "@photo-sphere-viewer/video-plugin": "^5.14.0", "@types/geojson": "^7946.0.16", "@zoom-image/core": "^0.41.0", "@zoom-image/svelte": "^0.3.0", diff --git a/web/src/lib/actions/shortcut.ts b/web/src/lib/actions/shortcut.ts index f7b3009403..8f01ce8924 100644 --- a/web/src/lib/actions/shortcut.ts +++ b/web/src/lib/actions/shortcut.ts @@ -39,13 +39,17 @@ export const shortcutLabel = (shortcut: Shortcut) => { /** Determines whether an event should be ignored. The event will be ignored if: * - The element dispatching the event is not the same as the element which the event listener is attached to * - The element dispatching the event is an input field + * - The element dispatching the event is a map canvas */ export const shouldIgnoreEvent = (event: KeyboardEvent | ClipboardEvent): boolean => { if (event.target === event.currentTarget) { return false; } const type = (event.target as HTMLInputElement).type; - return ['textarea', 'text', 'date', 'datetime-local', 'email', 'password'].includes(type); + return ( + ['textarea', 'text', 'date', 'datetime-local', 'email', 'password'].includes(type) || + (event.target instanceof HTMLCanvasElement && event.target.classList.contains('maplibregl-canvas')) + ); }; export const matchesShortcut = (event: KeyboardEvent, shortcut: Shortcut) => { diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte index bb826110b7..e4b588af8f 100644 --- a/web/src/lib/components/album-page/albums-list.svelte +++ b/web/src/lib/components/album-page/albums-list.svelte @@ -33,7 +33,6 @@ import { groupBy } from 'lodash-es'; import { onMount, type Snippet } from 'svelte'; import { t } from 'svelte-i18n'; - import { run } from 'svelte/legacy'; interface Props { ownedAlbums?: AlbumResponseDto[]; @@ -128,65 +127,45 @@ }, }; - let albums: AlbumResponseDto[] = $state([]); - let filteredAlbums: AlbumResponseDto[] = $state([]); - let groupedAlbums: AlbumGroup[] = $state([]); + let albums = $derived.by(() => { + switch (userSettings.filter) { + case AlbumFilter.Owned: { + return ownedAlbums; + } + case AlbumFilter.Shared: { + return sharedAlbums; + } + default: { + const nonOwnedAlbums = sharedAlbums.filter((album) => album.ownerId !== $user.id); + return nonOwnedAlbums.length > 0 ? ownedAlbums.concat(nonOwnedAlbums) : ownedAlbums; + } + } + }); + const normalizedSearchQuery = $derived(normalizeSearchString(searchQuery)); + let filteredAlbums = $derived( + normalizedSearchQuery + ? albums.filter(({ albumName }) => normalizeSearchString(albumName).includes(normalizedSearchQuery)) + : albums, + ); - let albumGroupOption: string = $state(AlbumGroupBy.None); + let albumGroupOption = $derived(getSelectedAlbumGroupOption(userSettings)); + let groupedAlbums = $derived.by(() => { + const groupFunc = groupOptions[albumGroupOption] ?? groupOptions[AlbumGroupBy.None]; + const groupedAlbums = groupFunc(stringToSortOrder(userSettings.groupOrder), filteredAlbums); - let albumToShare: AlbumResponseDto | null = $state(null); + return groupedAlbums.map((group) => ({ + id: group.id, + name: group.name, + albums: sortAlbums(group.albums, { sortBy: userSettings.sortBy, orderBy: userSettings.sortOrder }), + })); + }); let contextMenuPosition: ContextMenuPosition = $state({ x: 0, y: 0 }); let selectedAlbum: AlbumResponseDto | undefined = $state(); let isOpen = $state(false); - // Step 1: Filter between Owned and Shared albums, or both. - run(() => { - switch (userSettings.filter) { - case AlbumFilter.Owned: { - albums = ownedAlbums; - break; - } - case AlbumFilter.Shared: { - albums = sharedAlbums; - break; - } - default: { - const userId = $user.id; - const nonOwnedAlbums = sharedAlbums.filter((album) => album.ownerId !== userId); - albums = nonOwnedAlbums.length > 0 ? ownedAlbums.concat(nonOwnedAlbums) : ownedAlbums; - } - } - }); - - // Step 2: Filter using the given search query. - run(() => { - if (searchQuery) { - const searchAlbumNormalized = normalizeSearchString(searchQuery); - - filteredAlbums = albums.filter((album) => { - return normalizeSearchString(album.albumName).includes(searchAlbumNormalized); - }); - } else { - filteredAlbums = albums; - } - }); - - // Step 3: Group albums. - run(() => { - albumGroupOption = getSelectedAlbumGroupOption(userSettings); - const groupFunc = groupOptions[albumGroupOption] ?? groupOptions[AlbumGroupBy.None]; - groupedAlbums = groupFunc(stringToSortOrder(userSettings.groupOrder), filteredAlbums); - }); - - // Step 4: Sort albums amongst each group. - run(() => { - groupedAlbums = groupedAlbums.map((group) => ({ - id: group.id, - name: group.name, - albums: sortAlbums(group.albums, { sortBy: userSettings.sortBy, orderBy: userSettings.sortOrder }), - })); - + // TODO get rid of this + $effect(() => { albumGroupIds = groupedAlbums.map(({ id }) => id); }); @@ -231,7 +210,7 @@ const result = await modalManager.show(AlbumShareModal, { album: selectedAlbum }); switch (result?.action) { case 'sharedUsers': { - await handleAddUsers(result.data); + await handleAddUsers(selectedAlbum, result.data); break; } @@ -300,22 +279,17 @@ updateRecentAlbumInfo(album); }; - const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => { - if (!albumToShare) { - return; - } + const handleAddUsers = async (album: AlbumResponseDto, albumUsers: AlbumUserAddDto[]) => { try { - const album = await addUsersToAlbum({ - id: albumToShare.id, + const updatedAlbum = await addUsersToAlbum({ + id: album.id, addUsersDto: { albumUsers, }, }); - updateAlbumInfo(album); + updateAlbumInfo(updatedAlbum); } catch (error) { handleError(error, $t('errors.unable_to_add_album_users')); - } finally { - albumToShare = null; } }; diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 2ee4496830..60913ff47b 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -23,6 +23,7 @@ import { Icon, IconButton, LoadingSpinner, modalManager } from '@immich/ui'; import { mdiCalendar, + mdiCamera, mdiCameraIris, mdiClose, mdiEye, @@ -372,9 +373,9 @@ - {#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.fNumber} + {#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.exposureTime || asset.exifInfo?.iso}
-
+
{#if asset.exifInfo?.make || asset.exifInfo?.model} @@ -395,20 +396,34 @@

{/if} +
+ {#if asset.exifInfo.exposureTime} +

{`${asset.exifInfo.exposureTime} s`}

+ {/if} + + {#if asset.exifInfo.iso} +

{`ISO ${asset.exifInfo.iso}`}

+ {/if} +
+
+
+ {/if} + + {#if asset.exifInfo?.lensModel || asset.exifInfo?.fNumber || asset.exifInfo?.focalLength} +
+
+ +
{#if asset.exifInfo?.lensModel} - +

+ + {asset.exifInfo.lensModel} + +

{/if}
@@ -416,19 +431,9 @@

ƒ/{asset.exifInfo.fNumber.toLocaleString($locale)}

{/if} - {#if asset.exifInfo.exposureTime} -

{`${asset.exifInfo.exposureTime} s`}

- {/if} - {#if asset.exifInfo.focalLength}

{`${asset.exifInfo.focalLength.toLocaleString($locale)} mm`}

{/if} - - {#if asset.exifInfo.iso} -

- {`ISO ${asset.exifInfo.iso}`} -

- {/if}
diff --git a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte index cd9a010f78..00f3a6795f 100644 --- a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte +++ b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte @@ -1,4 +1,5 @@ diff --git a/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts b/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts index aa4bae8919..566b11b8b7 100644 --- a/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts +++ b/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts @@ -1,6 +1,5 @@ import { setDifference, type TimelineDate } from '$lib/utils/timeline-util'; import { AssetOrder } from '@immich/sdk'; -import { SvelteSet } from 'svelte/reactivity'; import type { DayGroup } from './day-group.svelte'; import type { MonthGroup } from './month-group.svelte'; import type { TimelineAsset } from './types'; @@ -10,8 +9,10 @@ export class GroupInsertionCache { [year: number]: { [month: number]: { [day: number]: DayGroup } }; } = {}; unprocessedAssets: TimelineAsset[] = []; - changedDayGroups = new SvelteSet(); - newDayGroups = new SvelteSet(); + // eslint-disable-next-line svelte/prefer-svelte-reactivity + changedDayGroups = new Set(); + // eslint-disable-next-line svelte/prefer-svelte-reactivity + newDayGroups = new Set(); getDayGroup({ year, month, day }: TimelineDate): DayGroup | undefined { return this.#lookupCache[year]?.[month]?.[day]; @@ -32,7 +33,8 @@ export class GroupInsertionCache { } get updatedBuckets() { - const updated = new SvelteSet(); + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const updated = new Set(); for (const group of this.changedDayGroups) { updated.add(group.monthGroup); } @@ -40,7 +42,8 @@ export class GroupInsertionCache { } get bucketsWithNewDayGroups() { - const updated = new SvelteSet(); + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const updated = new Set(); for (const group of this.newDayGroups) { updated.add(group.monthGroup); } diff --git a/web/src/lib/managers/timeline-manager/internal/load-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/load-support.svelte.ts index ec50e3d75e..859e818583 100644 --- a/web/src/lib/managers/timeline-manager/internal/load-support.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/load-support.svelte.ts @@ -46,7 +46,7 @@ export async function loadFromTimeBuckets( } } - const unprocessedAssets = monthGroup.addAssets(bucketResponse); + const unprocessedAssets = monthGroup.addAssets(bucketResponse, true); if (unprocessedAssets.length > 0) { console.error( `Warning: getTimeBucket API returning assets not in requested month: ${monthGroup.yearMonth.month}, ${JSON.stringify( diff --git a/web/src/lib/managers/timeline-manager/month-group.svelte.ts b/web/src/lib/managers/timeline-manager/month-group.svelte.ts index bef512c226..1d9e1bbaa7 100644 --- a/web/src/lib/managers/timeline-manager/month-group.svelte.ts +++ b/web/src/lib/managers/timeline-manager/month-group.svelte.ts @@ -153,7 +153,7 @@ export class MonthGroup { }; } - addAssets(bucketAssets: TimeBucketAssetResponseDto) { + addAssets(bucketAssets: TimeBucketAssetResponseDto, preSorted: boolean) { const addContext = new GroupInsertionCache(); for (let i = 0; i < bucketAssets.id.length; i++) { const { localDateTime, fileCreatedAt } = getTimes( @@ -194,6 +194,9 @@ export class MonthGroup { } this.addTimelineAsset(timelineAsset, addContext); } + if (preSorted) { + return addContext.unprocessedAssets; + } for (const group of addContext.existingDayGroups) { group.sortAssets(this.#sortOrder); diff --git a/web/src/lib/modals/HelpAndFeedbackModal.svelte b/web/src/lib/modals/HelpAndFeedbackModal.svelte index f25f7d1704..8b73978672 100644 --- a/web/src/lib/modals/HelpAndFeedbackModal.svelte +++ b/web/src/lib/modals/HelpAndFeedbackModal.svelte @@ -2,7 +2,7 @@ import { type ServerAboutResponseDto } from '@immich/sdk'; import { Icon, Modal, ModalBody } from '@immich/ui'; import { mdiBugOutline, mdiFaceAgent, mdiGit, mdiGithub, mdiInformationOutline } from '@mdi/js'; - import { siDiscord } from 'simple-icons'; + import { type SimpleIcon, siDiscord } from 'simple-icons'; import { t } from 'svelte-i18n'; interface Props { @@ -13,94 +13,57 @@ let { onClose, info }: Props = $props(); +{#snippet link(url: string, icon: string | SimpleIcon, text: string)} + +{/snippet} +

{$t('official_immich_resources')}

-
- +
+ {@render link( + `https://docs.${info.version}.archive.immich.app/overview/introduction`, + mdiInformationOutline, + $t('documentation'), + )} - + {@render link('https://github.com/immich-app/immich/', mdiGithub, $t('source'))} - + {@render link('https://discord.immich.app', siDiscord, $t('discord'))} - + {@render link( + 'https://github.com/immich-app/immich/issues/new/choose', + mdiBugOutline, + $t('bugs_and_feature_requests'), + )}
{#if info.thirdPartyBugFeatureUrl || info.thirdPartySourceUrl || info.thirdPartyDocumentationUrl || info.thirdPartySupportUrl}

{$t('third_party_resources')}

{$t('support_third_party_description')}

-
+
{#if info.thirdPartyDocumentationUrl} - + {@render link(info.thirdPartyDocumentationUrl, mdiInformationOutline, $t('documentation'))} {/if} {#if info.thirdPartySourceUrl} - + {@render link(info.thirdPartySourceUrl, mdiGit, $t('source'))} {/if} {#if info.thirdPartySupportUrl} - + {@render link(info.thirdPartySupportUrl, mdiFaceAgent, $t('support'))} {/if} {#if info.thirdPartyBugFeatureUrl} - + {@render link(info.thirdPartyBugFeatureUrl, mdiBugOutline, $t('bugs_and_feature_requests'))} {/if}
{/if}