Compare commits

..

6 Commits

Author SHA1 Message Date
Jason Rasmussen
b0291b6ad6 fix(server): correctly identify integers 2025-09-10 22:29:03 -04:00
Alex
2d2673c114 fix: repeat timeline migration for first time user (#21794) 2025-09-10 16:27:00 -05:00
Alex
56e5236a39 chore: clean up background tasks (#21785) 2025-09-10 16:17:37 -05:00
Jason Rasmussen
8529f92ebc fix(web): map in album shared link (#21793) 2025-09-10 20:19:43 +00:00
Jason Rasmussen
761ac074c9 fix(web): asset refresh (#21788) 2025-09-10 20:08:15 +00:00
Jason Rasmussen
7e377d3e42 chore: deprecate (#21791) 2025-09-10 15:35:41 -04:00
46 changed files with 514 additions and 373 deletions

View File

@@ -24,6 +24,15 @@ concurrency:
permissions: {}
jobs:
merge_translations:
uses: ./.github/workflows/merge-translations.yml
permissions:
pull-requests: write
secrets:
PUSH_O_MATIC_APP_ID: ${{ secrets.PUSH_O_MATIC_APP_ID }}
PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
bump_version:
runs-on: ubuntu-latest
outputs:

View File

@@ -176,7 +176,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
}
Future<void> _handleBackup({bool processBulk = true}) async {
if (!_isBackupEnabled) {
if (!_isBackupEnabled || _isCleanedUp) {
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
return;
}
@@ -205,30 +205,27 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
}
Future<void> _syncAssets({Duration? hashTimeout}) async {
final futures = <Future<void>>[];
await _ref.read(backgroundSyncProvider).syncLocal();
if (_isCleanedUp) {
return;
}
final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async {
if (_isCleanedUp) {
return;
}
await _ref.read(backgroundSyncProvider).syncRemote();
if (_isCleanedUp) {
return;
}
var hashFuture = _ref.read(backgroundSyncProvider).hashAssets();
if (hashTimeout != null) {
hashFuture = hashFuture.timeout(
hashTimeout,
onTimeout: () {
// Consume cancellation errors as we want to continue processing
},
);
}
var hashFuture = _ref.read(backgroundSyncProvider).hashAssets();
if (hashTimeout != null) {
hashFuture = hashFuture.timeout(
hashTimeout,
onTimeout: () {
// Consume cancellation errors as we want to continue processing
},
);
}
return hashFuture;
});
futures.add(localSyncFuture);
futures.add(_ref.read(backgroundSyncProvider).syncRemote());
await Future.wait(futures);
await hashFuture;
}
}

View File

@@ -5,6 +5,7 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:logging/logging.dart';
final syncLinkedAlbumServiceProvider = Provider(
(ref) => SyncLinkedAlbumService(
@@ -19,7 +20,9 @@ class SyncLinkedAlbumService {
final DriftRemoteAlbumRepository _remoteAlbumRepository;
final DriftAlbumApiRepository _albumApiRepository;
const SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
final _log = Logger("SyncLinkedAlbumService");
Future<void> syncLinkedAlbums(String userId) async {
final selectedAlbums = await _localAlbumRepository.getBackupAlbums();
@@ -48,8 +51,12 @@ class SyncLinkedAlbumService {
}
Future<void> manageLinkedAlbums(List<LocalAlbum> localAlbums, String ownerId) async {
for (final album in localAlbums) {
await _processLocalAlbum(album, ownerId);
try {
for (final album in localAlbums) {
await _processLocalAlbum(album, ownerId);
}
} catch (error, stackTrace) {
_log.severe("Error managing linked albums", error, stackTrace);
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -42,49 +44,13 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
Future<void> _handleMigration() async {
try {
if (widget.switchingToBeta) {
final assetNotifier = ref.read(assetProvider.notifier);
if (assetNotifier.mounted) {
assetNotifier.dispose();
}
final albumNotifier = ref.read(albumProvider.notifier);
if (albumNotifier.mounted) {
albumNotifier.dispose();
}
// Cancel uploads
await Store.put(StoreKey.backgroundBackup, false);
ref
.read(backupProvider.notifier)
.configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {});
ref.read(backupProvider.notifier).setAutoBackup(false);
ref.read(backupProvider.notifier).cancelBackup();
ref.read(manualUploadProvider.notifier).cancelBackup();
// Start listening to new websocket events
ref.read(websocketProvider.notifier).stopListenToOldEvents();
ref.read(websocketProvider.notifier).startListeningToBetaEvents();
final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
if (permission.isGranted) {
await ref.read(backgroundSyncProvider).syncLocal(full: true);
await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider));
await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider));
await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider));
await ref.read(backgroundServiceProvider).disableService();
}
} else {
await ref.read(backgroundSyncProvider).cancel();
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
ref.read(websocketProvider.notifier).startListeningToOldEvents();
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
await ref.read(backgroundWorkerFgServiceProvider).disable();
}
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
await _performMigrationLogic().timeout(
const Duration(minutes: 3),
onTimeout: () async {
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
},
);
if (mounted) {
setState(() {
@@ -102,6 +68,52 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
}
}
Future<void> _performMigrationLogic() async {
if (widget.switchingToBeta) {
final assetNotifier = ref.read(assetProvider.notifier);
if (assetNotifier.mounted) {
assetNotifier.dispose();
}
final albumNotifier = ref.read(albumProvider.notifier);
if (albumNotifier.mounted) {
albumNotifier.dispose();
}
// Cancel uploads
await Store.put(StoreKey.backgroundBackup, false);
ref
.read(backupProvider.notifier)
.configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {});
ref.read(backupProvider.notifier).setAutoBackup(false);
ref.read(backupProvider.notifier).cancelBackup();
ref.read(manualUploadProvider.notifier).cancelBackup();
// Start listening to new websocket events
ref.read(websocketProvider.notifier).stopListenToOldEvents();
ref.read(websocketProvider.notifier).startListeningToBetaEvents();
final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
if (permission.isGranted) {
await ref.read(backgroundSyncProvider).syncLocal(full: true);
await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider));
await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider));
await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider));
await ref.read(backgroundServiceProvider).disableService();
}
} else {
await ref.read(backgroundSyncProvider).cancel();
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
ref.read(websocketProvider.notifier).startListeningToOldEvents();
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
await ref.read(backgroundWorkerFgServiceProvider).disable();
}
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
}
@override
Widget build(BuildContext context) {
return Scaffold(

View File

@@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -47,11 +48,23 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
if (accessToken != null && serverUrl != null && endpoint != null) {
final infoProvider = ref.read(serverInfoProvider.notifier);
final wsProvider = ref.read(websocketProvider.notifier);
final backgroundManager = ref.read(backgroundSyncProvider);
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
(a) {
(_) async {
try {
wsProvider.connect();
infoProvider.getServerInfo();
if (Store.isBetaTimelineEnabled) {
await backgroundManager.syncLocal();
await backgroundManager.syncRemote();
await backgroundManager.hashAssets();
}
if (Store.get(StoreKey.syncAlbums, false)) {
await backgroundManager.syncLinkedAlbum();
}
} catch (e) {
log.severe('Failed establishing connection to the server: $e');
}

View File

@@ -7,8 +7,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
@@ -17,11 +15,7 @@ import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.da
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/migration.dart';
@RoutePage()
class TabShellPage extends ConsumerStatefulWidget {
@@ -32,28 +26,6 @@ class TabShellPage extends ConsumerStatefulWidget {
}
class _TabShellPageState extends ConsumerState<TabShellPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
ref.read(websocketProvider.notifier).connect();
final isEnableBackup = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
await runNewSync(ref, full: true).then((_) async {
if (isEnableBackup) {
final currentUser = ref.read(currentUserProvider);
if (currentUser == null) {
return;
}
await ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
}
});
});
}
@override
Widget build(BuildContext context) {
final isScreenLandscape = context.orientation == Orientation.landscape;

View File

@@ -148,30 +148,25 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
try {
// Run operations sequentially with state checks and error handling for each
_safeRun(backgroundManager.syncLocal(), "syncLocal");
_safeRun(backgroundManager.hashAssets(), "hashAssets");
_safeRun(backgroundManager.syncRemote(), "syncRemote").then((_) {
if (isAlbumLinkedSyncEnable) {
_safeRun(backgroundManager.syncLinkedAlbum(), "syncLinkedAlbum");
}
});
await _safeRun(backgroundManager.syncLocal(), "syncLocal");
await _safeRun(backgroundManager.syncRemote(), "syncRemote");
await _safeRun(backgroundManager.hashAssets(), "hashAssets");
if (isAlbumLinkedSyncEnable) {
await _safeRun(backgroundManager.syncLinkedAlbum(), "syncLinkedAlbum");
}
// Handle backup resume only if still active
if (isEnableBackup) {
final currentUser = _ref.read(currentUserProvider);
if (currentUser != null) {
_safeRun(_ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id), "handleBackupResume");
await _safeRun(
_ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id),
"handleBackupResume",
);
}
}
} catch (e, stackTrace) {
_log.severe("Error during background sync", e, stackTrace);
} finally {
// Ensure lock is released even if operations fail
try {
_log.info("Lock released after background sync operations");
} catch (lockError) {
_log.warning("Failed to release lock after error: $lockError");
}
}
}

View File

@@ -5,7 +5,6 @@ import 'dart:io';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
@@ -23,13 +22,8 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
// ignore: import_rule_photo_manager
import 'package:photo_manager/photo_manager.dart';
@@ -69,7 +63,10 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
// Handle migration only for this version
// TODO: remove when old timeline is removed
if (version == 15) {
final needBetaMigration = Store.tryGet(StoreKey.needBetaMigration);
if (version == 15 && needBetaMigration == null) {
// Check both databases directly instead of relying on cache
final isBeta = Store.tryGet(StoreKey.betaTimeline);
final isNewInstallation = await _isNewInstallation(db, drift);
@@ -77,6 +74,7 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
// For existing installations, only migrate if beta timeline is not enabled (null or false)
if (isNewInstallation || isBeta == true) {
await Store.put(StoreKey.needBetaMigration, false);
await Store.put(StoreKey.betaTimeline, true);
} else {
await resetDriftDatabase(drift);
await Store.put(StoreKey.needBetaMigration, true);
@@ -310,25 +308,6 @@ class _DeviceAsset {
const _DeviceAsset({required this.assetId, this.hash, this.dateTime});
}
Future<List<void>> runNewSync(WidgetRef ref, {bool full = false}) {
ref.read(backupProvider.notifier).cancelBackup();
final backgroundManager = ref.read(backgroundSyncProvider);
final isAlbumLinkedSyncEnable = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
return Future.wait([
backgroundManager.syncLocal(full: full).then((_) {
Logger("runNewSync").fine("Hashing assets after syncLocal");
return backgroundManager.hashAssets();
}),
backgroundManager.syncRemote().then((_) {
if (isAlbumLinkedSyncEnable) {
return backgroundManager.syncLinkedAlbum();
}
}),
]);
}
Future<void> resetDriftDatabase(Drift drift) async {
// https://github.com/simolus3/drift/commit/bd80a46264b6dd833ef4fd87fffc03f5a832ab41#diff-3f879e03b4a35779344ef16170b9353608dd9c42385f5402ec6035aac4dd8a04R76-R94
final database = drift.attachedDatabase;

View File

@@ -10,9 +10,11 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/oauth.provider.dart';
@@ -161,6 +163,18 @@ class LoginForm extends HookConsumerWidget {
serverEndpointController.text = 'http://10.1.15.216:2283/api';
}
Future<void> handleSyncFlow() async {
final backgroundManager = ref.read(backgroundSyncProvider);
await backgroundManager.syncLocal(full: true);
await backgroundManager.syncRemote();
await backgroundManager.hashAssets();
if (Store.get(StoreKey.syncAlbums, false)) {
await backgroundManager.syncLinkedAlbum();
}
}
login() async {
TextInput.finishAutofillContext();
@@ -178,7 +192,7 @@ class LoginForm extends HookConsumerWidget {
final isBeta = Store.isBetaTimelineEnabled;
if (isBeta) {
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
handleSyncFlow();
context.replaceRoute(const TabShellRoute());
return;
}
@@ -276,6 +290,7 @@ class LoginForm extends HookConsumerWidget {
}
if (isBeta) {
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
handleSyncFlow();
context.replaceRoute(const TabShellRoute());
return;
}

View File

@@ -104,7 +104,6 @@ class SyncStatusAndActions extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 16, bottom: 32),
child: ListView(
children: [
_SectionHeaderText(text: "assets".t(context: context)),
const _SyncStatsCounts(),
const Divider(height: 1, indent: 16, endIndent: 16),
const SizedBox(height: 24),
@@ -270,7 +269,10 @@ class _SyncStatsCounts extends ConsumerWidget {
final localHashedCount = snapshot.data![4]! as int;
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SectionHeaderText(text: "assets".t(context: context)),
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Flex(

View File

@@ -107,7 +107,7 @@ Class | Method | HTTP request | Description
*AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics |
*AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random |
*AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback |
*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | replaceAsset
*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace the asset with new file, without changing its id
*AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs |
*AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} |
*AssetsApi* | [**updateAssetMetadata**](doc//AssetsApi.md#updateassetmetadata) | **PUT** /assets/{id}/metadata |
@@ -128,6 +128,7 @@ Class | Method | HTTP request | Description
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
*DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} |
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random |
*DeprecatedApi* | [**replaceAsset**](doc//DeprecatedApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace the asset with new file, without changing its id
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info |
*DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} |

View File

@@ -608,8 +608,8 @@ class AssetsApi {
///
/// Parameters:
///
/// * [num] count:
Future<Response> getRandomWithHttpInfo({ num? count, }) async {
/// * [int] count:
Future<Response> getRandomWithHttpInfo({ int? count, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/assets/random';
@@ -642,8 +642,8 @@ class AssetsApi {
///
/// Parameters:
///
/// * [num] count:
Future<List<AssetResponseDto>?> getRandom({ num? count, }) async {
/// * [int] count:
Future<List<AssetResponseDto>?> getRandom({ int? count, }) async {
final response = await getRandomWithHttpInfo( count: count, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
@@ -729,9 +729,9 @@ class AssetsApi {
return null;
}
/// replaceAsset
/// Replace the asset with new file, without changing its id
///
/// Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.
/// This property was deprecated in v1.142.0. Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.
///
/// Note: This method returns the HTTP [Response].
///
@@ -823,9 +823,9 @@ class AssetsApi {
);
}
/// replaceAsset
/// Replace the asset with new file, without changing its id
///
/// Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.
/// This property was deprecated in v1.142.0. Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.
///
/// Parameters:
///

View File

@@ -75,8 +75,8 @@ class DeprecatedApi {
///
/// Parameters:
///
/// * [num] count:
Future<Response> getRandomWithHttpInfo({ num? count, }) async {
/// * [int] count:
Future<Response> getRandomWithHttpInfo({ int? count, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/assets/random';
@@ -109,8 +109,8 @@ class DeprecatedApi {
///
/// Parameters:
///
/// * [num] count:
Future<List<AssetResponseDto>?> getRandom({ num? count, }) async {
/// * [int] count:
Future<List<AssetResponseDto>?> getRandom({ int? count, }) async {
final response = await getRandomWithHttpInfo( count: count, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
@@ -127,4 +127,138 @@ class DeprecatedApi {
}
return null;
}
/// Replace the asset with new file, without changing its id
///
/// This property was deprecated in v1.142.0. Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [MultipartFile] assetData (required):
///
/// * [String] deviceAssetId (required):
///
/// * [String] deviceId (required):
///
/// * [DateTime] fileCreatedAt (required):
///
/// * [DateTime] fileModifiedAt (required):
///
/// * [String] key:
///
/// * [String] slug:
///
/// * [String] duration:
///
/// * [String] filename:
Future<Response> replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/assets/{id}/original'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
if (slug != null) {
queryParams.addAll(_queryParams('', 'slug', slug));
}
const contentTypes = <String>['multipart/form-data'];
bool hasFields = false;
final mp = MultipartRequest('PUT', Uri.parse(apiPath));
if (assetData != null) {
hasFields = true;
mp.fields[r'assetData'] = assetData.field;
mp.files.add(assetData);
}
if (deviceAssetId != null) {
hasFields = true;
mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId);
}
if (deviceId != null) {
hasFields = true;
mp.fields[r'deviceId'] = parameterToString(deviceId);
}
if (duration != null) {
hasFields = true;
mp.fields[r'duration'] = parameterToString(duration);
}
if (fileCreatedAt != null) {
hasFields = true;
mp.fields[r'fileCreatedAt'] = parameterToString(fileCreatedAt);
}
if (fileModifiedAt != null) {
hasFields = true;
mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt);
}
if (filename != null) {
hasFields = true;
mp.fields[r'filename'] = parameterToString(filename);
}
if (hasFields) {
postBody = mp;
}
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Replace the asset with new file, without changing its id
///
/// This property was deprecated in v1.142.0. Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [MultipartFile] assetData (required):
///
/// * [String] deviceAssetId (required):
///
/// * [String] deviceId (required):
///
/// * [DateTime] fileCreatedAt (required):
///
/// * [DateTime] fileModifiedAt (required):
///
/// * [String] key:
///
/// * [String] slug:
///
/// * [String] duration:
///
/// * [String] filename:
Future<AssetMediaResponseDto?> replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async {
final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, duration: duration, filename: filename, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetMediaResponseDto',) as AssetMediaResponseDto;
}
return null;
}
}

View File

@@ -167,14 +167,14 @@ class PeopleApi {
///
/// * [String] closestPersonId:
///
/// * [num] page:
/// * [int] page:
/// Page number for pagination
///
/// * [num] size:
/// * [int] size:
/// Number of items per page
///
/// * [bool] withHidden:
Future<Response> getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async {
Future<Response> getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/people';
@@ -223,14 +223,14 @@ class PeopleApi {
///
/// * [String] closestPersonId:
///
/// * [num] page:
/// * [int] page:
/// Page number for pagination
///
/// * [num] size:
/// * [int] size:
/// Number of items per page
///
/// * [bool] withHidden:
Future<PeopleResponseDto?> getAllPeople({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async {
Future<PeopleResponseDto?> getAllPeople({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async {
final response = await getAllPeopleWithHttpInfo( closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));

View File

@@ -348,9 +348,9 @@ class SearchApi {
///
/// * [List<String>] personIds:
///
/// * [num] rating:
/// * [int] rating:
///
/// * [num] size:
/// * [int] size:
///
/// * [String] state:
///
@@ -375,7 +375,7 @@ class SearchApi {
/// * [bool] withDeleted:
///
/// * [bool] withExif:
Future<Response> searchLargeAssetsWithHttpInfo({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, List<String>? personIds, num? rating, num? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
Future<Response> searchLargeAssetsWithHttpInfo({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, List<String>? personIds, int? rating, int? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/search/large-assets';
@@ -532,9 +532,9 @@ class SearchApi {
///
/// * [List<String>] personIds:
///
/// * [num] rating:
/// * [int] rating:
///
/// * [num] size:
/// * [int] size:
///
/// * [String] state:
///
@@ -559,7 +559,7 @@ class SearchApi {
/// * [bool] withDeleted:
///
/// * [bool] withExif:
Future<List<AssetResponseDto>?> searchLargeAssets({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, List<String>? personIds, num? rating, num? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
Future<List<AssetResponseDto>?> searchLargeAssets({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, List<String>? personIds, int? rating, int? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceId: deviceId, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));

View File

@@ -40,7 +40,7 @@ class AssetBulkUpdateDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? dateTimeRelative;
int? dateTimeRelative;
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -68,7 +68,7 @@ class AssetBulkUpdateDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? latitude;
double? latitude;
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -76,7 +76,7 @@ class AssetBulkUpdateDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? longitude;
double? longitude;
/// Minimum value: -1
/// Maximum value: 5
@@ -86,7 +86,7 @@ class AssetBulkUpdateDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? rating;
int? rating;
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -202,16 +202,16 @@ class AssetBulkUpdateDto {
return AssetBulkUpdateDto(
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
dateTimeRelative: num.parse('${json[r'dateTimeRelative']}'),
dateTimeRelative: mapValueOfType<int>(json, r'dateTimeRelative'),
description: mapValueOfType<String>(json, r'description'),
duplicateId: mapValueOfType<String>(json, r'duplicateId'),
ids: json[r'ids'] is Iterable
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
: const [],
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
latitude: num.parse('${json[r'latitude']}'),
longitude: num.parse('${json[r'longitude']}'),
rating: num.parse('${json[r'rating']}'),
latitude: (mapValueOfType<num>(json, r'latitude')).toDouble(),
longitude: (mapValueOfType<num>(json, r'longitude')).toDouble(),
rating: mapValueOfType<int>(json, r'rating'),
timeZone: mapValueOfType<String>(json, r'timeZone'),
visibility: AssetVisibility.fromJson(json[r'visibility']),
);

View File

@@ -23,7 +23,7 @@ class DatabaseBackupConfig {
bool enabled;
/// Minimum value: 1
num keepLastAmount;
int keepLastAmount;
@override
bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupConfig &&
@@ -60,7 +60,7 @@ class DatabaseBackupConfig {
return DatabaseBackupConfig(
cronExpression: mapValueOfType<String>(json, r'cronExpression')!,
enabled: mapValueOfType<bool>(json, r'enabled')!,
keepLastAmount: num.parse('${json[r'keepLastAmount']}'),
keepLastAmount: mapValueOfType<int>(json, r'keepLastAmount')!,
);
}
return null;

View File

@@ -45,25 +45,25 @@ class ExifResponseDto {
String? description;
num? exifImageHeight;
int? exifImageHeight;
num? exifImageWidth;
int? exifImageWidth;
String? exposureTime;
num? fNumber;
double? fNumber;
int? fileSizeInByte;
num? focalLength;
double? focalLength;
num? iso;
int? iso;
num? latitude;
double? latitude;
String? lensModel;
num? longitude;
double? longitude;
String? make;
@@ -75,7 +75,7 @@ class ExifResponseDto {
String? projectionType;
num? rating;
int? rating;
String? state;
@@ -263,38 +263,22 @@ class ExifResponseDto {
country: mapValueOfType<String>(json, r'country'),
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''),
description: mapValueOfType<String>(json, r'description'),
exifImageHeight: json[r'exifImageHeight'] == null
? null
: num.parse('${json[r'exifImageHeight']}'),
exifImageWidth: json[r'exifImageWidth'] == null
? null
: num.parse('${json[r'exifImageWidth']}'),
exifImageHeight: mapValueOfType<int>(json, r'exifImageHeight'),
exifImageWidth: mapValueOfType<int>(json, r'exifImageWidth'),
exposureTime: mapValueOfType<String>(json, r'exposureTime'),
fNumber: json[r'fNumber'] == null
? null
: num.parse('${json[r'fNumber']}'),
fNumber: (mapValueOfType<num>(json, r'fNumber'))?.toDouble(),
fileSizeInByte: mapValueOfType<int>(json, r'fileSizeInByte'),
focalLength: json[r'focalLength'] == null
? null
: num.parse('${json[r'focalLength']}'),
iso: json[r'iso'] == null
? null
: num.parse('${json[r'iso']}'),
latitude: json[r'latitude'] == null
? null
: num.parse('${json[r'latitude']}'),
focalLength: (mapValueOfType<num>(json, r'focalLength'))?.toDouble(),
iso: mapValueOfType<int>(json, r'iso'),
latitude: (mapValueOfType<num>(json, r'latitude'))?.toDouble(),
lensModel: mapValueOfType<String>(json, r'lensModel'),
longitude: json[r'longitude'] == null
? null
: num.parse('${json[r'longitude']}'),
longitude: (mapValueOfType<num>(json, r'longitude'))?.toDouble(),
make: mapValueOfType<String>(json, r'make'),
model: mapValueOfType<String>(json, r'model'),
modifyDate: mapDateTime(json, r'modifyDate', r''),
orientation: mapValueOfType<String>(json, r'orientation'),
projectionType: mapValueOfType<String>(json, r'projectionType'),
rating: json[r'rating'] == null
? null
: num.parse('${json[r'rating']}'),
rating: mapValueOfType<int>(json, r'rating'),
state: mapValueOfType<String>(json, r'state'),
timeZone: mapValueOfType<String>(json, r'timeZone'),
);

View File

@@ -207,7 +207,7 @@ class MetadataSearchDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? page;
int? page;
List<String> personIds;
@@ -227,7 +227,7 @@ class MetadataSearchDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? rating;
int? rating;
/// Minimum value: 1
/// Maximum value: 1000
@@ -237,7 +237,7 @@ class MetadataSearchDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? size;
int? size;
String? state;
@@ -685,13 +685,13 @@ class MetadataSearchDto {
order: AssetOrder.fromJson(json[r'order']) ?? AssetOrder.desc,
originalFileName: mapValueOfType<String>(json, r'originalFileName'),
originalPath: mapValueOfType<String>(json, r'originalPath'),
page: num.parse('${json[r'page']}'),
page: mapValueOfType<int>(json, r'page'),
personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
previewPath: mapValueOfType<String>(json, r'previewPath'),
rating: num.parse('${json[r'rating']}'),
size: num.parse('${json[r'size']}'),
rating: mapValueOfType<int>(json, r'rating'),
size: mapValueOfType<int>(json, r'size'),
state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)

View File

@@ -17,7 +17,7 @@ class OnThisDayDto {
});
/// Minimum value: 1
num year;
int year;
@override
bool operator ==(Object other) => identical(this, other) || other is OnThisDayDto &&
@@ -46,7 +46,7 @@ class OnThisDayDto {
final json = value.cast<String, dynamic>();
return OnThisDayDto(
year: num.parse('${json[r'year']}'),
year: mapValueOfType<int>(json, r'year')!,
);
}
return null;

View File

@@ -36,9 +36,9 @@ class PlacesResponseDto {
///
String? admin2name;
num latitude;
double latitude;
num longitude;
double longitude;
String name;
@@ -91,8 +91,8 @@ class PlacesResponseDto {
return PlacesResponseDto(
admin1name: mapValueOfType<String>(json, r'admin1name'),
admin2name: mapValueOfType<String>(json, r'admin2name'),
latitude: num.parse('${json[r'latitude']}'),
longitude: num.parse('${json[r'longitude']}'),
latitude: (mapValueOfType<num>(json, r'latitude')!).toDouble(),
longitude: (mapValueOfType<num>(json, r'longitude')!).toDouble(),
name: mapValueOfType<String>(json, r'name')!,
);
}

View File

@@ -141,7 +141,7 @@ class RandomSearchDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? rating;
int? rating;
/// Minimum value: 1
/// Maximum value: 1000
@@ -151,7 +151,7 @@ class RandomSearchDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? size;
int? size;
String? state;
@@ -513,8 +513,8 @@ class RandomSearchDto {
personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
rating: num.parse('${json[r'rating']}'),
size: num.parse('${json[r'size']}'),
rating: mapValueOfType<int>(json, r'rating'),
size: mapValueOfType<int>(json, r'size'),
state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)

View File

@@ -43,7 +43,7 @@ class SessionCreateDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? duration;
int? duration;
@override
bool operator ==(Object other) => identical(this, other) || other is SessionCreateDto &&
@@ -92,7 +92,7 @@ class SessionCreateDto {
return SessionCreateDto(
deviceOS: mapValueOfType<String>(json, r'deviceOS'),
deviceType: mapValueOfType<String>(json, r'deviceType'),
duration: num.parse('${json[r'duration']}'),
duration: mapValueOfType<int>(json, r'duration'),
);
}
return null;

View File

@@ -148,7 +148,7 @@ class SmartSearchDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? page;
int? page;
List<String> personIds;
@@ -176,7 +176,7 @@ class SmartSearchDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? rating;
int? rating;
/// Minimum value: 1
/// Maximum value: 1000
@@ -186,7 +186,7 @@ class SmartSearchDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? size;
int? size;
String? state;
@@ -544,14 +544,14 @@ class SmartSearchDto {
libraryId: mapValueOfType<String>(json, r'libraryId'),
make: mapValueOfType<String>(json, r'make'),
model: mapValueOfType<String>(json, r'model'),
page: num.parse('${json[r'page']}'),
page: mapValueOfType<int>(json, r'page'),
personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
query: mapValueOfType<String>(json, r'query'),
queryAssetId: mapValueOfType<String>(json, r'queryAssetId'),
rating: num.parse('${json[r'rating']}'),
size: num.parse('${json[r'size']}'),
rating: mapValueOfType<int>(json, r'rating'),
size: mapValueOfType<int>(json, r'size'),
state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)

View File

@@ -145,7 +145,7 @@ class StatisticsSearchDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? rating;
int? rating;
String? state;
@@ -448,7 +448,7 @@ class StatisticsSearchDto {
personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
rating: num.parse('${json[r'rating']}'),
rating: mapValueOfType<int>(json, r'rating'),
state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)

View File

@@ -28,7 +28,7 @@ class SystemConfigSmtpTransportDto {
/// Minimum value: 0
/// Maximum value: 65535
num port;
int port;
String username;
@@ -74,7 +74,7 @@ class SystemConfigSmtpTransportDto {
host: mapValueOfType<String>(json, r'host')!,
ignoreCert: mapValueOfType<bool>(json, r'ignoreCert')!,
password: mapValueOfType<String>(json, r'password')!,
port: num.parse('${json[r'port']}'),
port: mapValueOfType<int>(json, r'port')!,
username: mapValueOfType<String>(json, r'username')!,
);
}

View File

@@ -53,7 +53,7 @@ class UpdateAssetDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? latitude;
double? latitude;
String? livePhotoVideoId;
@@ -63,7 +63,7 @@ class UpdateAssetDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? longitude;
double? longitude;
/// Minimum value: -1
/// Maximum value: 5
@@ -73,7 +73,7 @@ class UpdateAssetDto {
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? rating;
int? rating;
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -166,10 +166,10 @@ class UpdateAssetDto {
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
description: mapValueOfType<String>(json, r'description'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
latitude: num.parse('${json[r'latitude']}'),
latitude: (mapValueOfType<num>(json, r'latitude')).toDouble(),
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
longitude: num.parse('${json[r'longitude']}'),
rating: num.parse('${json[r'rating']}'),
longitude: (mapValueOfType<num>(json, r'longitude')).toDouble(),
rating: mapValueOfType<int>(json, r'rating'),
visibility: AssetVisibility.fromJson(json[r'visibility']),
);
}

View File

@@ -2035,7 +2035,7 @@
"in": "query",
"schema": {
"minimum": 1,
"type": "number"
"type": "integer"
}
}
],
@@ -2504,7 +2504,8 @@
"description": "This endpoint requires the `asset.download` permission."
},
"put": {
"description": "Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.",
"deprecated": true,
"description": "This property was deprecated in v1.142.0. Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.",
"operationId": "replaceAsset",
"parameters": [
{
@@ -2566,12 +2567,14 @@
"api_key": []
}
],
"summary": "replaceAsset",
"summary": "Replace the asset with new file, without changing its id",
"tags": [
"Assets"
"Assets",
"Deprecated"
],
"x-immich-lifecycle": {
"addedAt": "v1.106.0"
"addedAt": "v1.106.0",
"deprecatedAt": "v1.142.0"
},
"x-immich-permission": "asset.replace"
}
@@ -5239,7 +5242,7 @@
"schema": {
"minimum": 1,
"default": 1,
"type": "number"
"type": "integer"
}
},
{
@@ -5251,7 +5254,7 @@
"minimum": 1,
"maximum": 1000,
"default": 500,
"type": "number"
"type": "integer"
}
},
{
@@ -5952,7 +5955,7 @@
"schema": {
"minimum": -1,
"maximum": 5,
"type": "number"
"type": "integer"
}
},
{
@@ -5962,7 +5965,7 @@
"schema": {
"minimum": 1,
"maximum": 1000,
"type": "number"
"type": "integer"
}
},
{
@@ -10405,7 +10408,7 @@
"type": "string"
},
"dateTimeRelative": {
"type": "number"
"type": "integer"
},
"description": {
"type": "string"
@@ -10425,15 +10428,17 @@
"type": "boolean"
},
"latitude": {
"format": "double",
"type": "number"
},
"longitude": {
"format": "double",
"type": "number"
},
"rating": {
"maximum": 5,
"minimum": -1,
"type": "number"
"type": "integer"
},
"timeZone": {
"type": "string"
@@ -11568,7 +11573,7 @@
},
"keepLastAmount": {
"minimum": 1,
"type": "number"
"type": "integer"
}
},
"required": [
@@ -11761,12 +11766,12 @@
"exifImageHeight": {
"default": null,
"nullable": true,
"type": "number"
"type": "integer"
},
"exifImageWidth": {
"default": null,
"nullable": true,
"type": "number"
"type": "integer"
},
"exposureTime": {
"default": null,
@@ -11775,6 +11780,7 @@
},
"fNumber": {
"default": null,
"format": "double",
"nullable": true,
"type": "number"
},
@@ -11786,16 +11792,18 @@
},
"focalLength": {
"default": null,
"format": "double",
"nullable": true,
"type": "number"
},
"iso": {
"default": null,
"nullable": true,
"type": "number"
"type": "integer"
},
"latitude": {
"default": null,
"format": "double",
"nullable": true,
"type": "number"
},
@@ -11806,6 +11814,7 @@
},
"longitude": {
"default": null,
"format": "double",
"nullable": true,
"type": "number"
},
@@ -11838,7 +11847,7 @@
"rating": {
"default": null,
"nullable": true,
"type": "number"
"type": "integer"
},
"state": {
"default": null,
@@ -12591,7 +12600,7 @@
},
"page": {
"minimum": 1,
"type": "number"
"type": "integer"
},
"personIds": {
"items": {
@@ -12606,12 +12615,12 @@
"rating": {
"maximum": 5,
"minimum": -1,
"type": "number"
"type": "integer"
},
"size": {
"maximum": 1000,
"minimum": 1,
"type": "number"
"type": "integer"
},
"state": {
"nullable": true,
@@ -12888,7 +12897,7 @@
"properties": {
"year": {
"minimum": 1,
"type": "number"
"type": "integer"
}
},
"required": [
@@ -13437,9 +13446,11 @@
"type": "string"
},
"latitude": {
"format": "double",
"type": "number"
},
"longitude": {
"format": "double",
"type": "number"
},
"name": {
@@ -13563,12 +13574,12 @@
"rating": {
"maximum": 5,
"minimum": -1,
"type": "number"
"type": "integer"
},
"size": {
"maximum": 1000,
"minimum": 1,
"type": "number"
"type": "integer"
},
"state": {
"nullable": true,
@@ -14251,7 +14262,7 @@
"duration": {
"description": "session duration, in seconds",
"minimum": 1,
"type": "number"
"type": "integer"
}
},
"type": "object"
@@ -14647,7 +14658,7 @@
},
"page": {
"minimum": 1,
"type": "number"
"type": "integer"
},
"personIds": {
"items": {
@@ -14666,12 +14677,12 @@
"rating": {
"maximum": 5,
"minimum": -1,
"type": "number"
"type": "integer"
},
"size": {
"maximum": 1000,
"minimum": 1,
"type": "number"
"type": "integer"
},
"state": {
"nullable": true,
@@ -14860,7 +14871,7 @@
"rating": {
"maximum": 5,
"minimum": -1,
"type": "number"
"type": "integer"
},
"state": {
"nullable": true,
@@ -16680,7 +16691,7 @@
"port": {
"maximum": 65535,
"minimum": 0,
"type": "number"
"type": "integer"
},
"username": {
"type": "string"
@@ -17296,6 +17307,7 @@
"type": "boolean"
},
"latitude": {
"format": "double",
"type": "number"
},
"livePhotoVideoId": {
@@ -17304,12 +17316,13 @@
"type": "string"
},
"longitude": {
"format": "double",
"type": "number"
},
"rating": {
"maximum": 5,
"minimum": -1,
"type": "number"
"type": "integer"
},
"visibility": {
"allOf": [

View File

@@ -2368,7 +2368,7 @@ export function downloadAsset({ id, key, slug }: {
}));
}
/**
* replaceAsset
* Replace the asset with new file, without changing its id
*/
export function replaceAsset({ id, key, slug, assetMediaReplaceDto }: {
id: string;

View File

@@ -96,8 +96,9 @@ export class AssetMediaController {
@Put(':id/original')
@UseInterceptors(FileUploadInterceptor)
@ApiConsumes('multipart/form-data')
@EndpointLifecycle({ addedAt: 'v1.106.0' })
@ApiOperation({
@EndpointLifecycle({
addedAt: 'v1.106.0',
deprecatedAt: 'v1.142.0',
summary: 'replaceAsset',
description: 'Replace the asset with new file, without changing its id',
})

View File

@@ -1,5 +1,5 @@
import { SetMetadata, applyDecorators } from '@nestjs/common';
import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
import { ApiExtension, ApiOperation, ApiOperationOptions, ApiProperty, ApiTags } from '@nestjs/swagger';
import _ from 'lodash';
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
import { ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum';
@@ -159,12 +159,21 @@ type LifecycleMetadata = {
deprecatedAt?: LifecycleRelease;
};
export const EndpointLifecycle = ({ addedAt, deprecatedAt }: LifecycleMetadata) => {
export const EndpointLifecycle = ({
addedAt,
deprecatedAt,
description,
...options
}: LifecycleMetadata & ApiOperationOptions) => {
const decorators: MethodDecorator[] = [ApiExtension(LIFECYCLE_EXTENSION, { addedAt, deprecatedAt })];
if (deprecatedAt) {
decorators.push(
ApiTags('Deprecated'),
ApiOperation({ deprecated: true, description: DEPRECATED_IN_PREFIX + deprecatedAt }),
ApiOperation({
deprecated: true,
description: DEPRECATED_IN_PREFIX + deprecatedAt + (description ? `. ${description}` : ''),
...options,
}),
);
}

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsArray,
@@ -46,17 +46,20 @@ export class UpdateAssetBase {
@ValidateGPS()
@IsLatitude()
@IsNotEmpty()
@ApiPropertyOptional({ type: 'number', format: 'double' })
latitude?: number;
@ValidateGPS()
@IsLongitude()
@IsNotEmpty()
@ApiPropertyOptional({ type: 'number', format: 'double' })
longitude?: number;
@Optional()
@IsInt()
@Max(5)
@Min(-1)
@ApiPropertyOptional({ type: 'integer' })
rating?: number;
@Optional()
@@ -74,6 +77,7 @@ export class AssetBulkUpdateDto extends UpdateAssetBase {
@IsNotSiblingOf(['dateTimeOriginal'])
@Optional()
@IsInt()
@ApiPropertyOptional({ type: 'integer' })
dateTimeRelative?: number;
@IsNotSiblingOf(['dateTimeOriginal'])
@@ -92,6 +96,7 @@ export class RandomAssetsDto {
@IsInt()
@IsPositive()
@Type(() => Number)
@ApiPropertyOptional({ type: 'integer' })
count?: number;
}

View File

@@ -1,30 +1,37 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Exif } from 'src/database';
export class ExifResponseDto {
make?: string | null = null;
model?: string | null = null;
@ApiPropertyOptional({ type: 'integer' })
exifImageWidth?: number | null = null;
@ApiPropertyOptional({ type: 'integer' })
exifImageHeight?: number | null = null;
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiPropertyOptional({ type: 'integer', format: 'int64' })
fileSizeInByte?: number | null = null;
orientation?: string | null = null;
dateTimeOriginal?: Date | null = null;
modifyDate?: Date | null = null;
timeZone?: string | null = null;
lensModel?: string | null = null;
@ApiPropertyOptional({ type: 'number', format: 'double' })
fNumber?: number | null = null;
@ApiPropertyOptional({ type: 'number', format: 'double' })
focalLength?: number | null = null;
@ApiPropertyOptional({ type: 'integer' })
iso?: number | null = null;
exposureTime?: string | null = null;
@ApiPropertyOptional({ type: 'number', format: 'double' })
latitude?: number | null = null;
@ApiPropertyOptional({ type: 'number', format: 'double' })
longitude?: number | null = null;
city?: string | null = null;
state?: string | null = null;
country?: string | null = null;
description?: string | null = null;
projectionType?: string | null = null;
@ApiPropertyOptional({ type: 'integer' })
rating?: number | null = null;
}

View File

@@ -4,12 +4,12 @@ import { IsLatitude, IsLongitude } from 'class-validator';
import { ValidateBoolean, ValidateDate } from 'src/validation';
export class MapReverseGeocodeDto {
@ApiProperty({ format: 'double' })
@ApiProperty({ type: 'number', format: 'double' })
@Type(() => Number)
@IsLatitude({ message: ({ property }) => `${property} must be a number between -90 and 90` })
lat!: number;
@ApiProperty({ format: 'double' })
@ApiProperty({ type: 'number', format: 'double' })
@Type(() => Number)
@IsLongitude({ message: ({ property }) => `${property} must be a number between -180 and 180` })
lon!: number;

View File

@@ -32,6 +32,7 @@ export class MemorySearchDto {
class OnThisDayDto {
@IsInt()
@IsPositive()
@ApiProperty({ type: 'integer' })
year!: number;
}

View File

@@ -89,14 +89,14 @@ export class PersonSearchDto {
closestAssetId?: string;
/** Page number for pagination */
@ApiPropertyOptional()
@ApiPropertyOptional({ type: 'integer' })
@IsInt()
@Min(1)
@Type(() => Number)
page: number = 1;
/** Number of items per page */
@ApiPropertyOptional()
@ApiPropertyOptional({ type: 'integer' })
@IsInt()
@Min(1)
@Max(1000)

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
import { Place } from 'src/database';
@@ -100,6 +100,7 @@ class BaseSearchDto {
@IsInt()
@Max(5)
@Min(-1)
@ApiPropertyOptional({ type: 'integer' })
rating?: number;
}
@@ -115,6 +116,7 @@ class BaseSearchWithResultsDto extends BaseSearchDto {
@Max(1000)
@Type(() => Number)
@Optional()
@ApiPropertyOptional({ type: 'integer' })
size?: number;
}
@@ -186,6 +188,7 @@ export class MetadataSearchDto extends RandomSearchDto {
@Min(1)
@Type(() => Number)
@Optional()
@ApiPropertyOptional({ type: 'integer' })
page?: number;
}
@@ -215,6 +218,7 @@ export class SmartSearchDto extends BaseSearchWithResultsDto {
@Min(1)
@Type(() => Number)
@Optional()
@ApiPropertyOptional({ type: 'integer' })
page?: number;
}
@@ -235,7 +239,9 @@ export class SearchPeopleDto {
export class PlacesResponseDto {
name!: string;
@ApiProperty({ type: 'number', format: 'double' })
latitude!: number;
@ApiProperty({ type: 'number', format: 'double' })
longitude!: number;
admin1name?: string;
admin2name?: string;

View File

@@ -1,3 +1,4 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Equals, IsInt, IsPositive, IsString } from 'class-validator';
import { Session } from 'src/database';
import { Optional, ValidateBoolean } from 'src/validation';
@@ -9,6 +10,7 @@ export class SessionCreateDto {
@IsInt()
@IsPositive()
@Optional()
@ApiPropertyOptional({ type: 'integer' })
duration?: number;
@IsString()

View File

@@ -53,6 +53,7 @@ export class DatabaseBackupConfig {
@IsInt()
@IsPositive()
@IsNotEmpty()
@ApiProperty({ type: 'integer' })
keepLastAmount!: number;
}
@@ -451,6 +452,7 @@ class SystemConfigSmtpTransportDto {
@IsNumber()
@Min(0)
@Max(65_535)
@ApiProperty({ type: 'integer' })
port!: number;
@IsString()

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsDateString, IsInt, IsPositive, ValidateNested } from 'class-validator';
import { AssetOrder, UserAvatarColor } from 'src/enum';
@@ -72,7 +72,7 @@ class DownloadUpdate implements Partial<DownloadResponse> {
@Optional()
@IsInt()
@IsPositive()
@ApiProperty({ type: 'integer' })
@ApiPropertyOptional({ type: 'integer' })
archiveSize?: number;
@ValidateBoolean({ optional: true })

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsEmail, IsInt, IsNotEmpty, IsString, Min } from 'class-validator';
import { User, UserAdmin } from 'src/database';
@@ -93,7 +93,7 @@ export class UserAdminCreateDto {
@Optional({ nullable: true })
@IsInt()
@Min(0)
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiPropertyOptional({ type: 'integer', format: 'int64' })
quotaSizeInBytes?: number | null;
@ValidateBoolean({ optional: true })
@@ -139,7 +139,7 @@ export class UserAdminUpdateDto {
@Optional({ nullable: true })
@IsInt()
@Min(0)
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiPropertyOptional({ type: 'integer', format: 'int64' })
quotaSizeInBytes?: number | null;
@ValidateBoolean({ optional: true })

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { authManager } from '$lib/managers/auth-manager.svelte';
import MapModal from '$lib/modals/MapModal.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { getAlbumInfo, type AlbumResponseDto, type MapMarkerResponseDto } from '@immich/sdk';
@@ -32,7 +33,7 @@
}
abortController = new AbortController();
let albumInfo: AlbumResponseDto = await getAlbumInfo({ id: album.id, withoutAssets: false });
let albumInfo: AlbumResponseDto = await getAlbumInfo({ id: album.id, withoutAssets: false, ...authManager.params });
let markers: MapMarkerResponseDto[] = [];
for (const asset of albumInfo.assets) {

View File

@@ -21,7 +21,6 @@
const handleAddTag = async () => {
const success = await modalManager.show(AssetTagModal, { assetIds: [asset.id] });
if (success) {
asset = await getAssetInfo({ id: asset.id });
}

View File

@@ -16,21 +16,14 @@
import { locale } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store';
import { preferences, user } from '$lib/stores/user.store';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
import { delay, isFlipped } from '$lib/utils/asset-utils';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
import { delay, getDimensions } from '$lib/utils/asset-utils';
import { getByteUnitString } from '$lib/utils/byte-units';
import { handleError } from '$lib/utils/handle-error';
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
import { fromISODateTime, fromISODateTimeUTC } from '$lib/utils/timeline-util';
import { getParentPath } from '$lib/utils/tree-utils';
import {
AssetMediaSize,
getAssetInfo,
updateAsset,
type AlbumResponseDto,
type AssetResponseDto,
type ExifResponseDto,
} from '@immich/sdk';
import { AssetMediaSize, getAssetInfo, updateAsset, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk';
import { IconButton } from '@immich/ui';
import {
mdiCalendar,
@@ -61,17 +54,28 @@
let { asset, albums = [], currentAlbum = null, onClose }: Props = $props();
const getDimensions = (exifInfo: ExifResponseDto) => {
const { exifImageWidth: width, exifImageHeight: height } = exifInfo;
if (isFlipped(exifInfo.orientation)) {
return { width: height, height: width };
}
return { width, height };
};
let showAssetPath = $state(false);
let showEditFaces = $state(false);
let isOwner = $derived($user?.id === asset.ownerId);
let people = $derived(asset.people || []);
let unassignedFaces = $derived(asset.unassignedFaces || []);
let showingHiddenPeople = $state(false);
let timeZone = $derived(asset.exifInfo?.timeZone);
let dateTime = $derived(
timeZone && asset.exifInfo?.dateTimeOriginal
? fromISODateTime(asset.exifInfo.dateTimeOriginal, timeZone)
: fromISODateTimeUTC(asset.localDateTime),
);
let latlng = $derived(
(() => {
const lat = asset.exifInfo?.latitude;
const lng = asset.exifInfo?.longitude;
if (lat && lng) {
return { lat: Number(lat.toFixed(7)), lng: Number(lng.toFixed(7)) };
}
})(),
);
let previousId: string | undefined = $state();
$effect(() => {
@@ -84,42 +88,6 @@
}
});
let isOwner = $derived($user?.id === asset.ownerId);
const handleNewAsset = async (newAsset: AssetResponseDto) => {
// TODO: check if reloading asset data is necessary
if (newAsset.id && !authManager.isSharedLink) {
const data = await getAssetInfo({ id: asset.id });
people = data?.people || [];
unassignedFaces = data?.unassignedFaces || [];
}
};
$effect(() => {
handlePromiseError(handleNewAsset(asset));
});
let latlng = $derived(
(() => {
const lat = asset.exifInfo?.latitude;
const lng = asset.exifInfo?.longitude;
if (lat && lng) {
return { lat: Number(lat.toFixed(7)), lng: Number(lng.toFixed(7)) };
}
})(),
);
let people = $state(asset.people || []);
let unassignedFaces = $state(asset.unassignedFaces || []);
let showingHiddenPeople = $state(false);
let timeZone = $derived(asset.exifInfo?.timeZone);
let dateTime = $derived(
timeZone && asset.exifInfo?.dateTimeOriginal
? fromISODateTime(asset.exifInfo.dateTimeOriginal, timeZone)
: fromISODateTimeUTC(asset.localDateTime),
);
const getMegapixel = (width: number, height: number): number | undefined => {
const megapixel = Math.round((height * width) / 1_000_000);
@@ -131,10 +99,7 @@
};
const handleRefreshPeople = async () => {
await getAssetInfo({ id: asset.id }).then((data) => {
people = data?.people || [];
unassignedFaces = data?.unassignedFaces || [];
});
asset = await getAssetInfo({ id: asset.id });
showEditFaces = false;
};

View File

@@ -2,11 +2,12 @@
import { shortcuts } from '$lib/actions/shortcut';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
import DuplicateAsset from '$lib/components/utilities-page/duplicates/duplicate-asset.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { handlePromiseError } from '$lib/utils';
import { suggestDuplicate } from '$lib/utils/duplicate-utils';
import { navigate } from '$lib/utils/navigation';
import { type AssetResponseDto } from '@immich/sdk';
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
import { Button } from '@immich/ui';
import { mdiCheck, mdiImageMultipleOutline, mdiTrashCanOutline } from '@mdi/js';
import { onDestroy, onMount } from 'svelte';
@@ -42,32 +43,32 @@
assetViewingStore.showAssetViewer(false);
});
const onNext = () => {
const onNext = async () => {
const index = getAssetIndex($viewingAsset.id) + 1;
if (index >= assets.length) {
return Promise.resolve(false);
return false;
}
setAsset(assets[index]);
return Promise.resolve(true);
await onViewAsset(assets[index]);
return true;
};
const onPrevious = () => {
const onPrevious = async () => {
const index = getAssetIndex($viewingAsset.id) - 1;
if (index < 0) {
return Promise.resolve(false);
return false;
}
setAsset(assets[index]);
return Promise.resolve(true);
await onViewAsset(assets[index]);
return true;
};
const onRandom = () => {
const onRandom = async () => {
if (assets.length <= 0) {
return Promise.resolve(undefined);
return;
}
const index = Math.floor(Math.random() * assets.length);
const asset = assets[index];
setAsset(asset);
return Promise.resolve(asset);
await onViewAsset(asset);
return { id: asset.id };
};
const onSelectAsset = (asset: AssetResponseDto) => {
@@ -86,6 +87,12 @@
selectedAssetIds = new SvelteSet(assets.map((asset) => asset.id));
};
const onViewAsset = async ({ id }: AssetResponseDto) => {
const asset = await getAssetInfo({ ...authManager.params, id });
setAsset(asset);
await navigate({ targetRoute: 'current', assetId: asset.id });
};
const handleResolve = () => {
const trashIds = assets.map((asset) => asset.id).filter((id) => !selectedAssetIds.has(id));
const duplicateAssetIds = assets.map((asset) => asset.id);
@@ -102,9 +109,7 @@
{ shortcut: { key: 'a' }, onShortcut: onSelectAll },
{
shortcut: { key: 's' },
onShortcut: () => {
setAsset(assets[0]);
},
onShortcut: () => onViewAsset(assets[0]),
},
{ shortcut: { key: 'd' }, onShortcut: onSelectNone },
{ shortcut: { key: 'c', shift: true }, onShortcut: handleResolve },
@@ -166,12 +171,7 @@
<div class="flex flex-wrap gap-1 mb-4 place-items-center place-content-center px-4 pt-4">
{#each assets as asset (asset.id)}
<DuplicateAsset
{asset}
{onSelectAsset}
isSelected={selectedAssetIds.has(asset.id)}
onViewAsset={(asset) => setAsset(asset)}
/>
<DuplicateAsset {asset} {onSelectAsset} isSelected={selectedAssetIds.has(asset.id)} {onViewAsset} />
{/each}
</div>
</div>

View File

@@ -34,6 +34,7 @@ import {
type AssetResponseDto,
type AssetTypeEnum,
type DownloadInfoDto,
type ExifResponseDto,
type StackResponseDto,
type UserPreferencesResponseDto,
type UserResponseDto,
@@ -328,6 +329,15 @@ export function isFlipped(orientation?: string | null) {
return value && (isRotated270CW(value) || isRotated90CW(value));
}
export const getDimensions = (exifInfo: ExifResponseDto) => {
const { exifImageWidth: width, exifImageHeight: height } = exifInfo;
if (isFlipped(exifInfo.orientation)) {
return { width: height, height: width };
}
return { width, height };
};
export function getFileSize(asset: AssetResponseDto, maxPrecision = 4): string {
const size = asset.exifInfo?.fileSizeInByte || 0;
return size > 0 ? getByteUnitString(size, undefined, maxPrecision) : 'Invalid Data';