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 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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
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.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));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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",
|
"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": {
|
||||||
|
|||||||
@@ -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
56
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
@@ -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 \
|
||||||
|
|||||||
@@ -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 \
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|
||||||
|
<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}
|
{#if asset.exifInfo?.lensModel}
|
||||||
<div class="flex gap-2 text-sm">
|
<p>
|
||||||
<p>
|
<a
|
||||||
<a
|
href={resolve(`${AppRoute.SEARCH}?${getMetadataSearchQuery({ lensModel: asset.exifInfo.lensModel })}`)}
|
||||||
href={resolve(
|
title="{$t('search_for')} {asset.exifInfo.lensModel}"
|
||||||
`${AppRoute.SEARCH}?${getMetadataSearchQuery({ lensModel: asset.exifInfo.lensModel })}`,
|
class="hover:text-primary line-clamp-1"
|
||||||
)}
|
>
|
||||||
title="{$t('search_for')} {asset.exifInfo.lensModel}"
|
{asset.exifInfo.lensModel}
|
||||||
class="hover:text-primary line-clamp-1"
|
</a>
|
||||||
>
|
</p>
|
||||||
{asset.exifInfo.lensModel}
|
|
||||||
</a>
|
|
||||||
</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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user