mirror of
https://github.com/immich-app/immich.git
synced 2025-12-21 09:15:44 +03:00
merge: remote-tracking branch 'origin/main' into feat/database-restores
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -363,14 +363,14 @@ extension on Iterable<PlatformAsset> {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -77,6 +77,7 @@ class AddActionButton extends ConsumerWidget {
|
||||
color: context.themeData.scaffoldBackgroundColor,
|
||||
position: _menuPosition(context),
|
||||
items: items,
|
||||
popUpAnimationStyle: AnimationStyle.noAnimation,
|
||||
);
|
||||
|
||||
if (selected == null) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
||||
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);
|
||||
}
|
||||
|
||||
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<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 {
|
||||
try {
|
||||
final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll();
|
||||
|
||||
165
mobile/test/domain/services/asset.service_test.dart
Normal file
165
mobile/test/domain/services/asset.service_test.dart
Normal 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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<String>;
|
||||
final moveArgs = verify(() => mockLocalFilesManager.moveToTrash(captureAny())).captured.single as List<String>;
|
||||
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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
332
plugins/package-lock.json
generated
332
plugins/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
56
pnpm-lock.yaml
generated
56
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import { Icon, IconButton, LoadingSpinner, modalManager } from '@immich/ui';
|
||||
import {
|
||||
mdiCalendar,
|
||||
mdiCamera,
|
||||
mdiCameraIris,
|
||||
mdiClose,
|
||||
mdiEye,
|
||||
@@ -372,9 +373,9 @@
|
||||
</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><Icon icon={mdiCameraIris} size="24" /></div>
|
||||
<div><Icon icon={mdiCamera} size="24" /></div>
|
||||
|
||||
<div>
|
||||
{#if asset.exifInfo?.make || asset.exifInfo?.model}
|
||||
@@ -395,20 +396,34 @@
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<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}
|
||||
<div class="flex gap-2 text-sm">
|
||||
<p>
|
||||
<a
|
||||
href={resolve(
|
||||
`${AppRoute.SEARCH}?${getMetadataSearchQuery({ lensModel: asset.exifInfo.lensModel })}`,
|
||||
)}
|
||||
title="{$t('search_for')} {asset.exifInfo.lensModel}"
|
||||
class="hover:text-primary line-clamp-1"
|
||||
>
|
||||
{asset.exifInfo.lensModel}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
<a
|
||||
href={resolve(`${AppRoute.SEARCH}?${getMetadataSearchQuery({ lensModel: asset.exifInfo.lensModel })}`)}
|
||||
title="{$t('search_for')} {asset.exifInfo.lensModel}"
|
||||
class="hover:text-primary line-clamp-1"
|
||||
>
|
||||
{asset.exifInfo.lensModel}
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2 text-sm">
|
||||
@@ -416,19 +431,9 @@
|
||||
<p>ƒ/{asset.exifInfo.fNumber.toLocaleString($locale)}</p>
|
||||
{/if}
|
||||
|
||||
{#if asset.exifInfo.exposureTime}
|
||||
<p>{`${asset.exifInfo.exposureTime} s`}</p>
|
||||
{/if}
|
||||
|
||||
{#if asset.exifInfo.focalLength}
|
||||
<p>{`${asset.exifInfo.focalLength.toLocaleString($locale)} mm`}</p>
|
||||
{/if}
|
||||
|
||||
{#if asset.exifInfo.iso}
|
||||
<p>
|
||||
{`ISO ${asset.exifInfo.iso}`}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { boundingBoxesArray, type Faces } from '$lib/stores/people.store';
|
||||
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
|
||||
import {
|
||||
EquirectangularAdapter,
|
||||
@@ -8,11 +9,21 @@
|
||||
type PluginConstructor,
|
||||
} from '@photo-sphere-viewer/core';
|
||||
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 { SettingsPlugin } from '@photo-sphere-viewer/settings-plugin';
|
||||
import '@photo-sphere-viewer/settings-plugin/index.css';
|
||||
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 {
|
||||
panorama: string | { source: string };
|
||||
originalPanorama?: string | { source: string };
|
||||
@@ -26,6 +37,62 @@
|
||||
let container: HTMLDivElement | undefined = $state();
|
||||
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(() => {
|
||||
if (!container) {
|
||||
return;
|
||||
@@ -34,6 +101,7 @@
|
||||
viewer = new Viewer({
|
||||
adapter,
|
||||
plugins: [
|
||||
MarkersPlugin,
|
||||
SettingsPlugin,
|
||||
[
|
||||
ResolutionPlugin,
|
||||
@@ -68,7 +136,7 @@
|
||||
zoomSpeed: 0.5,
|
||||
fisheye: false,
|
||||
});
|
||||
const resolutionPlugin = viewer.getPlugin(ResolutionPlugin) as ResolutionPlugin;
|
||||
const resolutionPlugin = viewer.getPlugin<ResolutionPlugin>(ResolutionPlugin);
|
||||
const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => {
|
||||
// zoomLevel range: [0, 100]
|
||||
if (Math.round(zoomLevel) >= 75) {
|
||||
@@ -89,6 +157,7 @@
|
||||
if (viewer) {
|
||||
viewer.destroy();
|
||||
}
|
||||
boundingBoxesUnsubscribe();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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<DayGroup>();
|
||||
newDayGroups = new SvelteSet<DayGroup>();
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
changedDayGroups = new Set<DayGroup>();
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
newDayGroups = new Set<DayGroup>();
|
||||
|
||||
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<MonthGroup>();
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const updated = new Set<MonthGroup>();
|
||||
for (const group of this.changedDayGroups) {
|
||||
updated.add(group.monthGroup);
|
||||
}
|
||||
@@ -40,7 +42,8 @@ export class GroupInsertionCache {
|
||||
}
|
||||
|
||||
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) {
|
||||
updated.add(group.monthGroup);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
</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">
|
||||
<ModalBody>
|
||||
<p>{$t('official_immich_resources')}</p>
|
||||
<div class="flex flex-col sm:grid sm:grid-cols-2 gap-2 mt-5">
|
||||
<div>
|
||||
<a href="https://docs.{info.version}.archive.immich.app/overview/introduction" 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>
|
||||
<div class="flex flex-col gap-2 mt-5">
|
||||
{@render link(
|
||||
`https://docs.${info.version}.archive.immich.app/overview/introduction`,
|
||||
mdiInformationOutline,
|
||||
$t('documentation'),
|
||||
)}
|
||||
|
||||
<div>
|
||||
<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>
|
||||
{@render link('https://github.com/immich-app/immich/', mdiGithub, $t('source'))}
|
||||
|
||||
<div>
|
||||
<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>
|
||||
{@render link('https://discord.immich.app', siDiscord, $t('discord'))}
|
||||
|
||||
<div>
|
||||
<a href="https://github.com/immich-app/immich/issues/new/choose" 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>
|
||||
{@render link(
|
||||
'https://github.com/immich-app/immich/issues/new/choose',
|
||||
mdiBugOutline,
|
||||
$t('bugs_and_feature_requests'),
|
||||
)}
|
||||
</div>
|
||||
{#if info.thirdPartyBugFeatureUrl || info.thirdPartySourceUrl || info.thirdPartyDocumentationUrl || info.thirdPartySupportUrl}
|
||||
<p class="mt-5">{$t('third_party_resources')}</p>
|
||||
<p class="text-sm mt-1">
|
||||
{$t('support_third_party_description')}
|
||||
</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}
|
||||
<div>
|
||||
<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>
|
||||
{@render link(info.thirdPartyDocumentationUrl, mdiInformationOutline, $t('documentation'))}
|
||||
{/if}
|
||||
|
||||
{#if info.thirdPartySourceUrl}
|
||||
<div>
|
||||
<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>
|
||||
{@render link(info.thirdPartySourceUrl, mdiGit, $t('source'))}
|
||||
{/if}
|
||||
|
||||
{#if info.thirdPartySupportUrl}
|
||||
<div>
|
||||
<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>
|
||||
{@render link(info.thirdPartySupportUrl, mdiFaceAgent, $t('support'))}
|
||||
{/if}
|
||||
|
||||
{#if info.thirdPartyBugFeatureUrl}
|
||||
<div>
|
||||
<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>
|
||||
{@render link(info.thirdPartyBugFeatureUrl, mdiBugOutline, $t('bugs_and_feature_requests'))}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user