merge: remote-tracking branch 'origin/main' into feat/database-restores

This commit is contained in:
izzy
2025-11-24 16:52:41 +00:00
31 changed files with 795 additions and 355 deletions

View File

@@ -14,15 +14,15 @@ When contributing code through a pull request, please check the following:
- [ ] `pnpm run check:typescript` (check typescript) - [ ] `pnpm run check:typescript` (check typescript)
- [ ] `pnpm test` (unit tests) - [ ] `pnpm test` (unit tests)
:::tip AIO
Run all web checks with `pnpm run check:all`
:::
## Documentation ## Documentation
- [ ] `pnpm run format` (formatting via Prettier) - [ ] `pnpm run format` (formatting via Prettier)
- [ ] Update the `_redirects` file if you have renamed a page or removed it from the documentation. - [ ] 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 ## Server Checks
- [ ] `pnpm run lint` (linting via ESLint) - [ ] `pnpm run lint` (linting via ESLint)

View File

@@ -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`. 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`. `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. When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSWORD` and `DB_DATABASE_NAME` database variables are ignored.

View File

@@ -50,7 +50,7 @@ const double kUploadStatusCanceled = -2.0;
const int kMinMonthsToEnableScrubberSnap = 12; 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 kImmichPlayStoreLink = "https://play.google.com/store/apps/details?id=app.alextran.immich";
const String kImmichLatestRelease = "https://github.com/immich-app/immich/releases/latest"; const String kImmichLatestRelease = "https://github.com/immich-app/immich/releases/latest";

View File

@@ -75,6 +75,20 @@ class AssetService {
isFlipped = false; 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 orientedWidth = isFlipped ? height : width;
final orientedHeight = isFlipped ? width : height; final orientedHeight = isFlipped ? width : height;
if (orientedWidth != null && orientedHeight != null && orientedHeight > 0) { if (orientedWidth != null && orientedHeight != null && orientedHeight > 0) {

View File

@@ -363,14 +363,14 @@ extension on Iterable<PlatformAsset> {
} }
} }
extension on PlatformAsset { extension PlatformToLocalAsset on PlatformAsset {
LocalAsset toLocalAsset() => LocalAsset( LocalAsset toLocalAsset() => LocalAsset(
id: id, id: id,
name: name, name: name,
checksum: null, checksum: null,
type: AssetType.values.elementAtOrNull(type) ?? AssetType.other, type: AssetType.values.elementAtOrNull(type) ?? AssetType.other,
createdAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(), createdAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(),
updatedAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(), updatedAt: tryFromSecondsSinceEpoch(updatedAt, isUtc: true) ?? DateTime.timestamp(),
width: width, width: width,
height: height, height: height,
durationInSeconds: durationInSeconds, durationInSeconds: durationInSeconds,

View File

@@ -261,7 +261,6 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
durationInSeconds: Value(asset.durationInSeconds), durationInSeconds: Value(asset.durationInSeconds),
id: asset.id, id: asset.id,
orientation: Value(asset.orientation), orientation: Value(asset.orientation),
checksum: const Value(null),
isFavorite: Value(asset.isFavorite), isFavorite: Value(asset.isFavorite),
); );
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>( batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(

View File

@@ -265,7 +265,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
row.deletedAt.isNull() & row.deletedAt.isNull() &
row.isFavorite.equals(true) & row.isFavorite.equals(true) &
row.ownerId.equals(userId) & row.ownerId.equals(userId) &
row.visibility.equalsValue(AssetVisibility.timeline), (row.visibility.equalsValue(AssetVisibility.timeline) | row.visibility.equalsValue(AssetVisibility.archive)),
groupBy: groupBy, groupBy: groupBy,
origin: TimelineOrigin.favorite, origin: TimelineOrigin.favorite,
); );

View File

@@ -24,6 +24,16 @@ class DriftMemoryPage extends HookConsumerWidget {
const DriftMemoryPage({required this.memories, required this.memoryIndex, super.key}); 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final currentMemory = useState(memories[memoryIndex]); final currentMemory = useState(memories[memoryIndex]);
@@ -202,6 +212,10 @@ class DriftMemoryPage extends HookConsumerWidget {
if (pageNumber < memories.length) { if (pageNumber < memories.length) {
currentMemoryIndex.value = pageNumber; currentMemoryIndex.value = pageNumber;
currentMemory.value = memories[pageNumber]; currentMemory.value = memories[pageNumber];
WidgetsBinding.instance.addPostFrameCallback((_) {
DriftMemoryPage.setMemory(ref, memories[pageNumber]);
});
} }
currentAssetPage.value = 0; currentAssetPage.value = 0;

View File

@@ -77,6 +77,7 @@ class AddActionButton extends ConsumerWidget {
color: context.themeData.scaffoldBackgroundColor, color: context.themeData.scaffoldBackgroundColor,
position: _menuPosition(context), position: _menuPosition(context),
items: items, items: items,
popUpAnimationStyle: AnimationStyle.noAnimation,
); );
if (selected == null) { if (selected == null) {

View File

@@ -3,10 +3,9 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/extensions/translate_extensions.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/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/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/providers/infrastructure/memory.provider.dart';
import 'package:immich_mobile/routing/router.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)), overlayColor: WidgetStateProperty.all(Colors.white.withValues(alpha: 0.1)),
onTap: (index) { onTap: (index) {
ref.read(hapticFeedbackProvider.notifier).heavyImpact(); ref.read(hapticFeedbackProvider.notifier).heavyImpact();
if (memories[index].assets.isNotEmpty) { if (memories[index].assets.isNotEmpty) {
final asset = memories[index].assets[0]; DriftMemoryPage.setMemory(ref, memories[index]);
ref.read(currentAssetNotifier.notifier).setAsset(asset);
if (asset.isVideo) {
ref.read(videoPlaybackValueProvider.notifier).reset();
} }
}
context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index)); context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index));
}, },
children: memories children: memories

View File

@@ -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/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.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/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/debug_print.dart';
import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
// ignore: import_rule_photo_manager // ignore: import_rule_photo_manager
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
const int targetVersion = 18; const int targetVersion = 19;
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async { Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
final hasVersion = Store.tryGet(StoreKey.version) != null; final hasVersion = Store.tryGet(StoreKey.version) != null;
@@ -78,6 +80,12 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
await Store.put(StoreKey.shouldResetSync, true); await Store.put(StoreKey.shouldResetSync, true);
} }
if (version < 19 && Store.isBetaTimelineEnabled) {
if (!await _populateUpdatedAtTime(drift)) {
return;
}
}
if (targetVersion >= 12) { if (targetVersion >= 12) {
await Store.put(StoreKey.version, targetVersion); await Store.put(StoreKey.version, targetVersion);
return; return;
@@ -221,6 +229,32 @@ Future<void> _migrateDeviceAsset(Isar db) async {
}); });
} }
Future<bool> _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<void> migrateDeviceAssetToSqlite(Isar db, Drift drift) async { Future<void> migrateDeviceAssetToSqlite(Isar db, Drift drift) async {
try { try {
final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll(); final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll();

View File

@@ -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');
}
});
});
}

