Compare commits
1 Commits
fix/19543
...
chore/temp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2344939b7d |
9
.github/workflows/prepare-release.yml
vendored
9
.github/workflows/prepare-release.yml
vendored
@@ -24,15 +24,6 @@ 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:
|
||||
|
||||
@@ -176,7 +176,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
}
|
||||
|
||||
Future<void> _handleBackup({bool processBulk = true}) async {
|
||||
if (!_isBackupEnabled || _isCleanedUp) {
|
||||
if (!_isBackupEnabled) {
|
||||
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
|
||||
return;
|
||||
}
|
||||
@@ -205,27 +205,30 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
}
|
||||
|
||||
Future<void> _syncAssets({Duration? hashTimeout}) async {
|
||||
await _ref.read(backgroundSyncProvider).syncLocal();
|
||||
if (_isCleanedUp) {
|
||||
return;
|
||||
}
|
||||
final futures = <Future<void>>[];
|
||||
|
||||
await _ref.read(backgroundSyncProvider).syncRemote();
|
||||
if (_isCleanedUp) {
|
||||
return;
|
||||
}
|
||||
final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async {
|
||||
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
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await hashFuture;
|
||||
return hashFuture;
|
||||
});
|
||||
|
||||
futures.add(localSyncFuture);
|
||||
futures.add(_ref.read(backgroundSyncProvider).syncRemote());
|
||||
|
||||
await Future.wait(futures);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ 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(
|
||||
@@ -20,9 +19,7 @@ class SyncLinkedAlbumService {
|
||||
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||
final DriftAlbumApiRepository _albumApiRepository;
|
||||
|
||||
SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
|
||||
|
||||
final _log = Logger("SyncLinkedAlbumService");
|
||||
const SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
|
||||
|
||||
Future<void> syncLinkedAlbums(String userId) async {
|
||||
final selectedAlbums = await _localAlbumRepository.getBackupAlbums();
|
||||
@@ -51,12 +48,8 @@ class SyncLinkedAlbumService {
|
||||
}
|
||||
|
||||
Future<void> manageLinkedAlbums(List<LocalAlbum> localAlbums, String ownerId) async {
|
||||
try {
|
||||
for (final album in localAlbums) {
|
||||
await _processLocalAlbum(album, ownerId);
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
_log.severe("Error managing linked albums", error, stackTrace);
|
||||
for (final album in localAlbums) {
|
||||
await _processLocalAlbum(album, ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -44,13 +42,49 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||
|
||||
Future<void> _handleMigration() async {
|
||||
try {
|
||||
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 (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);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -68,52 +102,6 @@ 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(
|
||||
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -48,23 +47,11 @@ 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(
|
||||
(_) async {
|
||||
(a) {
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ 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';
|
||||
@@ -15,7 +17,11 @@ 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 {
|
||||
@@ -26,6 +32,28 @@ 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;
|
||||
|
||||
@@ -148,25 +148,30 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
|
||||
try {
|
||||
// Run operations sequentially with state checks and error handling for each
|
||||
await _safeRun(backgroundManager.syncLocal(), "syncLocal");
|
||||
await _safeRun(backgroundManager.syncRemote(), "syncRemote");
|
||||
await _safeRun(backgroundManager.hashAssets(), "hashAssets");
|
||||
if (isAlbumLinkedSyncEnable) {
|
||||
await _safeRun(backgroundManager.syncLinkedAlbum(), "syncLinkedAlbum");
|
||||
}
|
||||
_safeRun(backgroundManager.syncLocal(), "syncLocal");
|
||||
_safeRun(backgroundManager.hashAssets(), "hashAssets");
|
||||
_safeRun(backgroundManager.syncRemote(), "syncRemote").then((_) {
|
||||
if (isAlbumLinkedSyncEnable) {
|
||||
_safeRun(backgroundManager.syncLinkedAlbum(), "syncLinkedAlbum");
|
||||
}
|
||||
});
|
||||
|
||||
// Handle backup resume only if still active
|
||||
if (isEnableBackup) {
|
||||
final currentUser = _ref.read(currentUserProvider);
|
||||
if (currentUser != null) {
|
||||
await _safeRun(
|
||||
_ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id),
|
||||
"handleBackupResume",
|
||||
);
|
||||
_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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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';
|
||||
@@ -22,8 +23,13 @@ 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';
|
||||
|
||||
@@ -63,10 +69,7 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
||||
|
||||
// Handle migration only for this version
|
||||
// TODO: remove when old timeline is removed
|
||||
final needBetaMigration = Store.tryGet(StoreKey.needBetaMigration);
|
||||
if (version == 15 && needBetaMigration == null) {
|
||||
// Check both databases directly instead of relying on cache
|
||||
|
||||
if (version == 15) {
|
||||
final isBeta = Store.tryGet(StoreKey.betaTimeline);
|
||||
final isNewInstallation = await _isNewInstallation(db, drift);
|
||||
|
||||
@@ -74,7 +77,6 @@ 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);
|
||||
@@ -308,6 +310,25 @@ 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;
|
||||
|
||||
@@ -10,11 +10,9 @@ 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';
|
||||
@@ -163,18 +161,6 @@ 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();
|
||||
|
||||
@@ -192,7 +178,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
final isBeta = Store.isBetaTimelineEnabled;
|
||||
if (isBeta) {
|
||||
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
||||
handleSyncFlow();
|
||||
|
||||
context.replaceRoute(const TabShellRoute());
|
||||
return;
|
||||
}
|
||||
@@ -290,7 +276,6 @@ class LoginForm extends HookConsumerWidget {
|
||||
}
|
||||
if (isBeta) {
|
||||
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
||||
handleSyncFlow();
|
||||
context.replaceRoute(const TabShellRoute());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ 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),
|
||||
@@ -269,10 +270,7 @@ 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(
|
||||
|
||||
3
mobile/openapi/README.md
generated
3
mobile/openapi/README.md
generated
@@ -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 | Replace the asset with new file, without changing its id
|
||||
*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | replaceAsset
|
||||
*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,7 +128,6 @@ 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} |
|
||||
|
||||
16
mobile/openapi/lib/api/assets_api.dart
generated
16
mobile/openapi/lib/api/assets_api.dart
generated
@@ -608,8 +608,8 @@ class AssetsApi {
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [int] count:
|
||||
Future<Response> getRandomWithHttpInfo({ int? count, }) async {
|
||||
/// * [num] count:
|
||||
Future<Response> getRandomWithHttpInfo({ num? count, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/random';
|
||||
|
||||
@@ -642,8 +642,8 @@ class AssetsApi {
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [int] count:
|
||||
Future<List<AssetResponseDto>?> getRandom({ int? count, }) async {
|
||||
/// * [num] count:
|
||||
Future<List<AssetResponseDto>?> getRandom({ num? 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;
|
||||
}
|
||||
|
||||
/// Replace the asset with new file, without changing its id
|
||||
/// replaceAsset
|
||||
///
|
||||
/// 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.
|
||||
/// 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 {
|
||||
);
|
||||
}
|
||||
|
||||
/// Replace the asset with new file, without changing its id
|
||||
/// replaceAsset
|
||||
///
|
||||
/// 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.
|
||||
/// Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
|
||||
142
mobile/openapi/lib/api/deprecated_api.dart
generated
142
mobile/openapi/lib/api/deprecated_api.dart
generated
@@ -75,8 +75,8 @@ class DeprecatedApi {
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [int] count:
|
||||
Future<Response> getRandomWithHttpInfo({ int? count, }) async {
|
||||
/// * [num] count:
|
||||
Future<Response> getRandomWithHttpInfo({ num? count, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/random';
|
||||
|
||||
@@ -109,8 +109,8 @@ class DeprecatedApi {
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [int] count:
|
||||
Future<List<AssetResponseDto>?> getRandom({ int? count, }) async {
|
||||
/// * [num] count:
|
||||
Future<List<AssetResponseDto>?> getRandom({ num? count, }) async {
|
||||
final response = await getRandomWithHttpInfo( count: count, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
@@ -127,138 +127,4 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
12
mobile/openapi/lib/api/people_api.dart
generated
12
mobile/openapi/lib/api/people_api.dart
generated
@@ -167,14 +167,14 @@ class PeopleApi {
|
||||
///
|
||||
/// * [String] closestPersonId:
|
||||
///
|
||||
/// * [int] page:
|
||||
/// * [num] page:
|
||||
/// Page number for pagination
|
||||
///
|
||||
/// * [int] size:
|
||||
/// * [num] size:
|
||||
/// Number of items per page
|
||||
///
|
||||
/// * [bool] withHidden:
|
||||
Future<Response> getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async {
|
||||
Future<Response> getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/people';
|
||||
|
||||
@@ -223,14 +223,14 @@ class PeopleApi {
|
||||
///
|
||||
/// * [String] closestPersonId:
|
||||
///
|
||||
/// * [int] page:
|
||||
/// * [num] page:
|
||||
/// Page number for pagination
|
||||
///
|
||||
/// * [int] size:
|
||||
/// * [num] size:
|
||||
/// Number of items per page
|
||||
///
|
||||
/// * [bool] withHidden:
|
||||
Future<PeopleResponseDto?> getAllPeople({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async {
|
||||
Future<PeopleResponseDto?> getAllPeople({ String? closestAssetId, String? closestPersonId, num? page, num? 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));
|
||||
|
||||
12
mobile/openapi/lib/api/search_api.dart
generated
12
mobile/openapi/lib/api/search_api.dart
generated
@@ -348,9 +348,9 @@ class SearchApi {
|
||||
///
|
||||
/// * [List<String>] personIds:
|
||||
///
|
||||
/// * [int] rating:
|
||||
/// * [num] rating:
|
||||
///
|
||||
/// * [int] size:
|
||||
/// * [num] 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, 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 {
|
||||
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 {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/search/large-assets';
|
||||
|
||||
@@ -532,9 +532,9 @@ class SearchApi {
|
||||
///
|
||||
/// * [List<String>] personIds:
|
||||
///
|
||||
/// * [int] rating:
|
||||
/// * [num] rating:
|
||||
///
|
||||
/// * [int] size:
|
||||
/// * [num] 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, 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 {
|
||||
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 {
|
||||
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));
|
||||
|
||||
16
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
16
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
@@ -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.
|
||||
///
|
||||
int? dateTimeRelative;
|
||||
num? 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.
|
||||
///
|
||||
double? latitude;
|
||||
num? 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.
|
||||
///
|
||||
double? longitude;
|
||||
num? 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.
|
||||
///
|
||||
int? rating;
|
||||
num? 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: mapValueOfType<int>(json, r'dateTimeRelative'),
|
||||
dateTimeRelative: num.parse('${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: (mapValueOfType<num>(json, r'latitude')).toDouble(),
|
||||
longitude: (mapValueOfType<num>(json, r'longitude')).toDouble(),
|
||||
rating: mapValueOfType<int>(json, r'rating'),
|
||||
latitude: num.parse('${json[r'latitude']}'),
|
||||
longitude: num.parse('${json[r'longitude']}'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
||||
visibility: AssetVisibility.fromJson(json[r'visibility']),
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ class DatabaseBackupConfig {
|
||||
bool enabled;
|
||||
|
||||
/// Minimum value: 1
|
||||
int keepLastAmount;
|
||||
num 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: mapValueOfType<int>(json, r'keepLastAmount')!,
|
||||
keepLastAmount: num.parse('${json[r'keepLastAmount']}'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
48
mobile/openapi/lib/model/exif_response_dto.dart
generated
48
mobile/openapi/lib/model/exif_response_dto.dart
generated
@@ -45,25 +45,25 @@ class ExifResponseDto {
|
||||
|
||||
String? description;
|
||||
|
||||
int? exifImageHeight;
|
||||
num? exifImageHeight;
|
||||
|
||||
int? exifImageWidth;
|
||||
num? exifImageWidth;
|
||||
|
||||
String? exposureTime;
|
||||
|
||||
double? fNumber;
|
||||
num? fNumber;
|
||||
|
||||
int? fileSizeInByte;
|
||||
|
||||
double? focalLength;
|
||||
num? focalLength;
|
||||
|
||||
int? iso;
|
||||
num? iso;
|
||||
|
||||
double? latitude;
|
||||
num? latitude;
|
||||
|
||||
String? lensModel;
|
||||
|
||||
double? longitude;
|
||||
num? longitude;
|
||||
|
||||
String? make;
|
||||
|
||||
@@ -75,7 +75,7 @@ class ExifResponseDto {
|
||||
|
||||
String? projectionType;
|
||||
|
||||
int? rating;
|
||||
num? rating;
|
||||
|
||||
String? state;
|
||||
|
||||
@@ -263,22 +263,38 @@ class ExifResponseDto {
|
||||
country: mapValueOfType<String>(json, r'country'),
|
||||
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''),
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
exifImageHeight: mapValueOfType<int>(json, r'exifImageHeight'),
|
||||
exifImageWidth: mapValueOfType<int>(json, r'exifImageWidth'),
|
||||
exifImageHeight: json[r'exifImageHeight'] == null
|
||||
? null
|
||||
: num.parse('${json[r'exifImageHeight']}'),
|
||||
exifImageWidth: json[r'exifImageWidth'] == null
|
||||
? null
|
||||
: num.parse('${json[r'exifImageWidth']}'),
|
||||
exposureTime: mapValueOfType<String>(json, r'exposureTime'),
|
||||
fNumber: (mapValueOfType<num>(json, r'fNumber'))?.toDouble(),
|
||||
fNumber: json[r'fNumber'] == null
|
||||
? null
|
||||
: num.parse('${json[r'fNumber']}'),
|
||||
fileSizeInByte: mapValueOfType<int>(json, r'fileSizeInByte'),
|
||||
focalLength: (mapValueOfType<num>(json, r'focalLength'))?.toDouble(),
|
||||
iso: mapValueOfType<int>(json, r'iso'),
|
||||
latitude: (mapValueOfType<num>(json, r'latitude'))?.toDouble(),
|
||||
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']}'),
|
||||
lensModel: mapValueOfType<String>(json, r'lensModel'),
|
||||
longitude: (mapValueOfType<num>(json, r'longitude'))?.toDouble(),
|
||||
longitude: json[r'longitude'] == null
|
||||
? null
|
||||
: num.parse('${json[r'longitude']}'),
|
||||
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: mapValueOfType<int>(json, r'rating'),
|
||||
rating: json[r'rating'] == null
|
||||
? null
|
||||
: num.parse('${json[r'rating']}'),
|
||||
state: mapValueOfType<String>(json, r'state'),
|
||||
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
||||
);
|
||||
|
||||
12
mobile/openapi/lib/model/metadata_search_dto.dart
generated
12
mobile/openapi/lib/model/metadata_search_dto.dart
generated
@@ -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.
|
||||
///
|
||||
int? page;
|
||||
num? 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.
|
||||
///
|
||||
int? rating;
|
||||
num? 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.
|
||||
///
|
||||
int? size;
|
||||
num? 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: mapValueOfType<int>(json, r'page'),
|
||||
page: num.parse('${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: mapValueOfType<int>(json, r'rating'),
|
||||
size: mapValueOfType<int>(json, r'size'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
size: num.parse('${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)
|
||||
|
||||
4
mobile/openapi/lib/model/on_this_day_dto.dart
generated
4
mobile/openapi/lib/model/on_this_day_dto.dart
generated
@@ -17,7 +17,7 @@ class OnThisDayDto {
|
||||
});
|
||||
|
||||
/// Minimum value: 1
|
||||
int year;
|
||||
num 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: mapValueOfType<int>(json, r'year')!,
|
||||
year: num.parse('${json[r'year']}'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -36,9 +36,9 @@ class PlacesResponseDto {
|
||||
///
|
||||
String? admin2name;
|
||||
|
||||
double latitude;
|
||||
num latitude;
|
||||
|
||||
double longitude;
|
||||
num longitude;
|
||||
|
||||
String name;
|
||||
|
||||
@@ -91,8 +91,8 @@ class PlacesResponseDto {
|
||||
return PlacesResponseDto(
|
||||
admin1name: mapValueOfType<String>(json, r'admin1name'),
|
||||
admin2name: mapValueOfType<String>(json, r'admin2name'),
|
||||
latitude: (mapValueOfType<num>(json, r'latitude')!).toDouble(),
|
||||
longitude: (mapValueOfType<num>(json, r'longitude')!).toDouble(),
|
||||
latitude: num.parse('${json[r'latitude']}'),
|
||||
longitude: num.parse('${json[r'longitude']}'),
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
);
|
||||
}
|
||||
|
||||
8
mobile/openapi/lib/model/random_search_dto.dart
generated
8
mobile/openapi/lib/model/random_search_dto.dart
generated
@@ -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.
|
||||
///
|
||||
int? rating;
|
||||
num? 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.
|
||||
///
|
||||
int? size;
|
||||
num? 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: mapValueOfType<int>(json, r'rating'),
|
||||
size: mapValueOfType<int>(json, r'size'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
size: num.parse('${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)
|
||||
|
||||
4
mobile/openapi/lib/model/session_create_dto.dart
generated
4
mobile/openapi/lib/model/session_create_dto.dart
generated
@@ -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.
|
||||
///
|
||||
int? duration;
|
||||
num? 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: mapValueOfType<int>(json, r'duration'),
|
||||
duration: num.parse('${json[r'duration']}'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
12
mobile/openapi/lib/model/smart_search_dto.dart
generated
12
mobile/openapi/lib/model/smart_search_dto.dart
generated
@@ -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.
|
||||
///
|
||||
int? page;
|
||||
num? 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.
|
||||
///
|
||||
int? rating;
|
||||
num? 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.
|
||||
///
|
||||
int? size;
|
||||
num? 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: mapValueOfType<int>(json, r'page'),
|
||||
page: num.parse('${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: mapValueOfType<int>(json, r'rating'),
|
||||
size: mapValueOfType<int>(json, r'size'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
size: num.parse('${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)
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
int? rating;
|
||||
num? 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: mapValueOfType<int>(json, r'rating'),
|
||||
rating: num.parse('${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)
|
||||
|
||||
@@ -28,7 +28,7 @@ class SystemConfigSmtpTransportDto {
|
||||
|
||||
/// Minimum value: 0
|
||||
/// Maximum value: 65535
|
||||
int port;
|
||||
num 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: mapValueOfType<int>(json, r'port')!,
|
||||
port: num.parse('${json[r'port']}'),
|
||||
username: mapValueOfType<String>(json, r'username')!,
|
||||
);
|
||||
}
|
||||
|
||||
12
mobile/openapi/lib/model/update_asset_dto.dart
generated
12
mobile/openapi/lib/model/update_asset_dto.dart
generated
@@ -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.
|
||||
///
|
||||
double? latitude;
|
||||
num? 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.
|
||||
///
|
||||
double? longitude;
|
||||
num? 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.
|
||||
///
|
||||
int? rating;
|
||||
num? 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: (mapValueOfType<num>(json, r'latitude')).toDouble(),
|
||||
latitude: num.parse('${json[r'latitude']}'),
|
||||
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||
longitude: (mapValueOfType<num>(json, r'longitude')).toDouble(),
|
||||
rating: mapValueOfType<int>(json, r'rating'),
|
||||
longitude: num.parse('${json[r'longitude']}'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
visibility: AssetVisibility.fromJson(json[r'visibility']),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2035,7 +2035,7 @@
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -2504,8 +2504,7 @@
|
||||
"description": "This endpoint requires the `asset.download` permission."
|
||||
},
|
||||
"put": {
|
||||
"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.",
|
||||
"description": "Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.",
|
||||
"operationId": "replaceAsset",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -2567,14 +2566,12 @@
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Replace the asset with new file, without changing its id",
|
||||
"summary": "replaceAsset",
|
||||
"tags": [
|
||||
"Assets",
|
||||
"Deprecated"
|
||||
"Assets"
|
||||
],
|
||||
"x-immich-lifecycle": {
|
||||
"addedAt": "v1.106.0",
|
||||
"deprecatedAt": "v1.142.0"
|
||||
"addedAt": "v1.106.0"
|
||||
},
|
||||
"x-immich-permission": "asset.replace"
|
||||
}
|
||||
@@ -5242,7 +5239,7 @@
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"default": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5254,7 +5251,7 @@
|
||||
"minimum": 1,
|
||||
"maximum": 1000,
|
||||
"default": 500,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5955,7 +5952,7 @@
|
||||
"schema": {
|
||||
"minimum": -1,
|
||||
"maximum": 5,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5965,7 +5962,7 @@
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"maximum": 1000,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -10408,7 +10405,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"dateTimeRelative": {
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
@@ -10428,17 +10425,15 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"latitude": {
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"longitude": {
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string"
|
||||
@@ -11573,7 +11568,7 @@
|
||||
},
|
||||
"keepLastAmount": {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -11766,12 +11761,12 @@
|
||||
"exifImageHeight": {
|
||||
"default": null,
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"exifImageWidth": {
|
||||
"default": null,
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"exposureTime": {
|
||||
"default": null,
|
||||
@@ -11780,7 +11775,6 @@
|
||||
},
|
||||
"fNumber": {
|
||||
"default": null,
|
||||
"format": "double",
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
@@ -11792,18 +11786,16 @@
|
||||
},
|
||||
"focalLength": {
|
||||
"default": null,
|
||||
"format": "double",
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
"iso": {
|
||||
"default": null,
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"latitude": {
|
||||
"default": null,
|
||||
"format": "double",
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
@@ -11814,7 +11806,6 @@
|
||||
},
|
||||
"longitude": {
|
||||
"default": null,
|
||||
"format": "double",
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
@@ -11847,7 +11838,7 @@
|
||||
"rating": {
|
||||
"default": null,
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"state": {
|
||||
"default": null,
|
||||
@@ -12600,7 +12591,7 @@
|
||||
},
|
||||
"page": {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"personIds": {
|
||||
"items": {
|
||||
@@ -12615,12 +12606,12 @@
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"size": {
|
||||
"maximum": 1000,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"state": {
|
||||
"nullable": true,
|
||||
@@ -12897,7 +12888,7 @@
|
||||
"properties": {
|
||||
"year": {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -13446,11 +13437,9 @@
|
||||
"type": "string"
|
||||
},
|
||||
"latitude": {
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"longitude": {
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
@@ -13574,12 +13563,12 @@
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"size": {
|
||||
"maximum": 1000,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"state": {
|
||||
"nullable": true,
|
||||
@@ -14262,7 +14251,7 @@
|
||||
"duration": {
|
||||
"description": "session duration, in seconds",
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -14658,7 +14647,7 @@
|
||||
},
|
||||
"page": {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"personIds": {
|
||||
"items": {
|
||||
@@ -14677,12 +14666,12 @@
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"size": {
|
||||
"maximum": 1000,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"state": {
|
||||
"nullable": true,
|
||||
@@ -14871,7 +14860,7 @@
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"state": {
|
||||
"nullable": true,
|
||||
@@ -16691,7 +16680,7 @@
|
||||
"port": {
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
@@ -17307,7 +17296,6 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"latitude": {
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"livePhotoVideoId": {
|
||||
@@ -17316,13 +17304,12 @@
|
||||
"type": "string"
|
||||
},
|
||||
"longitude": {
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"visibility": {
|
||||
"allOf": [
|
||||
|
||||
@@ -2368,7 +2368,7 @@ export function downloadAsset({ id, key, slug }: {
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Replace the asset with new file, without changing its id
|
||||
* replaceAsset
|
||||
*/
|
||||
export function replaceAsset({ id, key, slug, assetMediaReplaceDto }: {
|
||||
id: string;
|
||||
|
||||
@@ -96,9 +96,8 @@ export class AssetMediaController {
|
||||
@Put(':id/original')
|
||||
@UseInterceptors(FileUploadInterceptor)
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@EndpointLifecycle({
|
||||
addedAt: 'v1.106.0',
|
||||
deprecatedAt: 'v1.142.0',
|
||||
@EndpointLifecycle({ addedAt: 'v1.106.0' })
|
||||
@ApiOperation({
|
||||
summary: 'replaceAsset',
|
||||
description: 'Replace the asset with new file, without changing its id',
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SetMetadata, applyDecorators } from '@nestjs/common';
|
||||
import { ApiExtension, ApiOperation, ApiOperationOptions, ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiExtension, ApiOperation, 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,21 +159,12 @@ type LifecycleMetadata = {
|
||||
deprecatedAt?: LifecycleRelease;
|
||||
};
|
||||
|
||||
export const EndpointLifecycle = ({
|
||||
addedAt,
|
||||
deprecatedAt,
|
||||
description,
|
||||
...options
|
||||
}: LifecycleMetadata & ApiOperationOptions) => {
|
||||
export const EndpointLifecycle = ({ addedAt, deprecatedAt }: LifecycleMetadata) => {
|
||||
const decorators: MethodDecorator[] = [ApiExtension(LIFECYCLE_EXTENSION, { addedAt, deprecatedAt })];
|
||||
if (deprecatedAt) {
|
||||
decorators.push(
|
||||
ApiTags('Deprecated'),
|
||||
ApiOperation({
|
||||
deprecated: true,
|
||||
description: DEPRECATED_IN_PREFIX + deprecatedAt + (description ? `. ${description}` : ''),
|
||||
...options,
|
||||
}),
|
||||
ApiOperation({ deprecated: true, description: DEPRECATED_IN_PREFIX + deprecatedAt }),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
@@ -46,20 +46,17 @@ 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()
|
||||
@@ -77,7 +74,6 @@ export class AssetBulkUpdateDto extends UpdateAssetBase {
|
||||
@IsNotSiblingOf(['dateTimeOriginal'])
|
||||
@Optional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: 'integer' })
|
||||
dateTimeRelative?: number;
|
||||
|
||||
@IsNotSiblingOf(['dateTimeOriginal'])
|
||||
@@ -96,7 +92,6 @@ export class RandomAssetsDto {
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ type: 'integer' })
|
||||
count?: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,30 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ApiProperty } 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;
|
||||
@ApiPropertyOptional({ type: 'integer', format: 'int64' })
|
||||
|
||||
@ApiProperty({ 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ import { IsLatitude, IsLongitude } from 'class-validator';
|
||||
import { ValidateBoolean, ValidateDate } from 'src/validation';
|
||||
|
||||
export class MapReverseGeocodeDto {
|
||||
@ApiProperty({ type: 'number', format: 'double' })
|
||||
@ApiProperty({ format: 'double' })
|
||||
@Type(() => Number)
|
||||
@IsLatitude({ message: ({ property }) => `${property} must be a number between -90 and 90` })
|
||||
lat!: number;
|
||||
|
||||
@ApiProperty({ type: 'number', format: 'double' })
|
||||
@ApiProperty({ format: 'double' })
|
||||
@Type(() => Number)
|
||||
@IsLongitude({ message: ({ property }) => `${property} must be a number between -180 and 180` })
|
||||
lon!: number;
|
||||
|
||||
@@ -32,7 +32,6 @@ export class MemorySearchDto {
|
||||
class OnThisDayDto {
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
@ApiProperty({ type: 'integer' })
|
||||
year!: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,14 +89,14 @@ export class PersonSearchDto {
|
||||
closestAssetId?: string;
|
||||
|
||||
/** Page number for pagination */
|
||||
@ApiPropertyOptional({ type: 'integer' })
|
||||
@ApiPropertyOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
page: number = 1;
|
||||
|
||||
/** Number of items per page */
|
||||
@ApiPropertyOptional({ type: 'integer' })
|
||||
@ApiPropertyOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(1000)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
|
||||
import { Place } from 'src/database';
|
||||
@@ -100,7 +100,6 @@ class BaseSearchDto {
|
||||
@IsInt()
|
||||
@Max(5)
|
||||
@Min(-1)
|
||||
@ApiPropertyOptional({ type: 'integer' })
|
||||
rating?: number;
|
||||
}
|
||||
|
||||
@@ -116,7 +115,6 @@ class BaseSearchWithResultsDto extends BaseSearchDto {
|
||||
@Max(1000)
|
||||
@Type(() => Number)
|
||||
@Optional()
|
||||
@ApiPropertyOptional({ type: 'integer' })
|
||||
size?: number;
|
||||
}
|
||||
|
||||
@@ -188,7 +186,6 @@ export class MetadataSearchDto extends RandomSearchDto {
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
@Optional()
|
||||
@ApiPropertyOptional({ type: 'integer' })
|
||||
page?: number;
|
||||
}
|
||||
|
||||
@@ -218,7 +215,6 @@ export class SmartSearchDto extends BaseSearchWithResultsDto {
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
@Optional()
|
||||
@ApiPropertyOptional({ type: 'integer' })
|
||||
page?: number;
|
||||
}
|
||||
|
||||
@@ -239,9 +235,7 @@ 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;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Equals, IsInt, IsPositive, IsString } from 'class-validator';
|
||||
import { Session } from 'src/database';
|
||||
import { Optional, ValidateBoolean } from 'src/validation';
|
||||
@@ -10,7 +9,6 @@ export class SessionCreateDto {
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
@Optional()
|
||||
@ApiPropertyOptional({ type: 'integer' })
|
||||
duration?: number;
|
||||
|
||||
@IsString()
|
||||
|
||||
@@ -53,7 +53,6 @@ export class DatabaseBackupConfig {
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ type: 'integer' })
|
||||
keepLastAmount!: number;
|
||||
}
|
||||
|
||||
@@ -452,7 +451,6 @@ class SystemConfigSmtpTransportDto {
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(65_535)
|
||||
@ApiProperty({ type: 'integer' })
|
||||
port!: number;
|
||||
|
||||
@IsString()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ApiProperty } 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()
|
||||
@ApiPropertyOptional({ type: 'integer' })
|
||||
@ApiProperty({ type: 'integer' })
|
||||
archiveSize?: number;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ApiProperty } 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)
|
||||
@ApiPropertyOptional({ type: 'integer', format: 'int64' })
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
quotaSizeInBytes?: number | null;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
@@ -139,7 +139,7 @@ export class UserAdminUpdateDto {
|
||||
@Optional({ nullable: true })
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ApiPropertyOptional({ type: 'integer', format: 'int64' })
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
quotaSizeInBytes?: number | null;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<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';
|
||||
@@ -33,7 +32,7 @@
|
||||
}
|
||||
abortController = new AbortController();
|
||||
|
||||
let albumInfo: AlbumResponseDto = await getAlbumInfo({ id: album.id, withoutAssets: false, ...authManager.params });
|
||||
let albumInfo: AlbumResponseDto = await getAlbumInfo({ id: album.id, withoutAssets: false });
|
||||
|
||||
let markers: MapMarkerResponseDto[] = [];
|
||||
for (const asset of albumInfo.assets) {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
const handleAddTag = async () => {
|
||||
const success = await modalManager.show(AssetTagModal, { assetIds: [asset.id] });
|
||||
|
||||
if (success) {
|
||||
asset = await getAssetInfo({ id: asset.id });
|
||||
}
|
||||
|
||||
@@ -16,14 +16,21 @@
|
||||
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 } from '$lib/utils';
|
||||
import { delay, getDimensions } from '$lib/utils/asset-utils';
|
||||
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||
import { delay, isFlipped } 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 } from '@immich/sdk';
|
||||
import {
|
||||
AssetMediaSize,
|
||||
getAssetInfo,
|
||||
updateAsset,
|
||||
type AlbumResponseDto,
|
||||
type AssetResponseDto,
|
||||
type ExifResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { IconButton } from '@immich/ui';
|
||||
import {
|
||||
mdiCalendar,
|
||||
@@ -54,28 +61,17 @@
|
||||
|
||||
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(() => {
|
||||
@@ -88,6 +84,42 @@
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -99,7 +131,10 @@
|
||||
};
|
||||
|
||||
const handleRefreshPeople = async () => {
|
||||
asset = await getAssetInfo({ id: asset.id });
|
||||
await getAssetInfo({ id: asset.id }).then((data) => {
|
||||
people = data?.people || [];
|
||||
unassignedFaces = data?.unassignedFaces || [];
|
||||
});
|
||||
showEditFaces = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
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 { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
|
||||
import { type AssetResponseDto } from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { mdiCheck, mdiImageMultipleOutline, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
@@ -43,32 +42,32 @@
|
||||
assetViewingStore.showAssetViewer(false);
|
||||
});
|
||||
|
||||
const onNext = async () => {
|
||||
const onNext = () => {
|
||||
const index = getAssetIndex($viewingAsset.id) + 1;
|
||||
if (index >= assets.length) {
|
||||
return false;
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
await onViewAsset(assets[index]);
|
||||
return true;
|
||||
setAsset(assets[index]);
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
const onPrevious = async () => {
|
||||
const onPrevious = () => {
|
||||
const index = getAssetIndex($viewingAsset.id) - 1;
|
||||
if (index < 0) {
|
||||
return false;
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
await onViewAsset(assets[index]);
|
||||
return true;
|
||||
setAsset(assets[index]);
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
const onRandom = async () => {
|
||||
const onRandom = () => {
|
||||
if (assets.length <= 0) {
|
||||
return;
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
const index = Math.floor(Math.random() * assets.length);
|
||||
const asset = assets[index];
|
||||
await onViewAsset(asset);
|
||||
return { id: asset.id };
|
||||
setAsset(asset);
|
||||
return Promise.resolve(asset);
|
||||
};
|
||||
|
||||
const onSelectAsset = (asset: AssetResponseDto) => {
|
||||
@@ -87,12 +86,6 @@
|
||||
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);
|
||||
@@ -109,7 +102,9 @@
|
||||
{ shortcut: { key: 'a' }, onShortcut: onSelectAll },
|
||||
{
|
||||
shortcut: { key: 's' },
|
||||
onShortcut: () => onViewAsset(assets[0]),
|
||||
onShortcut: () => {
|
||||
setAsset(assets[0]);
|
||||
},
|
||||
},
|
||||
{ shortcut: { key: 'd' }, onShortcut: onSelectNone },
|
||||
{ shortcut: { key: 'c', shift: true }, onShortcut: handleResolve },
|
||||
@@ -171,7 +166,12 @@
|
||||
|
||||
<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} />
|
||||
<DuplicateAsset
|
||||
{asset}
|
||||
{onSelectAsset}
|
||||
isSelected={selectedAssetIds.has(asset.id)}
|
||||
onViewAsset={(asset) => setAsset(asset)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
type AssetResponseDto,
|
||||
type AssetTypeEnum,
|
||||
type DownloadInfoDto,
|
||||
type ExifResponseDto,
|
||||
type StackResponseDto,
|
||||
type UserPreferencesResponseDto,
|
||||
type UserResponseDto,
|
||||
@@ -329,15 +328,6 @@ 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';
|
||||
|
||||
Reference in New Issue
Block a user