View File

@@ -54,12 +54,7 @@ void main() {
when(() => mockNativeSyncApi.shouldFullSync()).thenAnswer((_) async => false); when(() => mockNativeSyncApi.shouldFullSync()).thenAnswer((_) async => false);
when(() => mockNativeSyncApi.getMediaChanges()).thenAnswer( when(() => mockNativeSyncApi.getMediaChanges()).thenAnswer(
(_) async => SyncDelta( (_) async => SyncDelta(hasChanges: false, updates: const [], deletes: const [], assetAlbums: const {}),
hasChanges: false,
updates: const [],
deletes: const [],
assetAlbums: const {},
),
); );
when(() => mockNativeSyncApi.getTrashedAssets()).thenAnswer((_) async => {}); when(() => mockNativeSyncApi.getTrashedAssets()).thenAnswer((_) async => {});
when(() => mockTrashedLocalAssetRepository.processTrashSnapshot(any())).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'); 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(); final assetEntity = MockAssetEntity();
when(() => assetEntity.getMediaUrl()).thenAnswer((_) async => 'content://local-trash'); when(() => assetEntity.getMediaUrl()).thenAnswer((_) async => 'content://local-trash');
when(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).thenAnswer((_) async => assetEntity); 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.processTrashSnapshot(any())).called(1);
verify(() => mockTrashedLocalAssetRepository.getToTrash()).called(1); verify(() => mockTrashedLocalAssetRepository.getToTrash()).called(1);
@@ -159,8 +160,7 @@ void main() {
verify(() => mockTrashedLocalAssetRepository.applyRestoredAssets(restoredIds)).called(1); verify(() => mockTrashedLocalAssetRepository.applyRestoredAssets(restoredIds)).called(1);
verify(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).called(1); verify(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).called(1);
final moveArgs = final moveArgs = verify(() => mockLocalFilesManager.moveToTrash(captureAny())).captured.single as List<String>;
verify(() => mockLocalFilesManager.moveToTrash(captureAny())).captured.single as List<String>;
expect(moveArgs, ['content://local-trash']); expect(moveArgs, ['content://local-trash']);
final trashArgs = final trashArgs =
verify(() => mockTrashedLocalAssetRepository.trashLocalAsset(captureAny())).captured.single verify(() => mockTrashedLocalAssetRepository.trashLocalAsset(captureAny())).captured.single
@@ -187,4 +187,25 @@ void main() {
verifyNever(() => mockTrashedLocalAssetRepository.trashLocalAsset(any())); 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));
});
});
} }

View File

@@ -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/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/log.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_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/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.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 MockDriftLocalAssetRepository extends Mock implements DriftLocalAssetRepository {}
class MockRemoteAssetRepository extends Mock implements RemoteAssetRepository {}
class MockTrashedLocalAssetRepository extends Mock implements DriftTrashedLocalAssetRepository {} class MockTrashedLocalAssetRepository extends Mock implements DriftTrashedLocalAssetRepository {}
class MockStorageRepository extends Mock implements StorageRepository {} class MockStorageRepository extends Mock implements StorageRepository {}

View File

@@ -5,6 +5,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:fake_async/fake_async.dart'; import 'package:fake_async/fake_async.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/album.entity.dart';
import 'package:immich_mobile/entities/android_device_asset.entity.dart'; import 'package:immich_mobile/entities/android_device_asset.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
@@ -116,4 +117,43 @@ abstract final class TestUtils {
} }
return result; 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,
);
}
} }

View File

@@ -1,385 +1,459 @@
{ {
"name": "js-pdk-template", "name": "plugins",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "js-pdk-template", "name": "plugins",
"version": "1.0.0", "version": "1.0.0",
"license": "BSD-3-Clause", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {
"@extism/js-pdk": "^1.0.1", "@extism/js-pdk": "^1.0.1",
"esbuild": "^0.19.6", "esbuild": "^0.25.0",
"typescript": "^5.3.2" "typescript": "^5.3.2"
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"aix" "aix"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [ "cpu": [
"x64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"netbsd" "netbsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "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, "optional": true,
"os": [ "os": [
"openbsd" "openbsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "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, "optional": true,
"os": [ "os": [
"sunos" "sunos"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@extism/js-pdk": { "node_modules/@extism/js-pdk": {
@@ -389,41 +463,45 @@
"dev": true "dev": true
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.19.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"bin": { "bin": {
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.19.12", "@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.19.12", "@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.19.12", "@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.19.12", "@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.19.12", "@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.19.12", "@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.19.12", "@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-x64": "0.19.12", "@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.19.12", "@esbuild/netbsd-x64": "0.25.12",
"@esbuild/sunos-x64": "0.19.12", "@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/win32-arm64": "0.19.12", "@esbuild/openbsd-x64": "0.25.12",
"@esbuild/win32-ia32": "0.19.12", "@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/win32-x64": "0.19.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": { "node_modules/typescript": {

View File

@@ -13,7 +13,7 @@
"license": "AGPL-3.0", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {
"@extism/js-pdk": "^1.0.1", "@extism/js-pdk": "^1.0.1",
"esbuild": "^0.19.6", "esbuild": "^0.25.0",
"typescript": "^5.3.2" "typescript": "^5.3.2"
} }
} }

56
pnpm-lock.yaml generated
View File

@@ -311,8 +311,8 @@ importers:
specifier: ^1.0.1 specifier: ^1.0.1
version: 1.1.1 version: 1.1.1
esbuild: esbuild:
specifier: ^0.19.6 specifier: ^0.25.0
version: 0.19.12 version: 0.25.12
typescript: typescript:
specifier: ^5.3.2 specifier: ^5.3.2
version: 5.9.3 version: 5.9.3
@@ -456,7 +456,7 @@ importers:
version: 5.10.0 version: 5.10.0
js-yaml: js-yaml:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0 version: 4.1.1
jsonwebtoken: jsonwebtoken:
specifier: ^9.0.2 specifier: ^9.0.2
version: 9.0.2 version: 9.0.2
@@ -726,19 +726,22 @@ importers:
specifier: ^7.4.47 specifier: ^7.4.47
version: 7.4.47 version: 7.4.47
'@photo-sphere-viewer/core': '@photo-sphere-viewer/core':
specifier: ^5.11.5 specifier: ^5.14.0
version: 5.14.0 version: 5.14.0
'@photo-sphere-viewer/equirectangular-video-adapter': '@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)) 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': '@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)) 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': '@photo-sphere-viewer/settings-plugin':
specifier: ^5.11.5 specifier: ^5.14.0
version: 5.14.0(@photo-sphere-viewer/core@5.14.0) version: 5.14.0(@photo-sphere-viewer/core@5.14.0)
'@photo-sphere-viewer/video-plugin': '@photo-sphere-viewer/video-plugin':
specifier: ^5.11.5 specifier: ^5.14.0
version: 5.14.0(@photo-sphere-viewer/core@5.14.0) version: 5.14.0(@photo-sphere-viewer/core@5.14.0)
'@types/geojson': '@types/geojson':
specifier: ^7946.0.16 specifier: ^7946.0.16
@@ -3573,6 +3576,11 @@ packages:
'@photo-sphere-viewer/core': 5.14.0 '@photo-sphere-viewer/core': 5.14.0
'@photo-sphere-viewer/video-plugin': 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': '@photo-sphere-viewer/resolution-plugin@5.14.0':
resolution: {integrity: sha512-PvDMX1h+8FzWdySxiorQ2bSmyBGTPsZjNNFRBqIfmb5C+01aWCIE7kuXodXGHwpXQNcOojsVX9IiX0Vz4CiW4A==} resolution: {integrity: sha512-PvDMX1h+8FzWdySxiorQ2bSmyBGTPsZjNNFRBqIfmb5C+01aWCIE7kuXodXGHwpXQNcOojsVX9IiX0Vz4CiW4A==}
peerDependencies: peerDependencies:
@@ -7697,14 +7705,18 @@ packages:
js-tokens@9.0.1: js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
js-yaml@3.14.1: js-yaml@3.14.2:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
hasBin: true hasBin: true
js-yaml@4.1.0: js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true hasBin: true
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
jsdom@20.0.3: jsdom@20.0.3:
resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -13545,7 +13557,7 @@ snapshots:
'@types/react-router-config': 5.0.11 '@types/react-router-config': 5.0.11
combine-promises: 1.2.0 combine-promises: 1.2.0
fs-extra: 11.3.2 fs-extra: 11.3.2
js-yaml: 4.1.0 js-yaml: 4.1.1
lodash: 4.17.21 lodash: 4.17.21
react: 18.3.1 react: 18.3.1
react-dom: 18.3.1(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) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
fs-extra: 11.3.2 fs-extra: 11.3.2
joi: 17.13.3 joi: 17.13.3
js-yaml: 4.1.0 js-yaml: 4.1.1
lodash: 4.17.21 lodash: 4.17.21
tslib: 2.8.1 tslib: 2.8.1
transitivePeerDependencies: transitivePeerDependencies:
@@ -14026,7 +14038,7 @@ snapshots:
globby: 11.1.0 globby: 11.1.0
gray-matter: 4.0.3 gray-matter: 4.0.3
jiti: 1.21.7 jiti: 1.21.7
js-yaml: 4.1.0 js-yaml: 4.1.1
lodash: 4.17.21 lodash: 4.17.21
micromatch: 4.0.8 micromatch: 4.0.8
p-queue: 6.6.2 p-queue: 6.6.2
@@ -14232,7 +14244,7 @@ snapshots:
globals: 14.0.0 globals: 14.0.0
ignore: 5.3.2 ignore: 5.3.2
import-fresh: 3.3.1 import-fresh: 3.3.1
js-yaml: 4.1.0 js-yaml: 4.1.1
minimatch: 3.1.2 minimatch: 3.1.2
strip-json-comments: 3.1.1 strip-json-comments: 3.1.1
transitivePeerDependencies: transitivePeerDependencies:
@@ -15374,6 +15386,10 @@ snapshots:
'@photo-sphere-viewer/video-plugin': 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)
three: 0.180.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))': '@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: dependencies:
'@photo-sphere-viewer/core': 5.14.0 '@photo-sphere-viewer/core': 5.14.0
@@ -17953,7 +17969,7 @@ snapshots:
cosmiconfig@8.3.6(typescript@5.8.3): cosmiconfig@8.3.6(typescript@5.8.3):
dependencies: dependencies:
import-fresh: 3.3.1 import-fresh: 3.3.1
js-yaml: 4.1.0 js-yaml: 4.1.1
parse-json: 5.2.0 parse-json: 5.2.0
path-type: 4.0.0 path-type: 4.0.0
optionalDependencies: optionalDependencies:
@@ -17962,7 +17978,7 @@ snapshots:
cosmiconfig@8.3.6(typescript@5.9.3): cosmiconfig@8.3.6(typescript@5.9.3):
dependencies: dependencies:
import-fresh: 3.3.1 import-fresh: 3.3.1
js-yaml: 4.1.0 js-yaml: 4.1.1
parse-json: 5.2.0 parse-json: 5.2.0
path-type: 4.0.0 path-type: 4.0.0
optionalDependencies: optionalDependencies:
@@ -19413,7 +19429,7 @@ snapshots:
gray-matter@4.0.3: gray-matter@4.0.3:
dependencies: dependencies:
js-yaml: 3.14.1 js-yaml: 3.14.2
kind-of: 6.0.3 kind-of: 6.0.3
section-matter: 1.0.0 section-matter: 1.0.0
strip-bom-string: 1.0.0 strip-bom-string: 1.0.0
@@ -20121,7 +20137,7 @@ snapshots:
js-tokens@9.0.1: {} js-tokens@9.0.1: {}
js-yaml@3.14.1: js-yaml@3.14.2:
dependencies: dependencies:
argparse: 1.0.10 argparse: 1.0.10
esprima: 4.0.1 esprima: 4.0.1
@@ -20130,6 +20146,10 @@ snapshots:
dependencies: dependencies:
argparse: 2.0.1 argparse: 2.0.1
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
jsdom@20.0.3(canvas@2.11.2): jsdom@20.0.3(canvas@2.11.2):
dependencies: dependencies:
abab: 2.0.6 abab: 2.0.6

View File

@@ -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 \ ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
CI=1 \ CI=1 \
COREPACK_HOME=/tmp \ 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 \ --mount=type=cache,id=mise-tools,target=/buildcache/mise \
cd plugins && mise run build 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 WORKDIR /usr/src/app
ENV NODE_ENV=production \ ENV NODE_ENV=production \

View File

@@ -1,5 +1,5 @@
# dev build # 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 \ ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
CI=1 \ CI=1 \

View File

@@ -154,6 +154,39 @@ describe(BackupService.name, () => {
mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); 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 () => { it('should run a database backup successfully', async () => {
const result = await sut.handleBackupDatabase(); const result = await sut.handleBackupDatabase();
expect(result).toBe(JobStatus.Success); expect(result).toBe(JobStatus.Success);

View File

@@ -69,7 +69,11 @@ export async function buildPostgresLaunchArguments(
args.push('--dbname'); 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 { } else {
args.push( args.push(
'--username', '--username',

View File

@@ -11,13 +11,13 @@
"preview": "vite preview", "preview": "vite preview",
"check:svelte": "svelte-check --no-tsconfig --fail-on-warnings", "check:svelte": "svelte-check --no-tsconfig --fail-on-warnings",
"check:typescript": "tsc --noEmit", "check:typescript": "tsc --noEmit",
"check:watch": "npm run check:svelte -- --watch", "check:watch": "pnpm run check:svelte --watch",
"check:code": "npm run format && npm run lint:p && npm run check:svelte && npm run check:typescript", "check:code": "pnpm run format && pnpm run lint && pnpm run check:svelte && pnpm run check:typescript",
"check:all": "npm run check:code && npm run test:cov", "check:all": "pnpm run check:code && pnpm run test:cov",
"lint": "eslint . --max-warnings 0 --concurrency 4", "lint": "eslint . --max-warnings 0 --concurrency 4",
"lint:fix": "npm run lint -- --fix", "lint:fix": "pnpm run lint --fix",
"format": "prettier --check .", "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", "format:i18n": "pnpm dlx sort-json ../i18n/*.json",
"test": "vitest --run", "test": "vitest --run",
"test:cov": "vitest --coverage", "test:cov": "vitest --coverage",
@@ -31,11 +31,12 @@
"@immich/ui": "^0.43.0", "@immich/ui": "^0.43.0",
"@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.11.5", "@photo-sphere-viewer/core": "^5.14.0",
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.11.5", "@photo-sphere-viewer/equirectangular-video-adapter": "^5.14.0",
"@photo-sphere-viewer/resolution-plugin": "^5.11.5", "@photo-sphere-viewer/markers-plugin": "^5.14.0",
"@photo-sphere-viewer/settings-plugin": "^5.11.5", "@photo-sphere-viewer/resolution-plugin": "^5.14.0",
"@photo-sphere-viewer/video-plugin": "^5.11.5", "@photo-sphere-viewer/settings-plugin": "^5.14.0",
"@photo-sphere-viewer/video-plugin": "^5.14.0",
"@types/geojson": "^7946.0.16", "@types/geojson": "^7946.0.16",
"@zoom-image/core": "^0.41.0", "@zoom-image/core": "^0.41.0",
"@zoom-image/svelte": "^0.3.0", "@zoom-image/svelte": "^0.3.0",

View File

@@ -39,13 +39,17 @@ export const shortcutLabel = (shortcut: Shortcut) => {
/** Determines whether an event should be ignored. The event will be ignored if: /** 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 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 an input field
* - The element dispatching the event is a map canvas
*/ */
export const shouldIgnoreEvent = (event: KeyboardEvent | ClipboardEvent): boolean => { export const shouldIgnoreEvent = (event: KeyboardEvent | ClipboardEvent): boolean => {
if (event.target === event.currentTarget) { if (event.target === event.currentTarget) {
return false; return false;
} }
const type = (event.target as HTMLInputElement).type; 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) => { export const matchesShortcut = (event: KeyboardEvent, shortcut: Shortcut) => {

View File

@@ -33,7 +33,6 @@
import { groupBy } from 'lodash-es'; import { groupBy } from 'lodash-es';
import { onMount, type Snippet } from 'svelte'; import { onMount, type Snippet } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { run } from 'svelte/legacy';
interface Props { interface Props {
ownedAlbums?: AlbumResponseDto[]; ownedAlbums?: AlbumResponseDto[];
@@ -128,65 +127,45 @@
}, },
}; };
let albums: AlbumResponseDto[] = $state([]); let albums = $derived.by(() => {
let filteredAlbums: AlbumResponseDto[] = $state([]); switch (userSettings.filter) {
let groupedAlbums: AlbumGroup[] = $state([]); 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 contextMenuPosition: ContextMenuPosition = $state({ x: 0, y: 0 });
let selectedAlbum: AlbumResponseDto | undefined = $state(); let selectedAlbum: AlbumResponseDto | undefined = $state();
let isOpen = $state(false); let isOpen = $state(false);
// Step 1: Filter between Owned and Shared albums, or both. // TODO get rid of this
run(() => { $effect(() => {
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 }),
}));
albumGroupIds = groupedAlbums.map(({ id }) => id); albumGroupIds = groupedAlbums.map(({ id }) => id);
}); });
@@ -231,7 +210,7 @@
const result = await modalManager.show(AlbumShareModal, { album: selectedAlbum }); const result = await modalManager.show(AlbumShareModal, { album: selectedAlbum });
switch (result?.action) { switch (result?.action) {
case 'sharedUsers': { case 'sharedUsers': {
await handleAddUsers(result.data); await handleAddUsers(selectedAlbum, result.data);
break; break;
} }
@@ -300,22 +279,17 @@
updateRecentAlbumInfo(album); updateRecentAlbumInfo(album);
}; };
const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => { const handleAddUsers = async (album: AlbumResponseDto, albumUsers: AlbumUserAddDto[]) => {
if (!albumToShare) {
return;
}
try { try {
const album = await addUsersToAlbum({ const updatedAlbum = await addUsersToAlbum({
id: albumToShare.id, id: album.id,
addUsersDto: { addUsersDto: {
albumUsers, albumUsers,
}, },
}); });
updateAlbumInfo(album); updateAlbumInfo(updatedAlbum);
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_add_album_users')); handleError(error, $t('errors.unable_to_add_album_users'));
} finally {
albumToShare = null;
} }
}; };

View File

@@ -23,6 +23,7 @@
import { Icon, IconButton, LoadingSpinner, modalManager } from '@immich/ui'; import { Icon, IconButton, LoadingSpinner, modalManager } from '@immich/ui';
import { import {
mdiCalendar, mdiCalendar,
mdiCamera,
mdiCameraIris, mdiCameraIris,
mdiClose, mdiClose,
mdiEye, mdiEye,
@@ -372,9 +373,9 @@
</div> </div>
</div> </div>
{#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.fNumber} {#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.exposureTime || asset.exifInfo?.iso}
<div class="flex gap-4 py-4"> <div class="flex gap-4 py-4">
<div><Icon icon={mdiCameraIris} size="24" /></div> <div><Icon icon={mdiCamera} size="24" /></div>
<div> <div>
{#if asset.exifInfo?.make || asset.exifInfo?.model} {#if asset.exifInfo?.make || asset.exifInfo?.model}
@@ -395,20 +396,34 @@
</p> </p>
{/if} {/if}
{#if asset.exifInfo?.lensModel}
<div class="flex gap-2 text-sm"> <div class="flex gap-2 text-sm">
{#if asset.exifInfo.exposureTime}
<p>{`${asset.exifInfo.exposureTime} s`}</p>
{/if}
{#if asset.exifInfo.iso}
<p>{`ISO ${asset.exifInfo.iso}`}</p>
{/if}
</div>
</div>
</div>
{/if}
{#if asset.exifInfo?.lensModel || asset.exifInfo?.fNumber || asset.exifInfo?.focalLength}
<div class="flex gap-4 py-4">
<div><Icon icon={mdiCameraIris} size="24" /></div>
<div>
{#if asset.exifInfo?.lensModel}
<p> <p>
<a <a
href={resolve( href={resolve(`${AppRoute.SEARCH}?${getMetadataSearchQuery({ lensModel: asset.exifInfo.lensModel })}`)}
`${AppRoute.SEARCH}?${getMetadataSearchQuery({ lensModel: asset.exifInfo.lensModel })}`,
)}
title="{$t('search_for')} {asset.exifInfo.lensModel}" title="{$t('search_for')} {asset.exifInfo.lensModel}"
class="hover:text-primary line-clamp-1" class="hover:text-primary line-clamp-1"
> >
{asset.exifInfo.lensModel} {asset.exifInfo.lensModel}
</a> </a>
</p> </p>
</div>
{/if} {/if}
<div class="flex gap-2 text-sm"> <div class="flex gap-2 text-sm">
@@ -416,19 +431,9 @@
<p>ƒ/{asset.exifInfo.fNumber.toLocaleString($locale)}</p> <p>ƒ/{asset.exifInfo.fNumber.toLocaleString($locale)}</p>
{/if} {/if}
{#if asset.exifInfo.exposureTime}
<p>{`${asset.exifInfo.exposureTime} s`}</p>
{/if}
{#if asset.exifInfo.focalLength} {#if asset.exifInfo.focalLength}
<p>{`${asset.exifInfo.focalLength.toLocaleString($locale)} mm`}</p> <p>{`${asset.exifInfo.focalLength.toLocaleString($locale)} mm`}</p>
{/if} {/if}
{#if asset.exifInfo.iso}
<p>
{`ISO ${asset.exifInfo.iso}`}
</p>
{/if}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { boundingBoxesArray, type Faces } from '$lib/stores/people.store';
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store'; import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
import { import {
EquirectangularAdapter, EquirectangularAdapter,
@@ -8,11 +9,21 @@
type PluginConstructor, type PluginConstructor,
} from '@photo-sphere-viewer/core'; } from '@photo-sphere-viewer/core';
import '@photo-sphere-viewer/core/index.css'; import '@photo-sphere-viewer/core/index.css';
import { MarkersPlugin } from '@photo-sphere-viewer/markers-plugin';
import '@photo-sphere-viewer/markers-plugin/index.css';
import { ResolutionPlugin } from '@photo-sphere-viewer/resolution-plugin'; import { ResolutionPlugin } from '@photo-sphere-viewer/resolution-plugin';
import { SettingsPlugin } from '@photo-sphere-viewer/settings-plugin'; import { SettingsPlugin } from '@photo-sphere-viewer/settings-plugin';
import '@photo-sphere-viewer/settings-plugin/index.css'; import '@photo-sphere-viewer/settings-plugin/index.css';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
// Adapted as well as possible from classlist 'border-solid border-white border-3 rounded-lg'
const FACE_BOX_SVG_STYLE = {
fill: 'rgba(0, 0, 0, 0)',
stroke: '#ffffff',
strokeWidth: '3px',
strokeLinejoin: 'round',
};
interface Props { interface Props {
panorama: string | { source: string }; panorama: string | { source: string };
originalPanorama?: string | { source: string }; originalPanorama?: string | { source: string };
@@ -26,6 +37,62 @@
let container: HTMLDivElement | undefined = $state(); let container: HTMLDivElement | undefined = $state();
let viewer: Viewer; let viewer: Viewer;
let animationInProgress: { cancel: () => void } | undefined;
let previousFaces: Faces[] = [];
const boundingBoxesUnsubscribe = boundingBoxesArray.subscribe((faces: Faces[]) => {
// Debounce; don't do anything when the data didn't actually change.
if (faces === previousFaces) {
return;
}
previousFaces = faces;
if (animationInProgress) {
animationInProgress.cancel();
animationInProgress = undefined;
}
if (!viewer || !viewer.state.textureData || !viewer.getPlugin(MarkersPlugin)) {
return;
}
const markersPlugin = viewer.getPlugin<MarkersPlugin>(MarkersPlugin);
// croppedWidth is the size of the texture, which might be cropped to be less than 360/180 degrees.
// This is what we want because the facial recognition is done on the image, not the sphere.
const currentTextureWidth = viewer.state.textureData.panoData.croppedWidth;
markersPlugin.clearMarkers();
for (const [index, face] of faces.entries()) {
const { boundingBoxX1: x1, boundingBoxY1: y1, boundingBoxX2: x2, boundingBoxY2: y2 } = face;
const ratio = currentTextureWidth / face.imageWidth;
// Pixel values are translated to spherical coordinates and only then added to the panorama;
// no need to recalculate when the texture image changes to the original size.
markersPlugin.addMarker({
id: `face_${index}`,
polygonPixels: [
[x1 * ratio, y1 * ratio],
[x2 * ratio, y1 * ratio],
[x2 * ratio, y2 * ratio],
[x1 * ratio, y2 * ratio],
],
svgStyle: FACE_BOX_SVG_STYLE,
});
}
// Smoothly pan to the highlighted (hovered-over) face.
if (faces.length === 1) {
const { boundingBoxX1: x1, boundingBoxY1: y1, boundingBoxX2: x2, boundingBoxY2: y2, imageWidth: w } = faces[0];
const ratio = currentTextureWidth / w;
const x = ((x1 + x2) * ratio) / 2;
const y = ((y1 + y2) * ratio) / 2;
animationInProgress = viewer.animate({
textureX: x,
textureY: y,
zoom: Math.min(viewer.getZoomLevel(), 75),
speed: 500, // duration in ms
});
}
});
onMount(() => { onMount(() => {
if (!container) { if (!container) {
return; return;
@@ -34,6 +101,7 @@
viewer = new Viewer({ viewer = new Viewer({
adapter, adapter,
plugins: [ plugins: [
MarkersPlugin,
SettingsPlugin, SettingsPlugin,
[ [
ResolutionPlugin, ResolutionPlugin,
@@ -68,7 +136,7 @@
zoomSpeed: 0.5, zoomSpeed: 0.5,
fisheye: false, fisheye: false,
}); });
const resolutionPlugin = viewer.getPlugin(ResolutionPlugin) as ResolutionPlugin; const resolutionPlugin = viewer.getPlugin<ResolutionPlugin>(ResolutionPlugin);
const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => { const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => {
// zoomLevel range: [0, 100] // zoomLevel range: [0, 100]
if (Math.round(zoomLevel) >= 75) { if (Math.round(zoomLevel) >= 75) {
@@ -89,6 +157,7 @@
if (viewer) { if (viewer) {
viewer.destroy(); viewer.destroy();
} }
boundingBoxesUnsubscribe();
}); });
</script> </script>

View File

@@ -1,6 +1,5 @@
import { setDifference, type TimelineDate } from '$lib/utils/timeline-util'; import { setDifference, type TimelineDate } from '$lib/utils/timeline-util';
import { AssetOrder } from '@immich/sdk'; import { AssetOrder } from '@immich/sdk';
import { SvelteSet } from 'svelte/reactivity';
import type { DayGroup } from './day-group.svelte'; import type { DayGroup } from './day-group.svelte';
import type { MonthGroup } from './month-group.svelte'; import type { MonthGroup } from './month-group.svelte';
import type { TimelineAsset } from './types'; import type { TimelineAsset } from './types';
@@ -10,8 +9,10 @@ export class GroupInsertionCache {
[year: number]: { [month: number]: { [day: number]: DayGroup } }; [year: number]: { [month: number]: { [day: number]: DayGroup } };
} = {}; } = {};
unprocessedAssets: TimelineAsset[] = []; unprocessedAssets: TimelineAsset[] = [];
changedDayGroups = new SvelteSet<DayGroup>(); // eslint-disable-next-line svelte/prefer-svelte-reactivity
newDayGroups = new SvelteSet<DayGroup>(); changedDayGroups = new Set<DayGroup>();
// eslint-disable-next-line svelte/prefer-svelte-reactivity
newDayGroups = new Set<DayGroup>();
getDayGroup({ year, month, day }: TimelineDate): DayGroup | undefined { getDayGroup({ year, month, day }: TimelineDate): DayGroup | undefined {
return this.#lookupCache[year]?.[month]?.[day]; return this.#lookupCache[year]?.[month]?.[day];
@@ -32,7 +33,8 @@ export class GroupInsertionCache {
} }
get updatedBuckets() { get updatedBuckets() {
const updated = new SvelteSet<MonthGroup>(); // eslint-disable-next-line svelte/prefer-svelte-reactivity
const updated = new Set<MonthGroup>();
for (const group of this.changedDayGroups) { for (const group of this.changedDayGroups) {
updated.add(group.monthGroup); updated.add(group.monthGroup);
} }
@@ -40,7 +42,8 @@ export class GroupInsertionCache {
} }
get bucketsWithNewDayGroups() { get bucketsWithNewDayGroups() {
const updated = new SvelteSet<MonthGroup>(); // eslint-disable-next-line svelte/prefer-svelte-reactivity
const updated = new Set<MonthGroup>();
for (const group of this.newDayGroups) { for (const group of this.newDayGroups) {
updated.add(group.monthGroup); updated.add(group.monthGroup);
} }

View File

@@ -46,7 +46,7 @@ export async function loadFromTimeBuckets(
} }
} }
const unprocessedAssets = monthGroup.addAssets(bucketResponse); const unprocessedAssets = monthGroup.addAssets(bucketResponse, true);
if (unprocessedAssets.length > 0) { if (unprocessedAssets.length > 0) {
console.error( console.error(
`Warning: getTimeBucket API returning assets not in requested month: ${monthGroup.yearMonth.month}, ${JSON.stringify( `Warning: getTimeBucket API returning assets not in requested month: ${monthGroup.yearMonth.month}, ${JSON.stringify(

View File

@@ -153,7 +153,7 @@ export class MonthGroup {
}; };
} }
addAssets(bucketAssets: TimeBucketAssetResponseDto) { addAssets(bucketAssets: TimeBucketAssetResponseDto, preSorted: boolean) {
const addContext = new GroupInsertionCache(); const addContext = new GroupInsertionCache();
for (let i = 0; i < bucketAssets.id.length; i++) { for (let i = 0; i < bucketAssets.id.length; i++) {
const { localDateTime, fileCreatedAt } = getTimes( const { localDateTime, fileCreatedAt } = getTimes(
@@ -194,6 +194,9 @@ export class MonthGroup {
} }
this.addTimelineAsset(timelineAsset, addContext); this.addTimelineAsset(timelineAsset, addContext);
} }
if (preSorted) {
return addContext.unprocessedAssets;
}
for (const group of addContext.existingDayGroups) { for (const group of addContext.existingDayGroups) {
group.sortAssets(this.#sortOrder); group.sortAssets(this.#sortOrder);

View File

@@ -2,7 +2,7 @@
import { type ServerAboutResponseDto } from '@immich/sdk'; import { type ServerAboutResponseDto } from '@immich/sdk';
import { Icon, Modal, ModalBody } from '@immich/ui'; import { Icon, Modal, ModalBody } from '@immich/ui';
import { mdiBugOutline, mdiFaceAgent, mdiGit, mdiGithub, mdiInformationOutline } from '@mdi/js'; 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'; import { t } from 'svelte-i18n';
interface Props { interface Props {
@@ -13,94 +13,57 @@
let { onClose, info }: Props = $props(); let { onClose, info }: Props = $props();
</script> </script>
{#snippet link(url: string, icon: string | SimpleIcon, text: string)}
<div>
<a href={url} target="_blank" rel="noreferrer">
<Icon {icon} size="1.5em" class="inline-block" />
<p class="font-medium text-primary text-sm underline inline-block">
{text}
</p>
</a>
</div>
{/snippet}
<Modal title={$t('support_and_feedback')} {onClose} size="small"> <Modal title={$t('support_and_feedback')} {onClose} size="small">
<ModalBody> <ModalBody>
<p>{$t('official_immich_resources')}</p> <p>{$t('official_immich_resources')}</p>
<div class="flex flex-col sm:grid sm:grid-cols-2 gap-2 mt-5"> <div class="flex flex-col gap-2 mt-5">
<div> {@render link(
<a href="https://docs.{info.version}.archive.immich.app/overview/introduction" target="_blank" rel="noreferrer"> `https://docs.${info.version}.archive.immich.app/overview/introduction`,
<Icon icon={mdiInformationOutline} size="1.5em" class="inline-block" /> mdiInformationOutline,
<p class="font-medium text-primary text-sm underline inline-block" id="documentation-label"> $t('documentation'),
{$t('documentation')} )}
</p>
</a>
</div>
<div> {@render link('https://github.com/immich-app/immich/', mdiGithub, $t('source'))}
<a href="https://github.com/immich-app/immich/" target="_blank" rel="noreferrer">
<Icon icon={mdiGithub} size="1.5em" class="inline-block" />
<p class="font-medium text-primary text-sm underline inline-block" id="github-label">
{$t('source')}
</p>
</a>
</div>
<div> {@render link('https://discord.immich.app', siDiscord, $t('discord'))}
<a href="https://discord.immich.app" target="_blank" rel="noreferrer">
<Icon icon={siDiscord} class="inline-block" size="1.5em" />
<p class="font-medium text-primary text-sm underline inline-block" id="github-label">
{$t('discord')}
</p>
</a>
</div>
<div> {@render link(
<a href="https://github.com/immich-app/immich/issues/new/choose" target="_blank" rel="noreferrer"> 'https://github.com/immich-app/immich/issues/new/choose',
<Icon icon={mdiBugOutline} size="1.5em" class="inline-block" /> mdiBugOutline,
<p class="font-medium text-primary text-sm underline inline-block" id="github-label"> $t('bugs_and_feature_requests'),
{$t('bugs_and_feature_requests')} )}
</p>
</a>
</div>
</div> </div>
{#if info.thirdPartyBugFeatureUrl || info.thirdPartySourceUrl || info.thirdPartyDocumentationUrl || info.thirdPartySupportUrl} {#if info.thirdPartyBugFeatureUrl || info.thirdPartySourceUrl || info.thirdPartyDocumentationUrl || info.thirdPartySupportUrl}
<p class="mt-5">{$t('third_party_resources')}</p> <p class="mt-5">{$t('third_party_resources')}</p>
<p class="text-sm mt-1"> <p class="text-sm mt-1">
{$t('support_third_party_description')} {$t('support_third_party_description')}
</p> </p>
<div class="flex flex-col sm:grid sm:grid-cols-2 gap-2 mt-5"> <div class="flex flex-col gap-2 mt-5">
{#if info.thirdPartyDocumentationUrl} {#if info.thirdPartyDocumentationUrl}
<div> {@render link(info.thirdPartyDocumentationUrl, mdiInformationOutline, $t('documentation'))}
<a href={info.thirdPartyDocumentationUrl} target="_blank" rel="noreferrer">
<Icon icon={mdiInformationOutline} size="1.5em" class="inline-block" />
<p class="font-medium text-primary text-sm underline inline-block" id="documentation-label">
{$t('documentation')}
</p>
</a>
</div>
{/if} {/if}
{#if info.thirdPartySourceUrl} {#if info.thirdPartySourceUrl}
<div> {@render link(info.thirdPartySourceUrl, mdiGit, $t('source'))}
<a href={info.thirdPartySourceUrl} target="_blank" rel="noreferrer">
<Icon icon={mdiGit} size="1.5em" class="inline-block" />
<p class="font-medium text-primary text-sm underline inline-block" id="github-label">
{$t('source')}
</p>
</a>
</div>
{/if} {/if}
{#if info.thirdPartySupportUrl} {#if info.thirdPartySupportUrl}
<div> {@render link(info.thirdPartySupportUrl, mdiFaceAgent, $t('support'))}
<a href={info.thirdPartySupportUrl} target="_blank" rel="noreferrer">
<Icon icon={mdiFaceAgent} class="inline-block" size="1.5em" />
<p class="font-medium text-primary text-sm underline inline-block" id="github-label">
{$t('support')}
</p>
</a>
</div>
{/if} {/if}
{#if info.thirdPartyBugFeatureUrl} {#if info.thirdPartyBugFeatureUrl}
<div> {@render link(info.thirdPartyBugFeatureUrl, mdiBugOutline, $t('bugs_and_feature_requests'))}
<a href={info.thirdPartyBugFeatureUrl} target="_blank" rel="noreferrer">
<Icon icon={mdiBugOutline} size="1.5em" class="inline-block" />
<p class="font-medium text-primary text-sm underline inline-block" id="github-label">
{$t('bugs_and_feature_requests')}
</p>
</a>
</div>
{/if} {/if}
</div> </div>
{/if} {/if}