From 067338b0edd8e1ed9e3c3c4245c31c1a55978f6b Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Thu, 17 Apr 2025 16:46:52 +0200 Subject: [PATCH 001/356] chore: remove transfer encoding header (#17671) --- mobile/lib/services/backup.service.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index a6468f249b..596ad8dc2e 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -351,7 +351,6 @@ class BackupService { ); baseRequest.headers.addAll(ApiService.getRequestHeaders()); - baseRequest.headers["Transfer-Encoding"] = "chunked"; baseRequest.fields['deviceAssetId'] = asset.localId!; baseRequest.fields['deviceId'] = deviceId; baseRequest.fields['fileCreatedAt'] = From 81ed54aa61a0ff6e94de6e52709d04c1db0a0d44 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Thu, 17 Apr 2025 20:55:27 +0530 Subject: [PATCH 002/356] feat: user sync stream (#16862) * refactor: user entity * chore: rebase fixes * refactor: remove int user Id * refactor: migrate store userId from int to string * refactor: rename uid to id * feat: drift * pr feedback * refactor: move common overrides to mixin * refactor: remove int user Id * refactor: migrate store userId from int to string * refactor: rename uid to id * feat: user & partner sync stream * pr changes * refactor: sync service and add tests * chore: remove generated change * chore: move sync model * rebase: convert string ids to byte uuids * rebase * add processing logs * batch db calls * rewrite isolate manager * rewrite with worker_manager * misc fixes * add sync order test --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex --- mobile/analysis_options.yaml | 8 +- mobile/devtools_options.yaml | 1 + mobile/lib/constants/constants.dart | 4 + .../domain/interfaces/sync_api.interface.dart | 7 +- .../interfaces/sync_stream.interface.dart | 10 + .../domain/models/sync/sync_event.model.dart | 14 - .../lib/domain/models/sync_event.model.dart | 13 + .../domain/services/sync_stream.service.dart | 217 +++++++-- mobile/lib/domain/utils/background_sync.dart | 37 ++ mobile/lib/extensions/string_extensions.dart | 9 + .../repositories/sync_api.repository.dart | 93 ++-- .../repositories/sync_stream.repository.dart | 104 ++++ mobile/lib/main.dart | 5 +- .../providers/background_sync.provider.dart | 8 + .../infrastructure/cancel.provider.dart | 12 + .../providers/infrastructure/db.provider.dart | 9 + .../infrastructure/sync_stream.provider.dart | 27 +- mobile/lib/services/auth.service.dart | 11 +- mobile/lib/utils/bootstrap.dart | 6 +- mobile/lib/utils/isolate.dart | 69 +++ mobile/pubspec.lock | 10 +- mobile/pubspec.yaml | 2 + mobile/test/domain/service.mock.dart | 3 + .../services/sync_stream_service_test.dart | 443 ++++++++++++++++++ mobile/test/fixtures/sync_stream.stub.dart | 45 ++ .../test/infrastructure/repository.mock.dart | 6 + mobile/test/service.mocks.dart | 1 - mobile/test/services/auth.service_test.dart | 8 + 28 files changed, 1065 insertions(+), 117 deletions(-) create mode 100644 mobile/lib/domain/interfaces/sync_stream.interface.dart delete mode 100644 mobile/lib/domain/models/sync/sync_event.model.dart create mode 100644 mobile/lib/domain/models/sync_event.model.dart create mode 100644 mobile/lib/domain/utils/background_sync.dart create mode 100644 mobile/lib/infrastructure/repositories/sync_stream.repository.dart create mode 100644 mobile/lib/providers/background_sync.provider.dart create mode 100644 mobile/lib/providers/infrastructure/cancel.provider.dart create mode 100644 mobile/lib/utils/isolate.dart create mode 100644 mobile/test/domain/services/sync_stream_service_test.dart create mode 100644 mobile/test/fixtures/sync_stream.stub.dart diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 04f3145908..854f852e3c 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -35,6 +35,7 @@ linter: analyzer: exclude: - openapi/** + - build/** - lib/generated_plugin_registrant.dart - lib/**/*.g.dart - lib/**/*.drift.dart @@ -92,6 +93,9 @@ custom_lint: allowed: # required / wanted - lib/repositories/*_api.repository.dart + - lib/domain/models/sync_event.model.dart + - lib/{domain,infrastructure}/**/sync_stream.* + - lib/{domain,infrastructure}/**/sync_api.* - lib/infrastructure/repositories/*_api.repository.dart - lib/infrastructure/utils/*.converter.dart # acceptable exceptions for the time being @@ -144,7 +148,9 @@ dart_code_metrics: - avoid-global-state - avoid-inverted-boolean-checks - avoid-late-final-reassignment - - avoid-local-functions + - avoid-local-functions: + exclude: + - test/**.dart - avoid-negated-conditions - avoid-nested-streams-and-futures - avoid-referencing-subclasses diff --git a/mobile/devtools_options.yaml b/mobile/devtools_options.yaml index fa0b357c4f..f592d85a9b 100644 --- a/mobile/devtools_options.yaml +++ b/mobile/devtools_options.yaml @@ -1,3 +1,4 @@ description: This file stores settings for Dart & Flutter DevTools. documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states extensions: + - drift: true \ No newline at end of file diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index 83d540d54c..a91e0a715d 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -5,5 +5,9 @@ const double downloadFailed = -2; // Number of log entries to retain on app start const int kLogTruncateLimit = 250; +// Sync +const int kSyncEventBatchSize = 5000; + +// Hash batch limits const int kBatchHashFileLimit = 128; const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB diff --git a/mobile/lib/domain/interfaces/sync_api.interface.dart b/mobile/lib/domain/interfaces/sync_api.interface.dart index fb8f1aa46e..44e22c5894 100644 --- a/mobile/lib/domain/interfaces/sync_api.interface.dart +++ b/mobile/lib/domain/interfaces/sync_api.interface.dart @@ -1,7 +1,8 @@ -import 'package:immich_mobile/domain/models/sync/sync_event.model.dart'; +import 'package:immich_mobile/domain/models/sync_event.model.dart'; +import 'package:openapi/api.dart'; abstract interface class ISyncApiRepository { - Future ack(String data); + Future ack(List data); - Stream> watchUserSyncEvent(); + Stream> getSyncEvents(List type); } diff --git a/mobile/lib/domain/interfaces/sync_stream.interface.dart b/mobile/lib/domain/interfaces/sync_stream.interface.dart new file mode 100644 index 0000000000..f9c52d7ee0 --- /dev/null +++ b/mobile/lib/domain/interfaces/sync_stream.interface.dart @@ -0,0 +1,10 @@ +import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:openapi/api.dart'; + +abstract interface class ISyncStreamRepository implements IDatabaseRepository { + Future updateUsersV1(Iterable data); + Future deleteUsersV1(Iterable data); + + Future updatePartnerV1(Iterable data); + Future deletePartnerV1(Iterable data); +} diff --git a/mobile/lib/domain/models/sync/sync_event.model.dart b/mobile/lib/domain/models/sync/sync_event.model.dart deleted file mode 100644 index f4642d59cf..0000000000 --- a/mobile/lib/domain/models/sync/sync_event.model.dart +++ /dev/null @@ -1,14 +0,0 @@ -class SyncEvent { - // dynamic - final dynamic data; - - final String ack; - - SyncEvent({ - required this.data, - required this.ack, - }); - - @override - String toString() => 'SyncEvent(data: $data, ack: $ack)'; -} diff --git a/mobile/lib/domain/models/sync_event.model.dart b/mobile/lib/domain/models/sync_event.model.dart new file mode 100644 index 0000000000..2ad8a75fe1 --- /dev/null +++ b/mobile/lib/domain/models/sync_event.model.dart @@ -0,0 +1,13 @@ +import 'package:openapi/api.dart'; + +class SyncEvent { + final SyncEntityType type; + // ignore: avoid-dynamic + final dynamic data; + final String ack; + + const SyncEvent({required this.type, required this.data, required this.ack}); + + @override + String toString() => 'SyncEvent(type: $type, data: $data, ack: $ack)'; +} diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 72e29b3677..8d7d87e35e 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -1,49 +1,200 @@ +// ignore_for_file: avoid-passing-async-when-sync-expected + import 'dart:async'; -import 'package:flutter/foundation.dart'; +import 'package:collection/collection.dart'; import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; +import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart'; +import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; +import 'package:worker_manager/worker_manager.dart'; + +const _kSyncTypeOrder = [ + SyncEntityType.userDeleteV1, + SyncEntityType.userV1, + SyncEntityType.partnerDeleteV1, + SyncEntityType.partnerV1, + SyncEntityType.assetDeleteV1, + SyncEntityType.assetV1, + SyncEntityType.assetExifV1, + SyncEntityType.partnerAssetDeleteV1, + SyncEntityType.partnerAssetV1, + SyncEntityType.partnerAssetExifV1, +]; class SyncStreamService { + final Logger _logger = Logger('SyncStreamService'); + final ISyncApiRepository _syncApiRepository; + final ISyncStreamRepository _syncStreamRepository; + final bool Function()? _cancelChecker; - SyncStreamService(this._syncApiRepository); + SyncStreamService({ + required ISyncApiRepository syncApiRepository, + required ISyncStreamRepository syncStreamRepository, + bool Function()? cancelChecker, + }) : _syncApiRepository = syncApiRepository, + _syncStreamRepository = syncStreamRepository, + _cancelChecker = cancelChecker; - StreamSubscription? _userSyncSubscription; + Future _handleSyncData( + SyncEntityType type, + // ignore: avoid-dynamic + Iterable data, + ) async { + if (data.isEmpty) { + _logger.warning("Received empty sync data for $type"); + return false; + } - void syncUsers() { - _userSyncSubscription = - _syncApiRepository.watchUserSyncEvent().listen((events) async { - for (final event in events) { - if (event.data is SyncUserV1) { - final data = event.data as SyncUserV1; - debugPrint("User Update: $data"); + _logger.fine("Processing sync data for $type of length ${data.length}"); - // final user = await _userRepository.get(data.id); - - // if (user == null) { - // continue; - // } - - // user.name = data.name; - // user.email = data.email; - // user.updatedAt = DateTime.now(); - - // await _userRepository.update(user); - // await _syncApiRepository.ack(event.ack); - } - - if (event.data is SyncUserDeleteV1) { - final data = event.data as SyncUserDeleteV1; - - debugPrint("User delete: $data"); - // await _syncApiRepository.ack(event.ack); - } + try { + if (type == SyncEntityType.partnerV1) { + return await _syncStreamRepository.updatePartnerV1(data.cast()); } + + if (type == SyncEntityType.partnerDeleteV1) { + return await _syncStreamRepository.deletePartnerV1(data.cast()); + } + + if (type == SyncEntityType.userV1) { + return await _syncStreamRepository.updateUsersV1(data.cast()); + } + + if (type == SyncEntityType.userDeleteV1) { + return await _syncStreamRepository.deleteUsersV1(data.cast()); + } + } catch (error, stack) { + _logger.severe("Error processing sync data for $type", error, stack); + return false; + } + + _logger.warning("Unknown sync data type: $type"); + return false; + } + + Future _syncEvent(List types) { + _logger.info("Syncing Events: $types"); + final streamCompleter = Completer(); + bool shouldComplete = false; + // the onDone callback might fire before the events are processed + // the following flag ensures that the onDone callback is not called + // before the events are processed and also that events are processed sequentially + Completer? mutex; + StreamSubscription? subscription; + try { + subscription = _syncApiRepository.getSyncEvents(types).listen( + (events) async { + if (events.isEmpty) { + _logger.warning("Received empty sync events"); + return; + } + + // If previous events are still being processed, wait for them to finish + if (mutex != null) { + await mutex!.future; + } + + if (_cancelChecker?.call() ?? false) { + _logger.info("Sync cancelled, stopping stream"); + subscription?.cancel(); + if (!streamCompleter.isCompleted) { + streamCompleter.completeError( + CanceledError(), + StackTrace.current, + ); + } + return; + } + + // Take control of the mutex and process the events + mutex = Completer(); + + try { + final eventsMap = events.groupListsBy((event) => event.type); + final Map acks = {}; + + for (final type in _kSyncTypeOrder) { + final data = eventsMap[type]; + if (data == null) { + continue; + } + + if (_cancelChecker?.call() ?? false) { + _logger.info("Sync cancelled, stopping stream"); + mutex?.complete(); + mutex = null; + if (!streamCompleter.isCompleted) { + streamCompleter.completeError( + CanceledError(), + StackTrace.current, + ); + } + + return; + } + + if (data.isEmpty) { + _logger.warning("Received empty sync events for $type"); + continue; + } + + if (await _handleSyncData(type, data.map((e) => e.data))) { + // ignore: avoid-unsafe-collection-methods + acks[type] = data.last.ack; + } else { + _logger.warning("Failed to handle sync events for $type"); + } + } + + if (acks.isNotEmpty) { + await _syncApiRepository.ack(acks.values.toList()); + } + _logger.info("$types events processed"); + } catch (error, stack) { + _logger.warning("Error handling sync events", error, stack); + } finally { + mutex?.complete(); + mutex = null; + } + + if (shouldComplete) { + _logger.info("Sync done, completing stream"); + if (!streamCompleter.isCompleted) streamCompleter.complete(); + } + }, + onError: (error, stack) { + _logger.warning("Error in sync stream for $types", error, stack); + // Do not proceed if the stream errors + if (!streamCompleter.isCompleted) { + // ignore: avoid-missing-completer-stack-trace + streamCompleter.completeError(error, stack); + } + }, + onDone: () { + _logger.info("$types stream done"); + if (mutex == null && !streamCompleter.isCompleted) { + streamCompleter.complete(); + } else { + // Marks the stream as done but does not complete the completer + // until the events are processed + shouldComplete = true; + } + }, + ); + } catch (error, stack) { + _logger.severe("Error starting sync stream", error, stack); + if (!streamCompleter.isCompleted) { + streamCompleter.completeError(error, stack); + } + } + return streamCompleter.future.whenComplete(() { + _logger.info("Sync stream completed"); + return subscription?.cancel(); }); } - Future dispose() async { - await _userSyncSubscription?.cancel(); - } + Future syncUsers() => + _syncEvent([SyncRequestType.usersV1, SyncRequestType.partnersV1]); } diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart new file mode 100644 index 0000000000..0bd456f0bb --- /dev/null +++ b/mobile/lib/domain/utils/background_sync.dart @@ -0,0 +1,37 @@ +// ignore_for_file: avoid-passing-async-when-sync-expected + +import 'dart:async'; + +import 'package:immich_mobile/providers/infrastructure/sync_stream.provider.dart'; +import 'package:immich_mobile/utils/isolate.dart'; +import 'package:worker_manager/worker_manager.dart'; + +class BackgroundSyncManager { + Cancelable? _userSyncTask; + + BackgroundSyncManager(); + + Future cancel() { + final futures = []; + if (_userSyncTask != null) { + futures.add(_userSyncTask!.future); + } + _userSyncTask?.cancel(); + _userSyncTask = null; + return Future.wait(futures); + } + + Future syncUsers() { + if (_userSyncTask != null) { + return _userSyncTask!.future; + } + + _userSyncTask = runInIsolateGentle( + computation: (ref) => ref.read(syncStreamServiceProvider).syncUsers(), + ); + _userSyncTask!.whenComplete(() { + _userSyncTask = null; + }); + return _userSyncTask!.future; + } +} diff --git a/mobile/lib/extensions/string_extensions.dart b/mobile/lib/extensions/string_extensions.dart index 67411013ee..73c8c2d34c 100644 --- a/mobile/lib/extensions/string_extensions.dart +++ b/mobile/lib/extensions/string_extensions.dart @@ -1,3 +1,7 @@ +import 'dart:typed_data'; + +import 'package:uuid/parsing.dart'; + extension StringExtension on String { String capitalize() { return split(" ") @@ -29,3 +33,8 @@ extension DurationExtension on String { return int.parse(this); } } + +extension UUIDExtension on String { + Uint8List toUuidByte({bool shouldValidate = false}) => + UuidParsing.parseAsByteList(this, validate: shouldValidate); +} diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index 88a6838c44..a26b867df6 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -1,37 +1,36 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; -import 'package:immich_mobile/domain/models/sync/sync_event.model.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:openapi/api.dart'; import 'package:http/http.dart' as http; +import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; +import 'package:immich_mobile/domain/models/sync_event.model.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:logging/logging.dart'; +import 'package:openapi/api.dart'; class SyncApiRepository implements ISyncApiRepository { + final Logger _logger = Logger('SyncApiRepository'); final ApiService _api; - const SyncApiRepository(this._api); + final int _batchSize; + SyncApiRepository(this._api, {int batchSize = kSyncEventBatchSize}) + : _batchSize = batchSize; @override - Stream> watchUserSyncEvent() { - return _getSyncStream( - SyncStreamDto(types: [SyncRequestType.usersV1]), - ); + Stream> getSyncEvents(List type) { + return _getSyncStream(SyncStreamDto(types: type)); } @override - Future ack(String data) { - return _api.syncApi.sendSyncAck(SyncAckSetDto(acks: [data])); + Future ack(List data) { + return _api.syncApi.sendSyncAck(SyncAckSetDto(acks: data)); } - Stream> _getSyncStream( - SyncStreamDto dto, { - int batchSize = 5000, - }) async* { + Stream> _getSyncStream(SyncStreamDto dto) async* { final client = http.Client(); final endpoint = "${_api.apiClient.basePath}/sync/stream"; - final headers = { + final headers = { 'Content-Type': 'application/json', 'Accept': 'application/jsonlines+json', }; @@ -61,52 +60,54 @@ class SyncApiRepository implements ISyncApiRepository { await for (final chunk in response.stream.transform(utf8.decoder)) { previousChunk += chunk; - final parts = previousChunk.split('\n'); + final parts = previousChunk.toString().split('\n'); previousChunk = parts.removeLast(); lines.addAll(parts); - if (lines.length < batchSize) { + if (lines.length < _batchSize) { continue; } - yield await compute(_parseSyncResponse, lines); + yield _parseSyncResponse(lines); lines.clear(); } } finally { if (lines.isNotEmpty) { - yield await compute(_parseSyncResponse, lines); + yield _parseSyncResponse(lines); } client.close(); } } + + List _parseSyncResponse(List lines) { + final List data = []; + + for (final line in lines) { + try { + final jsonData = jsonDecode(line); + final type = SyncEntityType.fromJson(jsonData['type'])!; + final dataJson = jsonData['data']; + final ack = jsonData['ack']; + final converter = _kResponseMap[type]; + if (converter == null) { + _logger.warning("[_parseSyncResponse] Unknown type $type"); + continue; + } + + data.add(SyncEvent(type: type, data: converter(dataJson), ack: ack)); + } catch (error, stack) { + _logger.severe("[_parseSyncResponse] Error parsing json", error, stack); + } + } + + return data; + } } +// ignore: avoid-dynamic const _kResponseMap = { SyncEntityType.userV1: SyncUserV1.fromJson, SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson, + SyncEntityType.partnerV1: SyncPartnerV1.fromJson, + SyncEntityType.partnerDeleteV1: SyncPartnerDeleteV1.fromJson, }; - -// Need to be outside of the class to be able to use compute -List _parseSyncResponse(List lines) { - final List data = []; - - for (var line in lines) { - try { - final jsonData = jsonDecode(line); - final type = SyncEntityType.fromJson(jsonData['type'])!; - final dataJson = jsonData['data']; - final ack = jsonData['ack']; - final converter = _kResponseMap[type]; - if (converter == null) { - debugPrint("[_parseSyncReponse] Unknown type $type"); - continue; - } - - data.add(SyncEvent(data: converter(dataJson), ack: ack)); - } catch (error, stack) { - debugPrint("[_parseSyncReponse] Error parsing json $error $stack"); - } - } - - return data; -} diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart new file mode 100644 index 0000000000..a947a9a66b --- /dev/null +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -0,0 +1,104 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart'; +import 'package:immich_mobile/extensions/string_extensions.dart'; +import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:logging/logging.dart'; +import 'package:openapi/api.dart'; + +class DriftSyncStreamRepository extends DriftDatabaseRepository + implements ISyncStreamRepository { + final Logger _logger = Logger('DriftSyncStreamRepository'); + final Drift _db; + + DriftSyncStreamRepository(super.db) : _db = db; + + @override + Future deleteUsersV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final user in data) { + batch.delete( + _db.userEntity, + UserEntityCompanion(id: Value(user.userId.toUuidByte())), + ); + } + }); + return true; + } catch (e, s) { + _logger.severe('Error while processing SyncUserDeleteV1', e, s); + return false; + } + } + + @override + Future updateUsersV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final user in data) { + final companion = UserEntityCompanion( + name: Value(user.name), + email: Value(user.email), + ); + + batch.insert( + _db.userEntity, + companion.copyWith(id: Value(user.id.toUuidByte())), + onConflict: DoUpdate((_) => companion), + ); + } + }); + return true; + } catch (e, s) { + _logger.severe('Error while processing SyncUserV1', e, s); + return false; + } + } + + @override + Future deletePartnerV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final partner in data) { + batch.delete( + _db.partnerEntity, + PartnerEntityCompanion( + sharedById: Value(partner.sharedById.toUuidByte()), + sharedWithId: Value(partner.sharedWithId.toUuidByte()), + ), + ); + } + }); + return true; + } catch (e, s) { + _logger.severe('Error while processing SyncPartnerDeleteV1', e, s); + return false; + } + } + + @override + Future updatePartnerV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final partner in data) { + final companion = + PartnerEntityCompanion(inTimeline: Value(partner.inTimeline)); + + batch.insert( + _db.partnerEntity, + companion.copyWith( + sharedById: Value(partner.sharedById.toUuidByte()), + sharedWithId: Value(partner.sharedWithId.toUuidByte()), + ), + onConflict: DoUpdate((_) => companion), + ); + } + }); + return true; + } catch (e, s) { + _logger.severe('Error while processing SyncPartnerV1', e, s); + return false; + } + } +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 1a434aa359..73af81d69d 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -11,6 +11,7 @@ import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; @@ -31,13 +32,15 @@ import 'package:immich_mobile/utils/migration.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:logging/logging.dart'; import 'package:timezone/data/latest.dart'; -import 'package:immich_mobile/generated/codegen_loader.g.dart'; +import 'package:worker_manager/worker_manager.dart'; void main() async { ImmichWidgetsBinding(); final db = await Bootstrap.initIsar(); await Bootstrap.initDomain(db); await initApp(); + // Warm-up isolate pool for worker manager + await workerManager.init(dynamicSpawning: true); await migrateDatabaseIfNeeded(db); HttpOverrides.global = HttpSSLCertOverride(); diff --git a/mobile/lib/providers/background_sync.provider.dart b/mobile/lib/providers/background_sync.provider.dart new file mode 100644 index 0000000000..83d103bb3b --- /dev/null +++ b/mobile/lib/providers/background_sync.provider.dart @@ -0,0 +1,8 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/utils/background_sync.dart'; + +final backgroundSyncProvider = Provider((ref) { + final manager = BackgroundSyncManager(); + ref.onDispose(manager.cancel); + return manager; +}); diff --git a/mobile/lib/providers/infrastructure/cancel.provider.dart b/mobile/lib/providers/infrastructure/cancel.provider.dart new file mode 100644 index 0000000000..6851861e1a --- /dev/null +++ b/mobile/lib/providers/infrastructure/cancel.provider.dart @@ -0,0 +1,12 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +/// Provider holding a boolean function that returns true when cancellation is requested. +/// A computation running in the isolate uses the function to implement cooperative cancellation. +final cancellationProvider = Provider( + // This will be overridden in the isolate's container. + // Throwing ensures it's not used without an override. + (ref) => throw UnimplementedError( + "cancellationProvider must be overridden in the isolate's ProviderContainer and not to be used in the root isolate", + ), + name: 'cancellationProvider', +); diff --git a/mobile/lib/providers/infrastructure/db.provider.dart b/mobile/lib/providers/infrastructure/db.provider.dart index 84010b3b96..4eefbc556c 100644 --- a/mobile/lib/providers/infrastructure/db.provider.dart +++ b/mobile/lib/providers/infrastructure/db.provider.dart @@ -1,4 +1,7 @@ +import 'dart:async'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:isar/isar.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -6,3 +9,9 @@ part 'db.provider.g.dart'; @Riverpod(keepAlive: true) Isar isar(Ref ref) => throw UnimplementedError('isar'); + +final driftProvider = Provider((ref) { + final drift = Drift(); + ref.onDispose(() => unawaited(drift.close())); + return drift; +}); diff --git a/mobile/lib/providers/infrastructure/sync_stream.provider.dart b/mobile/lib/providers/infrastructure/sync_stream.provider.dart index 64f1a6cb05..e313982a30 100644 --- a/mobile/lib/providers/infrastructure/sync_stream.provider.dart +++ b/mobile/lib/providers/infrastructure/sync_stream.provider.dart @@ -1,24 +1,23 @@ -import 'dart:async'; - import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/sync_stream.service.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; final syncStreamServiceProvider = Provider( - (ref) { - final instance = SyncStreamService( - ref.watch(syncApiRepositoryProvider), - ); - - ref.onDispose(() => unawaited(instance.dispose())); - - return instance; - }, + (ref) => SyncStreamService( + syncApiRepository: ref.watch(syncApiRepositoryProvider), + syncStreamRepository: ref.watch(syncStreamRepositoryProvider), + cancelChecker: ref.watch(cancellationProvider), + ), ); final syncApiRepositoryProvider = Provider( - (ref) => SyncApiRepository( - ref.watch(apiServiceProvider), - ), + (ref) => SyncApiRepository(ref.watch(apiServiceProvider)), +); + +final syncStreamRepositoryProvider = Provider( + (ref) => DriftSyncStreamRepository(ref.watch(driftProvider)), ); diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart index 20fa62dc4b..ec053c078b 100644 --- a/mobile/lib/services/auth.service.dart +++ b/mobile/lib/services/auth.service.dart @@ -3,12 +3,14 @@ import 'dart:io'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/interfaces/auth.interface.dart'; import 'package:immich_mobile/interfaces/auth_api.interface.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/repositories/auth.repository.dart'; import 'package:immich_mobile/repositories/auth_api.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; @@ -22,6 +24,7 @@ final authServiceProvider = Provider( ref.watch(authRepositoryProvider), ref.watch(apiServiceProvider), ref.watch(networkServiceProvider), + ref.watch(backgroundSyncProvider), ), ); @@ -30,6 +33,7 @@ class AuthService { final IAuthRepository _authRepository; final ApiService _apiService; final NetworkService _networkService; + final BackgroundSyncManager _backgroundSyncManager; final _log = Logger("AuthService"); @@ -38,6 +42,7 @@ class AuthService { this._authRepository, this._apiService, this._networkService, + this._backgroundSyncManager, ); /// Validates the provided server URL by resolving and setting the endpoint. @@ -115,8 +120,10 @@ class AuthService { /// - Asset ETag /// /// All deletions are executed in parallel using [Future.wait]. - Future clearLocalData() { - return Future.wait([ + Future clearLocalData() async { + // Cancel any ongoing background sync operations before clearing data + await _backgroundSyncManager.cancel(); + await Future.wait([ _authRepository.clearLocalData(), Store.delete(StoreKey.currentUser), Store.delete(StoreKey.accessToken), diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index 570752c6d9..26f3b49242 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -48,11 +48,15 @@ abstract final class Bootstrap { ); } - static Future initDomain(Isar db) async { + static Future initDomain( + Isar db, { + bool shouldBufferLogs = true, + }) async { await StoreService.init(storeRepository: IsarStoreRepository(db)); await LogService.init( logRepository: IsarLogRepository(db), storeRepository: IsarStoreRepository(db), + shouldBuffer: shouldBufferLogs, ); } } diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart new file mode 100644 index 0000000000..cfbb1b544f --- /dev/null +++ b/mobile/lib/utils/isolate.dart @@ -0,0 +1,69 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/utils/bootstrap.dart'; +import 'package:logging/logging.dart'; +import 'package:worker_manager/worker_manager.dart'; + +class InvalidIsolateUsageException implements Exception { + const InvalidIsolateUsageException(); + + @override + String toString() => + "IsolateHelper should only be used from the root isolate"; +} + +// !! Should be used only from the root isolate +Cancelable runInIsolateGentle({ + required Future Function(ProviderContainer ref) computation, + String? debugLabel, +}) { + final token = RootIsolateToken.instance; + if (token == null) { + throw const InvalidIsolateUsageException(); + } + + return workerManager.executeGentle((cancelledChecker) async { + BackgroundIsolateBinaryMessenger.ensureInitialized(token); + DartPluginRegistrant.ensureInitialized(); + + final db = await Bootstrap.initIsar(); + await Bootstrap.initDomain(db, shouldBufferLogs: false); + final ref = ProviderContainer( + overrides: [ + // TODO: Remove once isar is removed + dbProvider.overrideWithValue(db), + isarProvider.overrideWithValue(db), + cancellationProvider.overrideWithValue(cancelledChecker), + ], + ); + + Logger log = Logger("IsolateLogger"); + + try { + return await computation(ref); + } on CanceledError { + log.warning( + "Computation cancelled ${debugLabel == null ? '' : ' for $debugLabel'}", + ); + } catch (error, stack) { + log.severe( + "Error in runInIsolateGentle ${debugLabel == null ? '' : ' for $debugLabel'}", + error, + stack, + ); + } finally { + // Wait for the logs to flush + await Future.delayed(const Duration(seconds: 2)); + // Always close the new db connection on Isolate end + ref.read(driftProvider).close(); + ref.read(isarProvider).close(); + } + return null; + }); +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 3731832296..235b3f71c3 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1806,7 +1806,7 @@ packages: source: hosted version: "3.1.4" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff @@ -1933,6 +1933,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" + worker_manager: + dependency: "direct main" + description: + name: worker_manager + sha256: "086ed63e9b36266e851404ca90fd44e37c0f4c9bbf819e5f8d7c87f9741c0591" + url: "https://pub.dev" + source: hosted + version: "7.2.3" xdg_directories: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 03c39810f6..fdd91e1f87 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -60,7 +60,9 @@ dependencies: thumbhash: 0.1.0+1 timezone: ^0.9.4 url_launcher: ^6.3.1 + uuid: ^4.5.1 wakelock_plus: ^1.2.10 + worker_manager: ^7.2.3 native_video_player: git: diff --git a/mobile/test/domain/service.mock.dart b/mobile/test/domain/service.mock.dart index 53a173fc28..97a3f30294 100644 --- a/mobile/test/domain/service.mock.dart +++ b/mobile/test/domain/service.mock.dart @@ -1,7 +1,10 @@ import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; +import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:mocktail/mocktail.dart'; class MockStoreService extends Mock implements StoreService {} class MockUserService extends Mock implements UserService {} + +class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {} diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart new file mode 100644 index 0000000000..e1d8e6987f --- /dev/null +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -0,0 +1,443 @@ +// ignore_for_file: avoid-unnecessary-futures, avoid-async-call-in-sync-function + +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; +import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart'; +import 'package:immich_mobile/domain/models/sync_event.model.dart'; +import 'package:immich_mobile/domain/services/sync_stream.service.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:openapi/api.dart'; +import 'package:worker_manager/worker_manager.dart'; + +import '../../fixtures/sync_stream.stub.dart'; +import '../../infrastructure/repository.mock.dart'; + +class _CancellationWrapper { + const _CancellationWrapper(); + + bool isCancelled() => false; +} + +class _MockCancellationWrapper extends Mock implements _CancellationWrapper {} + +void main() { + late SyncStreamService sut; + late ISyncStreamRepository mockSyncStreamRepo; + late ISyncApiRepository mockSyncApiRepo; + late StreamController> streamController; + + successHandler(Invocation _) async => true; + failureHandler(Invocation _) async => false; + + setUp(() { + mockSyncStreamRepo = MockSyncStreamRepository(); + mockSyncApiRepo = MockSyncApiRepository(); + streamController = StreamController>.broadcast(); + + sut = SyncStreamService( + syncApiRepository: mockSyncApiRepo, + syncStreamRepository: mockSyncStreamRepo, + ); + + // Default stream setup - emits one batch and closes + when(() => mockSyncApiRepo.getSyncEvents(any())) + .thenAnswer((_) => streamController.stream); + + // Default ack setup + when(() => mockSyncApiRepo.ack(any())).thenAnswer((_) async => {}); + + // Register fallbacks for mocktail verification + registerFallbackValue([]); + registerFallbackValue([]); + registerFallbackValue([]); + registerFallbackValue([]); + + // Default successful repository calls + when(() => mockSyncStreamRepo.updateUsersV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deleteUsersV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updatePartnerV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deletePartnerV1(any())) + .thenAnswer(successHandler); + }); + + tearDown(() async { + if (!streamController.isClosed) { + await streamController.close(); + } + }); + + // Helper to trigger sync and add events to the stream + Future triggerSyncAndEmit(List events) async { + final future = sut.syncUsers(); // Start listening + await Future.delayed(Duration.zero); // Allow listener to attach + if (!streamController.isClosed) { + streamController.add(events); + await streamController.close(); // Close after emitting + } + await future; // Wait for processing to complete + } + + group("SyncStreamService", () { + test( + "completes successfully when stream emits data and handlers succeed", + () async { + final events = [ + ...SyncStreamStub.userEvents, + ...SyncStreamStub.partnerEvents, + ]; + final future = triggerSyncAndEmit(events); + await expectLater(future, completes); + // Verify ack includes last ack from each successfully handled type + verify( + () => + mockSyncApiRepo.ack(any(that: containsAll(["5", "2", "4", "3"]))), + ).called(1); + }, + ); + + test("completes successfully when stream emits an error", () async { + when(() => mockSyncApiRepo.getSyncEvents(any())) + .thenAnswer((_) => Stream.error(Exception("Stream Error"))); + // Should complete gracefully without throwing + await expectLater(sut.syncUsers(), throwsException); + verifyNever(() => mockSyncApiRepo.ack(any())); // No ack on stream error + }); + + test("throws when initial getSyncEvents call fails", () async { + final apiException = Exception("API Error"); + when(() => mockSyncApiRepo.getSyncEvents(any())).thenThrow(apiException); + // Should rethrow the exception from the initial call + await expectLater(sut.syncUsers(), throwsA(apiException)); + verifyNever(() => mockSyncApiRepo.ack(any())); + }); + + test( + "completes successfully when a repository handler throws an exception", + () async { + when(() => mockSyncStreamRepo.updateUsersV1(any())) + .thenThrow(Exception("Repo Error")); + final events = [ + ...SyncStreamStub.userEvents, + ...SyncStreamStub.partnerEvents, + ]; + // Should complete, but ack only for the successful types + await triggerSyncAndEmit(events); + // Only partner delete was successful by default setup + verify(() => mockSyncApiRepo.ack(["2", "4", "3"])).called(1); + }, + ); + + test( + "completes successfully but sends no ack when all handlers fail", + () async { + when(() => mockSyncStreamRepo.updateUsersV1(any())) + .thenAnswer(failureHandler); + when(() => mockSyncStreamRepo.deleteUsersV1(any())) + .thenAnswer(failureHandler); + when(() => mockSyncStreamRepo.updatePartnerV1(any())) + .thenAnswer(failureHandler); + when(() => mockSyncStreamRepo.deletePartnerV1(any())) + .thenAnswer(failureHandler); + + final events = [ + ...SyncStreamStub.userEvents, + ...SyncStreamStub.partnerEvents, + ]; + await triggerSyncAndEmit(events); + verifyNever(() => mockSyncApiRepo.ack(any())); + }, + ); + + test("sends ack only for types where handler returns true", () async { + // Mock specific handlers: user update fails, user delete succeeds + when(() => mockSyncStreamRepo.updateUsersV1(any())) + .thenAnswer(failureHandler); + when(() => mockSyncStreamRepo.deleteUsersV1(any())) + .thenAnswer(successHandler); + // partner update fails, partner delete succeeds + when(() => mockSyncStreamRepo.updatePartnerV1(any())) + .thenAnswer(failureHandler); + + final events = [ + ...SyncStreamStub.userEvents, + ...SyncStreamStub.partnerEvents, + ]; + await triggerSyncAndEmit(events); + + // Expect ack only for userDeleteV1 (ack: "2") and partnerDeleteV1 (ack: "4") + verify(() => mockSyncApiRepo.ack(any(that: containsAll(["2", "4"])))) + .called(1); + }); + + test("does not process or ack when stream emits an empty list", () async { + final future = sut.syncUsers(); + streamController.add([]); // Emit empty list + await streamController.close(); + await future; // Wait for completion + + verifyNever(() => mockSyncStreamRepo.updateUsersV1(any())); + verifyNever(() => mockSyncStreamRepo.deleteUsersV1(any())); + verifyNever(() => mockSyncStreamRepo.updatePartnerV1(any())); + verifyNever(() => mockSyncStreamRepo.deletePartnerV1(any())); + verifyNever(() => mockSyncApiRepo.ack(any())); + }); + + test("processes multiple batches sequentially using mutex", () async { + final completer1 = Completer(); + final completer2 = Completer(); + int callOrder = 0; + int handler1StartOrder = -1; + int handler2StartOrder = -1; + int handler1Calls = 0; + int handler2Calls = 0; + + when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer((_) async { + handler1Calls++; + handler1StartOrder = ++callOrder; + await completer1.future; + return true; + }); + when(() => mockSyncStreamRepo.updatePartnerV1(any())) + .thenAnswer((_) async { + handler2Calls++; + handler2StartOrder = ++callOrder; + await completer2.future; + return true; + }); + + final batch1 = SyncStreamStub.userEvents; + final batch2 = SyncStreamStub.partnerEvents; + + final syncFuture = sut.syncUsers(); + await pumpEventQueue(); + + streamController.add(batch1); + await pumpEventQueue(); + // Small delay to ensure the first handler starts + await Future.delayed(const Duration(milliseconds: 20)); + + expect(handler1StartOrder, 1, reason: "Handler 1 should start first"); + expect(handler1Calls, 1); + + streamController.add(batch2); + await pumpEventQueue(); + // Small delay + await Future.delayed(const Duration(milliseconds: 20)); + + expect(handler2StartOrder, -1, reason: "Handler 2 should wait"); + expect(handler2Calls, 0); + + completer1.complete(); + await pumpEventQueue(times: 40); + // Small delay to ensure the second handler starts + await Future.delayed(const Duration(milliseconds: 20)); + + expect(handler2StartOrder, 2, reason: "Handler 2 should start after H1"); + expect(handler2Calls, 1); + + completer2.complete(); + await pumpEventQueue(times: 40); + // Small delay before closing the stream + await Future.delayed(const Duration(milliseconds: 20)); + + if (!streamController.isClosed) { + await streamController.close(); + } + await pumpEventQueue(times: 40); + // Small delay to ensure the sync completes + await Future.delayed(const Duration(milliseconds: 20)); + + await syncFuture; + + verify(() => mockSyncStreamRepo.updateUsersV1(any())).called(1); + verify(() => mockSyncStreamRepo.updatePartnerV1(any())).called(1); + verify(() => mockSyncApiRepo.ack(any())).called(2); + }); + + test( + "stops processing and ack when cancel checker is completed", + () async { + final cancellationChecker = _MockCancellationWrapper(); + when(() => cancellationChecker.isCancelled()).thenAnswer((_) => false); + + sut = SyncStreamService( + syncApiRepository: mockSyncApiRepo, + syncStreamRepository: mockSyncStreamRepo, + cancelChecker: cancellationChecker.isCancelled, + ); + + final processingCompleter = Completer(); + bool handlerStarted = false; + + // Make handler wait so we can cancel it mid-flight + when(() => mockSyncStreamRepo.deleteUsersV1(any())) + .thenAnswer((_) async { + handlerStarted = true; + await processingCompleter + .future; // Wait indefinitely until test completes it + return true; + }); + + final syncFuture = sut.syncUsers(); + await pumpEventQueue(times: 30); + + streamController.add(SyncStreamStub.userEvents); + // Ensure processing starts + await Future.delayed(const Duration(milliseconds: 10)); + + expect(handlerStarted, isTrue, reason: "Handler should have started"); + + when(() => cancellationChecker.isCancelled()).thenAnswer((_) => true); + + // Allow cancellation logic to propagate + await Future.delayed(const Duration(milliseconds: 10)); + + // Complete the handler's completer after cancellation signal + // to ensure the cancellation logic itself isn't blocked by the handler. + processingCompleter.complete(); + + await expectLater(syncFuture, throwsA(isA())); + + // Verify that ack was NOT called because processing was cancelled + verifyNever(() => mockSyncApiRepo.ack(any())); + }, + ); + + test("completes successfully when ack call throws an exception", () async { + when(() => mockSyncApiRepo.ack(any())).thenThrow(Exception("Ack Error")); + final events = [ + ...SyncStreamStub.userEvents, + ...SyncStreamStub.partnerEvents, + ]; + + // Should still complete even if ack fails + await triggerSyncAndEmit(events); + verify(() => mockSyncApiRepo.ack(any())) + .called(1); // Verify ack was attempted + }); + + test("waits for processing to finish if onDone called early", () async { + final processingCompleter = Completer(); + bool handlerFinished = false; + + when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer((_) async { + await processingCompleter.future; // Wait inside handler + handlerFinished = true; + return true; + }); + + final syncFuture = sut.syncUsers(); + // Allow listener to attach + // This is necessary to ensure the stream is ready to receive events + await Future.delayed(Duration.zero); + + streamController.add(SyncStreamStub.userEvents); // Emit batch + await Future.delayed( + const Duration(milliseconds: 10), + ); // Ensure processing starts + + await streamController + .close(); // Close stream (triggers onDone internally) + await Future.delayed( + const Duration(milliseconds: 10), + ); // Give onDone a chance to fire + + // At this point, onDone was called, but processing is blocked + expect(handlerFinished, isFalse); + + processingCompleter.complete(); // Allow processing to finish + await syncFuture; // Now the main future should complete + + expect(handlerFinished, isTrue); + verify(() => mockSyncApiRepo.ack(any())).called(1); + }); + + test("processes events in the defined _kSyncTypeOrder", () async { + final future = sut.syncUsers(); + await pumpEventQueue(); + if (!streamController.isClosed) { + final events = [ + SyncEvent( + type: SyncEntityType.partnerV1, + data: SyncStreamStub.partnerV1, + ack: "1", + ), // Should be processed last + SyncEvent( + type: SyncEntityType.userV1, + data: SyncStreamStub.userV1Admin, + ack: "2", + ), // Should be processed second + SyncEvent( + type: SyncEntityType.partnerDeleteV1, + data: SyncStreamStub.partnerDeleteV1, + ack: "3", + ), // Should be processed third + SyncEvent( + type: SyncEntityType.userDeleteV1, + data: SyncStreamStub.userDeleteV1, + ack: "4", + ), // Should be processed first + ]; + + streamController.add(events); + await streamController.close(); + } + await future; + + verifyInOrder([ + () => mockSyncStreamRepo.deleteUsersV1(any()), + () => mockSyncStreamRepo.updateUsersV1(any()), + () => mockSyncStreamRepo.deletePartnerV1(any()), + () => mockSyncStreamRepo.updatePartnerV1(any()), + // Verify ack happens after all processing + () => mockSyncApiRepo.ack(any()), + ]); + }); + }); + + group("syncUsers", () { + test("calls getSyncEvents with correct types", () async { + // Need to close the stream for the future to complete + final future = sut.syncUsers(); + await streamController.close(); + await future; + + verify( + () => mockSyncApiRepo.getSyncEvents([ + SyncRequestType.usersV1, + SyncRequestType.partnersV1, + ]), + ).called(1); + }); + + test("calls repository methods with correctly grouped data", () async { + final events = [ + ...SyncStreamStub.userEvents, + ...SyncStreamStub.partnerEvents, + ]; + await triggerSyncAndEmit(events); + + // Verify each handler was called with the correct list of data payloads + verify( + () => mockSyncStreamRepo.updateUsersV1( + [SyncStreamStub.userV1Admin, SyncStreamStub.userV1User], + ), + ).called(1); + verify( + () => mockSyncStreamRepo.deleteUsersV1([SyncStreamStub.userDeleteV1]), + ).called(1); + verify( + () => mockSyncStreamRepo.updatePartnerV1([SyncStreamStub.partnerV1]), + ).called(1); + verify( + () => mockSyncStreamRepo + .deletePartnerV1([SyncStreamStub.partnerDeleteV1]), + ).called(1); + }); + }); +} diff --git a/mobile/test/fixtures/sync_stream.stub.dart b/mobile/test/fixtures/sync_stream.stub.dart new file mode 100644 index 0000000000..781e63a2bb --- /dev/null +++ b/mobile/test/fixtures/sync_stream.stub.dart @@ -0,0 +1,45 @@ +import 'package:immich_mobile/domain/models/sync_event.model.dart'; +import 'package:openapi/api.dart'; + +abstract final class SyncStreamStub { + static final userV1Admin = SyncUserV1( + deletedAt: DateTime(2020), + email: "admin@admin", + id: "1", + name: "Admin", + ); + static final userV1User = SyncUserV1( + deletedAt: DateTime(2021), + email: "user@user", + id: "2", + name: "User", + ); + static final userDeleteV1 = SyncUserDeleteV1(userId: "2"); + static final userEvents = [ + SyncEvent(type: SyncEntityType.userV1, data: userV1Admin, ack: "1"), + SyncEvent( + type: SyncEntityType.userDeleteV1, + data: userDeleteV1, + ack: "2", + ), + SyncEvent(type: SyncEntityType.userV1, data: userV1User, ack: "5"), + ]; + + static final partnerV1 = SyncPartnerV1( + inTimeline: true, + sharedById: "1", + sharedWithId: "2", + ); + static final partnerDeleteV1 = SyncPartnerDeleteV1( + sharedById: "3", + sharedWithId: "4", + ); + static final partnerEvents = [ + SyncEvent( + type: SyncEntityType.partnerDeleteV1, + data: partnerDeleteV1, + ack: "4", + ), + SyncEvent(type: SyncEntityType.partnerV1, data: partnerV1, ack: "3"), + ]; +} diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index 192858adff..c4a5680f71 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -1,6 +1,8 @@ import 'package:immich_mobile/domain/interfaces/device_asset.interface.dart'; import 'package:immich_mobile/domain/interfaces/log.interface.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart'; +import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; +import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart'; import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/domain/interfaces/user_api.interface.dart'; import 'package:mocktail/mocktail.dart'; @@ -14,5 +16,9 @@ class MockUserRepository extends Mock implements IUserRepository {} class MockDeviceAssetRepository extends Mock implements IDeviceAssetRepository {} +class MockSyncStreamRepository extends Mock implements ISyncStreamRepository {} + // API Repos class MockUserApiRepository extends Mock implements IUserApiRepository {} + +class MockSyncApiRepository extends Mock implements ISyncApiRepository {} diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index e1b8df40a3..87a8c01cf0 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -29,4 +29,3 @@ class MockSearchApi extends Mock implements SearchApi {} class MockAppSettingService extends Mock implements AppSettingsService {} class MockBackgroundService extends Mock implements BackgroundService {} - diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart index e4f011d940..4ada98a6c9 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -8,6 +8,7 @@ import 'package:isar/isar.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; +import '../domain/service.mock.dart'; import '../repository.mocks.dart'; import '../service.mocks.dart'; import '../test_utils.dart'; @@ -18,6 +19,7 @@ void main() { late MockAuthRepository authRepository; late MockApiService apiService; late MockNetworkService networkService; + late MockBackgroundSyncManager backgroundSyncManager; late Isar db; setUp(() async { @@ -25,12 +27,14 @@ void main() { authRepository = MockAuthRepository(); apiService = MockApiService(); networkService = MockNetworkService(); + backgroundSyncManager = MockBackgroundSyncManager(); sut = AuthService( authApiRepository, authRepository, apiService, networkService, + backgroundSyncManager, ); registerFallbackValue(Uri()); @@ -116,24 +120,28 @@ void main() { group('logout', () { test('Should logout user', () async { when(() => authApiRepository.logout()).thenAnswer((_) async => {}); + when(() => backgroundSyncManager.cancel()).thenAnswer((_) async => {}); when(() => authRepository.clearLocalData()) .thenAnswer((_) => Future.value(null)); await sut.logout(); verify(() => authApiRepository.logout()).called(1); + verify(() => backgroundSyncManager.cancel()).called(1); verify(() => authRepository.clearLocalData()).called(1); }); test('Should clear local data even on server error', () async { when(() => authApiRepository.logout()) .thenThrow(Exception('Server error')); + when(() => backgroundSyncManager.cancel()).thenAnswer((_) async => {}); when(() => authRepository.clearLocalData()) .thenAnswer((_) => Future.value(null)); await sut.logout(); verify(() => authApiRepository.logout()).called(1); + verify(() => backgroundSyncManager.cancel()).called(1); verify(() => authRepository.clearLocalData()).called(1); }); }); From e275f2d8b390cb95cb861118747be27507e34493 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 17 Apr 2025 14:41:06 -0400 Subject: [PATCH 003/356] feat: add foreign key indexes (#17672) --- server/src/decorators.ts | 3 +- .../1744900200559-AddForeignKeyIndexes.ts | 51 ++++++++++++++ server/src/schema/tables/activity.table.ts | 4 +- server/src/schema/tables/album-asset.table.ts | 18 +---- server/src/schema/tables/album.table.ts | 4 +- server/src/schema/tables/api-key.table.ts | 4 +- server/src/schema/tables/asset-audit.table.ts | 11 ++- server/src/schema/tables/asset-face.table.ts | 15 ++++- server/src/schema/tables/asset-files.table.ts | 11 +-- server/src/schema/tables/asset.table.ts | 16 ++--- server/src/schema/tables/exif.table.ts | 14 ++-- server/src/schema/tables/library.table.ts | 4 +- server/src/schema/tables/memory.table.ts | 4 +- .../src/schema/tables/memory_asset.table.ts | 4 +- .../src/schema/tables/partner-audit.table.ts | 11 ++- server/src/schema/tables/partner.table.ts | 20 +++--- server/src/schema/tables/person.table.ts | 4 +- server/src/schema/tables/session.table.ts | 4 +- .../schema/tables/shared-link-asset.table.ts | 4 +- server/src/schema/tables/shared-link.table.ts | 21 +++--- .../schema/tables/sync-checkpoint.table.ts | 13 +--- server/src/schema/tables/tag-asset.table.ts | 8 +-- server/src/schema/tables/tag-closure.table.ts | 8 +-- server/src/schema/tables/tag.table.ts | 11 +-- server/src/schema/tables/user-audit.table.ts | 5 +- .../src/schema/tables/user-metadata.table.ts | 8 ++- server/src/schema/tables/user.table.ts | 4 +- .../decorators/column-index.decorator.ts | 16 ----- .../from-code/decorators/column.decorator.ts | 6 +- .../foreign-key-column.decorator.ts | 2 - .../from-code/decorators/index.decorator.ts | 9 ++- server/src/sql-tools/from-code/index.ts | 16 +++-- .../processors/check-constraint.processor.ts | 4 +- .../processors/column-index.processor.ts | 32 --------- .../from-code/processors/column.processor.ts | 14 +--- .../foreign-key-constriant.processor.ts | 7 +- .../from-code/processors/index.processor.ts | 67 ++++++++++++++++++- .../primary-key-contraint.processor.ts | 4 +- .../from-code/processors/trigger.processor.ts | 6 +- .../sql-tools/from-code/processors/type.ts | 3 +- .../processors/unique-constraint.processor.ts | 33 ++++++++- .../sql-tools/from-code/register-function.ts | 35 +++++++++- .../src/sql-tools/from-code/register-item.ts | 2 - server/src/sql-tools/helpers.ts | 55 --------------- server/src/sql-tools/public_api.ts | 1 - .../sql-tools/column-index-name-default.ts | 5 +- server/test/sql-tools/column-index-name.ts | 46 +++++++++++++ .../foreign-key-inferred-type.stub.ts | 10 ++- ...foreign-key-with-unique-constraint.stub.ts | 10 ++- 49 files changed, 382 insertions(+), 285 deletions(-) create mode 100644 server/src/migrations/1744900200559-AddForeignKeyIndexes.ts delete mode 100644 server/src/sql-tools/from-code/decorators/column-index.decorator.ts delete mode 100644 server/src/sql-tools/from-code/processors/column-index.processor.ts create mode 100644 server/test/sql-tools/column-index-name.ts diff --git a/server/src/decorators.ts b/server/src/decorators.ts index 7085899af7..1af9342e0b 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -11,7 +11,8 @@ import { setUnion } from 'src/utils/set'; const GeneratedUuidV7Column = (options: Omit = {}) => Column({ ...options, type: 'uuid', nullable: false, default: () => `${immich_uuid_v7.name}()` }); -export const UpdateIdColumn = () => GeneratedUuidV7Column(); +export const UpdateIdColumn = (options: Omit = {}) => + GeneratedUuidV7Column(options); export const PrimaryGeneratedUuidV7Column = () => GeneratedUuidV7Column({ primary: true }); diff --git a/server/src/migrations/1744900200559-AddForeignKeyIndexes.ts b/server/src/migrations/1744900200559-AddForeignKeyIndexes.ts new file mode 100644 index 0000000000..db351d5bab --- /dev/null +++ b/server/src/migrations/1744900200559-AddForeignKeyIndexes.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddForeignKeyIndexes1744900200559 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE INDEX "IDX_0f6fc2fb195f24d19b0fb0d57c" ON "libraries" ("ownerId")`); + await queryRunner.query(`CREATE INDEX "IDX_91704e101438fd0653f582426d" ON "asset_stack" ("primaryAssetId")`); + await queryRunner.query(`CREATE INDEX "IDX_c05079e542fd74de3b5ecb5c1c" ON "asset_stack" ("ownerId")`); + await queryRunner.query(`CREATE INDEX "IDX_2c5ac0d6fb58b238fd2068de67" ON "assets" ("ownerId")`); + await queryRunner.query(`CREATE INDEX "IDX_16294b83fa8c0149719a1f631e" ON "assets" ("livePhotoVideoId")`); + await queryRunner.query(`CREATE INDEX "IDX_9977c3c1de01c3d848039a6b90" ON "assets" ("libraryId")`); + await queryRunner.query(`CREATE INDEX "IDX_f15d48fa3ea5e4bda05ca8ab20" ON "assets" ("stackId")`); + await queryRunner.query(`CREATE INDEX "IDX_b22c53f35ef20c28c21637c85f" ON "albums" ("ownerId")`); + await queryRunner.query(`CREATE INDEX "IDX_05895aa505a670300d4816debc" ON "albums" ("albumThumbnailAssetId")`); + await queryRunner.query(`CREATE INDEX "IDX_1af8519996fbfb3684b58df280" ON "activity" ("albumId")`); + await queryRunner.query(`CREATE INDEX "IDX_3571467bcbe021f66e2bdce96e" ON "activity" ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_8091ea76b12338cb4428d33d78" ON "activity" ("assetId")`); + await queryRunner.query(`CREATE INDEX "IDX_6c2e267ae764a9413b863a2934" ON "api_keys" ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_5527cc99f530a547093f9e577b" ON "person" ("ownerId")`); + await queryRunner.query(`CREATE INDEX "IDX_2bbabe31656b6778c6b87b6102" ON "person" ("faceAssetId")`); + await queryRunner.query(`CREATE INDEX "IDX_575842846f0c28fa5da46c99b1" ON "memories" ("ownerId")`); + await queryRunner.query(`CREATE INDEX "IDX_d7e875c6c60e661723dbf372fd" ON "partners" ("sharedWithId")`); + await queryRunner.query(`CREATE INDEX "IDX_57de40bc620f456c7311aa3a1e" ON "sessions" ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_66fe3837414c5a9f1c33ca4934" ON "shared_links" ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_d8ddd9d687816cc490432b3d4b" ON "session_sync_checkpoints" ("sessionId")`); + await queryRunner.query(`CREATE INDEX "IDX_9f9590cc11561f1f48ff034ef9" ON "tags" ("parentId")`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_66fe3837414c5a9f1c33ca4934";`); + await queryRunner.query(`DROP INDEX "IDX_91704e101438fd0653f582426d";`); + await queryRunner.query(`DROP INDEX "IDX_c05079e542fd74de3b5ecb5c1c";`); + await queryRunner.query(`DROP INDEX "IDX_5527cc99f530a547093f9e577b";`); + await queryRunner.query(`DROP INDEX "IDX_2bbabe31656b6778c6b87b6102";`); + await queryRunner.query(`DROP INDEX "IDX_0f6fc2fb195f24d19b0fb0d57c";`); + await queryRunner.query(`DROP INDEX "IDX_9f9590cc11561f1f48ff034ef9";`); + await queryRunner.query(`DROP INDEX "IDX_2c5ac0d6fb58b238fd2068de67";`); + await queryRunner.query(`DROP INDEX "IDX_16294b83fa8c0149719a1f631e";`); + await queryRunner.query(`DROP INDEX "IDX_9977c3c1de01c3d848039a6b90";`); + await queryRunner.query(`DROP INDEX "IDX_f15d48fa3ea5e4bda05ca8ab20";`); + await queryRunner.query(`DROP INDEX "IDX_b22c53f35ef20c28c21637c85f";`); + await queryRunner.query(`DROP INDEX "IDX_05895aa505a670300d4816debc";`); + await queryRunner.query(`DROP INDEX "IDX_57de40bc620f456c7311aa3a1e";`); + await queryRunner.query(`DROP INDEX "IDX_d8ddd9d687816cc490432b3d4b";`); + await queryRunner.query(`DROP INDEX "IDX_d7e875c6c60e661723dbf372fd";`); + await queryRunner.query(`DROP INDEX "IDX_575842846f0c28fa5da46c99b1";`); + await queryRunner.query(`DROP INDEX "IDX_6c2e267ae764a9413b863a2934";`); + await queryRunner.query(`DROP INDEX "IDX_1af8519996fbfb3684b58df280";`); + await queryRunner.query(`DROP INDEX "IDX_3571467bcbe021f66e2bdce96e";`); + await queryRunner.query(`DROP INDEX "IDX_8091ea76b12338cb4428d33d78";`); + } +} diff --git a/server/src/schema/tables/activity.table.ts b/server/src/schema/tables/activity.table.ts index e7a144722c..802a86a303 100644 --- a/server/src/schema/tables/activity.table.ts +++ b/server/src/schema/tables/activity.table.ts @@ -5,7 +5,6 @@ import { UserTable } from 'src/schema/tables/user.table'; import { Check, Column, - ColumnIndex, CreateDateColumn, ForeignKeyColumn, Index, @@ -51,7 +50,6 @@ export class ActivityTable { @Column({ type: 'boolean', default: false }) isLiked!: boolean; - @ColumnIndex('IDX_activity_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_activity_update_id' }) updateId!: string; } diff --git a/server/src/schema/tables/album-asset.table.ts b/server/src/schema/tables/album-asset.table.ts index 1b931e3116..8054009c39 100644 --- a/server/src/schema/tables/album-asset.table.ts +++ b/server/src/schema/tables/album-asset.table.ts @@ -1,25 +1,13 @@ import { AlbumTable } from 'src/schema/tables/album.table'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { ColumnIndex, CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools'; @Table({ name: 'albums_assets_assets', primaryConstraintName: 'PK_c67bc36fa845fb7b18e0e398180' }) export class AlbumAssetTable { - @ForeignKeyColumn(() => AlbumTable, { - onDelete: 'CASCADE', - onUpdate: 'CASCADE', - nullable: false, - primary: true, - }) - @ColumnIndex() + @ForeignKeyColumn(() => AlbumTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false, primary: true }) albumsId!: string; - @ForeignKeyColumn(() => AssetTable, { - onDelete: 'CASCADE', - onUpdate: 'CASCADE', - nullable: false, - primary: true, - }) - @ColumnIndex() + @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false, primary: true }) assetsId!: string; @CreateDateColumn() diff --git a/server/src/schema/tables/album.table.ts b/server/src/schema/tables/album.table.ts index cdfd092b1b..428947fa51 100644 --- a/server/src/schema/tables/album.table.ts +++ b/server/src/schema/tables/album.table.ts @@ -4,7 +4,6 @@ import { AssetTable } from 'src/schema/tables/asset.table'; import { UserTable } from 'src/schema/tables/user.table'; import { Column, - ColumnIndex, CreateDateColumn, DeleteDateColumn, ForeignKeyColumn, @@ -51,7 +50,6 @@ export class AlbumTable { @Column({ default: AssetOrder.DESC }) order!: AssetOrder; - @ColumnIndex('IDX_albums_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_albums_update_id' }) updateId?: string; } diff --git a/server/src/schema/tables/api-key.table.ts b/server/src/schema/tables/api-key.table.ts index 29c4ad2b0f..1d4cc83172 100644 --- a/server/src/schema/tables/api-key.table.ts +++ b/server/src/schema/tables/api-key.table.ts @@ -3,7 +3,6 @@ import { Permission } from 'src/enum'; import { UserTable } from 'src/schema/tables/user.table'; import { Column, - ColumnIndex, CreateDateColumn, ForeignKeyColumn, PrimaryGeneratedColumn, @@ -35,7 +34,6 @@ export class APIKeyTable { @Column({ array: true, type: 'character varying' }) permissions!: Permission[]; - @ColumnIndex({ name: 'IDX_api_keys_update_id' }) - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_api_keys_update_id' }) updateId?: string; } diff --git a/server/src/schema/tables/asset-audit.table.ts b/server/src/schema/tables/asset-audit.table.ts index 55d6f5c911..030256480c 100644 --- a/server/src/schema/tables/asset-audit.table.ts +++ b/server/src/schema/tables/asset-audit.table.ts @@ -1,20 +1,17 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, ColumnIndex, CreateDateColumn, Table } from 'src/sql-tools'; +import { Column, CreateDateColumn, Table } from 'src/sql-tools'; @Table('assets_audit') export class AssetAuditTable { @PrimaryGeneratedUuidV7Column() id!: string; - @ColumnIndex('IDX_assets_audit_asset_id') - @Column({ type: 'uuid' }) + @Column({ type: 'uuid', indexName: 'IDX_assets_audit_asset_id' }) assetId!: string; - @ColumnIndex('IDX_assets_audit_owner_id') - @Column({ type: 'uuid' }) + @Column({ type: 'uuid', indexName: 'IDX_assets_audit_owner_id' }) ownerId!: string; - @ColumnIndex('IDX_assets_audit_deleted_at') - @CreateDateColumn({ default: () => 'clock_timestamp()' }) + @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_assets_audit_deleted_at' }) deletedAt!: Date; } diff --git a/server/src/schema/tables/asset-face.table.ts b/server/src/schema/tables/asset-face.table.ts index 0ae99f44bf..52f4364a93 100644 --- a/server/src/schema/tables/asset-face.table.ts +++ b/server/src/schema/tables/asset-face.table.ts @@ -8,10 +8,21 @@ import { Column, DeleteDateColumn, ForeignKeyColumn, Index, PrimaryGeneratedColu @Index({ name: 'IDX_asset_faces_assetId_personId', columns: ['assetId', 'personId'] }) @Index({ columns: ['personId', 'assetId'] }) export class AssetFaceTable { - @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + @ForeignKeyColumn(() => AssetTable, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + // [assetId, personId] is the PK constraint + index: false, + }) assetId!: string; - @ForeignKeyColumn(() => PersonTable, { onDelete: 'SET NULL', onUpdate: 'CASCADE', nullable: true }) + @ForeignKeyColumn(() => PersonTable, { + onDelete: 'SET NULL', + onUpdate: 'CASCADE', + nullable: true, + // [personId, assetId] makes this redundant + index: false, + }) personId!: string | null; @Column({ default: 0, type: 'integer' }) diff --git a/server/src/schema/tables/asset-files.table.ts b/server/src/schema/tables/asset-files.table.ts index fb8750a8ef..0859bd5cf0 100644 --- a/server/src/schema/tables/asset-files.table.ts +++ b/server/src/schema/tables/asset-files.table.ts @@ -3,7 +3,6 @@ import { AssetFileType } from 'src/enum'; import { AssetTable } from 'src/schema/tables/asset.table'; import { Column, - ColumnIndex, CreateDateColumn, ForeignKeyColumn, PrimaryGeneratedColumn, @@ -19,8 +18,11 @@ export class AssetFileTable { @PrimaryGeneratedColumn() id!: string; - @ColumnIndex('IDX_asset_files_assetId') - @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + @ForeignKeyColumn(() => AssetTable, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + indexName: 'IDX_asset_files_assetId', + }) assetId?: string; @CreateDateColumn() @@ -35,7 +37,6 @@ export class AssetFileTable { @Column() path!: string; - @ColumnIndex('IDX_asset_files_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_asset_files_update_id' }) updateId?: string; } diff --git a/server/src/schema/tables/asset.table.ts b/server/src/schema/tables/asset.table.ts index 250c3546a2..9a9670cd0e 100644 --- a/server/src/schema/tables/asset.table.ts +++ b/server/src/schema/tables/asset.table.ts @@ -9,7 +9,6 @@ import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, Column, - ColumnIndex, CreateDateColumn, DeleteDateColumn, ForeignKeyColumn, @@ -78,8 +77,7 @@ export class AssetTable { @Column() originalPath!: string; - @ColumnIndex('idx_asset_file_created_at') - @Column({ type: 'timestamp with time zone' }) + @Column({ type: 'timestamp with time zone', indexName: 'idx_asset_file_created_at' }) fileCreatedAt!: Date; @Column({ type: 'timestamp with time zone' }) @@ -94,8 +92,7 @@ export class AssetTable { @Column({ type: 'character varying', nullable: true, default: '' }) encodedVideoPath!: string | null; - @Column({ type: 'bytea' }) - @ColumnIndex() + @Column({ type: 'bytea', index: true }) checksum!: Buffer; // sha1 checksum @Column({ type: 'boolean', default: true }) @@ -113,8 +110,7 @@ export class AssetTable { @Column({ type: 'boolean', default: false }) isArchived!: boolean; - @Column() - @ColumnIndex() + @Column({ index: true }) originalFileName!: string; @Column({ nullable: true }) @@ -141,14 +137,12 @@ export class AssetTable { @ForeignKeyColumn(() => StackTable, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' }) stackId?: string | null; - @ColumnIndex('IDX_assets_duplicateId') - @Column({ type: 'uuid', nullable: true }) + @Column({ type: 'uuid', nullable: true, indexName: 'IDX_assets_duplicateId' }) duplicateId!: string | null; @Column({ enum: assets_status_enum, default: AssetStatus.ACTIVE }) status!: AssetStatus; - @ColumnIndex('IDX_assets_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_assets_update_id' }) updateId?: string; } diff --git a/server/src/schema/tables/exif.table.ts b/server/src/schema/tables/exif.table.ts index e40ce94b4f..ca300945c3 100644 --- a/server/src/schema/tables/exif.table.ts +++ b/server/src/schema/tables/exif.table.ts @@ -1,6 +1,6 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { Column, ColumnIndex, ForeignKeyColumn, Table, UpdateDateColumn } from 'src/sql-tools'; +import { Column, ForeignKeyColumn, Table, UpdateDateColumn } from 'src/sql-tools'; @Table('exif') @UpdatedAtTrigger('asset_exif_updated_at') @@ -50,8 +50,7 @@ export class ExifTable { @Column({ type: 'double precision', nullable: true }) longitude!: number | null; - @ColumnIndex('exif_city') - @Column({ type: 'character varying', nullable: true }) + @Column({ type: 'character varying', nullable: true, indexName: 'exif_city' }) city!: string | null; @Column({ type: 'character varying', nullable: true }) @@ -69,8 +68,7 @@ export class ExifTable { @Column({ type: 'character varying', nullable: true }) exposureTime!: string | null; - @ColumnIndex('IDX_live_photo_cid') - @Column({ type: 'character varying', nullable: true }) + @Column({ type: 'character varying', nullable: true, indexName: 'IDX_live_photo_cid' }) livePhotoCID!: string | null; @Column({ type: 'character varying', nullable: true }) @@ -88,8 +86,7 @@ export class ExifTable { @Column({ type: 'integer', nullable: true }) bitsPerSample!: number | null; - @ColumnIndex('IDX_auto_stack_id') - @Column({ type: 'character varying', nullable: true }) + @Column({ type: 'character varying', nullable: true, indexName: 'IDX_auto_stack_id' }) autoStackId!: string | null; @Column({ type: 'integer', nullable: true }) @@ -98,7 +95,6 @@ export class ExifTable { @UpdateDateColumn({ default: () => 'clock_timestamp()' }) updatedAt?: Date; - @ColumnIndex('IDX_asset_exif_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_asset_exif_update_id' }) updateId?: string; } diff --git a/server/src/schema/tables/library.table.ts b/server/src/schema/tables/library.table.ts index 54b3752f41..8b21d5feb0 100644 --- a/server/src/schema/tables/library.table.ts +++ b/server/src/schema/tables/library.table.ts @@ -2,7 +2,6 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { UserTable } from 'src/schema/tables/user.table'; import { Column, - ColumnIndex, CreateDateColumn, DeleteDateColumn, ForeignKeyColumn, @@ -41,7 +40,6 @@ export class LibraryTable { @Column({ type: 'timestamp with time zone', nullable: true }) refreshedAt!: Date | null; - @ColumnIndex('IDX_libraries_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_libraries_update_id' }) updateId?: string; } diff --git a/server/src/schema/tables/memory.table.ts b/server/src/schema/tables/memory.table.ts index 1926405565..32dafe3384 100644 --- a/server/src/schema/tables/memory.table.ts +++ b/server/src/schema/tables/memory.table.ts @@ -3,7 +3,6 @@ import { MemoryType } from 'src/enum'; import { UserTable } from 'src/schema/tables/user.table'; import { Column, - ColumnIndex, CreateDateColumn, DeleteDateColumn, ForeignKeyColumn, @@ -55,7 +54,6 @@ export class MemoryTable { @Column({ type: 'timestamp with time zone', nullable: true }) hideAt?: Date; - @ColumnIndex('IDX_memories_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_memories_update_id' }) updateId?: string; } diff --git a/server/src/schema/tables/memory_asset.table.ts b/server/src/schema/tables/memory_asset.table.ts index 864e6291c7..0e5ca29a08 100644 --- a/server/src/schema/tables/memory_asset.table.ts +++ b/server/src/schema/tables/memory_asset.table.ts @@ -1,14 +1,12 @@ import { AssetTable } from 'src/schema/tables/asset.table'; import { MemoryTable } from 'src/schema/tables/memory.table'; -import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { ForeignKeyColumn, Table } from 'src/sql-tools'; @Table('memories_assets_assets') export class MemoryAssetTable { - @ColumnIndex() @ForeignKeyColumn(() => MemoryTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) memoriesId!: string; - @ColumnIndex() @ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) assetsId!: string; } diff --git a/server/src/schema/tables/partner-audit.table.ts b/server/src/schema/tables/partner-audit.table.ts index 08b6e94626..da5243dc75 100644 --- a/server/src/schema/tables/partner-audit.table.ts +++ b/server/src/schema/tables/partner-audit.table.ts @@ -1,20 +1,17 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, ColumnIndex, CreateDateColumn, Table } from 'src/sql-tools'; +import { Column, CreateDateColumn, Table } from 'src/sql-tools'; @Table('partners_audit') export class PartnerAuditTable { @PrimaryGeneratedUuidV7Column() id!: string; - @ColumnIndex('IDX_partners_audit_shared_by_id') - @Column({ type: 'uuid' }) + @Column({ type: 'uuid', indexName: 'IDX_partners_audit_shared_by_id' }) sharedById!: string; - @ColumnIndex('IDX_partners_audit_shared_with_id') - @Column({ type: 'uuid' }) + @Column({ type: 'uuid', indexName: 'IDX_partners_audit_shared_with_id' }) sharedWithId!: string; - @ColumnIndex('IDX_partners_audit_deleted_at') - @CreateDateColumn({ default: () => 'clock_timestamp()' }) + @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_partners_audit_deleted_at' }) deletedAt!: Date; } diff --git a/server/src/schema/tables/partner.table.ts b/server/src/schema/tables/partner.table.ts index 770107fe7a..0da60cfc0c 100644 --- a/server/src/schema/tables/partner.table.ts +++ b/server/src/schema/tables/partner.table.ts @@ -1,15 +1,7 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { partners_delete_audit } from 'src/schema/functions'; import { UserTable } from 'src/schema/tables/user.table'; -import { - AfterDeleteTrigger, - Column, - ColumnIndex, - CreateDateColumn, - ForeignKeyColumn, - Table, - UpdateDateColumn, -} from 'src/sql-tools'; +import { AfterDeleteTrigger, Column, CreateDateColumn, ForeignKeyColumn, Table, UpdateDateColumn } from 'src/sql-tools'; @Table('partners') @UpdatedAtTrigger('partners_updated_at') @@ -21,7 +13,12 @@ import { when: 'pg_trigger_depth() = 0', }) export class PartnerTable { - @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', primary: true }) + @ForeignKeyColumn(() => UserTable, { + onDelete: 'CASCADE', + primary: true, + // [sharedById, sharedWithId] is the PK constraint + index: false, + }) sharedById!: string; @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', primary: true }) @@ -36,7 +33,6 @@ export class PartnerTable { @Column({ type: 'boolean', default: false }) inTimeline!: boolean; - @ColumnIndex('IDX_partners_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_partners_update_id' }) updateId!: string; } diff --git a/server/src/schema/tables/person.table.ts b/server/src/schema/tables/person.table.ts index b96fc5b709..1320b91f18 100644 --- a/server/src/schema/tables/person.table.ts +++ b/server/src/schema/tables/person.table.ts @@ -4,7 +4,6 @@ import { UserTable } from 'src/schema/tables/user.table'; import { Check, Column, - ColumnIndex, CreateDateColumn, ForeignKeyColumn, PrimaryGeneratedColumn, @@ -49,7 +48,6 @@ export class PersonTable { @Column({ type: 'character varying', nullable: true, default: null }) color?: string | null; - @ColumnIndex('IDX_person_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_person_update_id' }) updateId!: string; } diff --git a/server/src/schema/tables/session.table.ts b/server/src/schema/tables/session.table.ts index a66732a7d9..ad43d0d6e4 100644 --- a/server/src/schema/tables/session.table.ts +++ b/server/src/schema/tables/session.table.ts @@ -2,7 +2,6 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { UserTable } from 'src/schema/tables/user.table'; import { Column, - ColumnIndex, CreateDateColumn, ForeignKeyColumn, PrimaryGeneratedColumn, @@ -35,7 +34,6 @@ export class SessionTable { @Column({ default: '' }) deviceOS!: string; - @ColumnIndex('IDX_sessions_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_sessions_update_id' }) updateId!: string; } diff --git a/server/src/schema/tables/shared-link-asset.table.ts b/server/src/schema/tables/shared-link-asset.table.ts index 1eb294c1e8..66c9068441 100644 --- a/server/src/schema/tables/shared-link-asset.table.ts +++ b/server/src/schema/tables/shared-link-asset.table.ts @@ -1,14 +1,12 @@ import { AssetTable } from 'src/schema/tables/asset.table'; import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; -import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { ForeignKeyColumn, Table } from 'src/sql-tools'; @Table('shared_link__asset') export class SharedLinkAssetTable { - @ColumnIndex() @ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) assetsId!: string; - @ColumnIndex() @ForeignKeyColumn(() => SharedLinkTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) sharedLinksId!: string; } diff --git a/server/src/schema/tables/shared-link.table.ts b/server/src/schema/tables/shared-link.table.ts index 36237c58ef..3bb36b36ed 100644 --- a/server/src/schema/tables/shared-link.table.ts +++ b/server/src/schema/tables/shared-link.table.ts @@ -1,15 +1,7 @@ import { SharedLinkType } from 'src/enum'; import { AlbumTable } from 'src/schema/tables/album.table'; import { UserTable } from 'src/schema/tables/user.table'; -import { - Column, - ColumnIndex, - CreateDateColumn, - ForeignKeyColumn, - PrimaryGeneratedColumn, - Table, - Unique, -} from 'src/sql-tools'; +import { Column, CreateDateColumn, ForeignKeyColumn, PrimaryGeneratedColumn, Table, Unique } from 'src/sql-tools'; @Table('shared_links') @Unique({ name: 'UQ_sharedlink_key', columns: ['key'] }) @@ -23,8 +15,7 @@ export class SharedLinkTable { @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) userId!: string; - @ColumnIndex('IDX_sharedlink_key') - @Column({ type: 'bytea' }) + @Column({ type: 'bytea', indexName: 'IDX_sharedlink_key' }) key!: Buffer; // use to access the inidividual asset @Column() @@ -39,8 +30,12 @@ export class SharedLinkTable { @Column({ type: 'boolean', default: false }) allowUpload!: boolean; - @ColumnIndex('IDX_sharedlink_albumId') - @ForeignKeyColumn(() => AlbumTable, { nullable: true, onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + @ForeignKeyColumn(() => AlbumTable, { + nullable: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + indexName: 'IDX_sharedlink_albumId', + }) albumId!: string; @Column({ type: 'boolean', default: true }) diff --git a/server/src/schema/tables/sync-checkpoint.table.ts b/server/src/schema/tables/sync-checkpoint.table.ts index 831205ce7a..21fd7983ac 100644 --- a/server/src/schema/tables/sync-checkpoint.table.ts +++ b/server/src/schema/tables/sync-checkpoint.table.ts @@ -1,15 +1,7 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { SyncEntityType } from 'src/enum'; import { SessionTable } from 'src/schema/tables/session.table'; -import { - Column, - ColumnIndex, - CreateDateColumn, - ForeignKeyColumn, - PrimaryColumn, - Table, - UpdateDateColumn, -} from 'src/sql-tools'; +import { Column, CreateDateColumn, ForeignKeyColumn, PrimaryColumn, Table, UpdateDateColumn } from 'src/sql-tools'; @Table('session_sync_checkpoints') @UpdatedAtTrigger('session_sync_checkpoints_updated_at') @@ -29,7 +21,6 @@ export class SessionSyncCheckpointTable { @Column() ack!: string; - @ColumnIndex('IDX_session_sync_checkpoints_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_session_sync_checkpoints_update_id' }) updateId!: string; } diff --git a/server/src/schema/tables/tag-asset.table.ts b/server/src/schema/tables/tag-asset.table.ts index 5f24799cec..8793af0a8a 100644 --- a/server/src/schema/tables/tag-asset.table.ts +++ b/server/src/schema/tables/tag-asset.table.ts @@ -1,15 +1,13 @@ import { AssetTable } from 'src/schema/tables/asset.table'; import { TagTable } from 'src/schema/tables/tag.table'; -import { ColumnIndex, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; +import { ForeignKeyColumn, Index, Table } from 'src/sql-tools'; @Index({ name: 'IDX_tag_asset_assetsId_tagsId', columns: ['assetsId', 'tagsId'] }) @Table('tag_asset') export class TagAssetTable { - @ColumnIndex() - @ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) + @ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true, index: true }) assetsId!: string; - @ColumnIndex() - @ForeignKeyColumn(() => TagTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) + @ForeignKeyColumn(() => TagTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true, index: true }) tagsId!: string; } diff --git a/server/src/schema/tables/tag-closure.table.ts b/server/src/schema/tables/tag-closure.table.ts index acde84b91d..8829e802e1 100644 --- a/server/src/schema/tables/tag-closure.table.ts +++ b/server/src/schema/tables/tag-closure.table.ts @@ -1,13 +1,11 @@ import { TagTable } from 'src/schema/tables/tag.table'; -import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools'; +import { ForeignKeyColumn, Table } from 'src/sql-tools'; @Table('tags_closure') export class TagClosureTable { - @ColumnIndex() - @ForeignKeyColumn(() => TagTable, { primary: true, onDelete: 'CASCADE', onUpdate: 'NO ACTION' }) + @ForeignKeyColumn(() => TagTable, { primary: true, onDelete: 'CASCADE', onUpdate: 'NO ACTION', index: true }) id_ancestor!: string; - @ColumnIndex() - @ForeignKeyColumn(() => TagTable, { primary: true, onDelete: 'CASCADE', onUpdate: 'NO ACTION' }) + @ForeignKeyColumn(() => TagTable, { primary: true, onDelete: 'CASCADE', onUpdate: 'NO ACTION', index: true }) id_descendant!: string; } diff --git a/server/src/schema/tables/tag.table.ts b/server/src/schema/tables/tag.table.ts index 5042e2eb0e..a9f2a57f27 100644 --- a/server/src/schema/tables/tag.table.ts +++ b/server/src/schema/tables/tag.table.ts @@ -2,7 +2,6 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { UserTable } from 'src/schema/tables/user.table'; import { Column, - ColumnIndex, CreateDateColumn, ForeignKeyColumn, PrimaryGeneratedColumn, @@ -18,7 +17,12 @@ export class TagTable { @PrimaryGeneratedColumn() id!: string; - @ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) + @ForeignKeyColumn(() => UserTable, { + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + // [userId, value] makes this redundant + index: false, + }) userId!: string; @Column() @@ -36,7 +40,6 @@ export class TagTable { @ForeignKeyColumn(() => TagTable, { nullable: true, onDelete: 'CASCADE' }) parentId?: string; - @ColumnIndex('IDX_tags_update_id') - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_tags_update_id' }) updateId!: string; } diff --git a/server/src/schema/tables/user-audit.table.ts b/server/src/schema/tables/user-audit.table.ts index 0f881ccc9a..e0c9afcdc3 100644 --- a/server/src/schema/tables/user-audit.table.ts +++ b/server/src/schema/tables/user-audit.table.ts @@ -1,13 +1,12 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, ColumnIndex, CreateDateColumn, Table } from 'src/sql-tools'; +import { Column, CreateDateColumn, Table } from 'src/sql-tools'; @Table('users_audit') export class UserAuditTable { @Column({ type: 'uuid' }) userId!: string; - @ColumnIndex('IDX_users_audit_deleted_at') - @CreateDateColumn({ default: () => 'clock_timestamp()' }) + @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_users_audit_deleted_at' }) deletedAt!: Date; @PrimaryGeneratedUuidV7Column() diff --git a/server/src/schema/tables/user-metadata.table.ts b/server/src/schema/tables/user-metadata.table.ts index 6d03acaf80..04b457867f 100644 --- a/server/src/schema/tables/user-metadata.table.ts +++ b/server/src/schema/tables/user-metadata.table.ts @@ -5,7 +5,13 @@ import { UserMetadata, UserMetadataItem } from 'src/types'; @Table('user_metadata') export class UserMetadataTable implements UserMetadataItem { - @ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) + @ForeignKeyColumn(() => UserTable, { + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + primary: true, + // [userId, key] is the PK constraint + index: false, + }) userId!: string; @PrimaryColumn({ type: 'character varying' }) diff --git a/server/src/schema/tables/user.table.ts b/server/src/schema/tables/user.table.ts index 5160f979b9..eeef923796 100644 --- a/server/src/schema/tables/user.table.ts +++ b/server/src/schema/tables/user.table.ts @@ -5,7 +5,6 @@ import { users_delete_audit } from 'src/schema/functions'; import { AfterDeleteTrigger, Column, - ColumnIndex, CreateDateColumn, DeleteDateColumn, Index, @@ -77,7 +76,6 @@ export class UserTable { @Column({ type: 'timestamp with time zone', default: () => 'now()' }) profileChangedAt!: Generated; - @ColumnIndex({ name: 'IDX_users_update_id' }) - @UpdateIdColumn() + @UpdateIdColumn({ indexName: 'IDX_users_update_id' }) updateId!: Generated; } diff --git a/server/src/sql-tools/from-code/decorators/column-index.decorator.ts b/server/src/sql-tools/from-code/decorators/column-index.decorator.ts deleted file mode 100644 index ab15292612..0000000000 --- a/server/src/sql-tools/from-code/decorators/column-index.decorator.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { register } from 'src/sql-tools/from-code/register'; -import { asOptions } from 'src/sql-tools/helpers'; - -export type ColumnIndexOptions = { - name?: string; - unique?: boolean; - expression?: string; - using?: string; - with?: string; - where?: string; - synchronize?: boolean; -}; -export const ColumnIndex = (options: string | ColumnIndexOptions = {}): PropertyDecorator => { - return (object: object, propertyName: string | symbol) => - void register({ type: 'columnIndex', item: { object, propertyName, options: asOptions(options) } }); -}; diff --git a/server/src/sql-tools/from-code/decorators/column.decorator.ts b/server/src/sql-tools/from-code/decorators/column.decorator.ts index 74a83cbcf3..7b00af80cc 100644 --- a/server/src/sql-tools/from-code/decorators/column.decorator.ts +++ b/server/src/sql-tools/from-code/decorators/column.decorator.ts @@ -15,13 +15,15 @@ export type ColumnBaseOptions = { synchronize?: boolean; storage?: ColumnStorage; identity?: boolean; + index?: boolean; + indexName?: string; + unique?: boolean; + uniqueConstraintName?: string; }; export type ColumnOptions = ColumnBaseOptions & { enum?: DatabaseEnum; array?: boolean; - unique?: boolean; - uniqueConstraintName?: string; }; export const Column = (options: string | ColumnOptions = {}): PropertyDecorator => { diff --git a/server/src/sql-tools/from-code/decorators/foreign-key-column.decorator.ts b/server/src/sql-tools/from-code/decorators/foreign-key-column.decorator.ts index 070aa5cb51..beb3aa6fd6 100644 --- a/server/src/sql-tools/from-code/decorators/foreign-key-column.decorator.ts +++ b/server/src/sql-tools/from-code/decorators/foreign-key-column.decorator.ts @@ -7,8 +7,6 @@ export type ForeignKeyColumnOptions = ColumnBaseOptions & { onUpdate?: Action; onDelete?: Action; constraintName?: string; - unique?: boolean; - uniqueConstraintName?: string; }; export const ForeignKeyColumn = (target: () => object, options: ForeignKeyColumnOptions): PropertyDecorator => { diff --git a/server/src/sql-tools/from-code/decorators/index.decorator.ts b/server/src/sql-tools/from-code/decorators/index.decorator.ts index cd76b5e36d..5d90c4f58d 100644 --- a/server/src/sql-tools/from-code/decorators/index.decorator.ts +++ b/server/src/sql-tools/from-code/decorators/index.decorator.ts @@ -1,8 +1,13 @@ -import { ColumnIndexOptions } from 'src/sql-tools/from-code/decorators/column-index.decorator'; import { register } from 'src/sql-tools/from-code/register'; import { asOptions } from 'src/sql-tools/helpers'; -export type IndexOptions = ColumnIndexOptions & { +export type IndexOptions = { + name?: string; + unique?: boolean; + expression?: string; + using?: string; + with?: string; + where?: string; columns?: string[]; synchronize?: boolean; }; diff --git a/server/src/sql-tools/from-code/index.ts b/server/src/sql-tools/from-code/index.ts index 3c74d2763c..95f1dbb22d 100644 --- a/server/src/sql-tools/from-code/index.ts +++ b/server/src/sql-tools/from-code/index.ts @@ -1,6 +1,5 @@ import 'reflect-metadata'; import { processCheckConstraints } from 'src/sql-tools/from-code/processors/check-constraint.processor'; -import { processColumnIndexes } from 'src/sql-tools/from-code/processors/column-index.processor'; import { processColumns } from 'src/sql-tools/from-code/processors/column.processor'; import { processConfigurationParameters } from 'src/sql-tools/from-code/processors/configuration-parameter.processor'; import { processDatabases } from 'src/sql-tools/from-code/processors/database.processor'; @@ -36,14 +35,21 @@ const processors: Processor[] = [ processUniqueConstraints, processCheckConstraints, processPrimaryKeyConstraints, - processIndexes, - processColumnIndexes, processForeignKeyConstraints, + processIndexes, processTriggers, ]; -export const schemaFromCode = () => { +export type SchemaFromCodeOptions = { + /** automatically create indexes on foreign key columns */ + createForeignKeyIndexes?: boolean; +}; +export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => { if (!initialized) { + const globalOptions = { + createForeignKeyIndexes: options.createForeignKeyIndexes ?? true, + }; + const builder: SchemaBuilder = { name: 'postgres', schemaName: 'public', @@ -58,7 +64,7 @@ export const schemaFromCode = () => { const items = getRegisteredItems(); for (const processor of processors) { - processor(builder, items); + processor(builder, items, globalOptions); } schema = { ...builder, tables: builder.tables.map(({ metadata: _, ...table }) => table) }; diff --git a/server/src/sql-tools/from-code/processors/check-constraint.processor.ts b/server/src/sql-tools/from-code/processors/check-constraint.processor.ts index d61ee18277..feb21b9894 100644 --- a/server/src/sql-tools/from-code/processors/check-constraint.processor.ts +++ b/server/src/sql-tools/from-code/processors/check-constraint.processor.ts @@ -1,6 +1,6 @@ import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { asCheckConstraintName } from 'src/sql-tools/helpers'; +import { asKey } from 'src/sql-tools/helpers'; import { DatabaseConstraintType } from 'src/sql-tools/types'; export const processCheckConstraints: Processor = (builder, items) => { @@ -24,3 +24,5 @@ export const processCheckConstraints: Processor = (builder, items) => { }); } }; + +const asCheckConstraintName = (table: string, expression: string) => asKey('CHK_', table, [expression]); diff --git a/server/src/sql-tools/from-code/processors/column-index.processor.ts b/server/src/sql-tools/from-code/processors/column-index.processor.ts deleted file mode 100644 index 0e40fa1ee3..0000000000 --- a/server/src/sql-tools/from-code/processors/column-index.processor.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor'; -import { onMissingTable } from 'src/sql-tools/from-code/processors/table.processor'; -import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { asIndexName } from 'src/sql-tools/helpers'; - -export const processColumnIndexes: Processor = (builder, items) => { - for (const { - item: { object, propertyName, options }, - } of items.filter((item) => item.type === 'columnIndex')) { - const { table, column } = resolveColumn(builder, object, propertyName); - if (!table) { - onMissingTable(builder, '@ColumnIndex', object); - continue; - } - - if (!column) { - onMissingColumn(builder, `@ColumnIndex`, object, propertyName); - continue; - } - - table.indexes.push({ - name: options.name || asIndexName(table.name, [column.name], options.where), - tableName: table.name, - unique: options.unique ?? false, - expression: options.expression, - using: options.using, - where: options.where, - columnNames: [column.name], - synchronize: options.synchronize ?? true, - }); - } -}; diff --git a/server/src/sql-tools/from-code/processors/column.processor.ts b/server/src/sql-tools/from-code/processors/column.processor.ts index 37f3f5d082..e8c2544f87 100644 --- a/server/src/sql-tools/from-code/processors/column.processor.ts +++ b/server/src/sql-tools/from-code/processors/column.processor.ts @@ -1,8 +1,8 @@ import { ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator'; import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type'; -import { asMetadataKey, asUniqueConstraintName, fromColumnValue } from 'src/sql-tools/helpers'; -import { DatabaseColumn, DatabaseConstraintType } from 'src/sql-tools/types'; +import { asMetadataKey, fromColumnValue } from 'src/sql-tools/helpers'; +import { DatabaseColumn } from 'src/sql-tools/types'; export const processColumns: Processor = (builder, items) => { for (const { @@ -54,16 +54,6 @@ export const processColumns: Processor = (builder, items) => { writeMetadata(object, propertyName, { name: column.name, options }); table.columns.push(column); - - if (type === 'column' && !options.primary && options.unique) { - table.constraints.push({ - type: DatabaseConstraintType.UNIQUE, - name: options.uniqueConstraintName || asUniqueConstraintName(table.name, [column.name]), - tableName: table.name, - columnNames: [column.name], - synchronize: options.synchronize ?? true, - }); - } } }; diff --git a/server/src/sql-tools/from-code/processors/foreign-key-constriant.processor.ts b/server/src/sql-tools/from-code/processors/foreign-key-constriant.processor.ts index 784a8b8e99..612b74c30f 100644 --- a/server/src/sql-tools/from-code/processors/foreign-key-constriant.processor.ts +++ b/server/src/sql-tools/from-code/processors/foreign-key-constriant.processor.ts @@ -1,7 +1,7 @@ import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor'; import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { asForeignKeyConstraintName, asRelationKeyConstraintName } from 'src/sql-tools/helpers'; +import { asKey } from 'src/sql-tools/helpers'; import { DatabaseActionType, DatabaseConstraintType } from 'src/sql-tools/types'; export const processForeignKeyConstraints: Processor = (builder, items) => { @@ -46,7 +46,7 @@ export const processForeignKeyConstraints: Processor = (builder, items) => { synchronize: options.synchronize ?? true, }); - if (options.unique) { + if (options.unique || options.uniqueConstraintName) { table.constraints.push({ name: options.uniqueConstraintName || asRelationKeyConstraintName(table.name, columnNames), tableName: table.name, @@ -57,3 +57,6 @@ export const processForeignKeyConstraints: Processor = (builder, items) => { } } }; + +const asForeignKeyConstraintName = (table: string, columns: string[]) => asKey('FK_', table, columns); +const asRelationKeyConstraintName = (table: string, columns: string[]) => asKey('REL_', table, columns); diff --git a/server/src/sql-tools/from-code/processors/index.processor.ts b/server/src/sql-tools/from-code/processors/index.processor.ts index 3625bf9784..f4c9c7cec1 100644 --- a/server/src/sql-tools/from-code/processors/index.processor.ts +++ b/server/src/sql-tools/from-code/processors/index.processor.ts @@ -1,8 +1,9 @@ +import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor'; import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { asIndexName } from 'src/sql-tools/helpers'; +import { asKey } from 'src/sql-tools/helpers'; -export const processIndexes: Processor = (builder, items) => { +export const processIndexes: Processor = (builder, items, config) => { for (const { item: { object, options }, } of items.filter((item) => item.type === 'index')) { @@ -24,4 +25,66 @@ export const processIndexes: Processor = (builder, items) => { synchronize: options.synchronize ?? true, }); } + + // column indexes + for (const { + type, + item: { object, propertyName, options }, + } of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) { + const { table, column } = resolveColumn(builder, object, propertyName); + if (!table) { + onMissingTable(builder, '@Column', object); + continue; + } + + if (!column) { + // should be impossible since they are created in `column.processor.ts` + onMissingColumn(builder, '@Column', object, propertyName); + continue; + } + + if (options.index === false) { + continue; + } + + const isIndexRequested = + options.indexName || options.index || (type === 'foreignKeyColumn' && config.createForeignKeyIndexes); + if (!isIndexRequested) { + continue; + } + + const indexName = options.indexName || asIndexName(table.name, [column.name]); + + const isIndexPresent = table.indexes.some((index) => index.name === indexName); + if (isIndexPresent) { + continue; + } + + const isOnlyPrimaryColumn = options.primary && table.columns.filter(({ primary }) => primary === true).length === 1; + if (isOnlyPrimaryColumn) { + // will have an index created by the primary key constraint + continue; + } + + table.indexes.push({ + name: indexName, + tableName: table.name, + unique: false, + columnNames: [column.name], + synchronize: options.synchronize ?? true, + }); + } +}; + +const asIndexName = (table: string, columns?: string[], where?: string) => { + const items: string[] = []; + for (const columnName of columns ?? []) { + items.push(columnName); + } + + if (where) { + items.push(where); + } + + return asKey('IDX_', table, items); }; diff --git a/server/src/sql-tools/from-code/processors/primary-key-contraint.processor.ts b/server/src/sql-tools/from-code/processors/primary-key-contraint.processor.ts index f123f2e495..74aecc5ea0 100644 --- a/server/src/sql-tools/from-code/processors/primary-key-contraint.processor.ts +++ b/server/src/sql-tools/from-code/processors/primary-key-contraint.processor.ts @@ -1,5 +1,5 @@ import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { asPrimaryKeyConstraintName } from 'src/sql-tools/helpers'; +import { asKey } from 'src/sql-tools/helpers'; import { DatabaseConstraintType } from 'src/sql-tools/types'; export const processPrimaryKeyConstraints: Processor = (builder) => { @@ -22,3 +22,5 @@ export const processPrimaryKeyConstraints: Processor = (builder) => { } } }; + +const asPrimaryKeyConstraintName = (table: string, columns: string[]) => asKey('PK_', table, columns); diff --git a/server/src/sql-tools/from-code/processors/trigger.processor.ts b/server/src/sql-tools/from-code/processors/trigger.processor.ts index 2f4cc04326..4b875f353b 100644 --- a/server/src/sql-tools/from-code/processors/trigger.processor.ts +++ b/server/src/sql-tools/from-code/processors/trigger.processor.ts @@ -1,6 +1,7 @@ +import { TriggerOptions } from 'src/sql-tools/from-code/decorators/trigger.decorator'; import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { asTriggerName } from 'src/sql-tools/helpers'; +import { asKey } from 'src/sql-tools/helpers'; export const processTriggers: Processor = (builder, items) => { for (const { @@ -26,3 +27,6 @@ export const processTriggers: Processor = (builder, items) => { }); } }; + +const asTriggerName = (table: string, trigger: TriggerOptions) => + asKey('TR_', table, [...trigger.actions, trigger.scope, trigger.timing, trigger.functionName]); diff --git a/server/src/sql-tools/from-code/processors/type.ts b/server/src/sql-tools/from-code/processors/type.ts index 5a69efbcf0..deb142d278 100644 --- a/server/src/sql-tools/from-code/processors/type.ts +++ b/server/src/sql-tools/from-code/processors/type.ts @@ -1,3 +1,4 @@ +import { SchemaFromCodeOptions } from 'src/sql-tools/from-code'; import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator'; import { RegisterItem } from 'src/sql-tools/from-code/register-item'; import { DatabaseSchema, DatabaseTable } from 'src/sql-tools/types'; @@ -6,4 +7,4 @@ import { DatabaseSchema, DatabaseTable } from 'src/sql-tools/types'; export type TableWithMetadata = DatabaseTable & { metadata: { options: TableOptions; object: Function } }; export type SchemaBuilder = Omit & { tables: TableWithMetadata[] }; -export type Processor = (builder: SchemaBuilder, items: RegisterItem[]) => void; +export type Processor = (builder: SchemaBuilder, items: RegisterItem[], options: SchemaFromCodeOptions) => void; diff --git a/server/src/sql-tools/from-code/processors/unique-constraint.processor.ts b/server/src/sql-tools/from-code/processors/unique-constraint.processor.ts index 74c0504f7e..9014378085 100644 --- a/server/src/sql-tools/from-code/processors/unique-constraint.processor.ts +++ b/server/src/sql-tools/from-code/processors/unique-constraint.processor.ts @@ -1,6 +1,7 @@ +import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor'; import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { asUniqueConstraintName } from 'src/sql-tools/helpers'; +import { asKey } from 'src/sql-tools/helpers'; import { DatabaseConstraintType } from 'src/sql-tools/types'; export const processUniqueConstraints: Processor = (builder, items) => { @@ -24,4 +25,34 @@ export const processUniqueConstraints: Processor = (builder, items) => { synchronize: options.synchronize ?? true, }); } + + // column level constraints + for (const { + type, + item: { object, propertyName, options }, + } of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) { + const { table, column } = resolveColumn(builder, object, propertyName); + if (!table) { + onMissingTable(builder, '@Column', object); + continue; + } + + if (!column) { + // should be impossible since they are created in `column.processor.ts` + onMissingColumn(builder, '@Column', object, propertyName); + continue; + } + + if (type === 'column' && !options.primary && (options.unique || options.uniqueConstraintName)) { + table.constraints.push({ + type: DatabaseConstraintType.UNIQUE, + name: options.uniqueConstraintName || asUniqueConstraintName(table.name, [column.name]), + tableName: table.name, + columnNames: [column.name], + synchronize: options.synchronize ?? true, + }); + } + } }; + +const asUniqueConstraintName = (table: string, columns: string[]) => asKey('UQ_', table, columns); diff --git a/server/src/sql-tools/from-code/register-function.ts b/server/src/sql-tools/from-code/register-function.ts index 69e1a0f8f3..be71e0dfd7 100644 --- a/server/src/sql-tools/from-code/register-function.ts +++ b/server/src/sql-tools/from-code/register-function.ts @@ -1,5 +1,4 @@ import { register } from 'src/sql-tools/from-code/register'; -import { asFunctionExpression } from 'src/sql-tools/helpers'; import { ColumnType, DatabaseFunction } from 'src/sql-tools/types'; export type FunctionOptions = { @@ -27,3 +26,37 @@ export const registerFunction = (options: FunctionOptions) => { return item; }; + +const asFunctionExpression = (options: FunctionOptions) => { + const name = options.name; + const sql: string[] = [ + `CREATE OR REPLACE FUNCTION ${name}(${(options.arguments || []).join(', ')})`, + `RETURNS ${options.returnType}`, + ]; + + const flags = [ + options.parallel ? `PARALLEL ${options.parallel.toUpperCase()}` : undefined, + options.strict ? 'STRICT' : undefined, + options.behavior ? options.behavior.toUpperCase() : undefined, + `LANGUAGE ${options.language ?? 'SQL'}`, + ].filter((x) => x !== undefined); + + if (flags.length > 0) { + sql.push(flags.join(' ')); + } + + if ('return' in options) { + sql.push(` RETURN ${options.return}`); + } + + if ('body' in options) { + sql.push( + // + `AS $$`, + ' ' + options.body.trim(), + `$$;`, + ); + } + + return sql.join('\n ').trim(); +}; diff --git a/server/src/sql-tools/from-code/register-item.ts b/server/src/sql-tools/from-code/register-item.ts index 08200cbc4f..4889ae34b9 100644 --- a/server/src/sql-tools/from-code/register-item.ts +++ b/server/src/sql-tools/from-code/register-item.ts @@ -1,5 +1,4 @@ import { CheckOptions } from 'src/sql-tools/from-code/decorators/check.decorator'; -import { ColumnIndexOptions } from 'src/sql-tools/from-code/decorators/column-index.decorator'; import { ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator'; import { ConfigurationParameterOptions } from 'src/sql-tools/from-code/decorators/configuration-parameter.decorator'; import { DatabaseOptions } from 'src/sql-tools/from-code/decorators/database.decorator'; @@ -21,7 +20,6 @@ export type RegisterItem = | { type: 'uniqueConstraint'; item: ClassBased<{ options: UniqueOptions }> } | { type: 'checkConstraint'; item: ClassBased<{ options: CheckOptions }> } | { type: 'column'; item: PropertyBased<{ options: ColumnOptions }> } - | { type: 'columnIndex'; item: PropertyBased<{ options: ColumnIndexOptions }> } | { type: 'function'; item: DatabaseFunction } | { type: 'enum'; item: DatabaseEnum } | { type: 'trigger'; item: ClassBased<{ options: TriggerOptions }> } diff --git a/server/src/sql-tools/helpers.ts b/server/src/sql-tools/helpers.ts index 364b695194..2802407ea6 100644 --- a/server/src/sql-tools/helpers.ts +++ b/server/src/sql-tools/helpers.ts @@ -1,7 +1,5 @@ import { createHash } from 'node:crypto'; import { ColumnValue } from 'src/sql-tools/from-code/decorators/column.decorator'; -import { TriggerOptions } from 'src/sql-tools/from-code/decorators/trigger.decorator'; -import { FunctionOptions } from 'src/sql-tools/from-code/register-function'; import { Comparer, DatabaseColumn, @@ -18,25 +16,6 @@ export const asSnakeCase = (name: string): string => name.replaceAll(/([a-z])([A // match TypeORM export const asKey = (prefix: string, tableName: string, values: string[]) => (prefix + sha1(`${tableName}_${values.toSorted().join('_')}`)).slice(0, 30); -export const asPrimaryKeyConstraintName = (table: string, columns: string[]) => asKey('PK_', table, columns); -export const asForeignKeyConstraintName = (table: string, columns: string[]) => asKey('FK_', table, columns); -export const asTriggerName = (table: string, trigger: TriggerOptions) => - asKey('TR_', table, [...trigger.actions, trigger.scope, trigger.timing, trigger.functionName]); -export const asRelationKeyConstraintName = (table: string, columns: string[]) => asKey('REL_', table, columns); -export const asUniqueConstraintName = (table: string, columns: string[]) => asKey('UQ_', table, columns); -export const asCheckConstraintName = (table: string, expression: string) => asKey('CHK_', table, [expression]); -export const asIndexName = (table: string, columns: string[] | undefined, where: string | undefined) => { - const items: string[] = []; - for (const columnName of columns ?? []) { - items.push(columnName); - } - - if (where) { - items.push(where); - } - - return asKey('IDX_', table, items); -}; export const asOptions = (options: string | T): T => { if (typeof options === 'string') { @@ -46,40 +25,6 @@ export const asOptions = (options: string | T): T = return options; }; -export const asFunctionExpression = (options: FunctionOptions) => { - const name = options.name; - const sql: string[] = [ - `CREATE OR REPLACE FUNCTION ${name}(${(options.arguments || []).join(', ')})`, - `RETURNS ${options.returnType}`, - ]; - - const flags = [ - options.parallel ? `PARALLEL ${options.parallel.toUpperCase()}` : undefined, - options.strict ? 'STRICT' : undefined, - options.behavior ? options.behavior.toUpperCase() : undefined, - `LANGUAGE ${options.language ?? 'SQL'}`, - ].filter((x) => x !== undefined); - - if (flags.length > 0) { - sql.push(flags.join(' ')); - } - - if ('return' in options) { - sql.push(` RETURN ${options.return}`); - } - - if ('body' in options) { - sql.push( - // - `AS $$`, - ' ' + options.body.trim(), - `$$;`, - ); - } - - return sql.join('\n ').trim(); -}; - export const sha1 = (value: string) => createHash('sha1').update(value).digest('hex'); export const hasMask = (input: number, mask: number) => (input & mask) === mask; diff --git a/server/src/sql-tools/public_api.ts b/server/src/sql-tools/public_api.ts index d916678d4a..b41cce4ab5 100644 --- a/server/src/sql-tools/public_api.ts +++ b/server/src/sql-tools/public_api.ts @@ -3,7 +3,6 @@ export { schemaFromCode } from 'src/sql-tools/from-code'; export * from 'src/sql-tools/from-code/decorators/after-delete.decorator'; export * from 'src/sql-tools/from-code/decorators/before-update.decorator'; export * from 'src/sql-tools/from-code/decorators/check.decorator'; -export * from 'src/sql-tools/from-code/decorators/column-index.decorator'; export * from 'src/sql-tools/from-code/decorators/column.decorator'; export * from 'src/sql-tools/from-code/decorators/configuration-parameter.decorator'; export * from 'src/sql-tools/from-code/decorators/create-date-column.decorator'; diff --git a/server/test/sql-tools/column-index-name-default.ts b/server/test/sql-tools/column-index-name-default.ts index e8b36ec119..cedae006be 100644 --- a/server/test/sql-tools/column-index-name-default.ts +++ b/server/test/sql-tools/column-index-name-default.ts @@ -1,9 +1,8 @@ -import { Column, ColumnIndex, DatabaseSchema, Table } from 'src/sql-tools'; +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; @Table() export class Table1 { - @ColumnIndex() - @Column() + @Column({ index: true }) column1!: string; } diff --git a/server/test/sql-tools/column-index-name.ts b/server/test/sql-tools/column-index-name.ts new file mode 100644 index 0000000000..8ba18a8851 --- /dev/null +++ b/server/test/sql-tools/column-index-name.ts @@ -0,0 +1,46 @@ +import { Column, DatabaseSchema, Table } from 'src/sql-tools'; + +@Table() +export class Table1 { + @Column({ indexName: 'IDX_test' }) + column1!: string; +} + +export const description = 'should create a column with an index if a name is provided'; +export const schema: DatabaseSchema = { + name: 'postgres', + schemaName: 'public', + functions: [], + enums: [], + extensions: [], + parameters: [], + tables: [ + { + name: 'table1', + columns: [ + { + name: 'column1', + tableName: 'table1', + type: 'character varying', + nullable: false, + isArray: false, + primary: false, + synchronize: true, + }, + ], + indexes: [ + { + name: 'IDX_test', + columnNames: ['column1'], + tableName: 'table1', + unique: false, + synchronize: true, + }, + ], + triggers: [], + constraints: [], + synchronize: true, + }, + ], + warnings: [], +}; diff --git a/server/test/sql-tools/foreign-key-inferred-type.stub.ts b/server/test/sql-tools/foreign-key-inferred-type.stub.ts index 2ecaafdcad..0b66a1acd4 100644 --- a/server/test/sql-tools/foreign-key-inferred-type.stub.ts +++ b/server/test/sql-tools/foreign-key-inferred-type.stub.ts @@ -60,7 +60,15 @@ export const schema: DatabaseSchema = { synchronize: true, }, ], - indexes: [], + indexes: [ + { + name: 'IDX_3fcca5cc563abf256fc346e3ff', + tableName: 'table2', + columnNames: ['parentId'], + unique: false, + synchronize: true, + }, + ], triggers: [], constraints: [ { diff --git a/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts b/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts index 0601a02d42..109a3dfc85 100644 --- a/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts +++ b/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts @@ -60,7 +60,15 @@ export const schema: DatabaseSchema = { synchronize: true, }, ], - indexes: [], + indexes: [ + { + name: 'IDX_3fcca5cc563abf256fc346e3ff', + tableName: 'table2', + columnNames: ['parentId'], + unique: false, + synchronize: true, + }, + ], triggers: [], constraints: [ { From 6474a78b8be31cf6a3c2fabf2f3177ffd40624e1 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 17 Apr 2025 17:38:47 -0400 Subject: [PATCH 004/356] feat: initial kysely migration file (#17678) --- .github/workflows/test.yml | 2 +- docs/docs/developer/database-migrations.md | 6 +- server/package.json | 5 +- server/src/bin/database.ts | 11 - server/src/bin/migrations.ts | 37 +- .../1744910873956-AddMissingIndex.ts | 13 + .../src/repositories/database.repository.ts | 75 ++-- .../1744910873969-InitialMigration.ts | 391 ++++++++++++++++++ .../src/schema/tables/geodata-places.table.ts | 5 +- .../from-code/processors/table.processor.ts | 7 + .../sql-tools/from-code/register-function.ts | 8 +- server/test/medium/globalSetup.ts | 78 +--- ....stub copy.ts => index-with-expression.ts} | 0 13 files changed, 499 insertions(+), 139 deletions(-) delete mode 100644 server/src/bin/database.ts create mode 100644 server/src/migrations/1744910873956-AddMissingIndex.ts create mode 100644 server/src/schema/migrations/1744910873969-InitialMigration.ts rename server/test/sql-tools/{index-with-where.stub copy.ts => index-with-expression.ts} (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5273698e4e..f5cb6c8d30 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -521,7 +521,7 @@ jobs: run: npm run migrations:run - name: Test npm run schema:reset command works - run: npm run typeorm:schema:reset + run: npm run schema:reset - name: Generate new migrations continue-on-error: true diff --git a/docs/docs/developer/database-migrations.md b/docs/docs/developer/database-migrations.md index 2cddf5f386..c0c61340cb 100644 --- a/docs/docs/developer/database-migrations.md +++ b/docs/docs/developer/database-migrations.md @@ -1,14 +1,14 @@ # Database Migrations -After making any changes in the `server/src/entities`, a database migration need to run in order to register the changes in the database. Follow the steps below to create a new migration. +After making any changes in the `server/src/schema`, a database migration need to run in order to register the changes in the database. Follow the steps below to create a new migration. 1. Run the command ```bash -npm run typeorm:migrations:generate +npm run migrations:generate ``` 2. Check if the migration file makes sense. -3. Move the migration file to folder `./server/src/migrations` in your code editor. +3. Move the migration file to folder `./server/src/schema/migrations` in your code editor. The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately. diff --git a/server/package.json b/server/package.json index 76415da7c8..8e149d961e 100644 --- a/server/package.json +++ b/server/package.json @@ -26,9 +26,8 @@ "migrations:generate": "node ./dist/bin/migrations.js generate", "migrations:create": "node ./dist/bin/migrations.js create", "migrations:run": "node ./dist/bin/migrations.js run", - "typeorm:migrations:revert": "typeorm migration:revert -d ./dist/bin/database.js", - "typeorm:schema:drop": "typeorm query -d ./dist/bin/database.js 'DROP schema public cascade; CREATE schema public;'", - "typeorm:schema:reset": "npm run typeorm:schema:drop && npm run migrations:run", + "schema:drop": "node ./dist/bin/migrations.js query 'DROP schema public cascade; CREATE schema public;'", + "schema:reset": "npm run schema:drop && npm run migrations:run", "kysely:codegen": "npx kysely-codegen --include-pattern=\"(public|vectors).*\" --dialect postgres --url postgres://postgres:postgres@localhost/immich --log-level debug --out-file=./src/db.d.ts", "sync:open-api": "node ./dist/bin/sync-open-api.js", "sync:sql": "node ./dist/bin/sync-sql.js", diff --git a/server/src/bin/database.ts b/server/src/bin/database.ts deleted file mode 100644 index 7ea56e0fc0..0000000000 --- a/server/src/bin/database.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ConfigRepository } from 'src/repositories/config.repository'; -import { DataSource } from 'typeorm'; - -const { database } = new ConfigRepository().getEnv(); - -/** - * @deprecated - DO NOT USE THIS - * - * this export is ONLY to be used for TypeORM commands in package.json#scripts - */ -export const dataSource = new DataSource({ ...database.config.typeorm, host: 'localhost' }); diff --git a/server/src/bin/migrations.ts b/server/src/bin/migrations.ts index fb50323dff..2ddc6776fb 100644 --- a/server/src/bin/migrations.ts +++ b/server/src/bin/migrations.ts @@ -1,8 +1,8 @@ #!/usr/bin/env node process.env.DB_URL = process.env.DB_URL || 'postgres://postgres:postgres@localhost:5432/immich'; -import { Kysely } from 'kysely'; -import { writeFileSync } from 'node:fs'; +import { Kysely, sql } from 'kysely'; +import { mkdirSync, writeFileSync } from 'node:fs'; import { basename, dirname, extname, join } from 'node:path'; import postgres from 'postgres'; import { ConfigRepository } from 'src/repositories/config.repository'; @@ -23,8 +23,13 @@ const main = async () => { } case 'run': { - const only = process.argv[3] as 'kysely' | 'typeorm' | undefined; - await run(only); + await runMigrations(); + return; + } + + case 'query': { + const query = process.argv[3]; + await runQuery(query); return; } @@ -48,14 +53,25 @@ const main = async () => { } }; -const run = async (only?: 'kysely' | 'typeorm') => { +const getDatabaseClient = () => { const configRepository = new ConfigRepository(); const { database } = configRepository.getEnv(); - const logger = new LoggingRepository(undefined, configRepository); - const db = new Kysely(getKyselyConfig(database.config.kysely)); - const databaseRepository = new DatabaseRepository(db, logger, configRepository); + return new Kysely(getKyselyConfig(database.config.kysely)); +}; - await databaseRepository.runMigrations({ only }); +const runQuery = async (query: string) => { + const db = getDatabaseClient(); + await sql.raw(query).execute(db); + await db.destroy(); +}; + +const runMigrations = async () => { + const configRepository = new ConfigRepository(); + const logger = new LoggingRepository(undefined, configRepository); + const db = getDatabaseClient(); + const databaseRepository = new DatabaseRepository(db, logger, configRepository); + await databaseRepository.runMigrations(); + await db.destroy(); }; const debug = async () => { @@ -81,7 +97,8 @@ const create = (path: string, up: string[], down: string[]) => { const filename = `${timestamp}-${name}.ts`; const folder = dirname(path); const fullPath = join(folder, filename); - writeFileSync(fullPath, asMigration('typeorm', { name, timestamp, up, down })); + mkdirSync(folder, { recursive: true }); + writeFileSync(fullPath, asMigration('kysely', { name, timestamp, up, down })); console.log(`Wrote ${fullPath}`); }; diff --git a/server/src/migrations/1744910873956-AddMissingIndex.ts b/server/src/migrations/1744910873956-AddMissingIndex.ts new file mode 100644 index 0000000000..38dd6f4958 --- /dev/null +++ b/server/src/migrations/1744910873956-AddMissingIndex.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddMissingIndex1744910873956 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_geodata_gist_earthcoord" ON "geodata_places" (ll_to_earth_public(latitude, longitude))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_geodata_gist_earthcoord";`); + } +} diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index ec0b263408..c70c2cbdd4 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import AsyncLock from 'async-lock'; import { FileMigrationProvider, Kysely, Migrator, sql, Transaction } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; -import { existsSync } from 'node:fs'; import { readdir } from 'node:fs/promises'; import { join } from 'node:path'; import semver from 'semver'; @@ -197,62 +196,54 @@ export class DatabaseRepository { return dimSize; } - async runMigrations(options?: { transaction?: 'all' | 'none' | 'each'; only?: 'kysely' | 'typeorm' }): Promise { + async runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise { const { database } = this.configRepository.getEnv(); - if (options?.only !== 'kysely') { - const dataSource = new DataSource(database.config.typeorm); - this.logger.log('Running migrations, this may take a while'); + this.logger.log('Running migrations, this may take a while'); + const tableExists = sql<{ result: string | null }>`select to_regclass('migrations') as "result"`; + const { rows } = await tableExists.execute(this.db); + const hasTypeOrmMigrations = !!rows[0]?.result; + if (hasTypeOrmMigrations) { this.logger.debug('Running typeorm migrations'); - + const dataSource = new DataSource(database.config.typeorm); await dataSource.initialize(); await dataSource.runMigrations(options); await dataSource.destroy(); - this.logger.debug('Finished running typeorm migrations'); } - if (options?.only !== 'typeorm') { - // eslint-disable-next-line unicorn/prefer-module - const migrationFolder = join(__dirname, '..', 'schema/migrations'); + this.logger.debug('Running kysely migrations'); + const migrator = new Migrator({ + db: this.db, + migrationLockTableName: 'kysely_migrations_lock', + migrationTableName: 'kysely_migrations', + provider: new FileMigrationProvider({ + fs: { readdir }, + path: { join }, + // eslint-disable-next-line unicorn/prefer-module + migrationFolder: join(__dirname, '..', 'schema/migrations'), + }), + }); - // TODO remove after we have at least one kysely migration - if (!existsSync(migrationFolder)) { - return; + const { error, results } = await migrator.migrateToLatest(); + + for (const result of results ?? []) { + if (result.status === 'Success') { + this.logger.log(`Migration "${result.migrationName}" succeeded`); } - this.logger.debug('Running kysely migrations'); - const migrator = new Migrator({ - db: this.db, - migrationLockTableName: 'kysely_migrations_lock', - migrationTableName: 'kysely_migrations', - provider: new FileMigrationProvider({ - fs: { readdir }, - path: { join }, - migrationFolder, - }), - }); - - const { error, results } = await migrator.migrateToLatest(); - - for (const result of results ?? []) { - if (result.status === 'Success') { - this.logger.log(`Migration "${result.migrationName}" succeeded`); - } - - if (result.status === 'Error') { - this.logger.warn(`Migration "${result.migrationName}" failed`); - } + if (result.status === 'Error') { + this.logger.warn(`Migration "${result.migrationName}" failed`); } - - if (error) { - this.logger.error(`Kysely migrations failed: ${error}`); - throw error; - } - - this.logger.debug('Finished running kysely migrations'); } + + if (error) { + this.logger.error(`Kysely migrations failed: ${error}`); + throw error; + } + + this.logger.debug('Finished running kysely migrations'); } async withLock(lock: DatabaseLock, callback: () => Promise): Promise { diff --git a/server/src/schema/migrations/1744910873969-InitialMigration.ts b/server/src/schema/migrations/1744910873969-InitialMigration.ts new file mode 100644 index 0000000000..e157607681 --- /dev/null +++ b/server/src/schema/migrations/1744910873969-InitialMigration.ts @@ -0,0 +1,391 @@ +import { Kysely, sql } from 'kysely'; +import { DatabaseExtension } from 'src/enum'; +import { ConfigRepository } from 'src/repositories/config.repository'; + +const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension; + +export async function up(db: Kysely): Promise { + await sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`.execute(db); + await sql`CREATE EXTENSION IF NOT EXISTS "unaccent";`.execute(db); + await sql`CREATE EXTENSION IF NOT EXISTS "cube";`.execute(db); + await sql`CREATE EXTENSION IF NOT EXISTS "earthdistance";`.execute(db); + await sql`CREATE EXTENSION IF NOT EXISTS "pg_trgm";`.execute(db); + await sql`CREATE EXTENSION IF NOT EXISTS "vectors";`.execute(db); + await sql`CREATE OR REPLACE FUNCTION immich_uuid_v7(p_timestamp timestamp with time zone default clock_timestamp()) + RETURNS uuid + VOLATILE LANGUAGE SQL + AS $$ + select encode( + set_bit( + set_bit( + overlay(uuid_send(gen_random_uuid()) + placing substring(int8send(floor(extract(epoch from p_timestamp) * 1000)::bigint) from 3) + from 1 for 6 + ), + 52, 1 + ), + 53, 1 + ), + 'hex')::uuid; + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION updated_at() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + DECLARE + clock_timestamp TIMESTAMP := clock_timestamp(); + BEGIN + new."updatedAt" = clock_timestamp; + new."updateId" = immich_uuid_v7(clock_timestamp); + return new; + END; + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION f_concat_ws(text, text[]) + RETURNS text + PARALLEL SAFE IMMUTABLE LANGUAGE SQL + AS $$SELECT array_to_string($2, $1)$$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION f_unaccent(text) + RETURNS text + PARALLEL SAFE STRICT IMMUTABLE LANGUAGE SQL + RETURN unaccent('unaccent', $1)`.execute(db); + await sql`CREATE OR REPLACE FUNCTION ll_to_earth_public(latitude double precision, longitude double precision) + RETURNS public.earth + PARALLEL SAFE STRICT IMMUTABLE LANGUAGE SQL + AS $$ + SELECT public.cube(public.cube(public.cube(public.earth()*cos(radians(latitude))*cos(radians(longitude))),public.earth()*cos(radians(latitude))*sin(radians(longitude))),public.earth()*sin(radians(latitude)))::public.earth + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION users_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO users_audit ("userId") + SELECT "id" + FROM OLD; + RETURN NULL; + END; + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION partners_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO partners_audit ("sharedById", "sharedWithId") + SELECT "sharedById", "sharedWithId" + FROM OLD; + RETURN NULL; + END; + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION assets_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO assets_audit ("assetId", "ownerId") + SELECT "id", "ownerId" + FROM OLD; + RETURN NULL; + END; + $$;`.execute(db); + if (vectorExtension === DatabaseExtension.VECTORS) { + await sql`SET search_path TO "$user", public, vectors`.execute(db); + await sql`SET vectors.pgvector_compatibility=on`.execute(db); + } + await sql`CREATE TYPE "assets_status_enum" AS ENUM ('active','trashed','deleted');`.execute(db); + await sql`CREATE TYPE "sourcetype" AS ENUM ('machine-learning','exif','manual');`.execute(db); + await sql`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying NOT NULL, "password" character varying NOT NULL DEFAULT '', "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "profileImagePath" character varying NOT NULL DEFAULT '', "isAdmin" boolean NOT NULL DEFAULT false, "shouldChangePassword" boolean NOT NULL DEFAULT true, "deletedAt" timestamp with time zone, "oauthId" character varying NOT NULL DEFAULT '', "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "storageLabel" character varying, "name" character varying NOT NULL DEFAULT '', "quotaSizeInBytes" bigint, "quotaUsageInBytes" bigint NOT NULL DEFAULT 0, "status" character varying NOT NULL DEFAULT 'active', "profileChangedAt" timestamp with time zone NOT NULL DEFAULT now(), "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "libraries" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "ownerId" uuid NOT NULL, "importPaths" text[] NOT NULL, "exclusionPatterns" text[] NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "refreshedAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "asset_stack" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "primaryAssetId" uuid NOT NULL, "ownerId" uuid NOT NULL);`.execute(db); + await sql`CREATE TABLE "assets" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "deviceAssetId" character varying NOT NULL, "ownerId" uuid NOT NULL, "deviceId" character varying NOT NULL, "type" character varying NOT NULL, "originalPath" character varying NOT NULL, "fileCreatedAt" timestamp with time zone NOT NULL, "fileModifiedAt" timestamp with time zone NOT NULL, "isFavorite" boolean NOT NULL DEFAULT false, "duration" character varying, "encodedVideoPath" character varying DEFAULT '', "checksum" bytea NOT NULL, "isVisible" boolean NOT NULL DEFAULT true, "livePhotoVideoId" uuid, "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "isArchived" boolean NOT NULL DEFAULT false, "originalFileName" character varying NOT NULL, "sidecarPath" character varying, "thumbhash" bytea, "isOffline" boolean NOT NULL DEFAULT false, "libraryId" uuid, "isExternal" boolean NOT NULL DEFAULT false, "deletedAt" timestamp with time zone, "localDateTime" timestamp with time zone NOT NULL, "stackId" uuid, "duplicateId" uuid, "status" assets_status_enum NOT NULL DEFAULT 'active', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "albums" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "ownerId" uuid NOT NULL, "albumName" character varying NOT NULL DEFAULT 'Untitled Album', "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "albumThumbnailAssetId" uuid, "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "description" text NOT NULL DEFAULT '', "deletedAt" timestamp with time zone, "isActivityEnabled" boolean NOT NULL DEFAULT true, "order" character varying NOT NULL DEFAULT 'desc', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`COMMENT ON COLUMN "albums"."albumThumbnailAssetId" IS 'Asset ID to be used as thumbnail';`.execute(db); + await sql`CREATE TABLE "activity" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "assetId" uuid, "comment" text, "isLiked" boolean NOT NULL DEFAULT false, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "albums_assets_assets" ("albumsId" uuid NOT NULL, "assetsId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now());`.execute(db); + await sql`CREATE TABLE "albums_shared_users_users" ("albumsId" uuid NOT NULL, "usersId" uuid NOT NULL, "role" character varying NOT NULL DEFAULT 'editor');`.execute(db); + await sql`CREATE TABLE "api_keys" ("name" character varying NOT NULL, "key" character varying NOT NULL, "userId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "permissions" character varying[] NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "assets_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "assetId" uuid NOT NULL, "ownerId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db); + await sql`CREATE TABLE "person" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "ownerId" uuid NOT NULL, "name" character varying NOT NULL DEFAULT '', "thumbnailPath" character varying NOT NULL DEFAULT '', "isHidden" boolean NOT NULL DEFAULT false, "birthDate" date, "faceAssetId" uuid, "isFavorite" boolean NOT NULL DEFAULT false, "color" character varying, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "asset_faces" ("assetId" uuid NOT NULL, "personId" uuid, "imageWidth" integer NOT NULL DEFAULT 0, "imageHeight" integer NOT NULL DEFAULT 0, "boundingBoxX1" integer NOT NULL DEFAULT 0, "boundingBoxY1" integer NOT NULL DEFAULT 0, "boundingBoxX2" integer NOT NULL DEFAULT 0, "boundingBoxY2" integer NOT NULL DEFAULT 0, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "sourceType" sourcetype NOT NULL DEFAULT 'machine-learning', "deletedAt" timestamp with time zone);`.execute(db); + await sql`CREATE TABLE "asset_files" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "assetId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "type" character varying NOT NULL, "path" character varying NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "asset_job_status" ("assetId" uuid NOT NULL, "facesRecognizedAt" timestamp with time zone, "metadataExtractedAt" timestamp with time zone, "duplicatesDetectedAt" timestamp with time zone, "previewAt" timestamp with time zone, "thumbnailAt" timestamp with time zone);`.execute(db); + await sql`CREATE TABLE "audit" ("id" serial NOT NULL, "entityType" character varying NOT NULL, "entityId" uuid NOT NULL, "action" character varying NOT NULL, "ownerId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now());`.execute(db); + await sql`CREATE TABLE "exif" ("assetId" uuid NOT NULL, "make" character varying, "model" character varying, "exifImageWidth" integer, "exifImageHeight" integer, "fileSizeInByte" bigint, "orientation" character varying, "dateTimeOriginal" timestamp with time zone, "modifyDate" timestamp with time zone, "lensModel" character varying, "fNumber" double precision, "focalLength" double precision, "iso" integer, "latitude" double precision, "longitude" double precision, "city" character varying, "state" character varying, "country" character varying, "description" text NOT NULL DEFAULT '', "fps" double precision, "exposureTime" character varying, "livePhotoCID" character varying, "timeZone" character varying, "projectionType" character varying, "profileDescription" character varying, "colorspace" character varying, "bitsPerSample" integer, "autoStackId" character varying, "rating" integer, "updatedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "face_search" ("faceId" uuid NOT NULL, "embedding" vector(512) NOT NULL);`.execute(db); + await sql`CREATE TABLE "geodata_places" ("id" integer NOT NULL, "name" character varying(200) NOT NULL, "longitude" double precision NOT NULL, "latitude" double precision NOT NULL, "countryCode" character(2) NOT NULL, "admin1Code" character varying(20), "admin2Code" character varying(80), "modificationDate" date NOT NULL, "admin1Name" character varying, "admin2Name" character varying, "alternateNames" character varying);`.execute(db); + await sql`CREATE TABLE "memories" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "ownerId" uuid NOT NULL, "type" character varying NOT NULL, "data" jsonb NOT NULL, "isSaved" boolean NOT NULL DEFAULT false, "memoryAt" timestamp with time zone NOT NULL, "seenAt" timestamp with time zone, "showAt" timestamp with time zone, "hideAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "memories_assets_assets" ("memoriesId" uuid NOT NULL, "assetsId" uuid NOT NULL);`.execute(db); + await sql`CREATE TABLE "move_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "entityId" uuid NOT NULL, "pathType" character varying NOT NULL, "oldPath" character varying NOT NULL, "newPath" character varying NOT NULL);`.execute(db); + await sql`CREATE TABLE "naturalearth_countries" ("id" integer NOT NULL GENERATED ALWAYS AS IDENTITY, "admin" character varying(50) NOT NULL, "admin_a3" character varying(3) NOT NULL, "type" character varying(50) NOT NULL, "coordinates" polygon NOT NULL);`.execute(db); + await sql`CREATE TABLE "partners_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "sharedById" uuid NOT NULL, "sharedWithId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db); + await sql`CREATE TABLE "partners" ("sharedById" uuid NOT NULL, "sharedWithId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "inTimeline" boolean NOT NULL DEFAULT false, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "sessions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "token" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "userId" uuid NOT NULL, "deviceType" character varying NOT NULL DEFAULT '', "deviceOS" character varying NOT NULL DEFAULT '', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "shared_links" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "description" character varying, "userId" uuid NOT NULL, "key" bytea NOT NULL, "type" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "expiresAt" timestamp with time zone, "allowUpload" boolean NOT NULL DEFAULT false, "albumId" uuid, "allowDownload" boolean NOT NULL DEFAULT true, "showExif" boolean NOT NULL DEFAULT true, "password" character varying);`.execute(db); + await sql`CREATE TABLE "shared_link__asset" ("assetsId" uuid NOT NULL, "sharedLinksId" uuid NOT NULL);`.execute(db); + await sql`CREATE TABLE "smart_search" ("assetId" uuid NOT NULL, "embedding" vector(512) NOT NULL);`.execute(db); + await sql`ALTER TABLE "smart_search" ALTER COLUMN "embedding" SET STORAGE EXTERNAL;`.execute(db); + await sql`CREATE TABLE "session_sync_checkpoints" ("sessionId" uuid NOT NULL, "type" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "ack" character varying NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "system_metadata" ("key" character varying NOT NULL, "value" jsonb NOT NULL);`.execute(db); + await sql`CREATE TABLE "tags" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "value" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "color" character varying, "parentId" uuid, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "tag_asset" ("assetsId" uuid NOT NULL, "tagsId" uuid NOT NULL);`.execute(db); + await sql`CREATE TABLE "tags_closure" ("id_ancestor" uuid NOT NULL, "id_descendant" uuid NOT NULL);`.execute(db); + await sql`CREATE TABLE "users_audit" ("userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), "id" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); + await sql`CREATE TABLE "user_metadata" ("userId" uuid NOT NULL, "key" character varying NOT NULL, "value" jsonb NOT NULL);`.execute(db); + await sql`CREATE TABLE "version_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "version" character varying NOT NULL);`.execute(db); + await sql`ALTER TABLE "users" ADD CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "libraries" ADD CONSTRAINT "PK_505fedfcad00a09b3734b4223de" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "PK_74a27e7fcbd5852463d0af3034b" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "assets" ADD CONSTRAINT "PK_da96729a8b113377cfb6a62439c" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "albums" ADD CONSTRAINT "PK_7f71c7b5bc7c87b8f94c9a93a00" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "activity" ADD CONSTRAINT "PK_24625a1d6b1b089c8ae206fe467" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "PK_c67bc36fa845fb7b18e0e398180" PRIMARY KEY ("albumsId", "assetsId");`.execute(db); + await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "PK_7df55657e0b2e8b626330a0ebc8" PRIMARY KEY ("albumsId", "usersId");`.execute(db); + await sql`ALTER TABLE "api_keys" ADD CONSTRAINT "PK_5c8a79801b44bd27b79228e1dad" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "assets_audit" ADD CONSTRAINT "PK_99bd5c015f81a641927a32b4212" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "person" ADD CONSTRAINT "PK_5fdaf670315c4b7e70cce85daa3" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "PK_6df76ab2eb6f5b57b7c2f1fc684" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "PK_c41dc3e9ef5e1c57ca5a08a0004" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "asset_job_status" ADD CONSTRAINT "PK_420bec36fc02813bddf5c8b73d4" PRIMARY KEY ("assetId");`.execute(db); + await sql`ALTER TABLE "audit" ADD CONSTRAINT "PK_1d3d120ddaf7bc9b1ed68ed463a" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "exif" ADD CONSTRAINT "PK_c0117fdbc50b917ef9067740c44" PRIMARY KEY ("assetId");`.execute(db); + await sql`ALTER TABLE "face_search" ADD CONSTRAINT "face_search_pkey" PRIMARY KEY ("faceId");`.execute(db); + await sql`ALTER TABLE "geodata_places" ADD CONSTRAINT "PK_c29918988912ef4036f3d7fbff4" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "memories" ADD CONSTRAINT "PK_aaa0692d9496fe827b0568612f8" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "PK_fcaf7112a013d1703c011c6793d" PRIMARY KEY ("memoriesId", "assetsId");`.execute(db); + await sql`ALTER TABLE "move_history" ADD CONSTRAINT "PK_af608f132233acf123f2949678d" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "naturalearth_countries" ADD CONSTRAINT "PK_21a6d86d1ab5d841648212e5353" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "partners_audit" ADD CONSTRAINT "PK_952b50217ff78198a7e380f0359" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "partners" ADD CONSTRAINT "PK_f1cc8f73d16b367f426261a8736" PRIMARY KEY ("sharedById", "sharedWithId");`.execute(db); + await sql`ALTER TABLE "sessions" ADD CONSTRAINT "PK_48cb6b5c20faa63157b3c1baf7f" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "PK_642e2b0f619e4876e5f90a43465" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "PK_9b4f3687f9b31d1e311336b05e3" PRIMARY KEY ("assetsId", "sharedLinksId");`.execute(db); + await sql`ALTER TABLE "smart_search" ADD CONSTRAINT "smart_search_pkey" PRIMARY KEY ("assetId");`.execute(db); + await sql`ALTER TABLE "session_sync_checkpoints" ADD CONSTRAINT "PK_b846ab547a702863ef7cd9412fb" PRIMARY KEY ("sessionId", "type");`.execute(db); + await sql`ALTER TABLE "system_metadata" ADD CONSTRAINT "PK_fa94f6857470fb5b81ec6084465" PRIMARY KEY ("key");`.execute(db); + await sql`ALTER TABLE "tags" ADD CONSTRAINT "PK_e7dc17249a1148a1970748eda99" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "PK_ef5346fe522b5fb3bc96454747e" PRIMARY KEY ("assetsId", "tagsId");`.execute(db); + await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "PK_eab38eb12a3ec6df8376c95477c" PRIMARY KEY ("id_ancestor", "id_descendant");`.execute(db); + await sql`ALTER TABLE "users_audit" ADD CONSTRAINT "PK_e9b2bdfd90e7eb5961091175180" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "user_metadata" ADD CONSTRAINT "PK_5931462150b3438cbc83277fe5a" PRIMARY KEY ("userId", "key");`.execute(db); + await sql`ALTER TABLE "version_history" ADD CONSTRAINT "PK_5db259cbb09ce82c0d13cfd1b23" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "libraries" ADD CONSTRAINT "FK_0f6fc2fb195f24d19b0fb0d57c1" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "FK_91704e101438fd0653f582426dc" FOREIGN KEY ("primaryAssetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION;`.execute(db); + await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "FK_c05079e542fd74de3b5ecb5c1c8" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_2c5ac0d6fb58b238fd2068de67d" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_16294b83fa8c0149719a1f631ef" FOREIGN KEY ("livePhotoVideoId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db); + await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_9977c3c1de01c3d848039a6b90c" FOREIGN KEY ("libraryId") REFERENCES "libraries" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_f15d48fa3ea5e4bda05ca8ab207" FOREIGN KEY ("stackId") REFERENCES "asset_stack" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db); + await sql`ALTER TABLE "albums" ADD CONSTRAINT "FK_b22c53f35ef20c28c21637c85f4" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "albums" ADD CONSTRAINT "FK_05895aa505a670300d4816debce" FOREIGN KEY ("albumThumbnailAssetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db); + await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_1af8519996fbfb3684b58df280b" FOREIGN KEY ("albumId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_8091ea76b12338cb4428d33d782" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "FK_e590fa396c6898fcd4a50e40927" FOREIGN KEY ("albumsId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "FK_4bd1303d199f4e72ccdf998c621" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "FK_427c350ad49bd3935a50baab737" FOREIGN KEY ("albumsId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "FK_f48513bf9bccefd6ff3ad30bd06" FOREIGN KEY ("usersId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "api_keys" ADD CONSTRAINT "FK_6c2e267ae764a9413b863a29342" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "person" ADD CONSTRAINT "FK_5527cc99f530a547093f9e577b6" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "person" ADD CONSTRAINT "FK_2bbabe31656b6778c6b87b61023" FOREIGN KEY ("faceAssetId") REFERENCES "asset_faces" ("id") ON UPDATE NO ACTION ON DELETE SET NULL;`.execute(db); + await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_02a43fd0b3c50fb6d7f0cb7282c" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_95ad7106dd7b484275443f580f9" FOREIGN KEY ("personId") REFERENCES "person" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db); + await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "FK_e3e103a5f1d8bc8402999286040" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "asset_job_status" ADD CONSTRAINT "FK_420bec36fc02813bddf5c8b73d4" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "exif" ADD CONSTRAINT "FK_c0117fdbc50b917ef9067740c44" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "face_search" ADD CONSTRAINT "face_search_faceId_fkey" FOREIGN KEY ("faceId") REFERENCES "asset_faces" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "memories" ADD CONSTRAINT "FK_575842846f0c28fa5da46c99b19" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e" FOREIGN KEY ("memoriesId") REFERENCES "memories" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "partners" ADD CONSTRAINT "FK_7e077a8b70b3530138610ff5e04" FOREIGN KEY ("sharedById") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "partners" ADD CONSTRAINT "FK_d7e875c6c60e661723dbf372fd3" FOREIGN KEY ("sharedWithId") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "sessions" ADD CONSTRAINT "FK_57de40bc620f456c7311aa3a1e6" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_66fe3837414c5a9f1c33ca49340" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66" FOREIGN KEY ("albumId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab" FOREIGN KEY ("sharedLinksId") REFERENCES "shared_links" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "smart_search" ADD CONSTRAINT "smart_search_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "session_sync_checkpoints" ADD CONSTRAINT "FK_d8ddd9d687816cc490432b3d4bc" FOREIGN KEY ("sessionId") REFERENCES "sessions" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "tags" ADD CONSTRAINT "FK_9f9590cc11561f1f48ff034ef99" FOREIGN KEY ("parentId") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42" FOREIGN KEY ("tagsId") REFERENCES "tags" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_15fbcbc67663c6bfc07b354c22c" FOREIGN KEY ("id_ancestor") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_b1a2a7ed45c29179b5ad51548a1" FOREIGN KEY ("id_descendant") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "user_metadata" ADD CONSTRAINT "FK_6afb43681a21cf7815932bc38ac" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "users" ADD CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email");`.execute(db); + await sql`ALTER TABLE "users" ADD CONSTRAINT "UQ_b309cf34fa58137c416b32cea3a" UNIQUE ("storageLabel");`.execute(db); + await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "REL_91704e101438fd0653f582426d" UNIQUE ("primaryAssetId");`.execute(db); + await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "UQ_assetId_type" UNIQUE ("assetId", "type");`.execute(db); + await sql`ALTER TABLE "move_history" ADD CONSTRAINT "UQ_newPath" UNIQUE ("newPath");`.execute(db); + await sql`ALTER TABLE "move_history" ADD CONSTRAINT "UQ_entityId_pathType" UNIQUE ("entityId", "pathType");`.execute(db); + await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "UQ_sharedlink_key" UNIQUE ("key");`.execute(db); + await sql`ALTER TABLE "tags" ADD CONSTRAINT "UQ_79d6f16e52bb2c7130375246793" UNIQUE ("userId", "value");`.execute(db); + await sql`ALTER TABLE "activity" ADD CONSTRAINT "CHK_2ab1e70f113f450eb40c1e3ec8" CHECK (("comment" IS NULL AND "isLiked" = true) OR ("comment" IS NOT NULL AND "isLiked" = false));`.execute(db); + await sql`ALTER TABLE "person" ADD CONSTRAINT "CHK_b0f82b0ed662bfc24fbb58bb45" CHECK ("birthDate" <= CURRENT_DATE);`.execute(db); + await sql`CREATE INDEX "IDX_users_updated_at_asc_id_asc" ON "users" ("updatedAt", "id")`.execute(db); + await sql`CREATE INDEX "IDX_users_update_id" ON "users" ("updateId")`.execute(db); + await sql`CREATE INDEX "IDX_0f6fc2fb195f24d19b0fb0d57c" ON "libraries" ("ownerId")`.execute(db); + await sql`CREATE INDEX "IDX_libraries_update_id" ON "libraries" ("updateId")`.execute(db); + await sql`CREATE INDEX "IDX_91704e101438fd0653f582426d" ON "asset_stack" ("primaryAssetId")`.execute(db); + await sql`CREATE INDEX "IDX_c05079e542fd74de3b5ecb5c1c" ON "asset_stack" ("ownerId")`.execute(db); + await sql`CREATE INDEX "idx_originalfilename_trigram" ON "assets" USING gin (f_unaccent("originalFileName") gin_trgm_ops)`.execute(db); + await sql`CREATE INDEX "IDX_asset_id_stackId" ON "assets" ("id", "stackId")`.execute(db); + await sql`CREATE INDEX "IDX_originalPath_libraryId" ON "assets" ("originalPath", "libraryId")`.execute(db); + await sql`CREATE INDEX "idx_local_date_time_month" ON "assets" ((date_trunc('MONTH'::text, ("localDateTime" AT TIME ZONE 'UTC'::text)) AT TIME ZONE 'UTC'::text))`.execute(db); + await sql`CREATE INDEX "idx_local_date_time" ON "assets" ((("localDateTime" at time zone 'UTC')::date))`.execute(db); + await sql`CREATE UNIQUE INDEX "UQ_assets_owner_library_checksum" ON "assets" ("ownerId", "libraryId", "checksum") WHERE ("libraryId" IS NOT NULL)`.execute(db); + await sql`CREATE UNIQUE INDEX "UQ_assets_owner_checksum" ON "assets" ("ownerId", "checksum") WHERE ("libraryId" IS NULL)`.execute(db); + await sql`CREATE INDEX "IDX_2c5ac0d6fb58b238fd2068de67" ON "assets" ("ownerId")`.execute(db); + await sql`CREATE INDEX "idx_asset_file_created_at" ON "assets" ("fileCreatedAt")`.execute(db); + await sql`CREATE INDEX "IDX_8d3efe36c0755849395e6ea866" ON "assets" ("checksum")`.execute(db); + await sql`CREATE INDEX "IDX_16294b83fa8c0149719a1f631e" ON "assets" ("livePhotoVideoId")`.execute(db); + await sql`CREATE INDEX "IDX_4d66e76dada1ca180f67a205dc" ON "assets" ("originalFileName")`.execute(db); + await sql`CREATE INDEX "IDX_9977c3c1de01c3d848039a6b90" ON "assets" ("libraryId")`.execute(db); + await sql`CREATE INDEX "IDX_f15d48fa3ea5e4bda05ca8ab20" ON "assets" ("stackId")`.execute(db); + await sql`CREATE INDEX "IDX_assets_duplicateId" ON "assets" ("duplicateId")`.execute(db); + await sql`CREATE INDEX "IDX_assets_update_id" ON "assets" ("updateId")`.execute(db); + await sql`CREATE INDEX "IDX_b22c53f35ef20c28c21637c85f" ON "albums" ("ownerId")`.execute(db); + await sql`CREATE INDEX "IDX_05895aa505a670300d4816debc" ON "albums" ("albumThumbnailAssetId")`.execute(db); + await sql`CREATE INDEX "IDX_albums_update_id" ON "albums" ("updateId")`.execute(db); + await sql`CREATE UNIQUE INDEX "IDX_activity_like" ON "activity" ("assetId", "userId", "albumId") WHERE ("isLiked" = true)`.execute(db); + await sql`CREATE INDEX "IDX_1af8519996fbfb3684b58df280" ON "activity" ("albumId")`.execute(db); + await sql`CREATE INDEX "IDX_3571467bcbe021f66e2bdce96e" ON "activity" ("userId")`.execute(db); + await sql`CREATE INDEX "IDX_8091ea76b12338cb4428d33d78" ON "activity" ("assetId")`.execute(db); + await sql`CREATE INDEX "IDX_activity_update_id" ON "activity" ("updateId")`.execute(db); + await sql`CREATE INDEX "IDX_e590fa396c6898fcd4a50e4092" ON "albums_assets_assets" ("albumsId")`.execute(db); + await sql`CREATE INDEX "IDX_4bd1303d199f4e72ccdf998c62" ON "albums_assets_assets" ("assetsId")`.execute(db); + await sql`CREATE INDEX "IDX_f48513bf9bccefd6ff3ad30bd0" ON "albums_shared_users_users" ("usersId")`.execute(db); + await sql`CREATE INDEX "IDX_427c350ad49bd3935a50baab73" ON "albums_shared_users_users" ("albumsId")`.execute(db); + await sql`CREATE INDEX "IDX_6c2e267ae764a9413b863a2934" ON "api_keys" ("userId")`.execute(db); + await sql`CREATE INDEX "IDX_api_keys_update_id" ON "api_keys" ("updateId")`.execute(db); + await sql`CREATE INDEX "IDX_assets_audit_asset_id" ON "assets_audit" ("assetId")`.execute(db); + await sql`CREATE INDEX "IDX_assets_audit_owner_id" ON "assets_audit" ("ownerId")`.execute(db); + await sql`CREATE INDEX "IDX_assets_audit_deleted_at" ON "assets_audit" ("deletedAt")`.execute(db); + await sql`CREATE INDEX "IDX_5527cc99f530a547093f9e577b" ON "person" ("ownerId")`.execute(db); + await sql`CREATE INDEX "IDX_2bbabe31656b6778c6b87b6102" ON "person" ("faceAssetId")`.execute(db); + await sql`CREATE INDEX "IDX_person_update_id" ON "person" ("updateId")`.execute(db); + await sql`CREATE INDEX "IDX_bf339a24070dac7e71304ec530" ON "asset_faces" ("personId", "assetId")`.execute(db); + await sql`CREATE INDEX "IDX_asset_faces_assetId_personId" ON "asset_faces" ("assetId", "personId")`.execute(db); + await sql`CREATE INDEX "IDX_02a43fd0b3c50fb6d7f0cb7282" ON "asset_faces" ("assetId")`.execute(db); + await sql`CREATE INDEX "IDX_95ad7106dd7b484275443f580f" ON "asset_faces" ("personId")`.execute(db); + await sql`CREATE INDEX "IDX_asset_files_assetId" ON "asset_files" ("assetId")`.execute(db); + await sql`CREATE INDEX "IDX_asset_files_update_id" ON "asset_files" ("updateId")`.execute(db); + await sql`CREATE INDEX "IDX_ownerId_createdAt" ON "audit" ("ownerId", "createdAt")`.execute(db); + await sql`CREATE INDEX "exif_city" ON "exif" ("city")`.execute(db); + await sql`CREATE INDEX "IDX_live_photo_cid" ON "exif" ("livePhotoCID")`.execute(db); + await sql`CREATE INDEX "IDX_auto_stack_id" ON "exif" ("autoStackId")`.execute(db); + await sql`CREATE INDEX "IDX_asset_exif_update_id" ON "exif" ("updateId")`.execute(db); + await sql`CREATE INDEX "face_index" ON "face_search" USING hnsw (embedding vector_cosine_ops) WITH (ef_construction = 300, m = 16)`.execute(db); + await sql`CREATE INDEX "IDX_geodata_gist_earthcoord" ON "geodata_places" (ll_to_earth_public(latitude, longitude))`.execute(db); + await sql`CREATE INDEX "idx_geodata_places_name" ON "geodata_places" USING gin (f_unaccent("name") gin_trgm_ops)`.execute(db); + await sql`CREATE INDEX "idx_geodata_places_admin2_name" ON "geodata_places" USING gin (f_unaccent("admin2Name") gin_trgm_ops)`.execute(db); + await sql`CREATE INDEX "idx_geodata_places_admin1_name" ON "geodata_places" USING gin (f_unaccent("admin1Name") gin_trgm_ops)`.execute(db); + await sql`CREATE INDEX "idx_geodata_places_alternate_names" ON "geodata_places" USING gin (f_unaccent("alternateNames") gin_trgm_ops)`.execute(db); + await sql`CREATE INDEX "IDX_575842846f0c28fa5da46c99b1" ON "memories" ("ownerId")`.execute(db); + await sql`CREATE INDEX "IDX_memories_update_id" ON "memories" ("updateId")`.execute(db); + await sql`CREATE INDEX "IDX_984e5c9ab1f04d34538cd32334" ON "memories_assets_assets" ("memoriesId")`.execute(db); + await sql`CREATE INDEX "IDX_6942ecf52d75d4273de19d2c16" ON "memories_assets_assets" ("assetsId")`.execute(db); + await sql`CREATE INDEX "IDX_partners_audit_shared_by_id" ON "partners_audit" ("sharedById")`.execute(db); + await sql`CREATE INDEX "IDX_partners_audit_shared_with_id" ON "partners_audit" ("sharedWithId")`.execute(db); + await sql`CREATE INDEX "IDX_partners_audit_deleted_at" ON "partners_audit" ("deletedAt")`.execute(db); + await sql`CREATE INDEX "IDX_7e077a8b70b3530138610ff5e0" ON "partners" ("sharedById")`.execute(db); + await sql`CREATE INDEX "IDX_d7e875c6c60e661723dbf372fd" ON "partners" ("sharedWithId")`.execute(db); + await sql`CREATE INDEX "IDX_partners_update_id" ON "partners" ("updateId")`.execute(db); + await sql`CREATE INDEX "IDX_57de40bc620f456c7311aa3a1e" ON "sessions" ("userId")`.execute(db); + await sql`CREATE INDEX "IDX_sessions_update_id" ON "sessions" ("updateId")`.execute(db); + await sql`CREATE INDEX "IDX_66fe3837414c5a9f1c33ca4934" ON "shared_links" ("userId")`.execute(db); + await sql`CREATE INDEX "IDX_sharedlink_key" ON "shared_links" ("key")`.execute(db); + await sql`CREATE INDEX "IDX_sharedlink_albumId" ON "shared_links" ("albumId")`.execute(db); + await sql`CREATE INDEX "IDX_5b7decce6c8d3db9593d6111a6" ON "shared_link__asset" ("assetsId")`.execute(db); + await sql`CREATE INDEX "IDX_c9fab4aa97ffd1b034f3d6581a" ON "shared_link__asset" ("sharedLinksId")`.execute(db); + await sql`CREATE INDEX "clip_index" ON "smart_search" USING hnsw (embedding vector_cosine_ops) WITH (ef_construction = 300, m = 16)`.execute(db); + await sql`CREATE INDEX "IDX_d8ddd9d687816cc490432b3d4b" ON "session_sync_checkpoints" ("sessionId")`.execute(db); + await sql`CREATE INDEX "IDX_session_sync_checkpoints_update_id" ON "session_sync_checkpoints" ("updateId")`.execute(db); + await sql`CREATE INDEX "IDX_92e67dc508c705dd66c9461557" ON "tags" ("userId")`.execute(db); + await sql`CREATE INDEX "IDX_9f9590cc11561f1f48ff034ef9" ON "tags" ("parentId")`.execute(db); + await sql`CREATE INDEX "IDX_tags_update_id" ON "tags" ("updateId")`.execute(db); + await sql`CREATE INDEX "IDX_tag_asset_assetsId_tagsId" ON "tag_asset" ("assetsId", "tagsId")`.execute(db); + await sql`CREATE INDEX "IDX_f8e8a9e893cb5c54907f1b798e" ON "tag_asset" ("assetsId")`.execute(db); + await sql`CREATE INDEX "IDX_e99f31ea4cdf3a2c35c7287eb4" ON "tag_asset" ("tagsId")`.execute(db); + await sql`CREATE INDEX "IDX_15fbcbc67663c6bfc07b354c22" ON "tags_closure" ("id_ancestor")`.execute(db); + await sql`CREATE INDEX "IDX_b1a2a7ed45c29179b5ad51548a" ON "tags_closure" ("id_descendant")`.execute(db); + await sql`CREATE INDEX "IDX_users_audit_deleted_at" ON "users_audit" ("deletedAt")`.execute(db); + await sql`CREATE INDEX "IDX_6afb43681a21cf7815932bc38a" ON "user_metadata" ("userId")`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "users_delete_audit" + AFTER DELETE ON "users" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION users_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "users_updated_at" + BEFORE UPDATE ON "users" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "libraries_updated_at" + BEFORE UPDATE ON "libraries" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "assets_delete_audit" + AFTER DELETE ON "assets" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION assets_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "assets_updated_at" + BEFORE UPDATE ON "assets" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "albums_updated_at" + BEFORE UPDATE ON "albums" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "activity_updated_at" + BEFORE UPDATE ON "activity" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "api_keys_updated_at" + BEFORE UPDATE ON "api_keys" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "person_updated_at" + BEFORE UPDATE ON "person" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_files_updated_at" + BEFORE UPDATE ON "asset_files" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_exif_updated_at" + BEFORE UPDATE ON "exif" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "memories_updated_at" + BEFORE UPDATE ON "memories" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "partners_delete_audit" + AFTER DELETE ON "partners" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION partners_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "partners_updated_at" + BEFORE UPDATE ON "partners" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "sessions_updated_at" + BEFORE UPDATE ON "sessions" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "session_sync_checkpoints_updated_at" + BEFORE UPDATE ON "session_sync_checkpoints" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "tags_updated_at" + BEFORE UPDATE ON "tags" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); +} + +export async function down(): Promise { +// not implemented +} diff --git a/server/src/schema/tables/geodata-places.table.ts b/server/src/schema/tables/geodata-places.table.ts index 631cfdff08..3e78b4cfcf 100644 --- a/server/src/schema/tables/geodata-places.table.ts +++ b/server/src/schema/tables/geodata-places.table.ts @@ -1,6 +1,6 @@ import { Column, Index, PrimaryColumn, Table } from 'src/sql-tools'; -@Table({ name: 'geodata_places' }) +@Table({ name: 'geodata_places', synchronize: false }) @Index({ name: 'idx_geodata_places_alternate_names', using: 'gin', @@ -26,11 +26,10 @@ import { Column, Index, PrimaryColumn, Table } from 'src/sql-tools'; synchronize: false, }) @Index({ - name: 'idx_geodata_places_gist_earthcoord', + name: 'IDX_geodata_gist_earthcoord', expression: 'll_to_earth_public(latitude, longitude)', synchronize: false, }) -@Table({ name: 'idx_geodata_places', synchronize: false }) export class GeodataPlacesTable { @PrimaryColumn({ type: 'integer' }) id!: number; diff --git a/server/src/sql-tools/from-code/processors/table.processor.ts b/server/src/sql-tools/from-code/processors/table.processor.ts index eb4b414576..4ef4e82020 100644 --- a/server/src/sql-tools/from-code/processors/table.processor.ts +++ b/server/src/sql-tools/from-code/processors/table.processor.ts @@ -6,6 +6,13 @@ export const processTables: Processor = (builder, items) => { for (const { item: { options, object }, } of items.filter((item) => item.type === 'table')) { + const test = readMetadata(object); + if (test) { + throw new Error( + `Table ${test.name} has already been registered. Does ${object.name} have two @Table() decorators?`, + ); + } + const tableName = options.name || asSnakeCase(object.name); writeMetadata(object, { name: tableName, options }); diff --git a/server/src/sql-tools/from-code/register-function.ts b/server/src/sql-tools/from-code/register-function.ts index be71e0dfd7..3e1e7054be 100644 --- a/server/src/sql-tools/from-code/register-function.ts +++ b/server/src/sql-tools/from-code/register-function.ts @@ -50,12 +50,8 @@ const asFunctionExpression = (options: FunctionOptions) => { } if ('body' in options) { - sql.push( - // - `AS $$`, - ' ' + options.body.trim(), - `$$;`, - ); + const body = options.body; + sql.push(...(body.includes('\n') ? [`AS $$`, ' ' + body.trim(), `$$;`] : [`AS $$${body}$$;`])); } return sql.join('\n ').trim(); diff --git a/server/test/medium/globalSetup.ts b/server/test/medium/globalSetup.ts index 89b25da766..46eb1a733f 100644 --- a/server/test/medium/globalSetup.ts +++ b/server/test/medium/globalSetup.ts @@ -1,10 +1,11 @@ -import { FileMigrationProvider, Kysely, Migrator } from 'kysely'; -import { mkdir, readdir } from 'node:fs/promises'; -import { join } from 'node:path'; +import { Kysely } from 'kysely'; import { parse } from 'pg-connection-string'; +import { DB } from 'src/db'; +import { ConfigRepository } from 'src/repositories/config.repository'; +import { DatabaseRepository } from 'src/repositories/database.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; import { getKyselyConfig } from 'src/utils/database'; import { GenericContainer, Wait } from 'testcontainers'; -import { DataSource } from 'typeorm'; const globalSetup = async () => { const postgresContainer = await new GenericContainer('tensorchord/pgvecto-rs:pg14-v0.2.0') @@ -36,66 +37,23 @@ const globalSetup = async () => { const postgresPort = postgresContainer.getMappedPort(5432); const postgresUrl = `postgres://postgres:postgres@localhost:${postgresPort}/immich`; + const parsed = parse(postgresUrl); + process.env.IMMICH_TEST_POSTGRES_URL = postgresUrl; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const modules = import.meta.glob('/src/migrations/*.ts', { eager: true }); - - const config = { - type: 'postgres' as const, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - migrations: Object.values(modules).map((module) => Object.values(module)[0]), - migrationsRun: false, - synchronize: false, - connectTimeoutMS: 10_000, // 10 seconds - parseInt8: true, - url: postgresUrl, - }; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const dataSource = new DataSource(config); - await dataSource.initialize(); - await dataSource.runMigrations(); - await dataSource.destroy(); - - // for whatever reason, importing from test/utils causes vitest to crash - // eslint-disable-next-line unicorn/prefer-module - const migrationFolder = join(__dirname, '..', 'schema/migrations'); - // TODO remove after we have at least one kysely migration - await mkdir(migrationFolder, { recursive: true }); - - const parsed = parse(process.env.IMMICH_TEST_POSTGRES_URL!); - - const parsedOptions = { - ...parsed, - ssl: false, - host: parsed.host ?? undefined, - port: parsed.port ? Number(parsed.port) : undefined, - database: parsed.database ?? undefined, - }; - - const db = new Kysely(getKyselyConfig(parsedOptions)); - - // TODO just call `databaseRepository.migrate()` (probably have to wait until TypeOrm is gone) - const migrator = new Migrator({ - db, - migrationLockTableName: 'kysely_migrations_lock', - migrationTableName: 'kysely_migrations', - provider: new FileMigrationProvider({ - fs: { readdir }, - path: { join }, - migrationFolder, + const db = new Kysely( + getKyselyConfig({ + ...parsed, + ssl: false, + host: parsed.host ?? undefined, + port: parsed.port ? Number(parsed.port) : undefined, + database: parsed.database ?? undefined, }), - }); + ); - const { error } = await migrator.migrateToLatest(); - if (error) { - console.error('Unable to run kysely migrations', error); - throw error; - } + const configRepository = new ConfigRepository(); + const logger = new LoggingRepository(undefined, configRepository); + await new DatabaseRepository(db, logger, configRepository).runMigrations(); await db.destroy(); }; diff --git a/server/test/sql-tools/index-with-where.stub copy.ts b/server/test/sql-tools/index-with-expression.ts similarity index 100% rename from server/test/sql-tools/index-with-where.stub copy.ts rename to server/test/sql-tools/index-with-expression.ts From 160bb492a2ae1344718578281756f79bb33f0489 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 18 Apr 2025 12:19:11 -0400 Subject: [PATCH 005/356] fix: skip initial kysely migration for existing installs (#17690) * fix: skip initial kysely migration for existing installs * Update docs/src/pages/errors.md --------- Co-authored-by: Alex --- docs/src/pages/errors.md | 5 +++++ server/src/repositories/logging.repository.ts | 14 ++++++++++++-- .../1744910873969-InitialMigration.ts | 19 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 docs/src/pages/errors.md diff --git a/docs/src/pages/errors.md b/docs/src/pages/errors.md new file mode 100644 index 0000000000..e9bf09770c --- /dev/null +++ b/docs/src/pages/errors.md @@ -0,0 +1,5 @@ +# Errors + +## TypeORM Upgrade + +The upgrade to Immich `v2.x.x` has a required upgrade path to `v1.132.0+`. This means it is required to start up the application at least once on version `1.132.0` (or later). Doing so will complete database schema upgrades that are required for `v2.0.0`. After Immich has successfully booted on this version, shut the system down and try the `v2.x.x` upgrade again. diff --git a/server/src/repositories/logging.repository.ts b/server/src/repositories/logging.repository.ts index 3f809db41e..05d2d45f4d 100644 --- a/server/src/repositories/logging.repository.ts +++ b/server/src/repositories/logging.repository.ts @@ -74,11 +74,21 @@ export class MyConsoleLogger extends ConsoleLogger { export class LoggingRepository { private logger: MyConsoleLogger; - constructor(@Inject(ClsService) cls: ClsService | undefined, configRepository: ConfigRepository) { - const { noColor } = configRepository.getEnv(); + constructor( + @Inject(ClsService) cls: ClsService | undefined, + @Inject(ConfigRepository) configRepository: ConfigRepository | undefined, + ) { + let noColor = false; + if (configRepository) { + noColor = configRepository.getEnv().noColor; + } this.logger = new MyConsoleLogger(cls, { context: LoggingRepository.name, color: !noColor }); } + static create() { + return new LoggingRepository(undefined, undefined); + } + setAppName(name: string): void { appName = name.charAt(0).toUpperCase() + name.slice(1); } diff --git a/server/src/schema/migrations/1744910873969-InitialMigration.ts b/server/src/schema/migrations/1744910873969-InitialMigration.ts index e157607681..459534a26a 100644 --- a/server/src/schema/migrations/1744910873969-InitialMigration.ts +++ b/server/src/schema/migrations/1744910873969-InitialMigration.ts @@ -1,10 +1,29 @@ import { Kysely, sql } from 'kysely'; import { DatabaseExtension } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension; +const lastMigrationSql = sql<{ name: string }>`SELECT "name" FROM "migrations" ORDER BY "timestamp" DESC LIMIT 1;`; +const tableExists = sql<{ result: string | null }>`select to_regclass('migrations') as "result"`; +const logger = LoggingRepository.create(); export async function up(db: Kysely): Promise { + const { rows } = await tableExists.execute(db); + const hasTypeOrmMigrations = !!rows[0]?.result; + if (hasTypeOrmMigrations) { + const { + rows: [lastMigration], + } = await lastMigrationSql.execute(db); + if (lastMigration?.name !== 'AddMissingIndex1744910873956') { + throw new Error( + 'Invalid upgrade path. For more information, see https://immich.app/errors#typeorm-upgrade', + ); + } + logger.log('Database has up to date TypeORM migrations, skipping initial Kysely migration'); + return; + } + await sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`.execute(db); await sql`CREATE EXTENSION IF NOT EXISTS "unaccent";`.execute(db); await sql`CREATE EXTENSION IF NOT EXISTS "cube";`.execute(db); From bd2deda50c4481903a04d51a5dc7042fdeea095a Mon Sep 17 00:00:00 2001 From: Yaros Date: Fri, 18 Apr 2025 18:19:51 +0200 Subject: [PATCH 006/356] feat(mobile): search on places page (#17679) * feat: search on places page * chore: use searchfield on people page --- i18n/en.json | 1 + .../people/people_collection.page.dart | 43 ++----------- .../places/places_collection.page.dart | 62 ++++++++++++++----- 3 files changed, 51 insertions(+), 55 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index 3b52c2019e..c4b4746871 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -996,6 +996,7 @@ "filetype": "Filetype", "filter": "Filter", "filter_people": "Filter people", + "filter_places": "Filter places", "find_them_fast": "Find them fast by name with search", "fix_incorrect_match": "Fix incorrect match", "folder": "Folder", diff --git a/mobile/lib/pages/library/people/people_collection.page.dart b/mobile/lib/pages/library/people/people_collection.page.dart index 5f587c0c76..27daf0a887 100644 --- a/mobile/lib/pages/library/people/people_collection.page.dart +++ b/mobile/lib/pages/library/people/people_collection.page.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/widgets/common/search_field.dart'; import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; @RoutePage() @@ -42,47 +42,12 @@ class PeopleCollectionPage extends HookConsumerWidget { appBar: AppBar( automaticallyImplyLeading: search.value == null, title: search.value != null - ? TextField( + ? SearchField( focusNode: formFocus, onTapOutside: (_) => formFocus.unfocus(), onChanged: (value) => search.value = value, - decoration: InputDecoration( - contentPadding: const EdgeInsets.only(left: 24), - filled: true, - fillColor: context.primaryColor.withValues(alpha: 0.1), - hintStyle: context.textTheme.bodyLarge?.copyWith( - color: context.themeData.colorScheme.onSurfaceSecondary, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(25), - borderSide: BorderSide( - color: context.colorScheme.surfaceContainerHighest, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(25), - borderSide: BorderSide( - color: context.colorScheme.surfaceContainerHighest, - ), - ), - disabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(25), - borderSide: BorderSide( - color: context.colorScheme.surfaceContainerHighest, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(25), - borderSide: BorderSide( - color: context.colorScheme.primary.withAlpha(150), - ), - ), - prefixIcon: Icon( - Icons.search_rounded, - color: context.colorScheme.primary, - ), - hintText: 'filter_people'.tr(), - ), + filled: true, + hintText: 'filter_people'.tr(), autofocus: true, ) : Text('people'.tr()), diff --git a/mobile/lib/pages/library/places/places_collection.page.dart b/mobile/lib/pages/library/places/places_collection.page.dart index d4da3ff37e..f9a2d4292c 100644 --- a/mobile/lib/pages/library/places/places_collection.page.dart +++ b/mobile/lib/pages/library/places/places_collection.page.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; @@ -12,6 +13,7 @@ import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/widgets/common/search_field.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -21,34 +23,62 @@ class PlacesCollectionPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final places = ref.watch(getAllPlacesProvider); + final formFocus = useFocusNode(); + final ValueNotifier search = useState(null); return Scaffold( appBar: AppBar( - title: Text('places'.tr()), + automaticallyImplyLeading: search.value == null, + title: search.value != null + ? SearchField( + autofocus: true, + filled: true, + focusNode: formFocus, + onChanged: (value) => search.value = value, + onTapOutside: (_) => formFocus.unfocus(), + hintText: 'filter_places'.tr(), + ) + : Text('places'.tr()), + actions: [ + IconButton( + icon: Icon(search.value != null ? Icons.close : Icons.search), + onPressed: () { + search.value = search.value == null ? '' : null; + }, + ), + ], ), body: ListView( shrinkWrap: true, children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - height: 200, - width: context.width, - child: MapThumbnail( - onTap: (_, __) => context.pushRoute(const MapRoute()), - zoom: 8, - centre: const LatLng( - 21.44950, - -157.91959, + if (search.value == null) + Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + height: 200, + width: context.width, + child: MapThumbnail( + onTap: (_, __) => context.pushRoute(const MapRoute()), + zoom: 8, + centre: const LatLng( + 21.44950, + -157.91959, + ), + showAttribution: false, + themeMode: + context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, ), - showAttribution: false, - themeMode: - context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, ), ), - ), places.when( data: (places) { + if (search.value != null) { + places = places.where((place) { + return place.label + .toLowerCase() + .contains(search.value!.toLowerCase()); + }).toList(); + } return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), From 0e6ac876450c6e6ba1072cf644abf395912e0fef Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 18 Apr 2025 14:01:16 -0500 Subject: [PATCH 007/356] feat(mobile): assets + exif stream sync placeholder (#17677) * feat(mobile): assets + exif stream sync placeholder * feat(mobile): assets + exif stream sync placeholder * refactor * fix: test * fix:test * refactor(mobile): sync stream service (#17687) * refactor: sync stream to use callbacks * pr feedback * pr feedback * pr feedback * fix: test --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex Tran --------- Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../domain/interfaces/sync_api.interface.dart | 8 +- .../interfaces/sync_stream.interface.dart | 16 +- .../domain/services/sync_stream.service.dart | 228 +++------ mobile/lib/domain/utils/background_sync.dart | 28 +- .../repositories/sync_api.repository.dart | 75 ++- .../repositories/sync_stream.repository.dart | 62 ++- mobile/lib/widgets/common/immich_app_bar.dart | 7 + mobile/test/api.mocks.dart | 2 + .../services/sync_stream_service_test.dart | 475 +++++------------- mobile/test/fixtures/sync_stream.stub.dart | 74 +-- .../sync_api_repository_test.dart | 299 +++++++++++ 11 files changed, 666 insertions(+), 608 deletions(-) create mode 100644 mobile/test/infrastructure/repositories/sync_api_repository_test.dart diff --git a/mobile/lib/domain/interfaces/sync_api.interface.dart b/mobile/lib/domain/interfaces/sync_api.interface.dart index 44e22c5894..57abed2e7f 100644 --- a/mobile/lib/domain/interfaces/sync_api.interface.dart +++ b/mobile/lib/domain/interfaces/sync_api.interface.dart @@ -1,8 +1,12 @@ +import 'package:http/http.dart' as http; import 'package:immich_mobile/domain/models/sync_event.model.dart'; -import 'package:openapi/api.dart'; abstract interface class ISyncApiRepository { Future ack(List data); - Stream> getSyncEvents(List type); + Future streamChanges( + Function(List, Function() abort) onData, { + int batchSize, + http.Client? httpClient, + }); } diff --git a/mobile/lib/domain/interfaces/sync_stream.interface.dart b/mobile/lib/domain/interfaces/sync_stream.interface.dart index f9c52d7ee0..5f61d6b52f 100644 --- a/mobile/lib/domain/interfaces/sync_stream.interface.dart +++ b/mobile/lib/domain/interfaces/sync_stream.interface.dart @@ -2,9 +2,17 @@ import 'package:immich_mobile/domain/interfaces/db.interface.dart'; import 'package:openapi/api.dart'; abstract interface class ISyncStreamRepository implements IDatabaseRepository { - Future updateUsersV1(Iterable data); - Future deleteUsersV1(Iterable data); + Future updateUsersV1(Iterable data); + Future deleteUsersV1(Iterable data); - Future updatePartnerV1(Iterable data); - Future deletePartnerV1(Iterable data); + Future updatePartnerV1(Iterable data); + Future deletePartnerV1(Iterable data); + + Future updateAssetsV1(Iterable data); + Future deleteAssetsV1(Iterable data); + Future updateAssetsExifV1(Iterable data); + + Future updatePartnerAssetsV1(Iterable data); + Future deletePartnerAssetsV1(Iterable data); + Future updatePartnerAssetsExifV1(Iterable data); } diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 8d7d87e35e..ac63734b07 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -2,25 +2,11 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart'; +import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; -import 'package:worker_manager/worker_manager.dart'; - -const _kSyncTypeOrder = [ - SyncEntityType.userDeleteV1, - SyncEntityType.userV1, - SyncEntityType.partnerDeleteV1, - SyncEntityType.partnerV1, - SyncEntityType.assetDeleteV1, - SyncEntityType.assetV1, - SyncEntityType.assetExifV1, - SyncEntityType.partnerAssetDeleteV1, - SyncEntityType.partnerAssetV1, - SyncEntityType.partnerAssetExifV1, -]; class SyncStreamService { final Logger _logger = Logger('SyncStreamService'); @@ -37,164 +23,70 @@ class SyncStreamService { _syncStreamRepository = syncStreamRepository, _cancelChecker = cancelChecker; - Future _handleSyncData( + bool get isCancelled => _cancelChecker?.call() ?? false; + + Future sync() => _syncApiRepository.streamChanges(_handleEvents); + + Future _handleEvents(List events, Function() abort) async { + List items = []; + for (final event in events) { + if (isCancelled) { + _logger.warning("Sync stream cancelled"); + abort(); + return; + } + + if (event.type != items.firstOrNull?.type) { + await _processBatch(items); + } + + items.add(event); + } + + await _processBatch(items); + } + + Future _processBatch(List batch) async { + if (batch.isEmpty) { + return; + } + + final type = batch.first.type; + await _handleSyncData(type, batch.map((e) => e.data)); + await _syncApiRepository.ack([batch.last.ack]); + batch.clear(); + } + + Future _handleSyncData( SyncEntityType type, // ignore: avoid-dynamic Iterable data, ) async { - if (data.isEmpty) { - _logger.warning("Received empty sync data for $type"); - return false; - } - _logger.fine("Processing sync data for $type of length ${data.length}"); - - try { - if (type == SyncEntityType.partnerV1) { - return await _syncStreamRepository.updatePartnerV1(data.cast()); - } - - if (type == SyncEntityType.partnerDeleteV1) { - return await _syncStreamRepository.deletePartnerV1(data.cast()); - } - - if (type == SyncEntityType.userV1) { - return await _syncStreamRepository.updateUsersV1(data.cast()); - } - - if (type == SyncEntityType.userDeleteV1) { - return await _syncStreamRepository.deleteUsersV1(data.cast()); - } - } catch (error, stack) { - _logger.severe("Error processing sync data for $type", error, stack); - return false; + // ignore: prefer-switch-expression + switch (type) { + case SyncEntityType.userV1: + return _syncStreamRepository.updateUsersV1(data.cast()); + case SyncEntityType.userDeleteV1: + return _syncStreamRepository.deleteUsersV1(data.cast()); + case SyncEntityType.partnerV1: + return _syncStreamRepository.updatePartnerV1(data.cast()); + case SyncEntityType.partnerDeleteV1: + return _syncStreamRepository.deletePartnerV1(data.cast()); + case SyncEntityType.assetV1: + return _syncStreamRepository.updateAssetsV1(data.cast()); + case SyncEntityType.assetDeleteV1: + return _syncStreamRepository.deleteAssetsV1(data.cast()); + case SyncEntityType.assetExifV1: + return _syncStreamRepository.updateAssetsExifV1(data.cast()); + case SyncEntityType.partnerAssetV1: + return _syncStreamRepository.updatePartnerAssetsV1(data.cast()); + case SyncEntityType.partnerAssetDeleteV1: + return _syncStreamRepository.deletePartnerAssetsV1(data.cast()); + case SyncEntityType.partnerAssetExifV1: + return _syncStreamRepository.updatePartnerAssetsExifV1(data.cast()); + default: + _logger.warning("Unknown sync data type: $type"); } - - _logger.warning("Unknown sync data type: $type"); - return false; } - - Future _syncEvent(List types) { - _logger.info("Syncing Events: $types"); - final streamCompleter = Completer(); - bool shouldComplete = false; - // the onDone callback might fire before the events are processed - // the following flag ensures that the onDone callback is not called - // before the events are processed and also that events are processed sequentially - Completer? mutex; - StreamSubscription? subscription; - try { - subscription = _syncApiRepository.getSyncEvents(types).listen( - (events) async { - if (events.isEmpty) { - _logger.warning("Received empty sync events"); - return; - } - - // If previous events are still being processed, wait for them to finish - if (mutex != null) { - await mutex!.future; - } - - if (_cancelChecker?.call() ?? false) { - _logger.info("Sync cancelled, stopping stream"); - subscription?.cancel(); - if (!streamCompleter.isCompleted) { - streamCompleter.completeError( - CanceledError(), - StackTrace.current, - ); - } - return; - } - - // Take control of the mutex and process the events - mutex = Completer(); - - try { - final eventsMap = events.groupListsBy((event) => event.type); - final Map acks = {}; - - for (final type in _kSyncTypeOrder) { - final data = eventsMap[type]; - if (data == null) { - continue; - } - - if (_cancelChecker?.call() ?? false) { - _logger.info("Sync cancelled, stopping stream"); - mutex?.complete(); - mutex = null; - if (!streamCompleter.isCompleted) { - streamCompleter.completeError( - CanceledError(), - StackTrace.current, - ); - } - - return; - } - - if (data.isEmpty) { - _logger.warning("Received empty sync events for $type"); - continue; - } - - if (await _handleSyncData(type, data.map((e) => e.data))) { - // ignore: avoid-unsafe-collection-methods - acks[type] = data.last.ack; - } else { - _logger.warning("Failed to handle sync events for $type"); - } - } - - if (acks.isNotEmpty) { - await _syncApiRepository.ack(acks.values.toList()); - } - _logger.info("$types events processed"); - } catch (error, stack) { - _logger.warning("Error handling sync events", error, stack); - } finally { - mutex?.complete(); - mutex = null; - } - - if (shouldComplete) { - _logger.info("Sync done, completing stream"); - if (!streamCompleter.isCompleted) streamCompleter.complete(); - } - }, - onError: (error, stack) { - _logger.warning("Error in sync stream for $types", error, stack); - // Do not proceed if the stream errors - if (!streamCompleter.isCompleted) { - // ignore: avoid-missing-completer-stack-trace - streamCompleter.completeError(error, stack); - } - }, - onDone: () { - _logger.info("$types stream done"); - if (mutex == null && !streamCompleter.isCompleted) { - streamCompleter.complete(); - } else { - // Marks the stream as done but does not complete the completer - // until the events are processed - shouldComplete = true; - } - }, - ); - } catch (error, stack) { - _logger.severe("Error starting sync stream", error, stack); - if (!streamCompleter.isCompleted) { - streamCompleter.completeError(error, stack); - } - } - return streamCompleter.future.whenComplete(() { - _logger.info("Sync stream completed"); - return subscription?.cancel(); - }); - } - - Future syncUsers() => - _syncEvent([SyncRequestType.usersV1, SyncRequestType.partnersV1]); } diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart index 0bd456f0bb..f63dc81ba9 100644 --- a/mobile/lib/domain/utils/background_sync.dart +++ b/mobile/lib/domain/utils/background_sync.dart @@ -7,31 +7,33 @@ import 'package:immich_mobile/utils/isolate.dart'; import 'package:worker_manager/worker_manager.dart'; class BackgroundSyncManager { - Cancelable? _userSyncTask; + Cancelable? _syncTask; BackgroundSyncManager(); Future cancel() { final futures = []; - if (_userSyncTask != null) { - futures.add(_userSyncTask!.future); + + if (_syncTask != null) { + futures.add(_syncTask!.future); } - _userSyncTask?.cancel(); - _userSyncTask = null; + _syncTask?.cancel(); + _syncTask = null; + return Future.wait(futures); } - Future syncUsers() { - if (_userSyncTask != null) { - return _userSyncTask!.future; + Future sync() { + if (_syncTask != null) { + return _syncTask!.future; } - _userSyncTask = runInIsolateGentle( - computation: (ref) => ref.read(syncStreamServiceProvider).syncUsers(), + _syncTask = runInIsolateGentle( + computation: (ref) => ref.read(syncStreamServiceProvider).sync(), ); - _userSyncTask!.whenComplete(() { - _userSyncTask = null; + _syncTask!.whenComplete(() { + _syncTask = null; }); - return _userSyncTask!.future; + return _syncTask!.future; } } diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index a26b867df6..dd1ea208ba 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -12,22 +12,22 @@ import 'package:openapi/api.dart'; class SyncApiRepository implements ISyncApiRepository { final Logger _logger = Logger('SyncApiRepository'); final ApiService _api; - final int _batchSize; - SyncApiRepository(this._api, {int batchSize = kSyncEventBatchSize}) - : _batchSize = batchSize; - - @override - Stream> getSyncEvents(List type) { - return _getSyncStream(SyncStreamDto(types: type)); - } + SyncApiRepository(this._api); @override Future ack(List data) { return _api.syncApi.sendSyncAck(SyncAckSetDto(acks: data)); } - Stream> _getSyncStream(SyncStreamDto dto) async* { - final client = http.Client(); + @override + Future streamChanges( + Function(List, Function() abort) onData, { + int batchSize = kSyncEventBatchSize, + http.Client? httpClient, + }) async { + // ignore: avoid-unused-assignment + final stopwatch = Stopwatch()..start(); + final client = httpClient ?? http.Client(); final endpoint = "${_api.apiClient.basePath}/sync/stream"; final headers = { @@ -35,20 +35,38 @@ class SyncApiRepository implements ISyncApiRepository { 'Accept': 'application/jsonlines+json', }; - final queryParams = []; final headerParams = {}; - await _api.applyToParams(queryParams, headerParams); + await _api.applyToParams([], headerParams); headers.addAll(headerParams); final request = http.Request('POST', Uri.parse(endpoint)); request.headers.addAll(headers); - request.body = jsonEncode(dto.toJson()); + request.body = jsonEncode( + SyncStreamDto( + types: [ + SyncRequestType.usersV1, + SyncRequestType.partnersV1, + SyncRequestType.assetsV1, + SyncRequestType.partnerAssetsV1, + SyncRequestType.assetExifsV1, + SyncRequestType.partnerAssetExifsV1, + ], + ).toJson(), + ); String previousChunk = ''; List lines = []; + bool shouldAbort = false; + + void abort() { + _logger.warning("Abort requested, stopping sync stream"); + shouldAbort = true; + } + try { - final response = await client.send(request); + final response = + await client.send(request).timeout(const Duration(seconds: 20)); if (response.statusCode != 200) { final errorBody = await response.stream.bytesToString(); @@ -59,27 +77,38 @@ class SyncApiRepository implements ISyncApiRepository { } await for (final chunk in response.stream.transform(utf8.decoder)) { + if (shouldAbort) { + break; + } + previousChunk += chunk; final parts = previousChunk.toString().split('\n'); previousChunk = parts.removeLast(); lines.addAll(parts); - if (lines.length < _batchSize) { + if (lines.length < batchSize) { continue; } - yield _parseSyncResponse(lines); + await onData(_parseLines(lines), abort); lines.clear(); } - } finally { - if (lines.isNotEmpty) { - yield _parseSyncResponse(lines); + + if (lines.isNotEmpty && !shouldAbort) { + await onData(_parseLines(lines), abort); } + } catch (error, stack) { + _logger.severe("error processing stream", error, stack); + return Future.error(error, stack); + } finally { client.close(); } + stopwatch.stop(); + _logger + .info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms"); } - List _parseSyncResponse(List lines) { + List _parseLines(List lines) { final List data = []; for (final line in lines) { @@ -110,4 +139,10 @@ const _kResponseMap = { SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson, SyncEntityType.partnerV1: SyncPartnerV1.fromJson, SyncEntityType.partnerDeleteV1: SyncPartnerDeleteV1.fromJson, + SyncEntityType.assetV1: SyncAssetV1.fromJson, + SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson, + SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson, + SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson, + SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson, + SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson, }; diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index a947a9a66b..5ad9a369df 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; @@ -15,7 +16,7 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository DriftSyncStreamRepository(super.db) : _db = db; @override - Future deleteUsersV1(Iterable data) async { + Future deleteUsersV1(Iterable data) async { try { await _db.batch((batch) { for (final user in data) { @@ -25,15 +26,14 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository ); } }); - return true; - } catch (e, s) { - _logger.severe('Error while processing SyncUserDeleteV1', e, s); - return false; + } catch (error, stack) { + _logger.severe('Error while processing SyncUserDeleteV1', error, stack); + rethrow; } } @override - Future updateUsersV1(Iterable data) async { + Future updateUsersV1(Iterable data) async { try { await _db.batch((batch) { for (final user in data) { @@ -49,15 +49,14 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository ); } }); - return true; - } catch (e, s) { - _logger.severe('Error while processing SyncUserV1', e, s); - return false; + } catch (error, stack) { + _logger.severe('Error while processing SyncUserV1', error, stack); + rethrow; } } @override - Future deletePartnerV1(Iterable data) async { + Future deletePartnerV1(Iterable data) async { try { await _db.batch((batch) { for (final partner in data) { @@ -70,15 +69,14 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository ); } }); - return true; } catch (e, s) { _logger.severe('Error while processing SyncPartnerDeleteV1', e, s); - return false; + rethrow; } } @override - Future updatePartnerV1(Iterable data) async { + Future updatePartnerV1(Iterable data) async { try { await _db.batch((batch) { for (final partner in data) { @@ -95,10 +93,42 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository ); } }); - return true; } catch (e, s) { _logger.severe('Error while processing SyncPartnerV1', e, s); - return false; + rethrow; } } + + // Assets + @override + Future updateAssetsV1(Iterable data) async { + debugPrint("updateAssetsV1 - ${data.length}"); + } + + @override + Future deleteAssetsV1(Iterable data) async { + debugPrint("deleteAssetsV1 - ${data.length}"); + } + + // Partner Assets + @override + Future updatePartnerAssetsV1(Iterable data) async { + debugPrint("updatePartnerAssetsV1 - ${data.length}"); + } + + @override + Future deletePartnerAssetsV1(Iterable data) async { + debugPrint("deletePartnerAssetsV1 - ${data.length}"); + } + + // EXIF + @override + Future updateAssetsExifV1(Iterable data) async { + debugPrint("updateAssetsExifV1 - ${data.length}"); + } + + @override + Future updatePartnerAssetsExifV1(Iterable data) async { + debugPrint("updatePartnerAssetsExifV1 - ${data.length}"); + } } diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index 51b4faa014..4f95e657d9 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -1,11 +1,13 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -178,6 +180,11 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { child: action, ), ), + if (kDebugMode) + IconButton( + onPressed: () => ref.read(backgroundSyncProvider).sync(), + icon: const Icon(Icons.sync), + ), if (showUploadButton) Padding( padding: const EdgeInsets.only(right: 20), diff --git a/mobile/test/api.mocks.dart b/mobile/test/api.mocks.dart index d502ea0675..b0a4e9b8fd 100644 --- a/mobile/test/api.mocks.dart +++ b/mobile/test/api.mocks.dart @@ -2,3 +2,5 @@ import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; class MockAssetsApi extends Mock implements AssetsApi {} + +class MockSyncApi extends Mock implements SyncApi {} diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index e1d8e6987f..b78a44342b 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -1,4 +1,4 @@ -// ignore_for_file: avoid-unnecessary-futures, avoid-async-call-in-sync-function +// ignore_for_file: avoid-declaring-call-method, avoid-unnecessary-futures import 'dart:async'; @@ -8,16 +8,22 @@ import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/services/sync_stream.service.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:openapi/api.dart'; -import 'package:worker_manager/worker_manager.dart'; import '../../fixtures/sync_stream.stub.dart'; import '../../infrastructure/repository.mock.dart'; +class _AbortCallbackWrapper { + const _AbortCallbackWrapper(); + + bool call() => false; +} + +class _MockAbortCallbackWrapper extends Mock implements _AbortCallbackWrapper {} + class _CancellationWrapper { const _CancellationWrapper(); - bool isCancelled() => false; + bool call() => false; } class _MockCancellationWrapper extends Mock implements _CancellationWrapper {} @@ -26,35 +32,26 @@ void main() { late SyncStreamService sut; late ISyncStreamRepository mockSyncStreamRepo; late ISyncApiRepository mockSyncApiRepo; - late StreamController> streamController; + late Function(List, Function()) handleEventsCallback; + late _MockAbortCallbackWrapper mockAbortCallbackWrapper; successHandler(Invocation _) async => true; - failureHandler(Invocation _) async => false; setUp(() { mockSyncStreamRepo = MockSyncStreamRepository(); mockSyncApiRepo = MockSyncApiRepository(); - streamController = StreamController>.broadcast(); + mockAbortCallbackWrapper = _MockAbortCallbackWrapper(); - sut = SyncStreamService( - syncApiRepository: mockSyncApiRepo, - syncStreamRepository: mockSyncStreamRepo, - ); + when(() => mockAbortCallbackWrapper()).thenReturn(false); - // Default stream setup - emits one batch and closes - when(() => mockSyncApiRepo.getSyncEvents(any())) - .thenAnswer((_) => streamController.stream); + when(() => mockSyncApiRepo.streamChanges(any())) + .thenAnswer((invocation) async { + // ignore: avoid-unsafe-collection-methods + handleEventsCallback = invocation.positionalArguments.first; + }); - // Default ack setup when(() => mockSyncApiRepo.ack(any())).thenAnswer((_) async => {}); - // Register fallbacks for mocktail verification - registerFallbackValue([]); - registerFallbackValue([]); - registerFallbackValue([]); - registerFallbackValue([]); - - // Default successful repository calls when(() => mockSyncStreamRepo.updateUsersV1(any())) .thenAnswer(successHandler); when(() => mockSyncStreamRepo.deleteUsersV1(any())) @@ -63,381 +60,163 @@ void main() { .thenAnswer(successHandler); when(() => mockSyncStreamRepo.deletePartnerV1(any())) .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updateAssetsV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deleteAssetsV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updateAssetsExifV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updatePartnerAssetsV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deletePartnerAssetsV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updatePartnerAssetsExifV1(any())) + .thenAnswer(successHandler); + + sut = SyncStreamService( + syncApiRepository: mockSyncApiRepo, + syncStreamRepository: mockSyncStreamRepo, + ); }); - tearDown(() async { - if (!streamController.isClosed) { - await streamController.close(); - } - }); - - // Helper to trigger sync and add events to the stream - Future triggerSyncAndEmit(List events) async { - final future = sut.syncUsers(); // Start listening - await Future.delayed(Duration.zero); // Allow listener to attach - if (!streamController.isClosed) { - streamController.add(events); - await streamController.close(); // Close after emitting - } - await future; // Wait for processing to complete + Future simulateEvents(List events) async { + await sut.sync(); + await handleEventsCallback(events, mockAbortCallbackWrapper.call); } - group("SyncStreamService", () { + group("SyncStreamService - _handleEvents", () { test( - "completes successfully when stream emits data and handlers succeed", + "processes events and acks successfully when handlers succeed", () async { final events = [ - ...SyncStreamStub.userEvents, - ...SyncStreamStub.partnerEvents, + SyncStreamStub.userDeleteV1, + SyncStreamStub.userV1Admin, + SyncStreamStub.userV1User, + SyncStreamStub.partnerDeleteV1, + SyncStreamStub.partnerV1, ]; - final future = triggerSyncAndEmit(events); - await expectLater(future, completes); - // Verify ack includes last ack from each successfully handled type - verify( - () => - mockSyncApiRepo.ack(any(that: containsAll(["5", "2", "4", "3"]))), - ).called(1); + + await simulateEvents(events); + + verifyInOrder([ + () => mockSyncStreamRepo.deleteUsersV1(any()), + () => mockSyncApiRepo.ack(["2"]), + () => mockSyncStreamRepo.updateUsersV1(any()), + () => mockSyncApiRepo.ack(["5"]), + () => mockSyncStreamRepo.deletePartnerV1(any()), + () => mockSyncApiRepo.ack(["4"]), + () => mockSyncStreamRepo.updatePartnerV1(any()), + () => mockSyncApiRepo.ack(["3"]), + ]); + verifyNever(() => mockAbortCallbackWrapper()); }, ); - test("completes successfully when stream emits an error", () async { - when(() => mockSyncApiRepo.getSyncEvents(any())) - .thenAnswer((_) => Stream.error(Exception("Stream Error"))); - // Should complete gracefully without throwing - await expectLater(sut.syncUsers(), throwsException); - verifyNever(() => mockSyncApiRepo.ack(any())); // No ack on stream error - }); - - test("throws when initial getSyncEvents call fails", () async { - final apiException = Exception("API Error"); - when(() => mockSyncApiRepo.getSyncEvents(any())).thenThrow(apiException); - // Should rethrow the exception from the initial call - await expectLater(sut.syncUsers(), throwsA(apiException)); - verifyNever(() => mockSyncApiRepo.ack(any())); - }); - - test( - "completes successfully when a repository handler throws an exception", - () async { - when(() => mockSyncStreamRepo.updateUsersV1(any())) - .thenThrow(Exception("Repo Error")); - final events = [ - ...SyncStreamStub.userEvents, - ...SyncStreamStub.partnerEvents, - ]; - // Should complete, but ack only for the successful types - await triggerSyncAndEmit(events); - // Only partner delete was successful by default setup - verify(() => mockSyncApiRepo.ack(["2", "4", "3"])).called(1); - }, - ); - - test( - "completes successfully but sends no ack when all handlers fail", - () async { - when(() => mockSyncStreamRepo.updateUsersV1(any())) - .thenAnswer(failureHandler); - when(() => mockSyncStreamRepo.deleteUsersV1(any())) - .thenAnswer(failureHandler); - when(() => mockSyncStreamRepo.updatePartnerV1(any())) - .thenAnswer(failureHandler); - when(() => mockSyncStreamRepo.deletePartnerV1(any())) - .thenAnswer(failureHandler); - - final events = [ - ...SyncStreamStub.userEvents, - ...SyncStreamStub.partnerEvents, - ]; - await triggerSyncAndEmit(events); - verifyNever(() => mockSyncApiRepo.ack(any())); - }, - ); - - test("sends ack only for types where handler returns true", () async { - // Mock specific handlers: user update fails, user delete succeeds - when(() => mockSyncStreamRepo.updateUsersV1(any())) - .thenAnswer(failureHandler); - when(() => mockSyncStreamRepo.deleteUsersV1(any())) - .thenAnswer(successHandler); - // partner update fails, partner delete succeeds - when(() => mockSyncStreamRepo.updatePartnerV1(any())) - .thenAnswer(failureHandler); - + test("processes final batch correctly", () async { final events = [ - ...SyncStreamStub.userEvents, - ...SyncStreamStub.partnerEvents, + SyncStreamStub.userDeleteV1, + SyncStreamStub.userV1Admin, ]; - await triggerSyncAndEmit(events); - // Expect ack only for userDeleteV1 (ack: "2") and partnerDeleteV1 (ack: "4") - verify(() => mockSyncApiRepo.ack(any(that: containsAll(["2", "4"])))) - .called(1); + await simulateEvents(events); + + verifyInOrder([ + () => mockSyncStreamRepo.deleteUsersV1(any()), + () => mockSyncApiRepo.ack(["2"]), + () => mockSyncStreamRepo.updateUsersV1(any()), + () => mockSyncApiRepo.ack(["1"]), + ]); + verifyNever(() => mockAbortCallbackWrapper()); }); - test("does not process or ack when stream emits an empty list", () async { - final future = sut.syncUsers(); - streamController.add([]); // Emit empty list - await streamController.close(); - await future; // Wait for completion + test("does not process or ack when event list is empty", () async { + await simulateEvents([]); verifyNever(() => mockSyncStreamRepo.updateUsersV1(any())); verifyNever(() => mockSyncStreamRepo.deleteUsersV1(any())); verifyNever(() => mockSyncStreamRepo.updatePartnerV1(any())); verifyNever(() => mockSyncStreamRepo.deletePartnerV1(any())); + verifyNever(() => mockAbortCallbackWrapper()); verifyNever(() => mockSyncApiRepo.ack(any())); }); - test("processes multiple batches sequentially using mutex", () async { - final completer1 = Completer(); - final completer2 = Completer(); - int callOrder = 0; - int handler1StartOrder = -1; - int handler2StartOrder = -1; - int handler1Calls = 0; - int handler2Calls = 0; + test("aborts and stops processing if cancelled during iteration", () async { + final cancellationChecker = _MockCancellationWrapper(); + when(() => cancellationChecker()).thenReturn(false); - when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer((_) async { - handler1Calls++; - handler1StartOrder = ++callOrder; - await completer1.future; - return true; - }); - when(() => mockSyncStreamRepo.updatePartnerV1(any())) - .thenAnswer((_) async { - handler2Calls++; - handler2StartOrder = ++callOrder; - await completer2.future; - return true; + sut = SyncStreamService( + syncApiRepository: mockSyncApiRepo, + syncStreamRepository: mockSyncStreamRepo, + cancelChecker: cancellationChecker.call, + ); + await sut.sync(); + + final events = [ + SyncStreamStub.userDeleteV1, + SyncStreamStub.userV1Admin, + SyncStreamStub.partnerDeleteV1, + ]; + + when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer((_) async { + when(() => cancellationChecker()).thenReturn(true); }); - final batch1 = SyncStreamStub.userEvents; - final batch2 = SyncStreamStub.partnerEvents; + await handleEventsCallback(events, mockAbortCallbackWrapper.call); - final syncFuture = sut.syncUsers(); - await pumpEventQueue(); + verify(() => mockSyncStreamRepo.deleteUsersV1(any())).called(1); + verifyNever(() => mockSyncStreamRepo.updateUsersV1(any())); + verifyNever(() => mockSyncStreamRepo.deletePartnerV1(any())); - streamController.add(batch1); - await pumpEventQueue(); - // Small delay to ensure the first handler starts - await Future.delayed(const Duration(milliseconds: 20)); + verify(() => mockAbortCallbackWrapper()).called(1); - expect(handler1StartOrder, 1, reason: "Handler 1 should start first"); - expect(handler1Calls, 1); - - streamController.add(batch2); - await pumpEventQueue(); - // Small delay - await Future.delayed(const Duration(milliseconds: 20)); - - expect(handler2StartOrder, -1, reason: "Handler 2 should wait"); - expect(handler2Calls, 0); - - completer1.complete(); - await pumpEventQueue(times: 40); - // Small delay to ensure the second handler starts - await Future.delayed(const Duration(milliseconds: 20)); - - expect(handler2StartOrder, 2, reason: "Handler 2 should start after H1"); - expect(handler2Calls, 1); - - completer2.complete(); - await pumpEventQueue(times: 40); - // Small delay before closing the stream - await Future.delayed(const Duration(milliseconds: 20)); - - if (!streamController.isClosed) { - await streamController.close(); - } - await pumpEventQueue(times: 40); - // Small delay to ensure the sync completes - await Future.delayed(const Duration(milliseconds: 20)); - - await syncFuture; - - verify(() => mockSyncStreamRepo.updateUsersV1(any())).called(1); - verify(() => mockSyncStreamRepo.updatePartnerV1(any())).called(1); - verify(() => mockSyncApiRepo.ack(any())).called(2); + verify(() => mockSyncApiRepo.ack(["2"])).called(1); }); test( - "stops processing and ack when cancel checker is completed", + "aborts and stops processing if cancelled before processing batch", () async { final cancellationChecker = _MockCancellationWrapper(); - when(() => cancellationChecker.isCancelled()).thenAnswer((_) => false); + when(() => cancellationChecker()).thenReturn(false); + + final processingCompleter = Completer(); + bool handler1Started = false; + when(() => mockSyncStreamRepo.deleteUsersV1(any())) + .thenAnswer((_) async { + handler1Started = true; + return processingCompleter.future; + }); sut = SyncStreamService( syncApiRepository: mockSyncApiRepo, syncStreamRepository: mockSyncStreamRepo, - cancelChecker: cancellationChecker.isCancelled, + cancelChecker: cancellationChecker.call, ); - final processingCompleter = Completer(); - bool handlerStarted = false; + await sut.sync(); - // Make handler wait so we can cancel it mid-flight - when(() => mockSyncStreamRepo.deleteUsersV1(any())) - .thenAnswer((_) async { - handlerStarted = true; - await processingCompleter - .future; // Wait indefinitely until test completes it - return true; - }); - - final syncFuture = sut.syncUsers(); - await pumpEventQueue(times: 30); - - streamController.add(SyncStreamStub.userEvents); - // Ensure processing starts - await Future.delayed(const Duration(milliseconds: 10)); - - expect(handlerStarted, isTrue, reason: "Handler should have started"); - - when(() => cancellationChecker.isCancelled()).thenAnswer((_) => true); - - // Allow cancellation logic to propagate - await Future.delayed(const Duration(milliseconds: 10)); - - // Complete the handler's completer after cancellation signal - // to ensure the cancellation logic itself isn't blocked by the handler. - processingCompleter.complete(); - - await expectLater(syncFuture, throwsA(isA())); - - // Verify that ack was NOT called because processing was cancelled - verifyNever(() => mockSyncApiRepo.ack(any())); - }, - ); - - test("completes successfully when ack call throws an exception", () async { - when(() => mockSyncApiRepo.ack(any())).thenThrow(Exception("Ack Error")); - final events = [ - ...SyncStreamStub.userEvents, - ...SyncStreamStub.partnerEvents, - ]; - - // Should still complete even if ack fails - await triggerSyncAndEmit(events); - verify(() => mockSyncApiRepo.ack(any())) - .called(1); // Verify ack was attempted - }); - - test("waits for processing to finish if onDone called early", () async { - final processingCompleter = Completer(); - bool handlerFinished = false; - - when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer((_) async { - await processingCompleter.future; // Wait inside handler - handlerFinished = true; - return true; - }); - - final syncFuture = sut.syncUsers(); - // Allow listener to attach - // This is necessary to ensure the stream is ready to receive events - await Future.delayed(Duration.zero); - - streamController.add(SyncStreamStub.userEvents); // Emit batch - await Future.delayed( - const Duration(milliseconds: 10), - ); // Ensure processing starts - - await streamController - .close(); // Close stream (triggers onDone internally) - await Future.delayed( - const Duration(milliseconds: 10), - ); // Give onDone a chance to fire - - // At this point, onDone was called, but processing is blocked - expect(handlerFinished, isFalse); - - processingCompleter.complete(); // Allow processing to finish - await syncFuture; // Now the main future should complete - - expect(handlerFinished, isTrue); - verify(() => mockSyncApiRepo.ack(any())).called(1); - }); - - test("processes events in the defined _kSyncTypeOrder", () async { - final future = sut.syncUsers(); - await pumpEventQueue(); - if (!streamController.isClosed) { final events = [ - SyncEvent( - type: SyncEntityType.partnerV1, - data: SyncStreamStub.partnerV1, - ack: "1", - ), // Should be processed last - SyncEvent( - type: SyncEntityType.userV1, - data: SyncStreamStub.userV1Admin, - ack: "2", - ), // Should be processed second - SyncEvent( - type: SyncEntityType.partnerDeleteV1, - data: SyncStreamStub.partnerDeleteV1, - ack: "3", - ), // Should be processed third - SyncEvent( - type: SyncEntityType.userDeleteV1, - data: SyncStreamStub.userDeleteV1, - ack: "4", - ), // Should be processed first + SyncStreamStub.userDeleteV1, + SyncStreamStub.userV1Admin, + SyncStreamStub.partnerDeleteV1, ]; - streamController.add(events); - await streamController.close(); - } - await future; + final processingFuture = + handleEventsCallback(events, mockAbortCallbackWrapper.call); + await pumpEventQueue(); - verifyInOrder([ - () => mockSyncStreamRepo.deleteUsersV1(any()), - () => mockSyncStreamRepo.updateUsersV1(any()), - () => mockSyncStreamRepo.deletePartnerV1(any()), - () => mockSyncStreamRepo.updatePartnerV1(any()), - // Verify ack happens after all processing - () => mockSyncApiRepo.ack(any()), - ]); - }); - }); + expect(handler1Started, isTrue); - group("syncUsers", () { - test("calls getSyncEvents with correct types", () async { - // Need to close the stream for the future to complete - final future = sut.syncUsers(); - await streamController.close(); - await future; + // Signal cancellation while handler 1 is waiting + when(() => cancellationChecker()).thenReturn(true); + await pumpEventQueue(); - verify( - () => mockSyncApiRepo.getSyncEvents([ - SyncRequestType.usersV1, - SyncRequestType.partnersV1, - ]), - ).called(1); - }); + processingCompleter.complete(); + await processingFuture; - test("calls repository methods with correctly grouped data", () async { - final events = [ - ...SyncStreamStub.userEvents, - ...SyncStreamStub.partnerEvents, - ]; - await triggerSyncAndEmit(events); + verifyNever(() => mockSyncStreamRepo.updateUsersV1(any())); - // Verify each handler was called with the correct list of data payloads - verify( - () => mockSyncStreamRepo.updateUsersV1( - [SyncStreamStub.userV1Admin, SyncStreamStub.userV1User], - ), - ).called(1); - verify( - () => mockSyncStreamRepo.deleteUsersV1([SyncStreamStub.userDeleteV1]), - ).called(1); - verify( - () => mockSyncStreamRepo.updatePartnerV1([SyncStreamStub.partnerV1]), - ).called(1); - verify( - () => mockSyncStreamRepo - .deletePartnerV1([SyncStreamStub.partnerDeleteV1]), - ).called(1); - }); + verify(() => mockSyncApiRepo.ack(["2"])).called(1); + }, + ); }); } diff --git a/mobile/test/fixtures/sync_stream.stub.dart b/mobile/test/fixtures/sync_stream.stub.dart index 781e63a2bb..ba97f1434a 100644 --- a/mobile/test/fixtures/sync_stream.stub.dart +++ b/mobile/test/fixtures/sync_stream.stub.dart @@ -2,44 +2,44 @@ import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:openapi/api.dart'; abstract final class SyncStreamStub { - static final userV1Admin = SyncUserV1( - deletedAt: DateTime(2020), - email: "admin@admin", - id: "1", - name: "Admin", - ); - static final userV1User = SyncUserV1( - deletedAt: DateTime(2021), - email: "user@user", - id: "2", - name: "User", - ); - static final userDeleteV1 = SyncUserDeleteV1(userId: "2"); - static final userEvents = [ - SyncEvent(type: SyncEntityType.userV1, data: userV1Admin, ack: "1"), - SyncEvent( - type: SyncEntityType.userDeleteV1, - data: userDeleteV1, - ack: "2", + static final userV1Admin = SyncEvent( + type: SyncEntityType.userV1, + data: SyncUserV1( + deletedAt: DateTime(2020), + email: "admin@admin", + id: "1", + name: "Admin", ), - SyncEvent(type: SyncEntityType.userV1, data: userV1User, ack: "5"), - ]; + ack: "1", + ); + static final userV1User = SyncEvent( + type: SyncEntityType.userV1, + data: SyncUserV1( + deletedAt: DateTime(2021), + email: "user@user", + id: "5", + name: "User", + ), + ack: "5", + ); + static final userDeleteV1 = SyncEvent( + type: SyncEntityType.userDeleteV1, + data: SyncUserDeleteV1(userId: "2"), + ack: "2", + ); - static final partnerV1 = SyncPartnerV1( - inTimeline: true, - sharedById: "1", - sharedWithId: "2", - ); - static final partnerDeleteV1 = SyncPartnerDeleteV1( - sharedById: "3", - sharedWithId: "4", - ); - static final partnerEvents = [ - SyncEvent( - type: SyncEntityType.partnerDeleteV1, - data: partnerDeleteV1, - ack: "4", + static final partnerV1 = SyncEvent( + type: SyncEntityType.partnerV1, + data: SyncPartnerV1( + inTimeline: true, + sharedById: "1", + sharedWithId: "2", ), - SyncEvent(type: SyncEntityType.partnerV1, data: partnerV1, ack: "3"), - ]; + ack: "3", + ); + static final partnerDeleteV1 = SyncEvent( + type: SyncEntityType.partnerDeleteV1, + data: SyncPartnerDeleteV1(sharedById: "3", sharedWithId: "4"), + ack: "4", + ); } diff --git a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart new file mode 100644 index 0000000000..55b03a8116 --- /dev/null +++ b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart @@ -0,0 +1,299 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:immich_mobile/domain/models/sync_event.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:openapi/api.dart'; + +import '../../api.mocks.dart'; +import '../../service.mocks.dart'; + +class MockHttpClient extends Mock implements http.Client {} + +class MockApiClient extends Mock implements ApiClient {} + +class MockStreamedResponse extends Mock implements http.StreamedResponse {} + +class FakeBaseRequest extends Fake implements http.BaseRequest {} + +String _createJsonLine(String type, Map data, String ack) { + return '${jsonEncode({'type': type, 'data': data, 'ack': ack})}\n'; +} + +void main() { + late SyncApiRepository sut; + late MockApiService mockApiService; + late MockApiClient mockApiClient; + late MockSyncApi mockSyncApi; + late MockHttpClient mockHttpClient; + late MockStreamedResponse mockStreamedResponse; + late StreamController> responseStreamController; + late int testBatchSize = 3; + + setUp(() { + mockApiService = MockApiService(); + mockApiClient = MockApiClient(); + mockSyncApi = MockSyncApi(); + mockHttpClient = MockHttpClient(); + mockStreamedResponse = MockStreamedResponse(); + responseStreamController = + StreamController>.broadcast(sync: true); + + registerFallbackValue(FakeBaseRequest()); + + when(() => mockApiService.apiClient).thenReturn(mockApiClient); + when(() => mockApiService.syncApi).thenReturn(mockSyncApi); + when(() => mockApiClient.basePath).thenReturn('http://demo.immich.app/api'); + when(() => mockApiService.applyToParams(any(), any())) + .thenAnswer((_) async => {}); + + // Mock HTTP client behavior + when(() => mockHttpClient.send(any())) + .thenAnswer((_) async => mockStreamedResponse); + when(() => mockStreamedResponse.statusCode).thenReturn(200); + when(() => mockStreamedResponse.stream) + .thenAnswer((_) => http.ByteStream(responseStreamController.stream)); + when(() => mockHttpClient.close()).thenAnswer((_) => {}); + + sut = SyncApiRepository(mockApiService); + }); + + tearDown(() async { + if (!responseStreamController.isClosed) { + await responseStreamController.close(); + } + }); + + Future streamChanges( + Function(List, Function() abort) onDataCallback, + ) { + return sut.streamChanges( + onDataCallback, + batchSize: testBatchSize, + httpClient: mockHttpClient, + ); + } + + test('streamChanges stops processing stream when abort is called', () async { + int onDataCallCount = 0; + bool abortWasCalledInCallback = false; + List receivedEventsBatch1 = []; + + onDataCallback(List events, Function() abort) { + onDataCallCount++; + if (onDataCallCount == 1) { + receivedEventsBatch1 = events; + abort(); + abortWasCalledInCallback = true; + } else { + fail("onData called more than once after abort was invoked"); + } + } + + final streamChangesFuture = streamChanges(onDataCallback); + + await pumpEventQueue(); + + for (int i = 0; i < testBatchSize; i++) { + responseStreamController.add( + utf8.encode( + _createJsonLine( + SyncEntityType.userDeleteV1.toString(), + SyncUserDeleteV1(userId: "user$i").toJson(), + 'ack$i', + ), + ), + ); + } + + for (int i = testBatchSize; i < testBatchSize * 2; i++) { + responseStreamController.add( + utf8.encode( + _createJsonLine( + SyncEntityType.userDeleteV1.toString(), + SyncUserDeleteV1(userId: "user$i").toJson(), + 'ack$i', + ), + ), + ); + } + + await responseStreamController.close(); + await expectLater(streamChangesFuture, completes); + + expect(onDataCallCount, 1); + expect(abortWasCalledInCallback, isTrue); + expect(receivedEventsBatch1.length, testBatchSize); + verify(() => mockHttpClient.close()).called(1); + }); + + test( + 'streamChanges does not process remaining lines in finally block if aborted', + () async { + int onDataCallCount = 0; + bool abortWasCalledInCallback = false; + + onDataCallback(List events, Function() abort) { + onDataCallCount++; + if (onDataCallCount == 1) { + abort(); + abortWasCalledInCallback = true; + } else { + fail("onData called more than once after abort was invoked"); + } + } + + final streamChangesFuture = streamChanges(onDataCallback); + + await pumpEventQueue(); + + for (int i = 0; i < testBatchSize; i++) { + responseStreamController.add( + utf8.encode( + _createJsonLine( + SyncEntityType.userDeleteV1.toString(), + SyncUserDeleteV1(userId: "user$i").toJson(), + 'ack$i', + ), + ), + ); + } + + // emit a single event to skip batching and trigger finally + responseStreamController.add( + utf8.encode( + _createJsonLine( + SyncEntityType.userDeleteV1.toString(), + SyncUserDeleteV1(userId: "user100").toJson(), + 'ack100', + ), + ), + ); + + await responseStreamController.close(); + await expectLater(streamChangesFuture, completes); + + expect(onDataCallCount, 1); + expect(abortWasCalledInCallback, isTrue); + verify(() => mockHttpClient.close()).called(1); + }, + ); + + test( + 'streamChanges processes remaining lines in finally block if not aborted', + () async { + int onDataCallCount = 0; + List receivedEventsBatch1 = []; + List receivedEventsBatch2 = []; + + onDataCallback(List events, Function() _) { + onDataCallCount++; + if (onDataCallCount == 1) { + receivedEventsBatch1 = events; + } else if (onDataCallCount == 2) { + receivedEventsBatch2 = events; + } else { + fail("onData called more than expected"); + } + } + + final streamChangesFuture = streamChanges(onDataCallback); + + await pumpEventQueue(); + + // Batch 1 + for (int i = 0; i < testBatchSize; i++) { + responseStreamController.add( + utf8.encode( + _createJsonLine( + SyncEntityType.userDeleteV1.toString(), + SyncUserDeleteV1(userId: "user$i").toJson(), + 'ack$i', + ), + ), + ); + } + + // Partial Batch 2 + responseStreamController.add( + utf8.encode( + _createJsonLine( + SyncEntityType.userDeleteV1.toString(), + SyncUserDeleteV1(userId: "user100").toJson(), + 'ack100', + ), + ), + ); + + await responseStreamController.close(); + await expectLater(streamChangesFuture, completes); + + expect(onDataCallCount, 2); + expect(receivedEventsBatch1.length, testBatchSize); + expect(receivedEventsBatch2.length, 1); + verify(() => mockHttpClient.close()).called(1); + }, + ); + + test('streamChanges handles stream error gracefully', () async { + final streamError = Exception("Network Error"); + int onDataCallCount = 0; + + onDataCallback(List events, Function() _) { + onDataCallCount++; + } + + final streamChangesFuture = streamChanges(onDataCallback); + + await pumpEventQueue(); + + responseStreamController.add( + utf8.encode( + _createJsonLine( + SyncEntityType.userDeleteV1.toString(), + SyncUserDeleteV1(userId: "user1").toJson(), + 'ack1', + ), + ), + ); + + responseStreamController.addError(streamError); + await expectLater(streamChangesFuture, throwsA(streamError)); + + expect(onDataCallCount, 0); + verify(() => mockHttpClient.close()).called(1); + }); + + test('streamChanges throws ApiException on non-200 status code', () async { + when(() => mockStreamedResponse.statusCode).thenReturn(401); + final errorBodyController = StreamController>(sync: true); + when(() => mockStreamedResponse.stream) + .thenAnswer((_) => http.ByteStream(errorBodyController.stream)); + + int onDataCallCount = 0; + + onDataCallback(List events, Function() _) { + onDataCallCount++; + } + + final future = streamChanges(onDataCallback); + + errorBodyController.add(utf8.encode('{"error":"Unauthorized"}')); + await errorBodyController.close(); + + await expectLater( + future, + throwsA( + isA() + .having((e) => e.code, 'code', 401) + .having((e) => e.message, 'message', contains('Unauthorized')), + ), + ); + + expect(onDataCallCount, 0); + verify(() => mockHttpClient.close()).called(1); + }); +} From 504930947d62194ab4cf2e9c219d9d0bc5f4fde0 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Fri, 18 Apr 2025 22:10:27 +0200 Subject: [PATCH 008/356] fix: various actions workflow security improvements (#17651) * fix: set persist-credentials explicitly for checkout https://woodruffw.github.io/zizmor/audits/#artipacked * fix: minimize permissions scope for workflows https://woodruffw.github.io/zizmor/audits/#excessive-permissions * fix: remove potential template injections https://woodruffw.github.io/zizmor/audits/#template-injection * fix: only pass needed secrets in workflow_call https://woodruffw.github.io/zizmor/audits/#secrets-inherit * fix: push perm for single-arch build jobs I hadn't realised these push to the registry too :x * chore: fix formatting * fix: $ * fix: retag job quoting --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .github/workflows/build-mobile.yml | 29 +++++-- .github/workflows/cache-cleanup.yml | 17 ++-- .github/workflows/cli.yml | 13 ++- .github/workflows/codeql-analysis.yml | 4 + .github/workflows/docker.yml | 85 +++++++++++++------ .github/workflows/docs-build.yml | 10 +++ .github/workflows/docs-deploy.yml | 20 ++++- .github/workflows/docs-destroy.yml | 7 ++ .github/workflows/fix-format.yml | 4 + .github/workflows/pr-label-validation.yml | 2 + .github/workflows/pr-labeler.yml | 2 + .../pr-require-conventional-commit.yml | 4 + .github/workflows/prepare-release.yml | 18 +++- .github/workflows/preview-label.yaml | 2 + .github/workflows/sdk.yml | 8 +- .github/workflows/static_analysis.yml | 16 +++- .github/workflows/test.yml | 78 ++++++++++++++++- .github/workflows/weblate-lock.yml | 13 +-- 18 files changed, 269 insertions(+), 63 deletions(-) diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 406e8f89e1..7217b5267e 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -7,6 +7,15 @@ on: ref: required: false type: string + secrets: + KEY_JKS: + required: true + ALIAS: + required: true + ANDROID_KEY_PASSWORD: + required: true + ANDROID_STORE_PASSWORD: + required: true pull_request: push: branches: [main] @@ -15,14 +24,21 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: pre-job: runs-on: ubuntu-latest + permissions: + contents: read outputs: should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }} steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false + - id: found_paths uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 with: @@ -38,22 +54,17 @@ jobs: build-sign-android: name: Build and sign Android needs: pre-job + permissions: + contents: read # Skip when PR from a fork if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && needs.pre-job.outputs.should_run == 'true' }} runs-on: macos-14 steps: - - name: Determine ref - id: get-ref - run: | - input_ref="${{ inputs.ref }}" - github_ref="${{ github.sha }}" - ref="${input_ref:-$github_ref}" - echo "ref=$ref" >> $GITHUB_OUTPUT - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: - ref: ${{ steps.get-ref.outputs.ref }} + ref: ${{ inputs.ref || github.sha }} + persist-credentials: false - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 with: diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml index 0cc73c46c3..84adde08cf 100644 --- a/.github/workflows/cache-cleanup.yml +++ b/.github/workflows/cache-cleanup.yml @@ -8,31 +8,38 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: cleanup: name: Cleanup runs-on: ubuntu-latest + permissions: + contents: read + actions: write steps: - name: Check out code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Cleanup + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REF: ${{ github.ref }} run: | gh extension install actions/gh-actions-cache REPO=${{ github.repository }} - BRANCH=${{ github.ref }} echo "Fetching list of cache keys" - cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 ) + cacheKeysForPR=$(gh actions-cache list -R $REPO -B ${REF} -L 100 | cut -f 1 ) ## Setting this to not fail the workflow while deleting cache keys. set +e echo "Deleting caches..." for cacheKey in $cacheKeysForPR do - gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm + gh actions-cache delete $cacheKey -R "$REPO" -B "${REF}" --confirm done echo "Done" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 05326d063e..231ad141e4 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -16,19 +16,23 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -permissions: - packages: write +permissions: {} jobs: publish: name: CLI Publish runs-on: ubuntu-latest + permissions: + contents: read defaults: run: working-directory: ./cli steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false + # Setup .npmrc file to publish to npm - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: @@ -48,11 +52,16 @@ jobs: docker: name: Docker runs-on: ubuntu-latest + permissions: + contents: read + packages: write needs: publish steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Set up QEMU uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 99ffee2e88..7d07785de7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -24,6 +24,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: analyze: name: Analyze @@ -43,6 +45,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d1bdb5e8e7..a78d3c25dc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -12,18 +12,21 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -permissions: - packages: write +permissions: {} jobs: pre-job: runs-on: ubuntu-latest + permissions: + contents: read outputs: should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }} steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - id: found_paths uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 with: @@ -45,6 +48,9 @@ jobs: retag_ml: name: Re-Tag ML needs: pre-job + permissions: + contents: read + packages: write if: ${{ needs.pre-job.outputs.should_run_ml == 'false' && !github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest strategy: @@ -58,18 +64,22 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Re-tag image + env: + REGISTRY_NAME: 'ghcr.io' + REPOSITORY: ${{ github.repository_owner }}/immich-machine-learning + TAG_OLD: main${{ matrix.suffix }} + TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }} + TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }} run: | - REGISTRY_NAME="ghcr.io" - REPOSITORY=${{ github.repository_owner }}/immich-machine-learning - TAG_OLD=main${{ matrix.suffix }} - TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }} - TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }} - docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD - docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD + docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" + docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" retag_server: name: Re-Tag Server needs: pre-job + permissions: + contents: read + packages: write if: ${{ needs.pre-job.outputs.should_run_server == 'false' && !github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest strategy: @@ -83,18 +93,22 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Re-tag image + env: + REGISTRY_NAME: 'ghcr.io' + REPOSITORY: ${{ github.repository_owner }}/immich-server + TAG_OLD: main${{ matrix.suffix }} + TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }} + TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }} run: | - REGISTRY_NAME="ghcr.io" - REPOSITORY=${{ github.repository_owner }}/immich-server - TAG_OLD=main${{ matrix.suffix }} - TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }} - TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }} - docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD - docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD + docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" + docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" build_and_push_ml: name: Build and Push ML needs: pre-job + permissions: + contents: read + packages: write if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }} runs-on: ${{ matrix.runner }} env: @@ -148,6 +162,8 @@ jobs: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 @@ -161,11 +177,14 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Generate cache key suffix + env: + REF: ${{ github.ref_name }} run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV else - echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV + SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g') + echo "CACHE_KEY_SUFFIX=${SUFFIX}" >> $GITHUB_ENV fi - name: Generate cache target @@ -175,7 +194,7 @@ jobs: # Essentially just ignore the cache output (forks can't write to registry cache) echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT else - echo "cache-to=type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT + echo "cache-to=type=registry,ref=${GHCR_REPO}-build-cache:${PLATFORM_PAIR}-${{ matrix.device }}-${CACHE_KEY_SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT fi - name: Generate docker image tags @@ -221,6 +240,10 @@ jobs: merge_ml: name: Merge & Push ML runs-on: ubuntu-latest + permissions: + contents: read + actions: read + packages: write if: ${{ needs.pre-job.outputs.should_run_ml == 'true' && !github.event.pull_request.head.repo.fork }} env: GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning @@ -308,15 +331,16 @@ jobs: fi TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - SOURCE_ARGS=$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *) - - echo "docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS" + SOURCE_ARGS=$(printf "${GHCR_REPO}@sha256:%s " *) docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS build_and_push_server: name: Build and Push Server runs-on: ${{ matrix.runner }} + permissions: + contents: read + packages: write needs: pre-job if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} env: @@ -340,6 +364,8 @@ jobs: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 @@ -353,11 +379,14 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Generate cache key suffix + env: + REF: ${{ github.ref_name }} run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV else - echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV + SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g') + echo "CACHE_KEY_SUFFIX=${SUFFIX}" >> $GITHUB_ENV fi - name: Generate cache target @@ -367,7 +396,7 @@ jobs: # Essentially just ignore the cache output (forks can't write to registry cache) echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT else - echo "cache-to=type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT + echo "cache-to=type=registry,ref=${GHCR_REPO}-build-cache:${PLATFORM_PAIR}-${CACHE_KEY_SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT fi - name: Generate docker image tags @@ -413,6 +442,10 @@ jobs: merge_server: name: Merge & Push Server runs-on: ubuntu-latest + permissions: + contents: read + actions: read + packages: write if: ${{ needs.pre-job.outputs.should_run_server == 'true' && !github.event.pull_request.head.repo.fork }} env: GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server @@ -486,15 +519,14 @@ jobs: fi TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - SOURCE_ARGS=$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *) - - echo "docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS" + SOURCE_ARGS=$(printf "${GHCR_REPO}@sha256:%s " *) docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS success-check-server: name: Docker Build & Push Server Success needs: [merge_server, retag_server] + permissions: {} runs-on: ubuntu-latest if: always() steps: @@ -508,6 +540,7 @@ jobs: success-check-ml: name: Docker Build & Push ML Success needs: [merge_ml, retag_ml] + permissions: {} runs-on: ubuntu-latest if: always() steps: diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index fdd30034ee..ece3bbd248 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -10,14 +10,20 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: pre-job: runs-on: ubuntu-latest + permissions: + contents: read outputs: should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }} steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - id: found_paths uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 with: @@ -33,6 +39,8 @@ jobs: build: name: Docs Build needs: pre-job + permissions: + contents: read if: ${{ needs.pre-job.outputs.should_run == 'true' }} runs-on: ubuntu-latest defaults: @@ -42,6 +50,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index f33c0c4c03..10277a0c5e 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -9,6 +9,9 @@ jobs: checks: name: Docs Deploy Checks runs-on: ubuntu-latest + permissions: + actions: read + pull-requests: read outputs: parameters: ${{ steps.parameters.outputs.result }} artifact: ${{ steps.get-artifact.outputs.result }} @@ -36,6 +39,8 @@ jobs: - name: Determine deploy parameters id: parameters uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + env: + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} with: script: | const eventType = context.payload.workflow_run.event; @@ -57,7 +62,8 @@ jobs: } else if (eventType == "pull_request") { let pull_number = context.payload.workflow_run.pull_requests[0]?.number; if(!pull_number) { - const response = await github.rest.search.issuesAndPullRequests({q: 'repo:${{ github.repository }} is:pr sha:${{ github.event.workflow_run.head_sha }}',per_page: 1,}) + const {HEAD_SHA} = process.env; + const response = await github.rest.search.issuesAndPullRequests({q: `repo:${{ github.repository }} is:pr sha:${HEAD_SHA}`,per_page: 1,}) const items = response.data.items if (items.length < 1) { throw new Error("No pull request found for the commit") @@ -95,10 +101,16 @@ jobs: name: Docs Deploy runs-on: ubuntu-latest needs: checks + permissions: + contents: read + actions: read + pull-requests: write if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }} steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Load parameters id: parameters @@ -162,9 +174,11 @@ jobs: - name: Output Cleaning id: clean + env: + TG_OUTPUT: ${{ steps.docs-output.outputs.tg_action_output }} run: | - TG_OUT=$(echo '${{ steps.docs-output.outputs.tg_action_output }}' | sed 's|%0A|\n|g ; s|%3C|<|g' | jq -c .) - echo "output=$TG_OUT" >> $GITHUB_OUTPUT + CLEANED=$(echo "$TG_OUTPUT" | sed 's|%0A|\n|g ; s|%3C|<|g' | jq -c .) + echo "output=$CLEANED" >> $GITHUB_OUTPUT - name: Publish to Cloudflare Pages uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1 diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml index 99499528b6..9d1e4b6612 100644 --- a/.github/workflows/docs-destroy.yml +++ b/.github/workflows/docs-destroy.yml @@ -3,13 +3,20 @@ on: pull_request_target: types: [closed] +permissions: {} + jobs: deploy: name: Docs Destroy runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Destroy Docs Subdomain env: diff --git a/.github/workflows/fix-format.yml b/.github/workflows/fix-format.yml index 9c52691a52..77b86cb0b8 100644 --- a/.github/workflows/fix-format.yml +++ b/.github/workflows/fix-format.yml @@ -4,11 +4,14 @@ on: pull_request: types: [labeled] +permissions: {} + jobs: fix-formatting: runs-on: ubuntu-latest if: ${{ github.event.label.name == 'fix:formatting' }} permissions: + contents: write pull-requests: write steps: - name: Generate a token @@ -23,6 +26,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.ref }} token: ${{ steps.generate-token.outputs.token }} + persist-credentials: true - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml index 247c625a96..8d34597a08 100644 --- a/.github/workflows/pr-label-validation.yml +++ b/.github/workflows/pr-label-validation.yml @@ -4,6 +4,8 @@ on: pull_request_target: types: [opened, labeled, unlabeled, synchronize] +permissions: {} + jobs: validate-release-label: runs-on: ubuntu-latest diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 1b43c89889..5704f4275f 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -2,6 +2,8 @@ name: 'Pull Request Labeler' on: - pull_request_target +permissions: {} + jobs: labeler: permissions: diff --git a/.github/workflows/pr-require-conventional-commit.yml b/.github/workflows/pr-require-conventional-commit.yml index 20dd0492f4..78ba77495c 100644 --- a/.github/workflows/pr-require-conventional-commit.yml +++ b/.github/workflows/pr-require-conventional-commit.yml @@ -4,9 +4,13 @@ on: pull_request: types: [opened, synchronize, reopened, edited] +permissions: {} + jobs: validate-pr-title: runs-on: ubuntu-latest + permissions: + pull-requests: write steps: - name: PR Conventional Commit Validation uses: ytanikin/PRConventionalCommits@b628c5a234cc32513014b7bfdd1e47b532124d98 # 1.3.0 diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index ffb24d8952..7971f7574a 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -21,13 +21,14 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }}-root cancel-in-progress: true +permissions: {} + jobs: bump_version: runs-on: ubuntu-latest - outputs: ref: ${{ steps.push-tag.outputs.commit_long_sha }} - + permissions: {} # No job-level permissions are needed because it uses the app-token steps: - name: Generate a token id: generate-token @@ -40,6 +41,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: token: ${{ steps.generate-token.outputs.token }} + persist-credentials: true - name: Install uv uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5 @@ -59,14 +61,20 @@ jobs: build_mobile: uses: ./.github/workflows/build-mobile.yml needs: bump_version - secrets: inherit + secrets: + KEY_JKS: ${{ secrets.KEY_JKS }} + ALIAS: ${{ secrets.ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} with: ref: ${{ needs.bump_version.outputs.ref }} prepare_release: runs-on: ubuntu-latest needs: build_mobile - + permissions: + actions: read # To download the app artifact + # No content permissions are needed because it uses the app-token steps: - name: Generate a token id: generate-token @@ -79,6 +87,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: token: ${{ steps.generate-token.outputs.token }} + persist-credentials: false - name: Download APK uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 @@ -90,6 +99,7 @@ jobs: with: draft: true tag_name: ${{ env.IMMICH_VERSION }} + token: ${{ steps.generate-token.outputs.token }} generate_release_notes: true body_path: misc/release/notes.tmpl files: | diff --git a/.github/workflows/preview-label.yaml b/.github/workflows/preview-label.yaml index 447a309a2e..4c445f13d0 100644 --- a/.github/workflows/preview-label.yaml +++ b/.github/workflows/preview-label.yaml @@ -4,6 +4,8 @@ on: pull_request: types: [labeled, closed] +permissions: {} + jobs: comment-status: runs-on: ubuntu-latest diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index cde2075423..482faf29f4 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -4,18 +4,22 @@ on: release: types: [published] -permissions: - packages: write +permissions: {} jobs: publish: name: Publish `@immich/sdk` runs-on: ubuntu-latest + permissions: + contents: read defaults: run: working-directory: ./open-api/typescript-sdk steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false + # Setup .npmrc file to publish to npm - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 615082f86a..1a3c11d3d5 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -9,14 +9,20 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: pre-job: runs-on: ubuntu-latest + permissions: + contents: read outputs: should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }} steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - id: found_paths uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 with: @@ -33,12 +39,14 @@ jobs: name: Run Dart Code Analysis needs: pre-job if: ${{ needs.pre-job.outputs.should_run == 'true' }} - runs-on: ubuntu-latest - + permissions: + contents: read steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Setup Flutter SDK uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2 @@ -69,9 +77,11 @@ jobs: - name: Verify files have not changed if: steps.verify-changed-files.outputs.files_changed == 'true' + env: + CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }} run: | echo "ERROR: Generated files not up to date! Run make_build inside the mobile directory" - echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}" + echo "Changed files: ${CHANGED_FILES}" exit 1 - name: Run dart analyze diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f5cb6c8d30..91389c25ff 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,9 +9,13 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: pre-job: runs-on: ubuntu-latest + permissions: + contents: read outputs: should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }} @@ -25,6 +29,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false + - id: found_paths uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 with: @@ -58,6 +65,8 @@ jobs: needs: pre-job if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} runs-on: ubuntu-latest + permissions: + contents: read defaults: run: working-directory: ./server @@ -65,6 +74,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -95,6 +106,8 @@ jobs: needs: pre-job if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }} runs-on: ubuntu-latest + permissions: + contents: read defaults: run: working-directory: ./cli @@ -102,6 +115,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -136,6 +151,8 @@ jobs: needs: pre-job if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }} runs-on: windows-latest + permissions: + contents: read defaults: run: working-directory: ./cli @@ -143,6 +160,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -170,6 +189,8 @@ jobs: needs: pre-job if: ${{ needs.pre-job.outputs.should_run_web == 'true' }} runs-on: ubuntu-latest + permissions: + contents: read defaults: run: working-directory: ./web @@ -177,6 +198,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -215,6 +238,8 @@ jobs: needs: pre-job if: ${{ needs.pre-job.outputs.should_run_e2e == 'true' }} runs-on: ubuntu-latest + permissions: + contents: read defaults: run: working-directory: ./e2e @@ -222,6 +247,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -254,6 +281,8 @@ jobs: needs: pre-job if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} runs-on: ubuntu-latest + permissions: + contents: read defaults: run: working-directory: ./server @@ -261,6 +290,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -279,6 +310,8 @@ jobs: needs: pre-job if: ${{ needs.pre-job.outputs.should_run_e2e_server_cli == 'true' }} runs-on: mich + permissions: + contents: read defaults: run: working-directory: ./e2e @@ -287,6 +320,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: + persist-credentials: false submodules: 'recursive' - name: Setup Node @@ -321,6 +355,8 @@ jobs: needs: pre-job if: ${{ needs.pre-job.outputs.should_run_e2e_web == 'true' }} runs-on: mich + permissions: + contents: read defaults: run: working-directory: ./e2e @@ -329,6 +365,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: + persist-credentials: false submodules: 'recursive' - name: Setup Node @@ -362,8 +399,13 @@ jobs: needs: pre-job if: ${{ needs.pre-job.outputs.should_run_mobile == 'true' }} runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false + - name: Setup Flutter SDK uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2 with: @@ -378,11 +420,16 @@ jobs: needs: pre-job if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }} runs-on: ubuntu-latest + permissions: + contents: read defaults: run: working-directory: ./machine-learning steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false + - name: Install uv uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5 - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 @@ -411,6 +458,8 @@ jobs: needs: pre-job if: ${{ needs.pre-job.outputs['should_run_.github'] == 'true' }} runs-on: ubuntu-latest + permissions: + contents: read defaults: run: working-directory: ./.github @@ -418,6 +467,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -434,22 +485,31 @@ jobs: shellcheck: name: ShellCheck runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false + - name: Run ShellCheck uses: ludeeus/action-shellcheck@master with: ignore_paths: >- **/open-api/** - **/openapi/** + **/openapi** **/node_modules/** generated-api-up-to-date: name: OpenAPI Clients runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -476,14 +536,18 @@ jobs: - name: Verify files have not changed if: steps.verify-changed-files.outputs.files_changed == 'true' + env: + CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }} run: | echo "ERROR: Generated files not up to date!" - echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}" + echo "Changed files: ${CHANGED_FILES}" exit 1 generated-typeorm-migrations-up-to-date: name: TypeORM Checks runs-on: ubuntu-latest + permissions: + contents: read services: postgres: image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 @@ -505,6 +569,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -535,9 +601,11 @@ jobs: server/src - name: Verify migration files have not changed if: steps.verify-changed-files.outputs.files_changed == 'true' + env: + CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }} run: | echo "ERROR: Generated migration files not up to date!" - echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}" + echo "Changed files: ${CHANGED_FILES}" cat ./src/*-TestMigration.ts exit 1 @@ -555,9 +623,11 @@ jobs: - name: Verify SQL files have not changed if: steps.verify-changed-sql-files.outputs.files_changed == 'true' + env: + CHANGED_FILES: ${{ steps.verify-changed-sql-files.outputs.changed_files }} run: | echo "ERROR: Generated SQL files not up to date!" - echo "Changed files: ${{ steps.verify-changed-sql-files.outputs.changed_files }}" + echo "Changed files: ${CHANGED_FILES}" exit 1 # mobile-integration-tests: diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index 69dce3ac41..2aef5c472a 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -4,30 +4,32 @@ on: pull_request: branches: [main] +permissions: {} + jobs: pre-job: runs-on: ubuntu-latest + permissions: + contents: read outputs: should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}} steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false - id: found_paths uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 with: filters: | i18n: - 'i18n/!(en)**\.json' - - name: Debug - run: | - echo "Should run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}" - echo "Found i18n paths: ${{ steps.found_paths.outputs.i18n }}" - echo "Head ref: ${{ github.head_ref }}" enforce-lock: name: Check Weblate Lock needs: [pre-job] runs-on: ubuntu-latest + permissions: {} if: ${{ needs.pre-job.outputs.should_run == 'true' }} steps: - name: Check weblate lock @@ -47,6 +49,7 @@ jobs: name: Weblate Lock Check Success needs: [enforce-lock] runs-on: ubuntu-latest + permissions: {} if: always() steps: - name: Any jobs failed? From 854ea13d6abcdb031e4f63974be049733c717f81 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Fri, 18 Apr 2025 22:52:41 +0200 Subject: [PATCH 009/356] chore: simplify asset getByIds (#17699) --- server/src/repositories/asset.repository.ts | 54 ++++----------------- 1 file changed, 9 insertions(+), 45 deletions(-) diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 52c390c162..af79fb7c5f 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -299,51 +299,15 @@ export class AssetRepository { @GenerateSql({ params: [[DummyValue.UUID]] }) @ChunkedArray() - async getByIds( - ids: string[], - { exifInfo, faces, files, library, owner, smartSearch, stack, tags }: GetByIdsRelations = {}, - ): Promise { - const res = await this.db - .selectFrom('assets') - .selectAll('assets') - .where('assets.id', '=', anyUuid(ids)) - .$if(!!exifInfo, withExif) - .$if(!!faces, (qb) => - qb.select((eb) => - faces?.person ? withFacesAndPeople(eb, faces.withDeleted) : withFaces(eb, faces?.withDeleted), - ), - ) - .$if(!!files, (qb) => qb.select(withFiles)) - .$if(!!library, (qb) => qb.select(withLibrary)) - .$if(!!owner, (qb) => qb.select(withOwner)) - .$if(!!smartSearch, withSmartSearch) - .$if(!!stack, (qb) => - qb - .leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId') - .$if(!stack!.assets, (qb) => qb.select((eb) => eb.fn.toJson(eb.table('asset_stack')).as('stack'))) - .$if(!!stack!.assets, (qb) => - qb - .leftJoinLateral( - (eb) => - eb - .selectFrom('assets as stacked') - .selectAll('asset_stack') - .select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets')) - .whereRef('stacked.stackId', '=', 'asset_stack.id') - .whereRef('stacked.id', '!=', 'asset_stack.primaryAssetId') - .where('stacked.deletedAt', 'is', null) - .where('stacked.isArchived', '=', false) - .groupBy('asset_stack.id') - .as('stacked_assets'), - (join) => join.on('asset_stack.id', 'is not', null), - ) - .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')), - ), - ) - .$if(!!tags, (qb) => qb.select(withTags)) - .execute(); - - return res as any as AssetEntity[]; + getByIds(ids: string[]): Promise { + return ( + this.db + // + .selectFrom('assets') + .selectAll('assets') + .where('assets.id', '=', anyUuid(ids)) + .execute() as Promise + ); } @GenerateSql({ params: [[DummyValue.UUID]] }) From 52ae06c1194e992df6407e67bdebf9fd9b41238f Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Fri, 18 Apr 2025 23:10:34 +0200 Subject: [PATCH 010/356] refactor: remove album entity, update types (#17450) --- server/src/database.ts | 85 ++++++----- server/src/db.d.ts | 6 +- server/src/dtos/album.dto.ts | 28 +++- server/src/dtos/asset-response.dto.ts | 50 ++++++- server/src/dtos/memory.dto.ts | 3 +- server/src/dtos/shared-link.dto.ts | 8 +- server/src/entities/album.entity.ts | 23 --- server/src/entities/asset.entity.ts | 24 ++-- server/src/entities/shared-link.entity.ts | 20 --- server/src/queries/asset.repository.sql | 21 +-- server/src/repositories/album.repository.ts | 53 +++---- .../src/repositories/asset-job.repository.ts | 17 ++- server/src/repositories/asset.repository.ts | 132 ++++++++---------- server/src/repositories/search.repository.ts | 35 +++-- .../repositories/shared-link.repository.ts | 41 +++--- server/src/repositories/stack.repository.ts | 5 +- server/src/services/album.service.ts | 8 +- .../src/services/asset-media.service.spec.ts | 9 +- server/src/services/asset-media.service.ts | 16 +-- server/src/services/asset.service.spec.ts | 7 +- server/src/services/asset.service.ts | 8 +- server/src/services/duplicate.service.ts | 2 +- server/src/services/job.service.spec.ts | 4 +- server/src/services/job.service.ts | 4 +- server/src/services/library.service.spec.ts | 18 +-- server/src/services/library.service.ts | 14 +- server/src/services/metadata.service.spec.ts | 9 +- server/src/services/metadata.service.ts | 2 +- server/src/services/person.service.ts | 8 +- server/src/services/search.service.spec.ts | 2 +- server/src/services/search.service.ts | 7 +- .../src/services/shared-link.service.spec.ts | 5 +- server/src/services/shared-link.service.ts | 10 +- server/src/services/sync.service.spec.ts | 3 +- server/src/services/view.service.ts | 3 +- server/src/utils/asset.util.ts | 7 +- server/test/fixtures/album.stub.ts | 22 +-- server/test/fixtures/asset.stub.ts | 93 +++++++++--- server/test/fixtures/auth.stub.ts | 9 +- server/test/fixtures/shared-link.stub.ts | 31 ++-- .../specs/services/memory.service.spec.ts | 9 +- .../specs/services/metadata.service.spec.ts | 2 +- .../repositories/asset.repository.mock.ts | 2 +- server/test/small.factory.ts | 4 +- 44 files changed, 473 insertions(+), 396 deletions(-) delete mode 100644 server/src/entities/album.entity.ts delete mode 100644 server/src/entities/shared-link.entity.ts diff --git a/server/src/database.ts b/server/src/database.ts index b504d39579..6e66cb4d3d 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -1,13 +1,14 @@ import { Selectable } from 'kysely'; -import { AssetJobStatus as DatabaseAssetJobStatus, Exif as DatabaseExif } from 'src/db'; +import { Albums, AssetJobStatus as DatabaseAssetJobStatus, Exif as DatabaseExif } from 'src/db'; +import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { AlbumUserRole, AssetFileType, - AssetStatus, AssetType, MemoryType, Permission, + SharedLinkType, SourceType, UserStatus, } from 'src/enum'; @@ -44,7 +45,7 @@ export type Library = { exclusionPatterns: string[]; deletedAt: Date | null; refreshedAt: Date | null; - assets?: Asset[]; + assets?: MapAsset[]; }; export type AuthApiKey = { @@ -96,7 +97,26 @@ export type Memory = { data: OnThisDayData; ownerId: string; isSaved: boolean; - assets: Asset[]; + assets: MapAsset[]; +}; + +export type Asset = { + id: string; + checksum: Buffer; + deviceAssetId: string; + deviceId: string; + fileCreatedAt: Date; + fileModifiedAt: Date; + isExternal: boolean; + isVisible: boolean; + libraryId: string | null; + livePhotoVideoId: string | null; + localDateTime: Date; + originalFileName: string; + originalPath: string; + ownerId: string; + sidecarPath: string | null; + type: AssetType; }; export type User = { @@ -128,39 +148,6 @@ export type StorageAsset = { encodedVideoPath: string | null; }; -export type Asset = { - createdAt: Date; - updatedAt: Date; - deletedAt: Date | null; - id: string; - updateId: string; - status: AssetStatus; - checksum: Buffer; - deviceAssetId: string; - deviceId: string; - duplicateId: string | null; - duration: string | null; - encodedVideoPath: string | null; - fileCreatedAt: Date | null; - fileModifiedAt: Date | null; - isArchived: boolean; - isExternal: boolean; - isFavorite: boolean; - isOffline: boolean; - isVisible: boolean; - libraryId: string | null; - livePhotoVideoId: string | null; - localDateTime: Date | null; - originalFileName: string; - originalPath: string; - ownerId: string; - sidecarPath: string | null; - stack?: Stack | null; - stackId: string | null; - thumbhash: Buffer | null; - type: AssetType; -}; - export type SidecarWriteAsset = { id: string; sidecarPath: string | null; @@ -173,7 +160,7 @@ export type Stack = { primaryAssetId: string; owner?: User; ownerId: string; - assets: AssetEntity[]; + assets: MapAsset[]; assetCount?: number; }; @@ -187,6 +174,28 @@ export type AuthSharedLink = { password: string | null; }; +export type SharedLink = { + id: string; + album?: Album | null; + albumId: string | null; + allowDownload: boolean; + allowUpload: boolean; + assets: MapAsset[]; + createdAt: Date; + description: string | null; + expiresAt: Date | null; + key: Buffer; + password: string | null; + showExif: boolean; + type: SharedLinkType; + userId: string; +}; + +export type Album = Selectable & { + owner: User; + assets: MapAsset[]; +}; + export type AuthSession = { id: string; }; diff --git a/server/src/db.d.ts b/server/src/db.d.ts index 7115b701ce..4e9738ecec 100644 --- a/server/src/db.d.ts +++ b/server/src/db.d.ts @@ -143,8 +143,8 @@ export interface Assets { duplicateId: string | null; duration: string | null; encodedVideoPath: Generated; - fileCreatedAt: Timestamp | null; - fileModifiedAt: Timestamp | null; + fileCreatedAt: Timestamp; + fileModifiedAt: Timestamp; id: Generated; isArchived: Generated; isExternal: Generated; @@ -153,7 +153,7 @@ export interface Assets { isVisible: Generated; libraryId: string | null; livePhotoVideoId: string | null; - localDateTime: Timestamp | null; + localDateTime: Timestamp; originalFileName: string; originalPath: string; ownerId: string; diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index c9934ec909..40e51ef729 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -2,10 +2,10 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { ArrayNotEmpty, IsArray, IsEnum, IsString, ValidateNested } from 'class-validator'; import _ from 'lodash'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AlbumUser, AuthSharedLink, User } from 'src/database'; +import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; -import { AlbumEntity } from 'src/entities/album.entity'; import { AlbumUserRole, AssetOrder } from 'src/enum'; import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; @@ -142,7 +142,23 @@ export class AlbumResponseDto { order?: AssetOrder; } -export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => { +export type MapAlbumDto = { + albumUsers?: AlbumUser[]; + assets?: MapAsset[]; + sharedLinks?: AuthSharedLink[]; + albumName: string; + description: string; + albumThumbnailAssetId: string | null; + createdAt: Date; + updatedAt: Date; + id: string; + ownerId: string; + owner: User; + isActivityEnabled: boolean; + order: AssetOrder; +}; + +export const mapAlbum = (entity: MapAlbumDto, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => { const albumUsers: AlbumUserResponseDto[] = []; if (entity.albumUsers) { @@ -159,7 +175,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt const assets = entity.assets || []; - const hasSharedLink = entity.sharedLinks?.length > 0; + const hasSharedLink = !!entity.sharedLinks && entity.sharedLinks.length > 0; const hasSharedUser = albumUsers.length > 0; let startDate = assets.at(0)?.localDateTime; @@ -190,5 +206,5 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt }; }; -export const mapAlbumWithAssets = (entity: AlbumEntity) => mapAlbum(entity, true); -export const mapAlbumWithoutAssets = (entity: AlbumEntity) => mapAlbum(entity, false); +export const mapAlbumWithAssets = (entity: MapAlbumDto) => mapAlbum(entity, true); +export const mapAlbumWithoutAssets = (entity: MapAlbumDto) => mapAlbum(entity, false); diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 985ad04729..c0e589f380 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { AssetFace } from 'src/database'; +import { Selectable } from 'kysely'; +import { AssetFace, AssetFile, Exif, Stack, Tag, User } from 'src/database'; import { PropertyLifecycle } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto'; @@ -11,8 +12,7 @@ import { } from 'src/dtos/person.dto'; import { TagResponseDto, mapTag } from 'src/dtos/tag.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; -import { AssetEntity } from 'src/entities/asset.entity'; -import { AssetType } from 'src/enum'; +import { AssetStatus, AssetType } from 'src/enum'; import { mimeTypes } from 'src/utils/mime-types'; export class SanitizedAssetResponseDto { @@ -56,6 +56,44 @@ export class AssetResponseDto extends SanitizedAssetResponseDto { resized?: boolean; } +export type MapAsset = { + createdAt: Date; + updatedAt: Date; + deletedAt: Date | null; + id: string; + updateId: string; + status: AssetStatus; + checksum: Buffer; + deviceAssetId: string; + deviceId: string; + duplicateId: string | null; + duration: string | null; + encodedVideoPath: string | null; + exifInfo?: Selectable | null; + faces?: AssetFace[]; + fileCreatedAt: Date; + fileModifiedAt: Date; + files?: AssetFile[]; + isArchived: boolean; + isExternal: boolean; + isFavorite: boolean; + isOffline: boolean; + isVisible: boolean; + libraryId: string | null; + livePhotoVideoId: string | null; + localDateTime: Date; + originalFileName: string; + originalPath: string; + owner?: User | null; + ownerId: string; + sidecarPath: string | null; + stack?: Stack | null; + stackId: string | null; + tags?: Tag[]; + thumbhash: Buffer | null; + type: AssetType; +}; + export class AssetStackResponseDto { id!: string; @@ -72,7 +110,7 @@ export type AssetMapOptions = { }; // TODO: this is inefficient -const peopleWithFaces = (faces: AssetFace[]): PersonWithFacesResponseDto[] => { +const peopleWithFaces = (faces?: AssetFace[]): PersonWithFacesResponseDto[] => { const result: PersonWithFacesResponseDto[] = []; if (faces) { for (const face of faces) { @@ -90,7 +128,7 @@ const peopleWithFaces = (faces: AssetFace[]): PersonWithFacesResponseDto[] => { return result; }; -const mapStack = (entity: AssetEntity) => { +const mapStack = (entity: { stack?: Stack | null }) => { if (!entity.stack) { return null; } @@ -111,7 +149,7 @@ export const hexOrBufferToBase64 = (encoded: string | Buffer) => { return encoded.toString('base64'); }; -export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto { +export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): AssetResponseDto { const { stripMetadata = false, withStack = false } = options; if (stripMetadata) { diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index b3054d7a4c..98231a9035 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -4,7 +4,6 @@ import { IsEnum, IsInt, IsObject, IsPositive, ValidateNested } from 'class-valid import { Memory } from 'src/database'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetEntity } from 'src/entities/asset.entity'; import { MemoryType } from 'src/enum'; import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; @@ -103,6 +102,6 @@ export const mapMemory = (entity: Memory, auth: AuthDto): MemoryResponseDto => { type: entity.type as MemoryType, data: entity.data as unknown as MemoryData, isSaved: entity.isSaved, - assets: ('assets' in entity ? entity.assets : []).map((asset) => mapAsset(asset as AssetEntity, { auth })), + assets: ('assets' in entity ? entity.assets : []).map((asset) => mapAsset(asset, { auth })), }; }; diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index 6bb8ab1f0d..8d373b40b6 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -1,9 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsString } from 'class-validator'; import _ from 'lodash'; +import { SharedLink } from 'src/database'; import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; -import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { SharedLinkType } from 'src/enum'; import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; @@ -102,7 +102,7 @@ export class SharedLinkResponseDto { showMetadata!: boolean; } -export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto { +export function mapSharedLink(sharedLink: SharedLink): SharedLinkResponseDto { const linkAssets = sharedLink.assets || []; return { @@ -122,7 +122,7 @@ export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseD }; } -export function mapSharedLinkWithoutMetadata(sharedLink: SharedLinkEntity): SharedLinkResponseDto { +export function mapSharedLinkWithoutMetadata(sharedLink: SharedLink): SharedLinkResponseDto { const linkAssets = sharedLink.assets || []; const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset); @@ -137,7 +137,7 @@ export function mapSharedLinkWithoutMetadata(sharedLink: SharedLinkEntity): Shar type: sharedLink.type, createdAt: sharedLink.createdAt, expiresAt: sharedLink.expiresAt, - assets: assets.map((asset) => mapAsset(asset, { stripMetadata: true })) as AssetResponseDto[], + assets: assets.map((asset) => mapAsset(asset, { stripMetadata: true })), album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined, allowUpload: sharedLink.allowUpload, allowDownload: sharedLink.allowDownload, diff --git a/server/src/entities/album.entity.ts b/server/src/entities/album.entity.ts deleted file mode 100644 index eb20c1afdd..0000000000 --- a/server/src/entities/album.entity.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { AlbumUser, User } from 'src/database'; -import { AssetEntity } from 'src/entities/asset.entity'; -import { SharedLinkEntity } from 'src/entities/shared-link.entity'; -import { AssetOrder } from 'src/enum'; - -export class AlbumEntity { - id!: string; - owner!: User; - ownerId!: string; - albumName!: string; - description!: string; - createdAt!: Date; - updatedAt!: Date; - updateId?: string; - deletedAt!: Date | null; - albumThumbnailAsset!: AssetEntity | null; - albumThumbnailAssetId!: string | null; - albumUsers!: AlbumUser[]; - assets!: AssetEntity[]; - sharedLinks!: SharedLinkEntity[]; - isActivityEnabled!: boolean; - order!: AssetOrder; -} diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts index 64c038a689..da291292e7 100644 --- a/server/src/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -1,12 +1,12 @@ import { DeduplicateJoinsPlugin, ExpressionBuilder, Kysely, SelectQueryBuilder, sql } from 'kysely'; import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'; -import { AssetFace, AssetFile, AssetJobStatus, columns, Exif, Stack, Tag, User } from 'src/database'; +import { AssetFace, AssetFile, AssetJobStatus, columns, Exif, Person, Stack, Tag, User } from 'src/database'; import { DB } from 'src/db'; -import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetFileType, AssetStatus, AssetType } from 'src/enum'; import { TimeBucketSize } from 'src/repositories/asset.repository'; import { AssetSearchBuilderOptions } from 'src/repositories/search.repository'; -import { anyUuid, asUuid } from 'src/utils/database'; +import { anyUuid, asUuid, toJson } from 'src/utils/database'; export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum'; @@ -37,13 +37,12 @@ export class AssetEntity { checksum!: Buffer; // sha1 checksum duration!: string | null; isVisible!: boolean; - livePhotoVideo!: AssetEntity | null; + livePhotoVideo!: MapAsset | null; livePhotoVideoId!: string | null; originalFileName!: string; sidecarPath!: string | null; exifInfo?: Exif; tags?: Tag[]; - sharedLinks!: SharedLinkEntity[]; faces!: AssetFace[]; stackId?: string | null; stack?: Stack | null; @@ -51,6 +50,7 @@ export class AssetEntity { duplicateId!: string | null; } +// TODO come up with a better query that only selects the fields we need export function withExif(qb: SelectQueryBuilder) { return qb .leftJoin('exif', 'assets.id', 'exif.assetId') @@ -66,7 +66,7 @@ export function withExifInner(qb: SelectQueryBuilder) { export function withSmartSearch(qb: SelectQueryBuilder) { return qb .leftJoin('smart_search', 'assets.id', 'smart_search.assetId') - .select((eb) => eb.fn.toJson(eb.table('smart_search')).as('smartSearch')); + .select((eb) => toJson(eb, 'smart_search').as('smartSearch')); } export function withFaces(eb: ExpressionBuilder, withDeletedFace?: boolean) { @@ -99,7 +99,7 @@ export function withFacesAndPeople(eb: ExpressionBuilder, withDele (join) => join.onTrue(), ) .selectAll('asset_faces') - .select((eb) => eb.table('person').as('person')) + .select((eb) => eb.table('person').$castTo().as('person')) .whereRef('asset_faces.assetId', '=', 'assets.id') .$if(!withDeletedFace, (qb) => qb.where('asset_faces.deletedAt', 'is', null)), ).as('faces'); @@ -136,13 +136,15 @@ export function hasTags(qb: SelectQueryBuilder, tagIds: stri } export function withOwner(eb: ExpressionBuilder) { - return jsonObjectFrom(eb.selectFrom('users').selectAll().whereRef('users.id', '=', 'assets.ownerId')).as('owner'); + return jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'assets.ownerId')).as( + 'owner', + ); } export function withLibrary(eb: ExpressionBuilder) { - return jsonObjectFrom(eb.selectFrom('libraries').selectAll().whereRef('libraries.id', '=', 'assets.libraryId')).as( - 'library', - ); + return jsonObjectFrom( + eb.selectFrom('libraries').selectAll('libraries').whereRef('libraries.id', '=', 'assets.libraryId'), + ).as('library'); } export function withTags(eb: ExpressionBuilder) { diff --git a/server/src/entities/shared-link.entity.ts b/server/src/entities/shared-link.entity.ts deleted file mode 100644 index 720ba424d1..0000000000 --- a/server/src/entities/shared-link.entity.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { AlbumEntity } from 'src/entities/album.entity'; -import { AssetEntity } from 'src/entities/asset.entity'; -import { SharedLinkType } from 'src/enum'; - -export class SharedLinkEntity { - id!: string; - description!: string | null; - password!: string | null; - userId!: string; - key!: Buffer; // use to access the inidividual asset - type!: SharedLinkType; - createdAt!: Date; - expiresAt!: Date | null; - allowUpload!: boolean; - allowDownload!: boolean; - showExif!: boolean; - assets!: AssetEntity[]; - album?: AlbumEntity; - albumId!: string | null; -} diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index cf17bb0276..a3dcb08c1e 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -82,7 +82,7 @@ from where "assets"."id" = any ($1::uuid[]) --- AssetRepository.getByIdsWithAllRelations +-- AssetRepository.getByIdsWithAllRelationsButStacks select "assets".*, ( @@ -127,28 +127,13 @@ select "assets"."id" = "tag_asset"."assetsId" ) as agg ) as "tags", - to_json("exif") as "exifInfo", - to_json("stacked_assets") as "stack" + to_json("exif") as "exifInfo" from "assets" left join "exif" on "assets"."id" = "exif"."assetId" left join "asset_stack" on "asset_stack"."id" = "assets"."stackId" - left join lateral ( - select - "asset_stack".*, - array_agg("stacked") as "assets" - from - "assets" as "stacked" - where - "stacked"."stackId" = "asset_stack"."id" - and "stacked"."id" != "asset_stack"."primaryAssetId" - and "stacked"."deletedAt" is null - and "stacked"."isArchived" = $1 - group by - "asset_stack"."id" - ) as "stacked_assets" on "asset_stack"."id" is not null where - "assets"."id" = any ($2::uuid[]) + "assets"."id" = any ($1::uuid[]) -- AssetRepository.deleteAll delete from "assets" diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index e21d5d73cd..1768135210 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -1,12 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { ExpressionBuilder, Insertable, Kysely, sql, Updateable } from 'kysely'; +import { ExpressionBuilder, Insertable, Kysely, NotNull, sql, Updateable } from 'kysely'; import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'; import { InjectKysely } from 'nestjs-kysely'; -import { columns } from 'src/database'; +import { columns, Exif } from 'src/database'; import { Albums, DB } from 'src/db'; import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { AlbumUserCreateDto } from 'src/dtos/album.dto'; -import { AlbumEntity } from 'src/entities/album.entity'; export interface AlbumAssetCount { albumId: string; @@ -21,9 +20,9 @@ export interface AlbumInfoOptions { } const withOwner = (eb: ExpressionBuilder) => { - return jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'albums.ownerId')).as( - 'owner', - ); + return jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'albums.ownerId')) + .$notNull() + .as('owner'); }; const withAlbumUsers = (eb: ExpressionBuilder) => { @@ -32,12 +31,14 @@ const withAlbumUsers = (eb: ExpressionBuilder) => { .selectFrom('albums_shared_users_users as album_users') .select('album_users.role') .select((eb) => - jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'album_users.usersId')).as( - 'user', - ), + jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'album_users.usersId')) + .$notNull() + .as('user'), ) .whereRef('album_users.albumsId', '=', 'albums.id'), - ).as('albumUsers'); + ) + .$notNull() + .as('albumUsers'); }; const withSharedLink = (eb: ExpressionBuilder) => { @@ -53,7 +54,7 @@ const withAssets = (eb: ExpressionBuilder) => { .selectFrom('assets') .selectAll('assets') .leftJoin('exif', 'assets.id', 'exif.assetId') - .select((eb) => eb.table('exif').as('exifInfo')) + .select((eb) => eb.table('exif').$castTo().as('exifInfo')) .innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id') .whereRef('albums_assets_assets.albumsId', '=', 'albums.id') .where('assets.deletedAt', 'is', null) @@ -69,7 +70,7 @@ export class AlbumRepository { constructor(@InjectKysely() private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, { withAssets: true }] }) - async getById(id: string, options: AlbumInfoOptions): Promise { + async getById(id: string, options: AlbumInfoOptions) { return this.db .selectFrom('albums') .selectAll('albums') @@ -79,11 +80,12 @@ export class AlbumRepository { .select(withAlbumUsers) .select(withSharedLink) .$if(options.withAssets, (eb) => eb.select(withAssets)) - .executeTakeFirst() as Promise; + .$narrowType<{ assets: NotNull }>() + .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) - async getByAssetId(ownerId: string, assetId: string): Promise { + async getByAssetId(ownerId: string, assetId: string) { return this.db .selectFrom('albums') .selectAll('albums') @@ -105,7 +107,7 @@ export class AlbumRepository { .select(withOwner) .select(withAlbumUsers) .orderBy('albums.createdAt', 'desc') - .execute() as unknown as Promise; + .execute(); } @GenerateSql({ params: [[DummyValue.UUID]] }) @@ -134,7 +136,7 @@ export class AlbumRepository { } @GenerateSql({ params: [DummyValue.UUID] }) - async getOwned(ownerId: string): Promise { + async getOwned(ownerId: string) { return this.db .selectFrom('albums') .selectAll('albums') @@ -144,14 +146,14 @@ export class AlbumRepository { .where('albums.ownerId', '=', ownerId) .where('albums.deletedAt', 'is', null) .orderBy('albums.createdAt', 'desc') - .execute() as unknown as Promise; + .execute(); } /** * Get albums shared with and shared by owner. */ @GenerateSql({ params: [DummyValue.UUID] }) - async getShared(ownerId: string): Promise { + async getShared(ownerId: string) { return this.db .selectFrom('albums') .selectAll('albums') @@ -176,14 +178,14 @@ export class AlbumRepository { .select(withOwner) .select(withSharedLink) .orderBy('albums.createdAt', 'desc') - .execute() as unknown as Promise; + .execute(); } /** * Get albums of owner that are _not_ shared */ @GenerateSql({ params: [DummyValue.UUID] }) - async getNotShared(ownerId: string): Promise { + async getNotShared(ownerId: string) { return this.db .selectFrom('albums') .selectAll('albums') @@ -203,7 +205,7 @@ export class AlbumRepository { ) .select(withOwner) .orderBy('albums.createdAt', 'desc') - .execute() as unknown as Promise; + .execute(); } async restoreAll(userId: string): Promise { @@ -262,7 +264,7 @@ export class AlbumRepository { await this.addAssets(this.db, albumId, assetIds); } - create(album: Insertable, assetIds: string[], albumUsers: AlbumUserCreateDto[]): Promise { + create(album: Insertable, assetIds: string[], albumUsers: AlbumUserCreateDto[]) { return this.db.transaction().execute(async (tx) => { const newAlbum = await tx.insertInto('albums').values(album).returning('albums.id').executeTakeFirst(); @@ -290,11 +292,12 @@ export class AlbumRepository { .select(withOwner) .select(withAssets) .select(withAlbumUsers) - .executeTakeFirst() as unknown as Promise; + .$narrowType<{ assets: NotNull }>() + .executeTakeFirstOrThrow(); }); } - update(id: string, album: Updateable): Promise { + update(id: string, album: Updateable) { return this.db .updateTable('albums') .set(album) @@ -303,7 +306,7 @@ export class AlbumRepository { .returning(withOwner) .returning(withSharedLink) .returning(withAlbumUsers) - .executeTakeFirst() as unknown as Promise; + .executeTakeFirstOrThrow(); } async delete(id: string): Promise { diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index b507fa5445..1bf08e81f5 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -8,7 +8,7 @@ import { DummyValue, GenerateSql } from 'src/decorators'; import { withExifInner, withFaces, withFiles } from 'src/entities/asset.entity'; import { AssetFileType } from 'src/enum'; import { StorageAsset } from 'src/types'; -import { asUuid } from 'src/utils/database'; +import { anyUuid, asUuid } from 'src/utils/database'; @Injectable() export class AssetJobRepository { @@ -149,6 +149,21 @@ export class AssetJobRepository { .executeTakeFirst(); } + getForSyncAssets(ids: string[]) { + return this.db + .selectFrom('assets') + .select([ + 'assets.id', + 'assets.isOffline', + 'assets.libraryId', + 'assets.originalPath', + 'assets.status', + 'assets.fileModifiedAt', + ]) + .where('assets.id', '=', anyUuid(ids)) + .execute(); + } + private storageTemplateAssetQuery() { return this.db .selectFrom('assets') diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index af79fb7c5f..7fb7056ba7 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -1,9 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { Insertable, Kysely, Selectable, UpdateResult, Updateable, sql } from 'kysely'; +import { Insertable, Kysely, NotNull, Selectable, UpdateResult, Updateable, sql } from 'kysely'; import { isEmpty, isUndefined, omitBy } from 'lodash'; import { InjectKysely } from 'nestjs-kysely'; +import { Stack } from 'src/database'; import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; +import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetEntity, hasPeople, @@ -23,7 +25,7 @@ import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum'; import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/repositories/search.repository'; import { anyUuid, asUuid, removeUndefinedKeys, unnest } from 'src/utils/database'; import { globToSqlPattern } from 'src/utils/misc'; -import { Paginated, PaginationOptions, paginationHelper } from 'src/utils/pagination'; +import { PaginationOptions, paginationHelper } from 'src/utils/pagination'; export type AssetStats = Record; @@ -141,12 +143,12 @@ export interface GetByIdsRelations { export interface DuplicateGroup { duplicateId: string; - assets: AssetEntity[]; + assets: MapAsset[]; } export interface DayOfYearAssets { yearsAgo: number; - assets: AssetEntity[]; + assets: MapAsset[]; } @Injectable() @@ -234,12 +236,12 @@ export class AssetRepository { .execute(); } - create(asset: Insertable): Promise { - return this.db.insertInto('assets').values(asset).returningAll().executeTakeFirst() as any as Promise; + create(asset: Insertable) { + return this.db.insertInto('assets').values(asset).returningAll().executeTakeFirstOrThrow(); } - createAll(assets: Insertable[]): Promise { - return this.db.insertInto('assets').values(assets).returningAll().execute() as any as Promise; + createAll(assets: Insertable[]) { + return this.db.insertInto('assets').values(assets).returningAll().execute(); } @GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] }) @@ -299,20 +301,13 @@ export class AssetRepository { @GenerateSql({ params: [[DummyValue.UUID]] }) @ChunkedArray() - getByIds(ids: string[]): Promise { - return ( - this.db - // - .selectFrom('assets') - .selectAll('assets') - .where('assets.id', '=', anyUuid(ids)) - .execute() as Promise - ); + getByIds(ids: string[]) { + return this.db.selectFrom('assets').selectAll('assets').where('assets.id', '=', anyUuid(ids)).execute(); } @GenerateSql({ params: [[DummyValue.UUID]] }) @ChunkedArray() - getByIdsWithAllRelations(ids: string[]): Promise { + getByIdsWithAllRelationsButStacks(ids: string[]) { return this.db .selectFrom('assets') .selectAll('assets') @@ -320,23 +315,8 @@ export class AssetRepository { .select(withTags) .$call(withExif) .leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId') - .leftJoinLateral( - (eb) => - eb - .selectFrom('assets as stacked') - .selectAll('asset_stack') - .select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets')) - .whereRef('stacked.stackId', '=', 'asset_stack.id') - .whereRef('stacked.id', '!=', 'asset_stack.primaryAssetId') - .where('stacked.deletedAt', 'is', null) - .where('stacked.isArchived', '=', false) - .groupBy('asset_stack.id') - .as('stacked_assets'), - (join) => join.on('asset_stack.id', 'is not', null), - ) - .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')) .where('assets.id', '=', anyUuid(ids)) - .execute() as any as Promise; + .execute(); } @GenerateSql({ params: [DummyValue.UUID] }) @@ -356,36 +336,29 @@ export class AssetRepository { return assets.map((asset) => asset.deviceAssetId); } - getByUserId( - pagination: PaginationOptions, - userId: string, - options: Omit = {}, - ): Paginated { + getByUserId(pagination: PaginationOptions, userId: string, options: Omit = {}) { return this.getAll(pagination, { ...options, userIds: [userId] }); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) - getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise { + getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string) { return this.db .selectFrom('assets') .selectAll('assets') .where('libraryId', '=', asUuid(libraryId)) .where('originalPath', '=', originalPath) .limit(1) - .executeTakeFirst() as any as Promise; + .executeTakeFirst(); } - async getAll( - pagination: PaginationOptions, - { orderDirection, ...options }: AssetSearchOptions = {}, - ): Paginated { + async getAll(pagination: PaginationOptions, { orderDirection, ...options }: AssetSearchOptions = {}) { const builder = searchAssetBuilder(this.db, options) .select(withFiles) .orderBy('assets.createdAt', orderDirection ?? 'asc') .limit(pagination.take + 1) .offset(pagination.skip ?? 0); const items = await builder.execute(); - return paginationHelper(items as any as AssetEntity[], pagination.take); + return paginationHelper(items, pagination.take); } /** @@ -420,23 +393,22 @@ export class AssetRepository { } @GenerateSql({ params: [DummyValue.UUID] }) - getById( - id: string, - { exifInfo, faces, files, library, owner, smartSearch, stack, tags }: GetByIdsRelations = {}, - ): Promise { + getById(id: string, { exifInfo, faces, files, library, owner, smartSearch, stack, tags }: GetByIdsRelations = {}) { return this.db .selectFrom('assets') .selectAll('assets') .where('assets.id', '=', asUuid(id)) .$if(!!exifInfo, withExif) - .$if(!!faces, (qb) => qb.select(faces?.person ? withFacesAndPeople : withFaces)) + .$if(!!faces, (qb) => qb.select(faces?.person ? withFacesAndPeople : withFaces).$narrowType<{ faces: NotNull }>()) .$if(!!library, (qb) => qb.select(withLibrary)) .$if(!!owner, (qb) => qb.select(withOwner)) .$if(!!smartSearch, withSmartSearch) .$if(!!stack, (qb) => qb .leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId') - .$if(!stack!.assets, (qb) => qb.select((eb) => eb.fn.toJson(eb.table('asset_stack')).as('stack'))) + .$if(!stack!.assets, (qb) => + qb.select((eb) => eb.fn.toJson(eb.table('asset_stack')).$castTo().as('stack')), + ) .$if(!!stack!.assets, (qb) => qb .leftJoinLateral( @@ -453,13 +425,13 @@ export class AssetRepository { .as('stacked_assets'), (join) => join.on('asset_stack.id', 'is not', null), ) - .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')), + .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo().as('stack')), ), ) .$if(!!files, (qb) => qb.select(withFiles)) .$if(!!tags, (qb) => qb.select(withTags)) .limit(1) - .executeTakeFirst() as any as Promise; + .executeTakeFirst(); } @GenerateSql({ params: [[DummyValue.UUID], { deviceId: DummyValue.STRING }] }) @@ -488,7 +460,7 @@ export class AssetRepository { .execute(); } - async update(asset: Updateable & { id: string }): Promise { + async update(asset: Updateable & { id: string }) { const value = omitBy(asset, isUndefined); delete value.id; if (!isEmpty(value)) { @@ -498,10 +470,10 @@ export class AssetRepository { .selectAll('assets') .$call(withExif) .$call((qb) => qb.select(withFacesAndPeople)) - .executeTakeFirst() as Promise; + .executeTakeFirst(); } - return this.getById(asset.id, { exifInfo: true, faces: { person: true } }) as Promise; + return this.getById(asset.id, { exifInfo: true, faces: { person: true } }); } async remove(asset: { id: string }): Promise { @@ -509,7 +481,7 @@ export class AssetRepository { } @GenerateSql({ params: [{ ownerId: DummyValue.UUID, libraryId: DummyValue.UUID, checksum: DummyValue.BUFFER }] }) - getByChecksum({ ownerId, libraryId, checksum }: AssetGetByChecksumOptions): Promise { + getByChecksum({ ownerId, libraryId, checksum }: AssetGetByChecksumOptions) { return this.db .selectFrom('assets') .selectAll('assets') @@ -517,7 +489,7 @@ export class AssetRepository { .where('checksum', '=', checksum) .$call((qb) => (libraryId ? qb.where('libraryId', '=', asUuid(libraryId)) : qb.where('libraryId', 'is', null))) .limit(1) - .executeTakeFirst() as Promise; + .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.UUID, [DummyValue.BUFFER]] }) @@ -544,7 +516,7 @@ export class AssetRepository { return asset?.id; } - findLivePhotoMatch(options: LivePhotoSearchOptions): Promise { + findLivePhotoMatch(options: LivePhotoSearchOptions) { const { ownerId, otherAssetId, livePhotoCID, type } = options; return this.db .selectFrom('assets') @@ -555,7 +527,7 @@ export class AssetRepository { .where('type', '=', type) .where('exif.livePhotoCID', '=', livePhotoCID) .limit(1) - .executeTakeFirst() as Promise; + .executeTakeFirst(); } @GenerateSql( @@ -564,7 +536,7 @@ export class AssetRepository { params: [DummyValue.PAGINATION, property], })), ) - async getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated { + async getWithout(pagination: PaginationOptions, property: WithoutProperty) { const items = await this.db .selectFrom('assets') .selectAll('assets') @@ -626,7 +598,7 @@ export class AssetRepository { .orderBy('createdAt') .execute(); - return paginationHelper(items as any as AssetEntity[], pagination.take); + return paginationHelper(items, pagination.take); } getStatistics(ownerId: string, { isArchived, isFavorite, isTrashed }: AssetStatsOptions): Promise { @@ -645,7 +617,7 @@ export class AssetRepository { .executeTakeFirstOrThrow(); } - getRandom(userIds: string[], take: number): Promise { + getRandom(userIds: string[], take: number) { return this.db .selectFrom('assets') .selectAll('assets') @@ -655,7 +627,7 @@ export class AssetRepository { .where('deletedAt', 'is', null) .orderBy((eb) => eb.fn('random')) .limit(take) - .execute() as any as Promise; + .execute(); } @GenerateSql({ params: [{ size: TimeBucketSize.MONTH }] }) @@ -708,7 +680,7 @@ export class AssetRepository { } @GenerateSql({ params: [DummyValue.TIME_BUCKET, { size: TimeBucketSize.MONTH, withStacked: true }] }) - async getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise { + async getTimeBucket(timeBucket: string, options: TimeBucketOptions) { return this.db .selectFrom('assets') .selectAll('assets') @@ -741,7 +713,7 @@ export class AssetRepository { .as('stacked_assets'), (join) => join.on('asset_stack.id', 'is not', null), ) - .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')), + .select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo()).as('stack')), ) .$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!)) .$if(options.isDuplicate !== undefined, (qb) => @@ -753,11 +725,11 @@ export class AssetRepository { .where('assets.isVisible', '=', true) .where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, '')) .orderBy('assets.localDateTime', options.order ?? 'desc') - .execute() as any as Promise; + .execute(); } @GenerateSql({ params: [DummyValue.UUID] }) - getDuplicates(userId: string): Promise { + getDuplicates(userId: string) { return ( this.db .with('duplicates', (qb) => @@ -774,9 +746,15 @@ export class AssetRepository { (join) => join.onTrue(), ) .select('assets.duplicateId') - .select((eb) => eb.fn('jsonb_agg', [eb.table('asset')]).as('assets')) + .select((eb) => + eb + .fn('jsonb_agg', [eb.table('asset')]) + .$castTo() + .as('assets'), + ) .where('assets.ownerId', '=', asUuid(userId)) .where('assets.duplicateId', 'is not', null) + .$narrowType<{ duplicateId: NotNull }>() .where('assets.deletedAt', 'is', null) .where('assets.isVisible', '=', true) .where('assets.stackId', 'is', null) @@ -801,7 +779,7 @@ export class AssetRepository { .where(({ not, exists }) => not(exists((eb) => eb.selectFrom('unique').whereRef('unique.duplicateId', '=', 'duplicates.duplicateId'))), ) - .execute() as any as Promise + .execute() ); } @@ -845,7 +823,7 @@ export class AssetRepository { }, ], }) - getAllForUserFullSync(options: AssetFullSyncOptions): Promise { + getAllForUserFullSync(options: AssetFullSyncOptions) { const { ownerId, lastId, updatedUntil, limit } = options; return this.db .selectFrom('assets') @@ -863,18 +841,18 @@ export class AssetRepository { .as('stacked_assets'), (join) => join.on('asset_stack.id', 'is not', null), ) - .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')) + .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo().as('stack')) .where('assets.ownerId', '=', asUuid(ownerId)) .where('assets.isVisible', '=', true) .where('assets.updatedAt', '<=', updatedUntil) .$if(!!lastId, (qb) => qb.where('assets.id', '>', lastId!)) .orderBy('assets.id') .limit(limit) - .execute() as any as Promise; + .execute(); } @GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE, limit: 100 }] }) - async getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise { + async getChangedDeltaSync(options: AssetDeltaSyncOptions) { return this.db .selectFrom('assets') .selectAll('assets') @@ -891,12 +869,12 @@ export class AssetRepository { .as('stacked_assets'), (join) => join.on('asset_stack.id', 'is not', null), ) - .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')) + .select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo()).as('stack')) .where('assets.ownerId', '=', anyUuid(options.userIds)) .where('assets.isVisible', '=', true) .where('assets.updatedAt', '>', options.updatedAfter) .limit(options.limit) - .execute() as any as Promise; + .execute(); } async upsertFile(file: Pick, 'assetId' | 'path' | 'type'>): Promise { diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index c86ae8f60e..95c350fe34 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -1,13 +1,13 @@ import { Injectable } from '@nestjs/common'; -import { Kysely, OrderByDirection, sql } from 'kysely'; +import { Kysely, OrderByDirection, Selectable, sql } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { randomUUID } from 'node:crypto'; -import { DB } from 'src/db'; +import { DB, Exif } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { AssetEntity, searchAssetBuilder } from 'src/entities/asset.entity'; +import { MapAsset } from 'src/dtos/asset-response.dto'; +import { searchAssetBuilder } from 'src/entities/asset.entity'; import { AssetStatus, AssetType } from 'src/enum'; import { anyUuid, asUuid } from 'src/utils/database'; -import { Paginated } from 'src/utils/pagination'; import { isValidInteger } from 'src/validation'; export interface SearchResult { @@ -216,7 +216,7 @@ export class SearchRepository { }, ], }) - async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated { + async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions) { const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirection; const items = await searchAssetBuilder(this.db, options) .orderBy('assets.fileCreatedAt', orderDirection) @@ -225,7 +225,7 @@ export class SearchRepository { .execute(); const hasNextPage = items.length > pagination.size; items.splice(pagination.size); - return { items: items as any as AssetEntity[], hasNextPage }; + return { items, hasNextPage }; } @GenerateSql({ @@ -240,7 +240,7 @@ export class SearchRepository { }, ], }) - async searchRandom(size: number, options: AssetSearchOptions): Promise { + async searchRandom(size: number, options: AssetSearchOptions) { const uuid = randomUUID(); const builder = searchAssetBuilder(this.db, options); const lessThan = builder @@ -251,8 +251,8 @@ export class SearchRepository { .where('assets.id', '>', uuid) .orderBy(sql`random()`) .limit(size); - const { rows } = await sql`${lessThan} union all ${greaterThan} limit ${size}`.execute(this.db); - return rows as any as AssetEntity[]; + const { rows } = await sql`${lessThan} union all ${greaterThan} limit ${size}`.execute(this.db); + return rows; } @GenerateSql({ @@ -268,17 +268,17 @@ export class SearchRepository { }, ], }) - async searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated { + async searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions) { if (!isValidInteger(pagination.size, { min: 1, max: 1000 })) { throw new Error(`Invalid value for 'size': ${pagination.size}`); } - const items = (await searchAssetBuilder(this.db, options) + const items = await searchAssetBuilder(this.db, options) .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') .orderBy(sql`smart_search.embedding <=> ${options.embedding}`) .limit(pagination.size + 1) .offset((pagination.page - 1) * pagination.size) - .execute()) as any as AssetEntity[]; + .execute(); const hasNextPage = items.length > pagination.size; items.splice(pagination.size); @@ -392,7 +392,7 @@ export class SearchRepository { } @GenerateSql({ params: [[DummyValue.UUID]] }) - getAssetsByCity(userIds: string[]): Promise { + getAssetsByCity(userIds: string[]) { return this.db .withRecursive('cte', (qb) => { const base = qb @@ -434,9 +434,14 @@ export class SearchRepository { .innerJoin('exif', 'assets.id', 'exif.assetId') .innerJoin('cte', 'assets.id', 'cte.assetId') .selectAll('assets') - .select((eb) => eb.fn('to_jsonb', [eb.table('exif')]).as('exifInfo')) + .select((eb) => + eb + .fn('to_jsonb', [eb.table('exif')]) + .$castTo>() + .as('exifInfo'), + ) .orderBy('exif.city') - .execute() as any as Promise; + .execute(); } async upsert(assetId: string, embedding: string): Promise { diff --git a/server/src/repositories/shared-link.repository.ts b/server/src/repositories/shared-link.repository.ts index 272d7f3794..67a97dc2d5 100644 --- a/server/src/repositories/shared-link.repository.ts +++ b/server/src/repositories/shared-link.repository.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { Insertable, Kysely, sql, Updateable } from 'kysely'; +import { Insertable, Kysely, NotNull, sql, Updateable } from 'kysely'; import { jsonObjectFrom } from 'kysely/helpers/postgres'; import _ from 'lodash'; import { InjectKysely } from 'nestjs-kysely'; -import { columns } from 'src/database'; +import { Album, columns } from 'src/database'; import { DB, SharedLinks } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { MapAsset } from 'src/dtos/asset-response.dto'; import { SharedLinkType } from 'src/enum'; export type SharedLinkSearchOptions = { @@ -19,7 +19,7 @@ export class SharedLinkRepository { constructor(@InjectKysely() private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) - get(userId: string, id: string): Promise { + get(userId: string, id: string) { return this.db .selectFrom('shared_links') .selectAll('shared_links') @@ -87,18 +87,23 @@ export class SharedLinkRepository { .as('album'), (join) => join.onTrue(), ) - .select((eb) => eb.fn.coalesce(eb.fn.jsonAgg('a').filterWhere('a.id', 'is not', null), sql`'[]'`).as('assets')) + .select((eb) => + eb.fn + .coalesce(eb.fn.jsonAgg('a').filterWhere('a.id', 'is not', null), sql`'[]'`) + .$castTo() + .as('assets'), + ) .groupBy(['shared_links.id', sql`"album".*`]) - .select((eb) => eb.fn.toJson('album').as('album')) + .select((eb) => eb.fn.toJson('album').$castTo().as('album')) .where('shared_links.id', '=', id) .where('shared_links.userId', '=', userId) .where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)])) .orderBy('shared_links.createdAt', 'desc') - .executeTakeFirst() as Promise; + .executeTakeFirst(); } @GenerateSql({ params: [{ userId: DummyValue.UUID, albumId: DummyValue.UUID }] }) - getAll({ userId, albumId }: SharedLinkSearchOptions): Promise { + getAll({ userId, albumId }: SharedLinkSearchOptions) { return this.db .selectFrom('shared_links') .selectAll('shared_links') @@ -115,6 +120,7 @@ export class SharedLinkRepository { (join) => join.onTrue(), ) .select('assets.assets') + .$narrowType<{ assets: NotNull }>() .leftJoinLateral( (eb) => eb @@ -152,12 +158,12 @@ export class SharedLinkRepository { .as('album'), (join) => join.onTrue(), ) - .select((eb) => eb.fn.toJson('album').as('album')) + .select((eb) => eb.fn.toJson('album').$castTo().as('album')) .where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)])) .$if(!!albumId, (eb) => eb.where('shared_links.albumId', '=', albumId!)) .orderBy('shared_links.createdAt', 'desc') .distinctOn(['shared_links.createdAt']) - .execute() as unknown as Promise; + .execute(); } @GenerateSql({ params: [DummyValue.BUFFER] }) @@ -177,7 +183,7 @@ export class SharedLinkRepository { .executeTakeFirst(); } - async create(entity: Insertable & { assetIds?: string[] }): Promise { + async create(entity: Insertable & { assetIds?: string[] }) { const { id } = await this.db .insertInto('shared_links') .values(_.omit(entity, 'assetIds')) @@ -194,7 +200,7 @@ export class SharedLinkRepository { return this.getSharedLinks(id); } - async update(entity: Updateable & { id: string; assetIds?: string[] }): Promise { + async update(entity: Updateable & { id: string; assetIds?: string[] }) { const { id } = await this.db .updateTable('shared_links') .set(_.omit(entity, 'assets', 'album', 'assetIds')) @@ -212,8 +218,8 @@ export class SharedLinkRepository { return this.getSharedLinks(id); } - async remove(entity: SharedLinkEntity): Promise { - await this.db.deleteFrom('shared_links').where('shared_links.id', '=', entity.id).execute(); + async remove(id: string): Promise { + await this.db.deleteFrom('shared_links').where('shared_links.id', '=', id).execute(); } private getSharedLinks(id: string) { @@ -236,9 +242,12 @@ export class SharedLinkRepository { (join) => join.onTrue(), ) .select((eb) => - eb.fn.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`).as('assets'), + eb.fn + .coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`) + .$castTo() + .as('assets'), ) .groupBy('shared_links.id') - .executeTakeFirstOrThrow() as Promise; + .executeTakeFirstOrThrow(); } } diff --git a/server/src/repositories/stack.repository.ts b/server/src/repositories/stack.repository.ts index 75dd9b497f..c9d69fb37f 100644 --- a/server/src/repositories/stack.repository.ts +++ b/server/src/repositories/stack.repository.ts @@ -5,7 +5,6 @@ import { InjectKysely } from 'nestjs-kysely'; import { columns } from 'src/database'; import { AssetStack, DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { AssetEntity } from 'src/entities/asset.entity'; import { asUuid } from 'src/utils/database'; export interface StackSearch { @@ -36,9 +35,7 @@ const withAssets = (eb: ExpressionBuilder, withTags = false) .select((eb) => eb.fn.toJson('exifInfo').as('exifInfo')) .where('assets.deletedAt', 'is', null) .whereRef('assets.stackId', '=', 'asset_stack.id'), - ) - .$castTo() - .as('assets'); + ).as('assets'); }; @Injectable() diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index eac000005b..1c612de8c0 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -6,15 +6,15 @@ import { AlbumStatisticsResponseDto, CreateAlbumDto, GetAlbumsDto, - UpdateAlbumDto, - UpdateAlbumUserDto, mapAlbum, + MapAlbumDto, mapAlbumWithAssets, mapAlbumWithoutAssets, + UpdateAlbumDto, + UpdateAlbumUserDto, } from 'src/dtos/album.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AlbumEntity } from 'src/entities/album.entity'; import { Permission } from 'src/enum'; import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository'; import { BaseService } from 'src/services/base.service'; @@ -39,7 +39,7 @@ export class AlbumService extends BaseService { async getAll({ user: { id: ownerId } }: AuthDto, { assetId, shared }: GetAlbumsDto): Promise { await this.albumRepository.updateThumbnails(); - let albums: AlbumEntity[]; + let albums: MapAlbumDto[]; if (assetId) { albums = await this.albumRepository.getByAssetId(ownerId, assetId); } else if (shared === true) { diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 0c1bbc3cee..a49230f852 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -8,6 +8,7 @@ import { Stats } from 'node:fs'; import { AssetFile } from 'src/database'; import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto'; import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto'; +import { MapAsset } from 'src/dtos/asset-response.dto'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; import { AssetFileType, AssetStatus, AssetType, CacheControl, JobName } from 'src/enum'; import { AuthRequest } from 'src/middleware/auth.guard'; @@ -173,7 +174,7 @@ const assetEntity = Object.freeze({ }, livePhotoVideoId: null, sidecarPath: null, -}) as AssetEntity; +} as MapAsset); const existingAsset = Object.freeze({ ...assetEntity, @@ -182,18 +183,18 @@ const existingAsset = Object.freeze({ checksum: Buffer.from('_getExistingAsset', 'utf8'), libraryId: 'libraryId', originalFileName: 'existing-filename.jpeg', -}) as AssetEntity; +}) as MapAsset; const sidecarAsset = Object.freeze({ ...existingAsset, sidecarPath: 'sidecar-path', checksum: Buffer.from('_getExistingAssetWithSideCar', 'utf8'), -}) as AssetEntity; +}) as MapAsset; const copiedAsset = Object.freeze({ id: 'copied-asset', originalPath: 'copied-path', -}) as AssetEntity; +}) as MapAsset; describe(AssetMediaService.name, () => { let sut: AssetMediaService; diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 2929950f4d..de40d8b304 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -2,6 +2,7 @@ import { BadRequestException, Injectable, InternalServerErrorException, NotFound import { extname } from 'node:path'; import sanitize from 'sanitize-filename'; import { StorageCore } from 'src/cores/storage.core'; +import { Asset } from 'src/database'; import { AssetBulkUploadCheckResponseDto, AssetMediaResponseDto, @@ -20,7 +21,7 @@ import { UploadFieldName, } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; +import { ASSET_CHECKSUM_CONSTRAINT } from 'src/entities/asset.entity'; import { AssetStatus, AssetType, CacheControl, JobName, Permission, StorageFolder } from 'src/enum'; import { AuthRequest } from 'src/middleware/auth.guard'; import { BaseService } from 'src/services/base.service'; @@ -212,7 +213,7 @@ export class AssetMediaService extends BaseService { const asset = await this.findOrFail(id); const size = dto.size ?? AssetMediaSize.THUMBNAIL; - const { thumbnailFile, previewFile, fullsizeFile } = getAssetFiles(asset.files); + const { thumbnailFile, previewFile, fullsizeFile } = getAssetFiles(asset.files ?? []); let filepath = previewFile?.path; if (size === AssetMediaSize.THUMBNAIL && thumbnailFile) { filepath = thumbnailFile.path; @@ -375,7 +376,7 @@ export class AssetMediaService extends BaseService { * Uses only vital properties excluding things like: stacks, faces, smart search info, etc, * and then queues a METADATA_EXTRACTION job. */ - private async createCopy(asset: AssetEntity): Promise { + private async createCopy(asset: Omit) { const created = await this.assetRepository.create({ ownerId: asset.ownerId, originalPath: asset.originalPath, @@ -398,12 +399,7 @@ export class AssetMediaService extends BaseService { return created; } - private async create( - ownerId: string, - dto: AssetMediaCreateDto, - file: UploadFile, - sidecarFile?: UploadFile, - ): Promise { + private async create(ownerId: string, dto: AssetMediaCreateDto, file: UploadFile, sidecarFile?: UploadFile) { const asset = await this.assetRepository.create({ ownerId, libraryId: null, @@ -444,7 +440,7 @@ export class AssetMediaService extends BaseService { } } - private async findOrFail(id: string): Promise { + private async findOrFail(id: string) { const asset = await this.assetRepository.getById(id, { files: true }); if (!asset) { throw new NotFoundException('Asset not found'); diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 5fc4984b62..a3f536ac33 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -1,8 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { mapAsset } from 'src/dtos/asset-response.dto'; +import { MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto'; -import { AssetEntity } from 'src/entities/asset.entity'; import { AssetStatus, AssetType, JobName, JobStatus } from 'src/enum'; import { AssetStats } from 'src/repositories/asset.repository'; import { AssetService } from 'src/services/asset.service'; @@ -35,7 +34,7 @@ describe(AssetService.name, () => { expect(sut).toBeDefined(); }); - const mockGetById = (assets: AssetEntity[]) => { + const mockGetById = (assets: MapAsset[]) => { mocks.asset.getById.mockImplementation((assetId) => Promise.resolve(assets.find((asset) => asset.id === assetId))); }; @@ -608,7 +607,7 @@ describe(AssetService.name, () => { mocks.asset.getById.mockResolvedValue({ ...assetStub.primaryImage, stack: { ...assetStub.primaryImage.stack, assets: assetStub.primaryImage.stack!.assets.slice(0, 2) }, - } as AssetEntity); + }); await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true }); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 1ded79680b..16f60907ba 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -5,6 +5,7 @@ import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { OnJob } from 'src/decorators'; import { AssetResponseDto, + MapAsset, MemoryLaneResponseDto, SanitizedAssetResponseDto, mapAsset, @@ -20,7 +21,6 @@ import { } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryLaneDto } from 'src/dtos/search.dto'; -import { AssetEntity } from 'src/entities/asset.entity'; import { AssetStatus, JobName, JobStatus, Permission, QueueName } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { ISidecarWriteJob, JobItem, JobOf } from 'src/types'; @@ -43,7 +43,7 @@ export class AssetService extends BaseService { yearsAgo, // TODO move this to clients title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} ago`, - assets: assets.map((asset) => mapAsset(asset as unknown as AssetEntity, { auth })), + assets: assets.map((asset) => mapAsset(asset, { auth })), }; }); } @@ -105,7 +105,7 @@ export class AssetService extends BaseService { const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto; const repos = { asset: this.assetRepository, event: this.eventRepository }; - let previousMotion: AssetEntity | null = null; + let previousMotion: MapAsset | null = null; if (rest.livePhotoVideoId) { await onBeforeLink(repos, { userId: auth.user.id, livePhotoVideoId: rest.livePhotoVideoId }); } else if (rest.livePhotoVideoId === null) { @@ -233,7 +233,7 @@ export class AssetService extends BaseService { } } - const { fullsizeFile, previewFile, thumbnailFile } = getAssetFiles(asset.files); + const { fullsizeFile, previewFile, thumbnailFile } = getAssetFiles(asset.files ?? []); const files = [thumbnailFile?.path, previewFile?.path, fullsizeFile?.path, asset.encodedVideoPath]; if (deleteOnDisk) { diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index 9f1ac3d4ce..c504b1a305 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -68,7 +68,7 @@ export class DuplicateService extends BaseService { return JobStatus.SKIPPED; } - const previewFile = getAssetFile(asset.files, AssetFileType.PREVIEW); + const previewFile = getAssetFile(asset.files || [], AssetFileType.PREVIEW); if (!previewFile) { this.logger.warn(`Asset ${id} is missing preview image`); return JobStatus.FAILED; diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index 134a86b69f..baac0af428 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -285,9 +285,9 @@ describe(JobService.name, () => { it(`should queue ${jobs.length} jobs when a ${item.name} job finishes successfully`, async () => { if (item.name === JobName.GENERATE_THUMBNAILS && item.data.source === 'upload') { if (item.data.id === 'asset-live-image') { - mocks.asset.getByIdsWithAllRelations.mockResolvedValue([assetStub.livePhotoStillAsset]); + mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([assetStub.livePhotoStillAsset as any]); } else { - mocks.asset.getByIdsWithAllRelations.mockResolvedValue([assetStub.livePhotoMotionAsset]); + mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([assetStub.livePhotoMotionAsset as any]); } } diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index 2f180edd40..edd018d7b1 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -254,7 +254,7 @@ export class JobService extends BaseService { case JobName.METADATA_EXTRACTION: { if (item.data.source === 'sidecar-write') { - const [asset] = await this.assetRepository.getByIdsWithAllRelations([item.data.id]); + const [asset] = await this.assetRepository.getByIdsWithAllRelationsButStacks([item.data.id]); if (asset) { this.eventRepository.clientSend('on_asset_update', asset.ownerId, mapAsset(asset)); } @@ -284,7 +284,7 @@ export class JobService extends BaseService { break; } - const [asset] = await this.assetRepository.getByIdsWithAllRelations([item.data.id]); + const [asset] = await this.assetRepository.getByIdsWithAllRelationsButStacks([item.data.id]); if (!asset) { this.logger.warn(`Could not find asset ${item.data.id} after generating thumbnails`); break; diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index aef02b7244..15b150f551 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -350,7 +350,7 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.asset.getByIds.mockResolvedValue([assetStub.external]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); mocks.storage.stat.mockRejectedValue(new Error('ENOENT, no such file or directory')); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); @@ -371,7 +371,7 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.asset.getByIds.mockResolvedValue([assetStub.external]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); mocks.storage.stat.mockRejectedValue(new Error('Could not read file')); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); @@ -392,7 +392,7 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.asset.getByIds.mockResolvedValue([assetStub.trashedOffline]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); mocks.storage.stat.mockRejectedValue(new Error('Could not read file')); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); @@ -410,7 +410,7 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.asset.getByIds.mockResolvedValue([assetStub.trashedOffline]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); @@ -431,7 +431,7 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.asset.getByIds.mockResolvedValue([assetStub.trashedOffline]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); @@ -451,7 +451,7 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.asset.getByIds.mockResolvedValue([assetStub.trashedOffline]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); @@ -471,7 +471,7 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.asset.getByIds.mockResolvedValue([assetStub.external]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); @@ -489,7 +489,7 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.asset.getByIds.mockResolvedValue([assetStub.trashedOffline]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); mocks.storage.stat.mockResolvedValue({ mtime: assetStub.trashedOffline.fileModifiedAt } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); @@ -518,7 +518,7 @@ describe(LibraryService.name, () => { const mtime = new Date(assetStub.external.fileModifiedAt.getDate() + 1); - mocks.asset.getByIds.mockResolvedValue([assetStub.external]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); mocks.storage.stat.mockResolvedValue({ mtime } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 8cc2cf48ff..2add5f484b 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -18,7 +18,6 @@ import { ValidateLibraryImportPathResponseDto, ValidateLibraryResponseDto, } from 'src/dtos/library.dto'; -import { AssetEntity } from 'src/entities/asset.entity'; import { AssetStatus, AssetType, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { AssetSyncResult } from 'src/repositories/library.repository'; @@ -467,7 +466,7 @@ export class LibraryService extends BaseService { @OnJob({ name: JobName.LIBRARY_SYNC_ASSETS, queue: QueueName.LIBRARY }) async handleSyncAssets(job: JobOf): Promise { - const assets = await this.assetRepository.getByIds(job.assetIds); + const assets = await this.assetJobRepository.getForSyncAssets(job.assetIds); const assetIdsToOffline: string[] = []; const trashedAssetIdsToOffline: string[] = []; @@ -561,7 +560,16 @@ export class LibraryService extends BaseService { return JobStatus.SUCCESS; } - private checkExistingAsset(asset: AssetEntity, stat: Stats | null): AssetSyncResult { + private checkExistingAsset( + asset: { + isOffline: boolean; + libraryId: string | null; + originalPath: string; + status: AssetStatus; + fileModifiedAt: Date; + }, + stat: Stats | null, + ): AssetSyncResult { if (!stat) { // File not found on disk or permission error if (asset.isOffline) { diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index ca1277a8c8..e412b1c31f 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -3,7 +3,7 @@ import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; import { constants } from 'node:fs/promises'; import { defaults } from 'src/config'; -import { AssetEntity } from 'src/entities/asset.entity'; +import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetType, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum'; import { WithoutProperty } from 'src/repositories/asset.repository'; import { ImmichTags } from 'src/repositories/metadata.repository'; @@ -549,7 +549,6 @@ describe(MetadataService.name, () => { livePhotoVideoId: null, libraryId: null, }); - mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]); mocks.storage.stat.mockResolvedValue({ size: 123_456, mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, @@ -719,7 +718,7 @@ describe(MetadataService.name, () => { }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); mocks.asset.create.mockImplementation( - (asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset }) as Promise, + (asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset }) as Promise, ); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); @@ -1394,7 +1393,7 @@ describe(MetadataService.name, () => { }); it('should set sidecar path if exists (sidecar named photo.xmp)', async () => { - mocks.asset.getByIds.mockResolvedValue([assetStub.sidecarWithoutExt]); + mocks.asset.getByIds.mockResolvedValue([assetStub.sidecarWithoutExt as any]); mocks.storage.checkFileExists.mockResolvedValueOnce(false); mocks.storage.checkFileExists.mockResolvedValueOnce(true); @@ -1446,7 +1445,7 @@ describe(MetadataService.name, () => { describe('handleSidecarDiscovery', () => { it('should skip hidden assets', async () => { - mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]); + mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset as any]); await sut.handleSidecarDiscovery({ id: assetStub.livePhotoMotionAsset.id }); expect(mocks.storage.checkFileExists).not.toHaveBeenCalled(); }); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index ab62c38ed0..faf146a2be 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -271,7 +271,7 @@ export class MetadataService extends BaseService { ]; if (this.isMotionPhoto(asset, exifTags)) { - promises.push(this.applyMotionPhotos(asset as unknown as Asset, exifTags, dates, stats)); + promises.push(this.applyMotionPhotos(asset, exifTags, dates, stats)); } if (isFaceImportEnabled(metadata) && this.hasTaggedFaces(exifTags)) { diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index a413c688f0..66d68857a0 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -2,7 +2,8 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/comm import { Insertable, Updateable } from 'kysely'; import { FACE_THUMBNAIL_SIZE, JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; -import { AssetFaces, FaceSearch, Person } from 'src/db'; +import { Person } from 'src/database'; +import { AssetFaces, FaceSearch } from 'src/db'; import { Chunked, OnJob } from 'src/decorators'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -315,6 +316,7 @@ export class PersonService extends BaseService { const facesToAdd: (Insertable & { id: string })[] = []; const embeddings: FaceSearch[] = []; const mlFaceIds = new Set(); + for (const face of asset.faces) { if (face.sourceType === SourceType.MACHINE_LEARNING) { mlFaceIds.add(face.id); @@ -477,7 +479,7 @@ export class PersonService extends BaseService { embedding: face.faceSearch.embedding, maxDistance: machineLearning.facialRecognition.maxDistance, numResults: machineLearning.facialRecognition.minFaces, - minBirthDate: face.asset.fileCreatedAt, + minBirthDate: face.asset.fileCreatedAt ?? undefined, }); // `matches` also includes the face itself @@ -503,7 +505,7 @@ export class PersonService extends BaseService { maxDistance: machineLearning.facialRecognition.maxDistance, numResults: 1, hasPerson: true, - minBirthDate: face.asset.fileCreatedAt, + minBirthDate: face.asset.fileCreatedAt ?? undefined, }); if (matchWithPerson.length > 0) { diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index 51c6b55e11..18699edf9a 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -45,7 +45,7 @@ describe(SearchService.name, () => { fieldName: 'exifInfo.city', items: [{ value: 'test-city', data: assetStub.withLocation.id }], }); - mocks.asset.getByIdsWithAllRelations.mockResolvedValue([assetStub.withLocation]); + mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([assetStub.withLocation]); const expectedResponse = [ { fieldName: 'exifInfo.city', items: [{ value: 'test-city', data: mapAsset(assetStub.withLocation) }] }, ]; diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index 1c0c0ad490..442d49136c 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { AssetMapOptions, AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetMapOptions, AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { mapPerson, PersonResponseDto } from 'src/dtos/person.dto'; import { @@ -14,7 +14,6 @@ import { SearchSuggestionType, SmartSearchDto, } from 'src/dtos/search.dto'; -import { AssetEntity } from 'src/entities/asset.entity'; import { AssetOrder } from 'src/enum'; import { SearchExploreItem } from 'src/repositories/search.repository'; import { BaseService } from 'src/services/base.service'; @@ -36,7 +35,7 @@ export class SearchService extends BaseService { async getExploreData(auth: AuthDto): Promise[]> { const options = { maxFields: 12, minAssetsPerField: 5 }; const cities = await this.assetRepository.getAssetIdByCity(auth.user.id, options); - const assets = await this.assetRepository.getByIdsWithAllRelations(cities.items.map(({ data }) => data)); + const assets = await this.assetRepository.getByIdsWithAllRelationsButStacks(cities.items.map(({ data }) => data)); const items = assets.map((asset) => ({ value: asset.exifInfo!.city!, data: mapAsset(asset, { auth }) })); return [{ fieldName: cities.fieldName, items }]; } @@ -139,7 +138,7 @@ export class SearchService extends BaseService { return [auth.user.id, ...partnerIds]; } - private mapResponse(assets: AssetEntity[], nextPage: string | null, options: AssetMapOptions): SearchResponseDto { + private mapResponse(assets: MapAsset[], nextPage: string | null, options: AssetMapOptions): SearchResponseDto { return { albums: { total: 0, count: 0, items: [], facets: [] }, assets: { diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts index 4d084d6e67..66a0a925c7 100644 --- a/server/src/services/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -244,7 +244,7 @@ describe(SharedLinkService.name, () => { await sut.remove(authStub.user1, sharedLinkStub.valid.id); expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id); - expect(mocks.sharedLink.remove).toHaveBeenCalledWith(sharedLinkStub.valid); + expect(mocks.sharedLink.remove).toHaveBeenCalledWith(sharedLinkStub.valid.id); }); }); @@ -333,8 +333,7 @@ describe(SharedLinkService.name, () => { }); it('should return metadata tags with a default image path if the asset id is not set', async () => { - mocks.sharedLink.get.mockResolvedValue({ ...sharedLinkStub.individual, album: undefined, assets: [] }); - + mocks.sharedLink.get.mockResolvedValue({ ...sharedLinkStub.individual, album: null, assets: [] }); await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({ description: '0 shared photos & videos', imageUrl: `https://my.immich.app/feature-panel.png`, diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 95f8cef5f8..17f1b974d2 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -1,4 +1,5 @@ import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common'; +import { SharedLink } from 'src/database'; import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -11,7 +12,6 @@ import { SharedLinkResponseDto, SharedLinkSearchDto, } from 'src/dtos/shared-link.dto'; -import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { Permission, SharedLinkType } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { getExternalDomain, OpenGraphTags } from 'src/utils/misc'; @@ -98,7 +98,7 @@ export class SharedLinkService extends BaseService { async remove(auth: AuthDto, id: string): Promise { const sharedLink = await this.findOrFail(auth.user.id, id); - await this.sharedLinkRepository.remove(sharedLink); + await this.sharedLinkRepository.remove(sharedLink.id); } // TODO: replace `userId` with permissions and access control checks @@ -182,7 +182,7 @@ export class SharedLinkService extends BaseService { const config = await this.getConfig({ withCache: true }); const sharedLink = await this.findOrFail(auth.sharedLink.userId, auth.sharedLink.id); const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id; - const assetCount = sharedLink.assets.length > 0 ? sharedLink.assets.length : sharedLink.album?.assets.length || 0; + const assetCount = sharedLink.assets.length > 0 ? sharedLink.assets.length : sharedLink.album?.assets?.length || 0; const imagePath = assetId ? `/api/assets/${assetId}/thumbnail?key=${sharedLink.key.toString('base64url')}` : '/feature-panel.png'; @@ -194,11 +194,11 @@ export class SharedLinkService extends BaseService { }; } - private mapToSharedLink(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) { + private mapToSharedLink(sharedLink: SharedLink, { withExif }: { withExif: boolean }) { return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithoutMetadata(sharedLink); } - private validateAndRefreshToken(sharedLink: SharedLinkEntity, dto: SharedLinkPasswordDto): string { + private validateAndRefreshToken(sharedLink: SharedLink, dto: SharedLinkPasswordDto): string { const token = this.cryptoRepository.hashSha256(`${sharedLink.id}-${sharedLink.password}`); const sharedLinkTokens = dto.token?.split(',') || []; if (sharedLink.password !== dto.password && !sharedLinkTokens.includes(token)) { diff --git a/server/src/services/sync.service.spec.ts b/server/src/services/sync.service.spec.ts index 5f7357c64d..5b50340a9f 100644 --- a/server/src/services/sync.service.spec.ts +++ b/server/src/services/sync.service.spec.ts @@ -1,5 +1,4 @@ import { mapAsset } from 'src/dtos/asset-response.dto'; -import { AssetEntity } from 'src/entities/asset.entity'; import { SyncService } from 'src/services/sync.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; @@ -63,7 +62,7 @@ describe(SyncService.name, () => { it('should return a response requiring a full sync when there are too many changes', async () => { mocks.partner.getAll.mockResolvedValue([]); mocks.asset.getChangedDeltaSync.mockResolvedValue( - Array.from({ length: 10_000 }).fill(assetStub.image), + Array.from({ length: 10_000 }).fill(assetStub.image), ); await expect( sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), diff --git a/server/src/services/view.service.ts b/server/src/services/view.service.ts index 5871b04b32..9d1ee3cf89 100644 --- a/server/src/services/view.service.ts +++ b/server/src/services/view.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetEntity } from 'src/entities/asset.entity'; import { BaseService } from 'src/services/base.service'; @Injectable() @@ -12,6 +11,6 @@ export class ViewService extends BaseService { async getAssetsByOriginalPath(auth: AuthDto, path: string): Promise { const assets = await this.viewRepository.getAssetsByOriginalPath(auth.user.id, path); - return assets.map((asset) => mapAsset(asset as unknown as AssetEntity, { auth })); + return assets.map((asset) => mapAsset(asset, { auth })); } } diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index a15f006cda..8905f84165 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -13,11 +13,8 @@ import { PartnerRepository } from 'src/repositories/partner.repository'; import { IBulkAsset, ImmichFile, UploadFile } from 'src/types'; import { checkAccess } from 'src/utils/access'; -export const getAssetFile = ( - files: T[], - type: AssetFileType | GeneratedImageType, -) => { - return (files || []).find((file) => file.type === type); +export const getAssetFile = (files: AssetFile[], type: AssetFileType | GeneratedImageType) => { + return files.find((file) => file.type === type); }; export const getAssetFiles = (files: AssetFile[]) => ({ diff --git a/server/test/fixtures/album.stub.ts b/server/test/fixtures/album.stub.ts index 5a1c141512..fd6a8678a0 100644 --- a/server/test/fixtures/album.stub.ts +++ b/server/test/fixtures/album.stub.ts @@ -1,11 +1,10 @@ -import { AlbumEntity } from 'src/entities/album.entity'; import { AlbumUserRole, AssetOrder } from 'src/enum'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; export const albumStub = { - empty: Object.freeze({ + empty: Object.freeze({ id: 'album-1', albumName: 'Empty album', description: '', @@ -21,8 +20,9 @@ export const albumStub = { albumUsers: [], isActivityEnabled: true, order: AssetOrder.DESC, + updateId: '42', }), - sharedWithUser: Object.freeze({ + sharedWithUser: Object.freeze({ id: 'album-2', albumName: 'Empty album shared with user', description: '', @@ -43,8 +43,9 @@ export const albumStub = { ], isActivityEnabled: true, order: AssetOrder.DESC, + updateId: '42', }), - sharedWithMultiple: Object.freeze({ + sharedWithMultiple: Object.freeze({ id: 'album-3', albumName: 'Empty album shared with users', description: '', @@ -69,8 +70,9 @@ export const albumStub = { ], isActivityEnabled: true, order: AssetOrder.DESC, + updateId: '42', }), - sharedWithAdmin: Object.freeze({ + sharedWithAdmin: Object.freeze({ id: 'album-3', albumName: 'Empty album shared with admin', description: '', @@ -91,8 +93,9 @@ export const albumStub = { ], isActivityEnabled: true, order: AssetOrder.DESC, + updateId: '42', }), - oneAsset: Object.freeze({ + oneAsset: Object.freeze({ id: 'album-4', albumName: 'Album with one asset', description: '', @@ -108,8 +111,9 @@ export const albumStub = { albumUsers: [], isActivityEnabled: true, order: AssetOrder.DESC, + updateId: '42', }), - twoAssets: Object.freeze({ + twoAssets: Object.freeze({ id: 'album-4a', albumName: 'Album with two assets', description: '', @@ -125,8 +129,9 @@ export const albumStub = { albumUsers: [], isActivityEnabled: true, order: AssetOrder.DESC, + updateId: '42', }), - emptyWithValidThumbnail: Object.freeze({ + emptyWithValidThumbnail: Object.freeze({ id: 'album-5', albumName: 'Empty album with valid thumbnail', description: '', @@ -142,5 +147,6 @@ export const albumStub = { albumUsers: [], isActivityEnabled: true, order: AssetOrder.DESC, + updateId: '42', }), }; diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 16e4f20bb3..d1b8e7cf28 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -1,5 +1,5 @@ -import { AssetFile, Exif } from 'src/database'; -import { AssetEntity } from 'src/entities/asset.entity'; +import { AssetFace, AssetFile, Exif } from 'src/database'; +import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetFileType, AssetStatus, AssetType } from 'src/enum'; import { StorageAsset } from 'src/types'; import { authStub } from 'test/fixtures/auth.stub'; @@ -26,13 +26,15 @@ const fullsizeFile: AssetFile = { const files: AssetFile[] = [fullsizeFile, previewFile, thumbnailFile]; -export const stackStub = (stackId: string, assets: AssetEntity[]) => { +export const stackStub = (stackId: string, assets: (MapAsset & { exifInfo: Exif })[]) => { return { id: stackId, assets, ownerId: assets[0].ownerId, primaryAsset: assets[0], primaryAssetId: assets[0].id, + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), }; }; @@ -85,9 +87,12 @@ export const assetStub = { isExternal: false, duplicateId: null, isOffline: false, + libraryId: null, + stackId: null, + updateId: '42', }), - noWebpPath: Object.freeze({ + noWebpPath: Object.freeze({ id: 'asset-id', status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', @@ -122,9 +127,12 @@ export const assetStub = { deletedAt: null, duplicateId: null, isOffline: false, + libraryId: null, + stackId: null, + updateId: '42', }), - noThumbhash: Object.freeze({ + noThumbhash: Object.freeze({ id: 'asset-id', status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', @@ -156,6 +164,9 @@ export const assetStub = { deletedAt: null, duplicateId: null, isOffline: false, + libraryId: null, + stackId: null, + updateId: '42', }), primaryImage: Object.freeze({ @@ -195,12 +206,13 @@ export const assetStub = { } as Exif, stackId: 'stack-1', stack: stackStub('stack-1', [ - { id: 'primary-asset-id' } as AssetEntity, - { id: 'stack-child-asset-1' } as AssetEntity, - { id: 'stack-child-asset-2' } as AssetEntity, + { id: 'primary-asset-id' } as MapAsset & { exifInfo: Exif }, + { id: 'stack-child-asset-1' } as MapAsset & { exifInfo: Exif }, + { id: 'stack-child-asset-2' } as MapAsset & { exifInfo: Exif }, ]), duplicateId: null, isOffline: false, + updateId: '42', libraryId: null, }), @@ -229,6 +241,9 @@ export const assetStub = { isExternal: false, livePhotoVideo: null, livePhotoVideoId: null, + updateId: 'foo', + libraryId: null, + stackId: null, sharedLinks: [], originalFileName: 'asset-id.jpg', faces: [], @@ -241,10 +256,10 @@ export const assetStub = { } as Exif, duplicateId: null, isOffline: false, - libraryId: null, + stack: null, }), - trashed: Object.freeze({ + trashed: Object.freeze({ id: 'asset-id', deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -281,9 +296,12 @@ export const assetStub = { duplicateId: null, isOffline: false, status: AssetStatus.TRASHED, + libraryId: null, + stackId: null, + updateId: '42', }), - trashedOffline: Object.freeze({ + trashedOffline: Object.freeze({ id: 'asset-id', status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', @@ -321,8 +339,10 @@ export const assetStub = { } as Exif, duplicateId: null, isOffline: true, + stackId: null, + updateId: '42', }), - archived: Object.freeze({ + archived: Object.freeze({ id: 'asset-id', status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', @@ -359,9 +379,12 @@ export const assetStub = { } as Exif, duplicateId: null, isOffline: false, + libraryId: null, + stackId: null, + updateId: '42', }), - external: Object.freeze({ + external: Object.freeze({ id: 'asset-id', status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', @@ -397,9 +420,12 @@ export const assetStub = { } as Exif, duplicateId: null, isOffline: false, + updateId: '42', + stackId: null, + stack: null, }), - image1: Object.freeze({ + image1: Object.freeze({ id: 'asset-id-1', status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', @@ -434,9 +460,13 @@ export const assetStub = { } as Exif, duplicateId: null, isOffline: false, + updateId: '42', + stackId: null, + libraryId: null, + stack: null, }), - imageFrom2015: Object.freeze({ + imageFrom2015: Object.freeze({ id: 'asset-id-1', status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', @@ -510,7 +540,9 @@ export const assetStub = { deletedAt: null, duplicateId: null, isOffline: false, + updateId: '42', libraryId: null, + stackId: null, }), livePhotoMotionAsset: Object.freeze({ @@ -527,7 +559,7 @@ export const assetStub = { timeZone: `America/New_York`, }, libraryId: null, - } as AssetEntity & { libraryId: string | null; files: AssetFile[]; exifInfo: Exif }), + } as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif }), livePhotoStillAsset: Object.freeze({ id: 'live-photo-still-asset', @@ -544,7 +576,8 @@ export const assetStub = { timeZone: `America/New_York`, }, files, - } as AssetEntity & { libraryId: string | null }), + faces: [] as AssetFace[], + } as MapAsset & { faces: AssetFace[] }), livePhotoWithOriginalFileName: Object.freeze({ id: 'live-photo-still-asset', @@ -562,7 +595,8 @@ export const assetStub = { timeZone: `America/New_York`, }, libraryId: null, - } as AssetEntity & { libraryId: string | null }), + faces: [] as AssetFace[], + } as MapAsset & { faces: AssetFace[] }), withLocation: Object.freeze({ id: 'asset-with-favorite-id', @@ -590,6 +624,9 @@ export const assetStub = { isVisible: true, livePhotoVideo: null, livePhotoVideoId: null, + updateId: 'foo', + libraryId: null, + stackId: null, sharedLinks: [], originalFileName: 'asset-id.ext', faces: [], @@ -604,7 +641,7 @@ export const assetStub = { deletedAt: null, duplicateId: null, isOffline: false, - libraryId: null, + tags: [], }), sidecar: Object.freeze({ @@ -639,10 +676,12 @@ export const assetStub = { deletedAt: null, duplicateId: null, isOffline: false, + updateId: 'foo', libraryId: null, + stackId: null, }), - sidecarWithoutExt: Object.freeze({ + sidecarWithoutExt: Object.freeze({ id: 'asset-id', status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', @@ -676,7 +715,7 @@ export const assetStub = { isOffline: false, }), - hasEncodedVideo: Object.freeze({ + hasEncodedVideo: Object.freeze({ id: 'asset-id', status: AssetStatus.ACTIVE, originalFileName: 'asset-id.ext', @@ -711,9 +750,13 @@ export const assetStub = { deletedAt: null, duplicateId: null, isOffline: false, + updateId: '42', + libraryId: null, + stackId: null, + stack: null, }), - hasFileExtension: Object.freeze({ + hasFileExtension: Object.freeze({ id: 'asset-id', status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', @@ -788,6 +831,9 @@ export const assetStub = { } as Exif, duplicateId: null, isOffline: false, + updateId: '42', + libraryId: null, + stackId: null, }), imageHif: Object.freeze({ @@ -827,5 +873,8 @@ export const assetStub = { } as Exif, duplicateId: null, isOffline: false, + updateId: '42', + libraryId: null, + stackId: null, }), }; diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts index dfa21fc707..9ef55398d3 100644 --- a/server/test/fixtures/auth.stub.ts +++ b/server/test/fixtures/auth.stub.ts @@ -1,6 +1,5 @@ import { Session } from 'src/database'; import { AuthDto } from 'src/dtos/auth.dto'; -import { SharedLinkEntity } from 'src/entities/shared-link.entity'; const authUser = { admin: { @@ -42,14 +41,16 @@ export const authStub = { id: 'token-id', } as Session, }), - adminSharedLink: Object.freeze({ + adminSharedLink: Object.freeze({ user: authUser.admin, sharedLink: { id: '123', showExif: true, allowDownload: true, allowUpload: true, - key: Buffer.from('shared-link-key'), - } as SharedLinkEntity, + expiresAt: null, + password: null, + userId: '42', + }, }), }; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 4eba0de845..a4d83863c7 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -1,10 +1,9 @@ import { UserAdmin } from 'src/database'; import { AlbumResponseDto } from 'src/dtos/album.dto'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { AssetResponseDto, MapAsset } from 'src/dtos/asset-response.dto'; import { ExifResponseDto } from 'src/dtos/exif.dto'; import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto'; import { mapUser } from 'src/dtos/user.dto'; -import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { AssetOrder, AssetStatus, AssetType, SharedLinkType } from 'src/enum'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; @@ -113,12 +112,12 @@ export const sharedLinkStub = { allowUpload: true, allowDownload: true, showExif: true, - album: undefined, + albumId: null, + album: null, description: null, assets: [assetStub.image], password: 'password', - albumId: null, - } as SharedLinkEntity), + }), valid: Object.freeze({ id: '123', userId: authStub.admin.user.id, @@ -130,12 +129,12 @@ export const sharedLinkStub = { allowUpload: true, allowDownload: true, showExif: true, - album: undefined, albumId: null, description: null, password: null, - assets: [], - } as SharedLinkEntity), + assets: [] as MapAsset[], + album: null, + }), expired: Object.freeze({ id: '123', userId: authStub.admin.user.id, @@ -150,9 +149,10 @@ export const sharedLinkStub = { description: null, password: null, albumId: null, - assets: [], - } as SharedLinkEntity), - readonlyNoExif: Object.freeze({ + assets: [] as MapAsset[], + album: null, + }), + readonlyNoExif: Object.freeze({ id: '123', userId: authStub.admin.user.id, key: sharedLinkBytes, @@ -168,6 +168,7 @@ export const sharedLinkStub = { albumId: 'album-123', album: { id: 'album-123', + updateId: '42', ownerId: authStub.admin.user.id, owner: userStub.admin, albumName: 'Test Album', @@ -239,17 +240,22 @@ export const sharedLinkStub = { colorspace: 'sRGB', autoStackId: null, rating: 3, + updatedAt: today, + updateId: '42', }, sharedLinks: [], faces: [], sidecarPath: null, deletedAt: null, duplicateId: null, + updateId: '42', + libraryId: null, + stackId: null, }, ], }, }), - passwordRequired: Object.freeze({ + passwordRequired: Object.freeze({ id: '123', userId: authStub.admin.user.id, key: sharedLinkBytes, @@ -263,6 +269,7 @@ export const sharedLinkStub = { password: 'password', assets: [], albumId: null, + album: null, }), }; diff --git a/server/test/medium/specs/services/memory.service.spec.ts b/server/test/medium/specs/services/memory.service.spec.ts index 172c48ca5b..445434d60a 100644 --- a/server/test/medium/specs/services/memory.service.spec.ts +++ b/server/test/medium/specs/services/memory.service.spec.ts @@ -39,9 +39,12 @@ describe(MemoryService.name, () => { it('should create a memory from an asset', async () => { const { sut, repos, getRepository } = createSut(); - const now = DateTime.fromObject({ year: 2025, month: 2, day: 25 }, { zone: 'utc' }); + const now = DateTime.fromObject({ year: 2025, month: 2, day: 25 }, { zone: 'utc' }) as DateTime; const user = mediumFactory.userInsert(); - const asset = mediumFactory.assetInsert({ ownerId: user.id, localDateTime: now.minus({ years: 1 }).toISO() }); + const asset = mediumFactory.assetInsert({ + ownerId: user.id, + localDateTime: now.minus({ years: 1 }).toISO(), + }); const jobStatus = mediumFactory.assetJobStatusInsert({ assetId: asset.id }); const userRepo = getRepository('user'); @@ -86,7 +89,7 @@ describe(MemoryService.name, () => { it('should not generate a memory twice for the same day', async () => { const { sut, repos, getRepository } = createSut(); - const now = DateTime.fromObject({ year: 2025, month: 2, day: 20 }, { zone: 'utc' }); + const now = DateTime.fromObject({ year: 2025, month: 2, day: 20 }, { zone: 'utc' }) as DateTime; const assetRepo = getRepository('asset'); const memoryRepo = getRepository('memory'); diff --git a/server/test/medium/specs/services/metadata.service.spec.ts b/server/test/medium/specs/services/metadata.service.spec.ts index b25cce2724..13b9867373 100644 --- a/server/test/medium/specs/services/metadata.service.spec.ts +++ b/server/test/medium/specs/services/metadata.service.spec.ts @@ -118,7 +118,7 @@ describe(MetadataService.name, () => { process.env.TZ = serverTimeZone ?? undefined; const { filePath } = await createTestFile(exifData); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ id: 'asset-1', originalPath: filePath } as never); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ id: 'asset-1', originalPath: filePath } as any); await sut.handleMetadataExtraction({ id: 'asset-1' }); diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 2418b6aa64..d540e55b2a 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -11,7 +11,7 @@ export const newAssetRepositoryMock = (): Mocked = {}) => { }; }; -const assetFactory = (asset: Partial = {}) => ({ +const assetFactory = (asset: Partial = {}) => ({ id: newUuid(), createdAt: newDate(), updatedAt: newDate(), From dd1fcd5be50a3b39dba55ff5610883997674d3d7 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Fri, 18 Apr 2025 23:39:56 +0200 Subject: [PATCH 011/356] chore: remove asset entity (#17703) --- server/src/database.ts | 7 +- server/src/entities/asset.entity.ts | 272 ------------------ .../src/repositories/asset-job.repository.ts | 3 +- server/src/repositories/asset.repository.ts | 18 +- server/src/repositories/search.repository.ts | 3 +- server/src/repositories/view-repository.ts | 3 +- server/src/schema/tables/asset.table.ts | 2 +- .../src/services/asset-media.service.spec.ts | 8 +- server/src/services/asset-media.service.ts | 2 +- server/src/services/memory.service.spec.ts | 8 + server/src/services/search.service.spec.ts | 23 ++ server/src/utils/database.ts | 231 +++++++++++++++ 12 files changed, 281 insertions(+), 299 deletions(-) delete mode 100644 server/src/entities/asset.entity.ts diff --git a/server/src/database.ts b/server/src/database.ts index 6e66cb4d3d..27094958ed 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -1,7 +1,6 @@ import { Selectable } from 'kysely'; -import { Albums, AssetJobStatus as DatabaseAssetJobStatus, Exif as DatabaseExif } from 'src/db'; +import { Albums, Exif as DatabaseExif } from 'src/db'; import { MapAsset } from 'src/dtos/asset-response.dto'; -import { AssetEntity } from 'src/entities/asset.entity'; import { AlbumUserRole, AssetFileType, @@ -265,10 +264,6 @@ export type AssetFace = { person?: Person | null; }; -export type AssetJobStatus = Selectable & { - asset: AssetEntity; -}; - const userColumns = ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'] as const; export const columns = { diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts deleted file mode 100644 index da291292e7..0000000000 --- a/server/src/entities/asset.entity.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { DeduplicateJoinsPlugin, ExpressionBuilder, Kysely, SelectQueryBuilder, sql } from 'kysely'; -import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'; -import { AssetFace, AssetFile, AssetJobStatus, columns, Exif, Person, Stack, Tag, User } from 'src/database'; -import { DB } from 'src/db'; -import { MapAsset } from 'src/dtos/asset-response.dto'; -import { AssetFileType, AssetStatus, AssetType } from 'src/enum'; -import { TimeBucketSize } from 'src/repositories/asset.repository'; -import { AssetSearchBuilderOptions } from 'src/repositories/search.repository'; -import { anyUuid, asUuid, toJson } from 'src/utils/database'; - -export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum'; - -export class AssetEntity { - id!: string; - deviceAssetId!: string; - owner!: User; - ownerId!: string; - libraryId?: string | null; - deviceId!: string; - type!: AssetType; - status!: AssetStatus; - originalPath!: string; - files!: AssetFile[]; - thumbhash!: Buffer | null; - encodedVideoPath!: string | null; - createdAt!: Date; - updatedAt!: Date; - updateId?: string; - deletedAt!: Date | null; - fileCreatedAt!: Date; - localDateTime!: Date; - fileModifiedAt!: Date; - isFavorite!: boolean; - isArchived!: boolean; - isExternal!: boolean; - isOffline!: boolean; - checksum!: Buffer; // sha1 checksum - duration!: string | null; - isVisible!: boolean; - livePhotoVideo!: MapAsset | null; - livePhotoVideoId!: string | null; - originalFileName!: string; - sidecarPath!: string | null; - exifInfo?: Exif; - tags?: Tag[]; - faces!: AssetFace[]; - stackId?: string | null; - stack?: Stack | null; - jobStatus?: AssetJobStatus; - duplicateId!: string | null; -} - -// TODO come up with a better query that only selects the fields we need -export function withExif(qb: SelectQueryBuilder) { - return qb - .leftJoin('exif', 'assets.id', 'exif.assetId') - .select((eb) => eb.fn.toJson(eb.table('exif')).$castTo().as('exifInfo')); -} - -export function withExifInner(qb: SelectQueryBuilder) { - return qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .select((eb) => eb.fn.toJson(eb.table('exif')).$castTo().as('exifInfo')); -} - -export function withSmartSearch(qb: SelectQueryBuilder) { - return qb - .leftJoin('smart_search', 'assets.id', 'smart_search.assetId') - .select((eb) => toJson(eb, 'smart_search').as('smartSearch')); -} - -export function withFaces(eb: ExpressionBuilder, withDeletedFace?: boolean) { - return jsonArrayFrom( - eb - .selectFrom('asset_faces') - .selectAll('asset_faces') - .whereRef('asset_faces.assetId', '=', 'assets.id') - .$if(!withDeletedFace, (qb) => qb.where('asset_faces.deletedAt', 'is', null)), - ).as('faces'); -} - -export function withFiles(eb: ExpressionBuilder, type?: AssetFileType) { - return jsonArrayFrom( - eb - .selectFrom('asset_files') - .select(columns.assetFiles) - .whereRef('asset_files.assetId', '=', 'assets.id') - .$if(!!type, (qb) => qb.where('asset_files.type', '=', type!)), - ).as('files'); -} - -export function withFacesAndPeople(eb: ExpressionBuilder, withDeletedFace?: boolean) { - return jsonArrayFrom( - eb - .selectFrom('asset_faces') - .leftJoinLateral( - (eb) => - eb.selectFrom('person').selectAll('person').whereRef('asset_faces.personId', '=', 'person.id').as('person'), - (join) => join.onTrue(), - ) - .selectAll('asset_faces') - .select((eb) => eb.table('person').$castTo().as('person')) - .whereRef('asset_faces.assetId', '=', 'assets.id') - .$if(!withDeletedFace, (qb) => qb.where('asset_faces.deletedAt', 'is', null)), - ).as('faces'); -} - -export function hasPeople(qb: SelectQueryBuilder, personIds: string[]) { - return qb.innerJoin( - (eb) => - eb - .selectFrom('asset_faces') - .select('assetId') - .where('personId', '=', anyUuid(personIds!)) - .where('deletedAt', 'is', null) - .groupBy('assetId') - .having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length) - .as('has_people'), - (join) => join.onRef('has_people.assetId', '=', 'assets.id'), - ); -} - -export function hasTags(qb: SelectQueryBuilder, tagIds: string[]) { - return qb.innerJoin( - (eb) => - eb - .selectFrom('tag_asset') - .select('assetsId') - .innerJoin('tags_closure', 'tag_asset.tagsId', 'tags_closure.id_descendant') - .where('tags_closure.id_ancestor', '=', anyUuid(tagIds)) - .groupBy('assetsId') - .having((eb) => eb.fn.count('tags_closure.id_ancestor').distinct(), '>=', tagIds.length) - .as('has_tags'), - (join) => join.onRef('has_tags.assetsId', '=', 'assets.id'), - ); -} - -export function withOwner(eb: ExpressionBuilder) { - return jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'assets.ownerId')).as( - 'owner', - ); -} - -export function withLibrary(eb: ExpressionBuilder) { - return jsonObjectFrom( - eb.selectFrom('libraries').selectAll('libraries').whereRef('libraries.id', '=', 'assets.libraryId'), - ).as('library'); -} - -export function withTags(eb: ExpressionBuilder) { - return jsonArrayFrom( - eb - .selectFrom('tags') - .select(columns.tag) - .innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId') - .whereRef('assets.id', '=', 'tag_asset.assetsId'), - ).as('tags'); -} - -export function truncatedDate(size: TimeBucketSize) { - return sql`date_trunc(${size}, "localDateTime" at time zone 'UTC') at time zone 'UTC'`; -} - -export function withTagId(qb: SelectQueryBuilder, tagId: string) { - return qb.where((eb) => - eb.exists( - eb - .selectFrom('tags_closure') - .innerJoin('tag_asset', 'tag_asset.tagsId', 'tags_closure.id_descendant') - .whereRef('tag_asset.assetsId', '=', 'assets.id') - .where('tags_closure.id_ancestor', '=', tagId), - ), - ); -} - -const joinDeduplicationPlugin = new DeduplicateJoinsPlugin(); - -/** TODO: This should only be used for search-related queries, not as a general purpose query builder */ -export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuilderOptions) { - options.isArchived ??= options.withArchived ? undefined : false; - options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline); - return kysely - .withPlugin(joinDeduplicationPlugin) - .selectFrom('assets') - .selectAll('assets') - .$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!)) - .$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!)) - .$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!)) - .$if(!!options.createdAfter, (qb) => qb.where('assets.createdAt', '>=', options.createdAfter!)) - .$if(!!options.updatedBefore, (qb) => qb.where('assets.updatedAt', '<=', options.updatedBefore!)) - .$if(!!options.updatedAfter, (qb) => qb.where('assets.updatedAt', '>=', options.updatedAfter!)) - .$if(!!options.trashedBefore, (qb) => qb.where('assets.deletedAt', '<=', options.trashedBefore!)) - .$if(!!options.trashedAfter, (qb) => qb.where('assets.deletedAt', '>=', options.trashedAfter!)) - .$if(!!options.takenBefore, (qb) => qb.where('assets.fileCreatedAt', '<=', options.takenBefore!)) - .$if(!!options.takenAfter, (qb) => qb.where('assets.fileCreatedAt', '>=', options.takenAfter!)) - .$if(options.city !== undefined, (qb) => - qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where('exif.city', options.city === null ? 'is' : '=', options.city!), - ) - .$if(options.state !== undefined, (qb) => - qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where('exif.state', options.state === null ? 'is' : '=', options.state!), - ) - .$if(options.country !== undefined, (qb) => - qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where('exif.country', options.country === null ? 'is' : '=', options.country!), - ) - .$if(options.make !== undefined, (qb) => - qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where('exif.make', options.make === null ? 'is' : '=', options.make!), - ) - .$if(options.model !== undefined, (qb) => - qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where('exif.model', options.model === null ? 'is' : '=', options.model!), - ) - .$if(options.lensModel !== undefined, (qb) => - qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where('exif.lensModel', options.lensModel === null ? 'is' : '=', options.lensModel!), - ) - .$if(options.rating !== undefined, (qb) => - qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where('exif.rating', options.rating === null ? 'is' : '=', options.rating!), - ) - .$if(!!options.checksum, (qb) => qb.where('assets.checksum', '=', options.checksum!)) - .$if(!!options.deviceAssetId, (qb) => qb.where('assets.deviceAssetId', '=', options.deviceAssetId!)) - .$if(!!options.deviceId, (qb) => qb.where('assets.deviceId', '=', options.deviceId!)) - .$if(!!options.id, (qb) => qb.where('assets.id', '=', asUuid(options.id!))) - .$if(!!options.libraryId, (qb) => qb.where('assets.libraryId', '=', asUuid(options.libraryId!))) - .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!))) - .$if(!!options.encodedVideoPath, (qb) => qb.where('assets.encodedVideoPath', '=', options.encodedVideoPath!)) - .$if(!!options.originalPath, (qb) => - qb.where(sql`f_unaccent(assets."originalPath")`, 'ilike', sql`'%' || f_unaccent(${options.originalPath}) || '%'`), - ) - .$if(!!options.originalFileName, (qb) => - qb.where( - sql`f_unaccent(assets."originalFileName")`, - 'ilike', - sql`'%' || f_unaccent(${options.originalFileName}) || '%'`, - ), - ) - .$if(!!options.description, (qb) => - qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where(sql`f_unaccent(exif.description)`, 'ilike', sql`'%' || f_unaccent(${options.description}) || '%'`), - ) - .$if(!!options.type, (qb) => qb.where('assets.type', '=', options.type!)) - .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) - .$if(options.isOffline !== undefined, (qb) => qb.where('assets.isOffline', '=', options.isOffline!)) - .$if(options.isVisible !== undefined, (qb) => qb.where('assets.isVisible', '=', options.isVisible!)) - .$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!)) - .$if(options.isEncoded !== undefined, (qb) => - qb.where('assets.encodedVideoPath', options.isEncoded ? 'is not' : 'is', null), - ) - .$if(options.isMotion !== undefined, (qb) => - qb.where('assets.livePhotoVideoId', options.isMotion ? 'is not' : 'is', null), - ) - .$if(!!options.isNotInAlbum, (qb) => - qb.where((eb) => - eb.not(eb.exists((eb) => eb.selectFrom('albums_assets_assets').whereRef('assetsId', '=', 'assets.id'))), - ), - ) - .$if(!!options.withExif, withExifInner) - .$if(!!(options.withFaces || options.withPeople || options.personIds), (qb) => qb.select(withFacesAndPeople)) - .$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null)); -} diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index 1bf08e81f5..4a2d52566f 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -5,10 +5,9 @@ import { InjectKysely } from 'nestjs-kysely'; import { columns } from 'src/database'; import { DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { withExifInner, withFaces, withFiles } from 'src/entities/asset.entity'; import { AssetFileType } from 'src/enum'; import { StorageAsset } from 'src/types'; -import { anyUuid, asUuid } from 'src/utils/database'; +import { anyUuid, asUuid, withExifInner, withFaces, withFiles } from 'src/utils/database'; @Injectable() export class AssetJobRepository { diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 7fb7056ba7..7a68ba907f 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -6,11 +6,16 @@ import { Stack } from 'src/database'; import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; import { MapAsset } from 'src/dtos/asset-response.dto'; +import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum'; +import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/repositories/search.repository'; import { - AssetEntity, + anyUuid, + asUuid, hasPeople, + removeUndefinedKeys, searchAssetBuilder, truncatedDate, + unnest, withExif, withFaces, withFacesAndPeople, @@ -20,10 +25,7 @@ import { withSmartSearch, withTagId, withTags, -} from 'src/entities/asset.entity'; -import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum'; -import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/repositories/search.repository'; -import { anyUuid, asUuid, removeUndefinedKeys, unnest } from 'src/utils/database'; +} from 'src/utils/database'; import { globToSqlPattern } from 'src/utils/misc'; import { PaginationOptions, paginationHelper } from 'src/utils/pagination'; @@ -128,8 +130,6 @@ export interface AssetGetByChecksumOptions { libraryId?: string; } -export type AssetPathEntity = Pick; - export interface GetByIdsRelations { exifInfo?: boolean; faces?: { person?: boolean; withDeleted?: boolean }; @@ -493,13 +493,13 @@ export class AssetRepository { } @GenerateSql({ params: [DummyValue.UUID, [DummyValue.BUFFER]] }) - getByChecksums(userId: string, checksums: Buffer[]): Promise { + getByChecksums(userId: string, checksums: Buffer[]) { return this.db .selectFrom('assets') .select(['id', 'checksum', 'deletedAt']) .where('ownerId', '=', asUuid(userId)) .where('checksum', 'in', checksums) - .execute() as any as Promise; + .execute(); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] }) diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 95c350fe34..5c1ebae69d 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -5,9 +5,8 @@ import { randomUUID } from 'node:crypto'; import { DB, Exif } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { MapAsset } from 'src/dtos/asset-response.dto'; -import { searchAssetBuilder } from 'src/entities/asset.entity'; import { AssetStatus, AssetType } from 'src/enum'; -import { anyUuid, asUuid } from 'src/utils/database'; +import { anyUuid, asUuid, searchAssetBuilder } from 'src/utils/database'; import { isValidInteger } from 'src/validation'; export interface SearchResult { diff --git a/server/src/repositories/view-repository.ts b/server/src/repositories/view-repository.ts index ae2303e9e2..e32933065c 100644 --- a/server/src/repositories/view-repository.ts +++ b/server/src/repositories/view-repository.ts @@ -2,8 +2,7 @@ import { Kysely } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { withExif } from 'src/entities/asset.entity'; -import { asUuid } from 'src/utils/database'; +import { asUuid, withExif } from 'src/utils/database'; export class ViewRepository { constructor(@InjectKysely() private db: Kysely) {} diff --git a/server/src/schema/tables/asset.table.ts b/server/src/schema/tables/asset.table.ts index 9a9670cd0e..19ec8d2ef4 100644 --- a/server/src/schema/tables/asset.table.ts +++ b/server/src/schema/tables/asset.table.ts @@ -1,5 +1,4 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { ASSET_CHECKSUM_CONSTRAINT } from 'src/entities/asset.entity'; import { AssetStatus, AssetType } from 'src/enum'; import { assets_status_enum } from 'src/schema/enums'; import { assets_delete_audit } from 'src/schema/functions'; @@ -17,6 +16,7 @@ import { Table, UpdateDateColumn, } from 'src/sql-tools'; +import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database'; @Table('assets') @UpdatedAtTrigger('assets_updated_at') diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index a49230f852..d25067f1c9 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -9,10 +9,10 @@ import { AssetFile } from 'src/database'; import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto'; import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto'; import { MapAsset } from 'src/dtos/asset-response.dto'; -import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; import { AssetFileType, AssetStatus, AssetType, CacheControl, JobName } from 'src/enum'; import { AuthRequest } from 'src/middleware/auth.guard'; import { AssetMediaService } from 'src/services/asset-media.service'; +import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database'; import { ImmichFileResponse } from 'src/utils/file'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; @@ -820,8 +820,8 @@ describe(AssetMediaService.name, () => { const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex'); mocks.asset.getByChecksums.mockResolvedValue([ - { id: 'asset-1', checksum: file1 } as AssetEntity, - { id: 'asset-2', checksum: file2 } as AssetEntity, + { id: 'asset-1', checksum: file1, deletedAt: null }, + { id: 'asset-2', checksum: file2, deletedAt: null }, ]); await expect( @@ -857,7 +857,7 @@ describe(AssetMediaService.name, () => { const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex'); const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex'); - mocks.asset.getByChecksums.mockResolvedValue([{ id: 'asset-1', checksum: file1 } as AssetEntity]); + mocks.asset.getByChecksums.mockResolvedValue([{ id: 'asset-1', checksum: file1, deletedAt: null }]); await expect( sut.bulkUploadCheck(authStub.admin, { diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index de40d8b304..78e23fa802 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -21,13 +21,13 @@ import { UploadFieldName, } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { ASSET_CHECKSUM_CONSTRAINT } from 'src/entities/asset.entity'; import { AssetStatus, AssetType, CacheControl, JobName, Permission, StorageFolder } from 'src/enum'; import { AuthRequest } from 'src/middleware/auth.guard'; import { BaseService } from 'src/services/base.service'; import { UploadFile } from 'src/types'; import { requireUploadAccess } from 'src/utils/access'; import { asRequest, getAssetFiles, onBeforeLink } from 'src/utils/asset.util'; +import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database'; import { getFilenameExtension, getFileNameWithoutExtension, ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; import { fromChecksum } from 'src/utils/request'; diff --git a/server/src/services/memory.service.spec.ts b/server/src/services/memory.service.spec.ts index 7ce8b1ab46..d55c58d9af 100644 --- a/server/src/services/memory.service.spec.ts +++ b/server/src/services/memory.service.spec.ts @@ -15,6 +15,14 @@ describe(MemoryService.name, () => { expect(sut).toBeDefined(); }); + describe('onMemoryCleanup', () => { + it('should clean up memories', async () => { + mocks.memory.cleanup.mockResolvedValue([]); + await sut.onMemoriesCleanup(); + expect(mocks.memory.cleanup).toHaveBeenCalled(); + }); + }); + describe('search', () => { it('should search memories', async () => { const [userId] = newUuids(); diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index 18699edf9a..d87ccbde1d 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -39,6 +39,29 @@ describe(SearchService.name, () => { }); }); + describe('searchPlaces', () => { + it('should search places', async () => { + mocks.search.searchPlaces.mockResolvedValue([ + { + id: 42, + name: 'my place', + latitude: 420, + longitude: 69, + admin1Code: null, + admin1Name: null, + admin2Code: null, + admin2Name: null, + alternateNames: null, + countryCode: 'US', + modificationDate: new Date(), + }, + ]); + + await sut.searchPlaces({ name: 'place' }); + expect(mocks.search.searchPlaces).toHaveBeenCalledWith('place'); + }); + }); + describe('getExploreData', () => { it('should get assets by city and tag', async () => { mocks.asset.getAssetIdByCity.mockResolvedValue({ diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index a9c7b09c5a..1af0aa4b4e 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -1,15 +1,24 @@ import { + DeduplicateJoinsPlugin, Expression, ExpressionBuilder, ExpressionWrapper, + Kysely, KyselyConfig, Nullable, Selectable, + SelectQueryBuilder, Simplify, sql, } from 'kysely'; import { PostgresJSDialect } from 'kysely-postgres-js'; +import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'; import postgres, { Notice } from 'postgres'; +import { columns, Exif, Person } from 'src/database'; +import { DB } from 'src/db'; +import { AssetFileType } from 'src/enum'; +import { TimeBucketSize } from 'src/repositories/asset.repository'; +import { AssetSearchBuilderOptions } from 'src/repositories/search.repository'; type Ssl = 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object; @@ -112,3 +121,225 @@ export function toJson >; } + +export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum'; +// TODO come up with a better query that only selects the fields we need + +export function withExif(qb: SelectQueryBuilder) { + return qb + .leftJoin('exif', 'assets.id', 'exif.assetId') + .select((eb) => eb.fn.toJson(eb.table('exif')).$castTo().as('exifInfo')); +} + +export function withExifInner(qb: SelectQueryBuilder) { + return qb + .innerJoin('exif', 'assets.id', 'exif.assetId') + .select((eb) => eb.fn.toJson(eb.table('exif')).$castTo().as('exifInfo')); +} + +export function withSmartSearch(qb: SelectQueryBuilder) { + return qb + .leftJoin('smart_search', 'assets.id', 'smart_search.assetId') + .select((eb) => toJson(eb, 'smart_search').as('smartSearch')); +} + +export function withFaces(eb: ExpressionBuilder, withDeletedFace?: boolean) { + return jsonArrayFrom( + eb + .selectFrom('asset_faces') + .selectAll('asset_faces') + .whereRef('asset_faces.assetId', '=', 'assets.id') + .$if(!withDeletedFace, (qb) => qb.where('asset_faces.deletedAt', 'is', null)), + ).as('faces'); +} + +export function withFiles(eb: ExpressionBuilder, type?: AssetFileType) { + return jsonArrayFrom( + eb + .selectFrom('asset_files') + .select(columns.assetFiles) + .whereRef('asset_files.assetId', '=', 'assets.id') + .$if(!!type, (qb) => qb.where('asset_files.type', '=', type!)), + ).as('files'); +} + +export function withFacesAndPeople(eb: ExpressionBuilder, withDeletedFace?: boolean) { + return jsonArrayFrom( + eb + .selectFrom('asset_faces') + .leftJoinLateral( + (eb) => + eb.selectFrom('person').selectAll('person').whereRef('asset_faces.personId', '=', 'person.id').as('person'), + (join) => join.onTrue(), + ) + .selectAll('asset_faces') + .select((eb) => eb.table('person').$castTo().as('person')) + .whereRef('asset_faces.assetId', '=', 'assets.id') + .$if(!withDeletedFace, (qb) => qb.where('asset_faces.deletedAt', 'is', null)), + ).as('faces'); +} + +export function hasPeople(qb: SelectQueryBuilder, personIds: string[]) { + return qb.innerJoin( + (eb) => + eb + .selectFrom('asset_faces') + .select('assetId') + .where('personId', '=', anyUuid(personIds!)) + .where('deletedAt', 'is', null) + .groupBy('assetId') + .having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length) + .as('has_people'), + (join) => join.onRef('has_people.assetId', '=', 'assets.id'), + ); +} + +export function hasTags(qb: SelectQueryBuilder, tagIds: string[]) { + return qb.innerJoin( + (eb) => + eb + .selectFrom('tag_asset') + .select('assetsId') + .innerJoin('tags_closure', 'tag_asset.tagsId', 'tags_closure.id_descendant') + .where('tags_closure.id_ancestor', '=', anyUuid(tagIds)) + .groupBy('assetsId') + .having((eb) => eb.fn.count('tags_closure.id_ancestor').distinct(), '>=', tagIds.length) + .as('has_tags'), + (join) => join.onRef('has_tags.assetsId', '=', 'assets.id'), + ); +} + +export function withOwner(eb: ExpressionBuilder) { + return jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'assets.ownerId')).as( + 'owner', + ); +} + +export function withLibrary(eb: ExpressionBuilder) { + return jsonObjectFrom( + eb.selectFrom('libraries').selectAll('libraries').whereRef('libraries.id', '=', 'assets.libraryId'), + ).as('library'); +} + +export function withTags(eb: ExpressionBuilder) { + return jsonArrayFrom( + eb + .selectFrom('tags') + .select(columns.tag) + .innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId') + .whereRef('assets.id', '=', 'tag_asset.assetsId'), + ).as('tags'); +} + +export function truncatedDate(size: TimeBucketSize) { + return sql`date_trunc(${size}, "localDateTime" at time zone 'UTC') at time zone 'UTC'`; +} + +export function withTagId(qb: SelectQueryBuilder, tagId: string) { + return qb.where((eb) => + eb.exists( + eb + .selectFrom('tags_closure') + .innerJoin('tag_asset', 'tag_asset.tagsId', 'tags_closure.id_descendant') + .whereRef('tag_asset.assetsId', '=', 'assets.id') + .where('tags_closure.id_ancestor', '=', tagId), + ), + ); +} +const joinDeduplicationPlugin = new DeduplicateJoinsPlugin(); +/** TODO: This should only be used for search-related queries, not as a general purpose query builder */ + +export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuilderOptions) { + options.isArchived ??= options.withArchived ? undefined : false; + options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline); + return kysely + .withPlugin(joinDeduplicationPlugin) + .selectFrom('assets') + .selectAll('assets') + .$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!)) + .$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!)) + .$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!)) + .$if(!!options.createdAfter, (qb) => qb.where('assets.createdAt', '>=', options.createdAfter!)) + .$if(!!options.updatedBefore, (qb) => qb.where('assets.updatedAt', '<=', options.updatedBefore!)) + .$if(!!options.updatedAfter, (qb) => qb.where('assets.updatedAt', '>=', options.updatedAfter!)) + .$if(!!options.trashedBefore, (qb) => qb.where('assets.deletedAt', '<=', options.trashedBefore!)) + .$if(!!options.trashedAfter, (qb) => qb.where('assets.deletedAt', '>=', options.trashedAfter!)) + .$if(!!options.takenBefore, (qb) => qb.where('assets.fileCreatedAt', '<=', options.takenBefore!)) + .$if(!!options.takenAfter, (qb) => qb.where('assets.fileCreatedAt', '>=', options.takenAfter!)) + .$if(options.city !== undefined, (qb) => + qb + .innerJoin('exif', 'assets.id', 'exif.assetId') + .where('exif.city', options.city === null ? 'is' : '=', options.city!), + ) + .$if(options.state !== undefined, (qb) => + qb + .innerJoin('exif', 'assets.id', 'exif.assetId') + .where('exif.state', options.state === null ? 'is' : '=', options.state!), + ) + .$if(options.country !== undefined, (qb) => + qb + .innerJoin('exif', 'assets.id', 'exif.assetId') + .where('exif.country', options.country === null ? 'is' : '=', options.country!), + ) + .$if(options.make !== undefined, (qb) => + qb + .innerJoin('exif', 'assets.id', 'exif.assetId') + .where('exif.make', options.make === null ? 'is' : '=', options.make!), + ) + .$if(options.model !== undefined, (qb) => + qb + .innerJoin('exif', 'assets.id', 'exif.assetId') + .where('exif.model', options.model === null ? 'is' : '=', options.model!), + ) + .$if(options.lensModel !== undefined, (qb) => + qb + .innerJoin('exif', 'assets.id', 'exif.assetId') + .where('exif.lensModel', options.lensModel === null ? 'is' : '=', options.lensModel!), + ) + .$if(options.rating !== undefined, (qb) => + qb + .innerJoin('exif', 'assets.id', 'exif.assetId') + .where('exif.rating', options.rating === null ? 'is' : '=', options.rating!), + ) + .$if(!!options.checksum, (qb) => qb.where('assets.checksum', '=', options.checksum!)) + .$if(!!options.deviceAssetId, (qb) => qb.where('assets.deviceAssetId', '=', options.deviceAssetId!)) + .$if(!!options.deviceId, (qb) => qb.where('assets.deviceId', '=', options.deviceId!)) + .$if(!!options.id, (qb) => qb.where('assets.id', '=', asUuid(options.id!))) + .$if(!!options.libraryId, (qb) => qb.where('assets.libraryId', '=', asUuid(options.libraryId!))) + .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!))) + .$if(!!options.encodedVideoPath, (qb) => qb.where('assets.encodedVideoPath', '=', options.encodedVideoPath!)) + .$if(!!options.originalPath, (qb) => + qb.where(sql`f_unaccent(assets."originalPath")`, 'ilike', sql`'%' || f_unaccent(${options.originalPath}) || '%'`), + ) + .$if(!!options.originalFileName, (qb) => + qb.where( + sql`f_unaccent(assets."originalFileName")`, + 'ilike', + sql`'%' || f_unaccent(${options.originalFileName}) || '%'`, + ), + ) + .$if(!!options.description, (qb) => + qb + .innerJoin('exif', 'assets.id', 'exif.assetId') + .where(sql`f_unaccent(exif.description)`, 'ilike', sql`'%' || f_unaccent(${options.description}) || '%'`), + ) + .$if(!!options.type, (qb) => qb.where('assets.type', '=', options.type!)) + .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) + .$if(options.isOffline !== undefined, (qb) => qb.where('assets.isOffline', '=', options.isOffline!)) + .$if(options.isVisible !== undefined, (qb) => qb.where('assets.isVisible', '=', options.isVisible!)) + .$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!)) + .$if(options.isEncoded !== undefined, (qb) => + qb.where('assets.encodedVideoPath', options.isEncoded ? 'is not' : 'is', null), + ) + .$if(options.isMotion !== undefined, (qb) => + qb.where('assets.livePhotoVideoId', options.isMotion ? 'is not' : 'is', null), + ) + .$if(!!options.isNotInAlbum, (qb) => + qb.where((eb) => + eb.not(eb.exists((eb) => eb.selectFrom('albums_assets_assets').whereRef('assetsId', '=', 'assets.id'))), + ), + ) + .$if(!!options.withExif, withExifInner) + .$if(!!(options.withFaces || options.withPeople || options.personIds), (qb) => qb.select(withFacesAndPeople)) + .$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null)); +} From 9e063c993c36f814c48ee0dd1fbebfaa24a0536d Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Mon, 21 Apr 2025 00:54:37 -0400 Subject: [PATCH 012/356] fix(docs): Database dump warnings (#17676) * docs * admin page * roadmap * whitespace * whitespace * no danger --- .../docs/administration/backup-and-restore.md | 31 ++++++++++++------- docs/src/pages/roadmap.tsx | 4 +-- i18n/en.json | 10 +++--- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/docs/docs/administration/backup-and-restore.md b/docs/docs/administration/backup-and-restore.md index 817a7dca6d..7e55e4e88f 100644 --- a/docs/docs/administration/backup-and-restore.md +++ b/docs/docs/administration/backup-and-restore.md @@ -23,23 +23,32 @@ Refer to the official [postgres documentation](https://www.postgresql.org/docs/c It is not recommended to directly backup the `DB_DATA_LOCATION` folder. Doing so while the database is running can lead to a corrupted backup that cannot be restored. ::: -### Automatic Database Backups +### Automatic Database Dumps -For convenience, Immich will automatically create database backups by default. The backups are stored in `UPLOAD_LOCATION/backups`. -As mentioned above, you should make your own backup of these together with the asset folders as noted below. -You can adjust the schedule and amount of kept backups in the [admin settings](http://my.immich.app/admin/system-settings?isOpen=backup). -By default, Immich will keep the last 14 backups and create a new backup every day at 2:00 AM. +:::warning +The automatic database dumps can be used to restore the database in the event of damage to the Postgres database files. +There is no monitoring for these dumps and you will not be notified if they are unsuccessful. +::: -#### Trigger Backup +:::caution +The database dumps do **NOT** contain any pictures or videos, only metadata. They are only usable with a copy of the other files in `UPLOAD_LOCATION` as outlined below. +::: -You are able to trigger a backup in the [admin job status page](http://my.immich.app/admin/jobs-status). -Visit the page, open the "Create job" modal from the top right, select "Backup Database" and click "Confirm". -A job will run and trigger a backup, you can verify this worked correctly by checking the logs or the backup folder. -This backup will count towards the last X backups that will be kept based on your settings. +For disaster-recovery purposes, Immich will automatically create database dumps. The dumps are stored in `UPLOAD_LOCATION/backups`. +Please be sure to make your own, independent backup of the database together with the asset folders as noted below. +You can adjust the schedule and amount of kept database dumps in the [admin settings](http://my.immich.app/admin/system-settings?isOpen=backup). +By default, Immich will keep the last 14 database dumps and create a new dump every day at 2:00 AM. + +#### Trigger Dump + +You are able to trigger a database dump in the [admin job status page](http://my.immich.app/admin/jobs-status). +Visit the page, open the "Create job" modal from the top right, select "Create Database Dump" and click "Confirm". +A job will run and trigger a dump, you can verify this worked correctly by checking the logs or the `backups/` folder. +This dumps will count towards the last `X` dumps that will be kept based on your settings. #### Restoring -We hope to make restoring simpler in future versions, for now you can find the backups in the `UPLOAD_LOCATION/backups` folder on your host. +We hope to make restoring simpler in future versions, for now you can find the database dumps in the `UPLOAD_LOCATION/backups` folder on your host. Then please follow the steps in the following section for restoring the database. ### Manual Backup and Restore diff --git a/docs/src/pages/roadmap.tsx b/docs/src/pages/roadmap.tsx index b7ded8e8c9..205b976aed 100644 --- a/docs/src/pages/roadmap.tsx +++ b/docs/src/pages/roadmap.tsx @@ -266,8 +266,8 @@ const milestones: Item[] = [ withRelease({ icon: mdiDatabaseOutline, iconColor: 'brown', - title: 'Automatic database backups', - description: 'Database backups are now integrated into the Immich server', + title: 'Automatic database dumps', + description: 'Database dumps are now integrated into the Immich server', release: 'v1.120.0', }), { diff --git a/i18n/en.json b/i18n/en.json index c4b4746871..9951717de6 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "Are you sure you want to disable all login methods? Login will be completely disabled.", "authentication_settings_reenable": "To re-enable, use a Server Command.", "background_task_job": "Background Tasks", - "backup_database": "Backup Database", - "backup_database_enable_description": "Enable database backups", - "backup_keep_last_amount": "Amount of previous backups to keep", - "backup_settings": "Backup Settings", - "backup_settings_description": "Manage database backup settings", + "backup_database": "Create Database Dump", + "backup_database_enable_description": "Enable database dumps", + "backup_keep_last_amount": "Amount of previous dumps to keep", + "backup_settings": "Database Dump Settings", + "backup_settings_description": "Manage database dump settings. Note: These jobs are not monitored and you will not be notified of failure.", "check_all": "Check All", "cleanup": "Cleanup", "cleared_jobs": "Cleared jobs for: {job}", From 21a6eb30ff7333dbdf4899cd05686c0bd6cfe535 Mon Sep 17 00:00:00 2001 From: aviv926 <51673860+aviv926@users.noreply.github.com> Date: Mon, 21 Apr 2025 07:55:58 +0300 Subject: [PATCH 013/356] feat(docs): documentation update (#17720) Documentation update --- .../docs/features/ml-hardware-acceleration.md | 2 +- docs/src/pages/roadmap.tsx | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/docs/features/ml-hardware-acceleration.md b/docs/docs/features/ml-hardware-acceleration.md index 8371e726b9..a94f8c8c64 100644 --- a/docs/docs/features/ml-hardware-acceleration.md +++ b/docs/docs/features/ml-hardware-acceleration.md @@ -42,7 +42,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele - The GPU must have compute capability 5.2 or greater. - The server must have the official NVIDIA driver installed. -- The installed driver must be >= 535 (it must support CUDA 12.2). +- The installed driver must be >= 545 (it must support CUDA 12.3). - On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed. #### ROCm diff --git a/docs/src/pages/roadmap.tsx b/docs/src/pages/roadmap.tsx index 205b976aed..4dc391cb27 100644 --- a/docs/src/pages/roadmap.tsx +++ b/docs/src/pages/roadmap.tsx @@ -76,6 +76,7 @@ import { mdiWeb, mdiDatabaseOutline, mdiLinkEdit, + mdiTagFaces, mdiMovieOpenPlayOutline, } from '@mdi/js'; import Layout from '@theme/Layout'; @@ -83,6 +84,8 @@ import React from 'react'; import { Item, Timeline } from '../components/timeline'; const releases = { + 'v1.130.0': new Date(2025, 2, 25), + 'v1.127.0': new Date(2025, 1, 26), 'v1.122.0': new Date(2024, 11, 5), 'v1.120.0': new Date(2024, 10, 6), 'v1.114.0': new Date(2024, 8, 6), @@ -242,6 +245,21 @@ const roadmap: Item[] = [ ]; const milestones: Item[] = [ + withRelease({ + icon: mdiFolderMultiple, + iconColor: 'brown', + title: 'Folders view in the mobile app', + description: 'Browse your photos and videos in their folder structure inside the mobile app', + release: 'v1.130.0', + }), + withRelease({ + icon: mdiTagFaces, + iconColor: 'teal', + title: 'Manual face tagging', + description: + 'Manually tag or remove faces in photos and videos, even when automatic detection misses or misidentifies them.', + release: 'v1.127.0', + }), { icon: mdiStar, iconColor: 'gold', @@ -300,7 +318,7 @@ const milestones: Item[] = [ withRelease({ icon: mdiFolderMultiple, iconColor: 'brown', - title: 'Folders', + title: 'Folders view', description: 'Browse your photos and videos in their folder structure', release: 'v1.113.0', }), From c49fd2065bcce54415066bb49a604f4a5fabb17e Mon Sep 17 00:00:00 2001 From: Yaros Date: Mon, 21 Apr 2025 07:18:25 +0200 Subject: [PATCH 014/356] chore(mobile): bump ios deployment target (#17715) * chore: bump ios deployment target * podfile --------- Co-authored-by: Alex --- mobile/ios/Podfile | 4 ++-- mobile/ios/Podfile.lock | 4 ++-- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mobile/ios/Podfile b/mobile/ios/Podfile index a98032db20..ca0166a382 100644 --- a/mobile/ios/Podfile +++ b/mobile/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +platform :ios, '14.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -45,7 +45,7 @@ post_install do |installer| installer.generated_projects.each do |project| project.targets.each do |target| target.build_configurations.each do |config| - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' end end end diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 908ee84aed..9740d6aa52 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -224,7 +224,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: - background_downloader: b42a56120f5348bff70e74222f0e9e6f7f1a1537 + background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c @@ -261,6 +261,6 @@ SPEC CHECKSUMS: url_launcher_ios: 694010445543906933d732453a59da0a173ae33d wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49 -PODFILE CHECKSUM: 03b7eead4ee77b9e778179eeb0f3b5513617451c +PODFILE CHECKSUM: 7ce312f2beab01395db96f6969d90a447279cf45 COCOAPODS: 1.16.2 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 83c231d741..69f122cf17 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -546,7 +546,7 @@ DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -690,7 +690,7 @@ DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -720,7 +720,7 @@ DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", From f0ff8581da6a8e67338f2123c8c24f7557058bf1 Mon Sep 17 00:00:00 2001 From: Yaros Date: Mon, 21 Apr 2025 07:55:13 +0200 Subject: [PATCH 015/356] feat(mobile): map improvements (#17714) * fix: remove unnecessary db operations in map * feat: use user's location for map thumbnails * chore: refactored handleMapEvents * fix: location fails fetching & update geolocator * chore: minor refactor * chore: small style tweak --------- Co-authored-by: Alex --- mobile/lib/pages/library/library.page.dart | 145 +++++++++++------- .../places/places_collection.page.dart | 15 +- mobile/lib/pages/search/map/map.page.dart | 16 +- .../search/map/map_location_picker.page.dart | 2 +- mobile/lib/routing/router.gr.dart | 72 ++++++++- mobile/lib/utils/map_utils.dart | 21 ++- mobile/lib/widgets/map/map_asset_grid.dart | 57 +++++-- .../widgets/search/search_map_thumbnail.dart | 2 +- mobile/pubspec.lock | 12 +- mobile/pubspec.yaml | 2 +- 10 files changed, 240 insertions(+), 104 deletions(-) diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index 1852fb7877..c08a1c715d 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -1,6 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; @@ -12,6 +13,7 @@ import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/utils/map_utils.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; import 'package:immich_mobile/widgets/common/user_avatar.dart'; @@ -297,32 +299,34 @@ class LocalAlbumsCollectionCard extends HookConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( + SizedBox( height: size, width: size, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withAlpha(30), - context.colorScheme.primary.withAlpha(25), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20)), + gradient: LinearGradient( + colors: [ + context.colorScheme.primary.withAlpha(30), + context.colorScheme.primary.withAlpha(25), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: GridView.count( + crossAxisCount: 2, + padding: const EdgeInsets.all(12), + crossAxisSpacing: 8, + mainAxisSpacing: 8, + physics: const NeverScrollableScrollPhysics(), + children: albums.take(4).map((album) { + return AlbumThumbnailCard( + album: album, + showTitle: false, + ); + }).toList(), ), - ), - child: GridView.count( - crossAxisCount: 2, - padding: const EdgeInsets.all(12), - crossAxisSpacing: 8, - mainAxisSpacing: 8, - physics: const NeverScrollableScrollPhysics(), - children: albums.take(4).map((album) { - return AlbumThumbnailCard( - album: album, - showTitle: false, - ); - }).toList(), ), ), Padding( @@ -353,43 +357,66 @@ class PlacesCollectionCard extends StatelessWidget { final widthFactor = isTablet ? 0.25 : 0.5; final size = context.width * widthFactor - 20.0; - return GestureDetector( - onTap: () => context.pushRoute(const PlacesCollectionRoute()), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: size, - width: size, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: context.colorScheme.secondaryContainer.withAlpha(100), - ), - child: IgnorePointer( - child: MapThumbnail( - zoom: 8, - centre: const LatLng( - 21.44950, - -157.91959, - ), - showAttribution: false, - themeMode: - context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'places'.tr(), - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], + return FutureBuilder<(Position?, LocationPermission?)>( + future: MapUtils.checkPermAndGetLocation( + context: context, + silent: true, ), + builder: (context, snapshot) { + var position = snapshot.data?.$1; + return GestureDetector( + onTap: () => context.pushRoute( + PlacesCollectionRoute( + currentLocation: position != null + ? LatLng(position.latitude, position.longitude) + : null, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: size, + width: size, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all(Radius.circular(20)), + color: context.colorScheme.secondaryContainer + .withAlpha(100), + ), + child: IgnorePointer( + child: snapshot.connectionState == + ConnectionState.waiting + ? const Center(child: CircularProgressIndicator()) + : MapThumbnail( + zoom: 8, + centre: LatLng( + position?.latitude ?? 21.44950, + position?.longitude ?? -157.91959, + ), + showAttribution: false, + themeMode: context.isDarkTheme + ? ThemeMode.dark + : ThemeMode.light, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'places'.tr(), + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + }, ); }, ); diff --git a/mobile/lib/pages/library/places/places_collection.page.dart b/mobile/lib/pages/library/places/places_collection.page.dart index f9a2d4292c..5f2dea0dec 100644 --- a/mobile/lib/pages/library/places/places_collection.page.dart +++ b/mobile/lib/pages/library/places/places_collection.page.dart @@ -19,7 +19,8 @@ import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() class PlacesCollectionPage extends HookConsumerWidget { - const PlacesCollectionPage({super.key}); + const PlacesCollectionPage({super.key, this.currentLocation}); + final LatLng? currentLocation; @override Widget build(BuildContext context, WidgetRef ref) { final places = ref.watch(getAllPlacesProvider); @@ -58,12 +59,14 @@ class PlacesCollectionPage extends HookConsumerWidget { height: 200, width: context.width, child: MapThumbnail( - onTap: (_, __) => context.pushRoute(const MapRoute()), + onTap: (_, __) => context + .pushRoute(MapRoute(initialLocation: currentLocation)), zoom: 8, - centre: const LatLng( - 21.44950, - -157.91959, - ), + centre: currentLocation ?? + const LatLng( + 21.44950, + -157.91959, + ), showAttribution: false, themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart index 0e64759241..b80b96f94f 100644 --- a/mobile/lib/pages/search/map/map.page.dart +++ b/mobile/lib/pages/search/map/map.page.dart @@ -34,7 +34,8 @@ import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() class MapPage extends HookConsumerWidget { - const MapPage({super.key}); + const MapPage({super.key, this.initialLocation}); + final LatLng? initialLocation; @override Widget build(BuildContext context, WidgetRef ref) { @@ -235,7 +236,8 @@ class MapPage extends HookConsumerWidget { } void onZoomToLocation() async { - final (location, error) = await MapUtils.checkPermAndGetLocation(context); + final (location, error) = + await MapUtils.checkPermAndGetLocation(context: context); if (error != null) { if (error == LocationPermission.unableToDetermine && context.mounted) { ImmichToast.show( @@ -272,6 +274,7 @@ class MapPage extends HookConsumerWidget { body: Stack( children: [ _MapWithMarker( + initialLocation: initialLocation, style: style, selectedMarker: selectedMarker, onMapCreated: onMapCreated, @@ -303,6 +306,7 @@ class MapPage extends HookConsumerWidget { body: Stack( children: [ _MapWithMarker( + initialLocation: initialLocation, style: style, selectedMarker: selectedMarker, onMapCreated: onMapCreated, @@ -368,6 +372,7 @@ class _MapWithMarker extends StatelessWidget { final OnStyleLoadedCallback onStyleLoaded; final Function()? onMarkerTapped; final ValueNotifier<_AssetMarkerMeta?> selectedMarker; + final LatLng? initialLocation; const _MapWithMarker({ required this.style, @@ -377,6 +382,7 @@ class _MapWithMarker extends StatelessWidget { required this.onStyleLoaded, required this.selectedMarker, this.onMarkerTapped, + this.initialLocation, }); @override @@ -389,8 +395,10 @@ class _MapWithMarker extends StatelessWidget { children: [ style.widgetWhen( onData: (style) => MapLibreMap( - initialCameraPosition: - const CameraPosition(target: LatLng(0, 0)), + initialCameraPosition: CameraPosition( + target: initialLocation ?? const LatLng(0, 0), + zoom: initialLocation != null ? 12 : 0, + ), styleString: style, // This is needed to update the selectedMarker's position on map camera updates // The changes are notified through the mapController ValueListener which is added in [onMapCreated] diff --git a/mobile/lib/pages/search/map/map_location_picker.page.dart b/mobile/lib/pages/search/map/map_location_picker.page.dart index 9d526d8080..f27deae052 100644 --- a/mobile/lib/pages/search/map/map_location_picker.page.dart +++ b/mobile/lib/pages/search/map/map_location_picker.page.dart @@ -46,7 +46,7 @@ class MapLocationPickerPage extends HookConsumerWidget { Future getCurrentLocation() async { var (currentLocation, _) = - await MapUtils.checkPermAndGetLocation(context); + await MapUtils.checkPermAndGetLocation(context: context); if (currentLocation == null) { return; diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index a78371e05e..89e83e8159 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1024,10 +1024,17 @@ class MapLocationPickerRouteArgs { /// generated route for /// [MapPage] -class MapRoute extends PageRouteInfo { - const MapRoute({List? children}) - : super( +class MapRoute extends PageRouteInfo { + MapRoute({ + Key? key, + LatLng? initialLocation, + List? children, + }) : super( MapRoute.name, + args: MapRouteArgs( + key: key, + initialLocation: initialLocation, + ), initialChildren: children, ); @@ -1036,11 +1043,32 @@ class MapRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - return const MapPage(); + final args = + data.argsAs(orElse: () => const MapRouteArgs()); + return MapPage( + key: args.key, + initialLocation: args.initialLocation, + ); }, ); } +class MapRouteArgs { + const MapRouteArgs({ + this.key, + this.initialLocation, + }); + + final Key? key; + + final LatLng? initialLocation; + + @override + String toString() { + return 'MapRouteArgs{key: $key, initialLocation: $initialLocation}'; + } +} + /// generated route for /// [MemoryPage] class MemoryRoute extends PageRouteInfo { @@ -1333,10 +1361,17 @@ class PhotosRoute extends PageRouteInfo { /// generated route for /// [PlacesCollectionPage] -class PlacesCollectionRoute extends PageRouteInfo { - const PlacesCollectionRoute({List? children}) - : super( +class PlacesCollectionRoute extends PageRouteInfo { + PlacesCollectionRoute({ + Key? key, + LatLng? currentLocation, + List? children, + }) : super( PlacesCollectionRoute.name, + args: PlacesCollectionRouteArgs( + key: key, + currentLocation: currentLocation, + ), initialChildren: children, ); @@ -1345,11 +1380,32 @@ class PlacesCollectionRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - return const PlacesCollectionPage(); + final args = data.argsAs( + orElse: () => const PlacesCollectionRouteArgs()); + return PlacesCollectionPage( + key: args.key, + currentLocation: args.currentLocation, + ); }, ); } +class PlacesCollectionRouteArgs { + const PlacesCollectionRouteArgs({ + this.key, + this.currentLocation, + }); + + final Key? key; + + final LatLng? currentLocation; + + @override + String toString() { + return 'PlacesCollectionRouteArgs{key: $key, currentLocation: $currentLocation}'; + } +} + /// generated route for /// [RecentlyAddedPage] class RecentlyAddedRoute extends PageRouteInfo { diff --git a/mobile/lib/utils/map_utils.dart b/mobile/lib/utils/map_utils.dart index 44f7ebf271..df1ff28d8f 100644 --- a/mobile/lib/utils/map_utils.dart +++ b/mobile/lib/utils/map_utils.dart @@ -64,12 +64,13 @@ class MapUtils { 'features': markers.map(_addFeature).toList(), }; - static Future<(Position?, LocationPermission?)> checkPermAndGetLocation( - BuildContext context, - ) async { + static Future<(Position?, LocationPermission?)> checkPermAndGetLocation({ + required BuildContext context, + bool silent = false, + }) async { try { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); - if (!serviceEnabled) { + if (!serviceEnabled && !silent) { showDialog( context: context, builder: (context) => _LocationServiceDisabledDialog(), @@ -80,7 +81,7 @@ class MapUtils { LocationPermission permission = await Geolocator.checkPermission(); bool shouldRequestPermission = false; - if (permission == LocationPermission.denied) { + if (permission == LocationPermission.denied && !silent) { shouldRequestPermission = await showDialog( context: context, builder: (context) => _LocationPermissionDisabledDialog(), @@ -94,15 +95,19 @@ class MapUtils { permission == LocationPermission.deniedForever) { // Open app settings only if you did not request for permission before if (permission == LocationPermission.deniedForever && - !shouldRequestPermission) { + !shouldRequestPermission && + !silent) { await Geolocator.openAppSettings(); } return (null, LocationPermission.deniedForever); } Position currentUserLocation = await Geolocator.getCurrentPosition( - desiredAccuracy: LocationAccuracy.medium, - timeLimit: const Duration(seconds: 5), + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 0, + timeLimit: Duration(seconds: 5), + ), ); return (currentUserLocation, null); } catch (error, stack) { diff --git a/mobile/lib/widgets/map/map_asset_grid.dart b/mobile/lib/widgets/map/map_asset_grid.dart index 18003cf293..a9ddc86df9 100644 --- a/mobile/lib/widgets/map/map_asset_grid.dart +++ b/mobile/lib/widgets/map/map_asset_grid.dart @@ -46,12 +46,39 @@ class MapAssetGrid extends HookConsumerWidget { final gridScrollThrottler = useThrottler(interval: const Duration(milliseconds: 300)); + // Add a cache for assets we've already loaded + final assetCache = useRef>({}); + void handleMapEvents(MapEvent event) async { if (event is MapAssetsInBoundsUpdated) { - assetsInBounds.value = await ref - .read(dbProvider) - .assets - .getAllByRemoteId(event.assetRemoteIds); + final assetIds = event.assetRemoteIds; + final missingIds = []; + final currentAssets = []; + + for (final id in assetIds) { + final asset = assetCache.value[id]; + if (asset != null) { + currentAssets.add(asset); + } else { + missingIds.add(id); + } + } + + // Only fetch missing assets + if (missingIds.isNotEmpty) { + final newAssets = + await ref.read(dbProvider).assets.getAllByRemoteId(missingIds); + + // Add new assets to cache and current list + for (final asset in newAssets) { + if (asset.remoteId != null) { + assetCache.value[asset.remoteId!] = asset; + currentAssets.add(asset); + } + } + } + + assetsInBounds.value = currentAssets; return; } } @@ -124,7 +151,7 @@ class MapAssetGrid extends HookConsumerWidget { alignment: Alignment.bottomCenter, child: FractionallySizedBox( // Place it just below the drag handle - heightFactor: 0.80, + heightFactor: 0.87, child: assetsInBounds.value.isNotEmpty ? ref .watch(assetsTimelineProvider(assetsInBounds.value)) @@ -251,8 +278,18 @@ class _MapSheetDragRegion extends StatelessWidget { const SizedBox(height: 15), const CustomDraggingHandle(), const SizedBox(height: 15), - Text(assetsInBoundsText, style: context.textTheme.bodyLarge), - const Divider(height: 35), + Center( + child: Text( + assetsInBoundsText, + style: TextStyle( + fontSize: 20, + color: context.textTheme.displayLarge?.color + ?.withValues(alpha: 0.75), + fontWeight: FontWeight.w500, + ), + ), + ), + const SizedBox(height: 8), ], ), ValueListenableBuilder( @@ -260,14 +297,14 @@ class _MapSheetDragRegion extends StatelessWidget { builder: (_, value, __) => Visibility( visible: value != null, child: Positioned( - right: 15, - top: 15, + right: 18, + top: 24, child: IconButton( icon: Icon( Icons.map_outlined, color: context.textTheme.displayLarge?.color, ), - iconSize: 20, + iconSize: 24, tooltip: 'Zoom to bounds', onPressed: () => onZoomToAsset?.call(value!), ), diff --git a/mobile/lib/widgets/search/search_map_thumbnail.dart b/mobile/lib/widgets/search/search_map_thumbnail.dart index b4a12ab826..78af8f936b 100644 --- a/mobile/lib/widgets/search/search_map_thumbnail.dart +++ b/mobile/lib/widgets/search/search_map_thumbnail.dart @@ -20,7 +20,7 @@ class SearchMapThumbnail extends StatelessWidget { return ThumbnailWithInfoContainer( label: 'search_page_your_map'.tr(), onTap: () { - context.pushRoute(const MapRoute()); + context.pushRoute(MapRoute()); }, child: IgnorePointer( child: MapThumbnail( diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 235b3f71c3..9e8aced11c 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -696,18 +696,18 @@ packages: dependency: "direct main" description: name: geolocator - sha256: "6cb9fb6e5928b58b9a84bdf85012d757fd07aab8215c5205337021c4999bad27" + sha256: e7ebfa04ce451daf39b5499108c973189a71a919aa53c1204effda1c5b93b822 url: "https://pub.dev" source: hosted - version: "11.1.0" + version: "14.0.0" geolocator_android: dependency: transitive description: name: geolocator_android - sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47" + sha256: "114072db5d1dce0ec0b36af2697f55c133bc89a2c8dd513e137c0afe59696ed4" url: "https://pub.dev" source: hosted - version: "4.6.1" + version: "5.0.1+1" geolocator_apple: dependency: transitive description: @@ -728,10 +728,10 @@ packages: dependency: transitive description: name: geolocator_web - sha256: "49d8f846ebeb5e2b6641fe477a7e97e5dd73f03cbfef3fd5c42177b7300fb0ed" + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.1.3" geolocator_windows: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index fdd91e1f87..44d2e7e5d1 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: flutter_udid: ^3.0.0 flutter_web_auth_2: ^5.0.0-alpha.0 fluttertoast: ^8.2.12 - geolocator: ^11.0.0 + geolocator: ^14.0.0 hooks_riverpod: ^2.6.1 http: ^1.3.0 image_picker: ^1.1.2 From 488dc4efbdcb64fd84d4cf67f8b145160313a1c4 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 21 Apr 2025 10:49:26 -0400 Subject: [PATCH 016/356] refactor: notification-admin controller (#17748) --- mobile/openapi/README.md | 4 ++-- mobile/openapi/lib/api.dart | 2 +- ..._api.dart => notifications_admin_api.dart} | 24 +++++++++---------- open-api/immich-openapi-specs.json | 12 +++++----- open-api/typescript-sdk/src/fetch-client.ts | 8 +++---- server/src/controllers/index.ts | 4 ++-- ...er.ts => notification-admin.controller.ts} | 10 ++++---- .../notification-settings.svelte | 4 ++-- .../template-settings.svelte | 4 ++-- 9 files changed, 36 insertions(+), 36 deletions(-) rename mobile/openapi/lib/api/{notifications_api.dart => notifications_admin_api.dart} (75%) rename server/src/controllers/{notification.controller.ts => notification-admin.controller.ts} (80%) diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 0ae07e9efd..fef299b5af 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -145,8 +145,8 @@ Class | Method | HTTP request | Description *MemoriesApi* | [**removeMemoryAssets**](doc//MemoriesApi.md#removememoryassets) | **DELETE** /memories/{id}/assets | *MemoriesApi* | [**searchMemories**](doc//MemoriesApi.md#searchmemories) | **GET** /memories | *MemoriesApi* | [**updateMemory**](doc//MemoriesApi.md#updatememory) | **PUT** /memories/{id} | -*NotificationsApi* | [**getNotificationTemplate**](doc//NotificationsApi.md#getnotificationtemplate) | **POST** /notifications/templates/{name} | -*NotificationsApi* | [**sendTestEmail**](doc//NotificationsApi.md#sendtestemail) | **POST** /notifications/test-email | +*NotificationsAdminApi* | [**getNotificationTemplateAdmin**](doc//NotificationsAdminApi.md#getnotificationtemplateadmin) | **POST** /notifications/admin/templates/{name} | +*NotificationsAdminApi* | [**sendTestEmailAdmin**](doc//NotificationsAdminApi.md#sendtestemailadmin) | **POST** /notifications/admin/test-email | *OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback | *OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link | *OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect | diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 3986362c96..ff5a95bbbc 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -44,7 +44,7 @@ part 'api/jobs_api.dart'; part 'api/libraries_api.dart'; part 'api/map_api.dart'; part 'api/memories_api.dart'; -part 'api/notifications_api.dart'; +part 'api/notifications_admin_api.dart'; part 'api/o_auth_api.dart'; part 'api/partners_api.dart'; part 'api/people_api.dart'; diff --git a/mobile/openapi/lib/api/notifications_api.dart b/mobile/openapi/lib/api/notifications_admin_api.dart similarity index 75% rename from mobile/openapi/lib/api/notifications_api.dart rename to mobile/openapi/lib/api/notifications_admin_api.dart index 518a1baa4a..c58bf8978d 100644 --- a/mobile/openapi/lib/api/notifications_api.dart +++ b/mobile/openapi/lib/api/notifications_admin_api.dart @@ -11,20 +11,20 @@ part of openapi.api; -class NotificationsApi { - NotificationsApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; +class NotificationsAdminApi { + NotificationsAdminApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; final ApiClient apiClient; - /// Performs an HTTP 'POST /notifications/templates/{name}' operation and returns the [Response]. + /// Performs an HTTP 'POST /notifications/admin/templates/{name}' operation and returns the [Response]. /// Parameters: /// /// * [String] name (required): /// /// * [TemplateDto] templateDto (required): - Future getNotificationTemplateWithHttpInfo(String name, TemplateDto templateDto,) async { + Future getNotificationTemplateAdminWithHttpInfo(String name, TemplateDto templateDto,) async { // ignore: prefer_const_declarations - final apiPath = r'/notifications/templates/{name}' + final apiPath = r'/notifications/admin/templates/{name}' .replaceAll('{name}', name); // ignore: prefer_final_locals @@ -53,8 +53,8 @@ class NotificationsApi { /// * [String] name (required): /// /// * [TemplateDto] templateDto (required): - Future getNotificationTemplate(String name, TemplateDto templateDto,) async { - final response = await getNotificationTemplateWithHttpInfo(name, templateDto,); + Future getNotificationTemplateAdmin(String name, TemplateDto templateDto,) async { + final response = await getNotificationTemplateAdminWithHttpInfo(name, templateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -68,13 +68,13 @@ class NotificationsApi { return null; } - /// Performs an HTTP 'POST /notifications/test-email' operation and returns the [Response]. + /// Performs an HTTP 'POST /notifications/admin/test-email' operation and returns the [Response]. /// Parameters: /// /// * [SystemConfigSmtpDto] systemConfigSmtpDto (required): - Future sendTestEmailWithHttpInfo(SystemConfigSmtpDto systemConfigSmtpDto,) async { + Future sendTestEmailAdminWithHttpInfo(SystemConfigSmtpDto systemConfigSmtpDto,) async { // ignore: prefer_const_declarations - final apiPath = r'/notifications/test-email'; + final apiPath = r'/notifications/admin/test-email'; // ignore: prefer_final_locals Object? postBody = systemConfigSmtpDto; @@ -100,8 +100,8 @@ class NotificationsApi { /// Parameters: /// /// * [SystemConfigSmtpDto] systemConfigSmtpDto (required): - Future sendTestEmail(SystemConfigSmtpDto systemConfigSmtpDto,) async { - final response = await sendTestEmailWithHttpInfo(systemConfigSmtpDto,); + Future sendTestEmailAdmin(SystemConfigSmtpDto systemConfigSmtpDto,) async { + final response = await sendTestEmailAdminWithHttpInfo(systemConfigSmtpDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 4e8e7ab834..169c076341 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -3485,9 +3485,9 @@ ] } }, - "/notifications/templates/{name}": { + "/notifications/admin/templates/{name}": { "post": { - "operationId": "getNotificationTemplate", + "operationId": "getNotificationTemplateAdmin", "parameters": [ { "name": "name", @@ -3532,13 +3532,13 @@ } ], "tags": [ - "Notifications" + "Notifications (Admin)" ] } }, - "/notifications/test-email": { + "/notifications/admin/test-email": { "post": { - "operationId": "sendTestEmail", + "operationId": "sendTestEmailAdmin", "parameters": [], "requestBody": { "content": { @@ -3574,7 +3574,7 @@ } ], "tags": [ - "Notifications" + "Notifications (Admin)" ] } }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index f82f5bc9a7..e45449c9cd 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -2318,26 +2318,26 @@ export function addMemoryAssets({ id, bulkIdsDto }: { body: bulkIdsDto }))); } -export function getNotificationTemplate({ name, templateDto }: { +export function getNotificationTemplateAdmin({ name, templateDto }: { name: string; templateDto: TemplateDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: TemplateResponseDto; - }>(`/notifications/templates/${encodeURIComponent(name)}`, oazapfts.json({ + }>(`/notifications/admin/templates/${encodeURIComponent(name)}`, oazapfts.json({ ...opts, method: "POST", body: templateDto }))); } -export function sendTestEmail({ systemConfigSmtpDto }: { +export function sendTestEmailAdmin({ systemConfigSmtpDto }: { systemConfigSmtpDto: SystemConfigSmtpDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: TestEmailResponseDto; - }>("/notifications/test-email", oazapfts.json({ + }>("/notifications/admin/test-email", oazapfts.json({ ...opts, method: "POST", body: systemConfigSmtpDto diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index c9d63f8bcd..0da0aac8b1 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -13,7 +13,7 @@ import { JobController } from 'src/controllers/job.controller'; import { LibraryController } from 'src/controllers/library.controller'; import { MapController } from 'src/controllers/map.controller'; import { MemoryController } from 'src/controllers/memory.controller'; -import { NotificationController } from 'src/controllers/notification.controller'; +import { NotificationAdminController } from 'src/controllers/notification-admin.controller'; import { OAuthController } from 'src/controllers/oauth.controller'; import { PartnerController } from 'src/controllers/partner.controller'; import { PersonController } from 'src/controllers/person.controller'; @@ -47,7 +47,7 @@ export const controllers = [ LibraryController, MapController, MemoryController, - NotificationController, + NotificationAdminController, OAuthController, PartnerController, PersonController, diff --git a/server/src/controllers/notification.controller.ts b/server/src/controllers/notification-admin.controller.ts similarity index 80% rename from server/src/controllers/notification.controller.ts rename to server/src/controllers/notification-admin.controller.ts index 39946a9fc9..f3ce4cdac9 100644 --- a/server/src/controllers/notification.controller.ts +++ b/server/src/controllers/notification-admin.controller.ts @@ -7,22 +7,22 @@ import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { EmailTemplate } from 'src/repositories/notification.repository'; import { NotificationService } from 'src/services/notification.service'; -@ApiTags('Notifications') -@Controller('notifications') -export class NotificationController { +@ApiTags('Notifications (Admin)') +@Controller('notifications/admin') +export class NotificationAdminController { constructor(private service: NotificationService) {} @Post('test-email') @HttpCode(HttpStatus.OK) @Authenticated({ admin: true }) - sendTestEmail(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto): Promise { + sendTestEmailAdmin(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto): Promise { return this.service.sendTestEmail(auth.user.id, dto); } @Post('templates/:name') @HttpCode(HttpStatus.OK) @Authenticated({ admin: true }) - getNotificationTemplate( + getNotificationTemplateAdmin( @Auth() auth: AuthDto, @Param('name') name: EmailTemplate, @Body() dto: TemplateDto, diff --git a/web/src/lib/components/admin-page/settings/notification-settings/notification-settings.svelte b/web/src/lib/components/admin-page/settings/notification-settings/notification-settings.svelte index a2b6305f76..24e672607d 100644 --- a/web/src/lib/components/admin-page/settings/notification-settings/notification-settings.svelte +++ b/web/src/lib/components/admin-page/settings/notification-settings/notification-settings.svelte @@ -12,7 +12,7 @@ import { SettingInputFieldType } from '$lib/constants'; import { user } from '$lib/stores/user.store'; import { handleError } from '$lib/utils/handle-error'; - import { sendTestEmail, type SystemConfigDto } from '@immich/sdk'; + import { sendTestEmailAdmin, type SystemConfigDto } from '@immich/sdk'; import { Button } from '@immich/ui'; import { isEqual } from 'lodash-es'; import { t } from 'svelte-i18n'; @@ -40,7 +40,7 @@ isSending = true; try { - await sendTestEmail({ + await sendTestEmailAdmin({ systemConfigSmtpDto: { enabled: config.notifications.smtp.enabled, transport: { diff --git a/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte b/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte index b418aebb2b..06d8196935 100644 --- a/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte +++ b/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte @@ -6,7 +6,7 @@ import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte'; import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte'; import { handleError } from '$lib/utils/handle-error'; - import { type SystemConfigDto, type SystemConfigTemplateEmailsDto, getNotificationTemplate } from '@immich/sdk'; + import { type SystemConfigDto, type SystemConfigTemplateEmailsDto, getNotificationTemplateAdmin } from '@immich/sdk'; import { Button } from '@immich/ui'; import { mdiEyeOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -25,7 +25,7 @@ const getTemplate = async (name: string, template: string) => { try { loadingPreview = true; - const result = await getNotificationTemplate({ name, templateDto: { template } }); + const result = await getNotificationTemplateAdmin({ name, templateDto: { template } }); htmlPreview = result.html; } catch (error) { handleError(error, 'Could not load template.'); From 56a4aa9ffe61eb5bf83fe511067e789123d9464d Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 21 Apr 2025 12:53:37 -0400 Subject: [PATCH 017/356] refactor: email repository (#17746) --- .../notification-admin.controller.ts | 2 +- server/src/emails/album-invite.email.tsx | 2 +- server/src/emails/album-update.email.tsx | 2 +- server/src/emails/test.email.tsx | 2 +- server/src/emails/welcome.email.tsx | 2 +- ...itory.spec.ts => email.repository.spec.ts} | 8 +- ...tion.repository.ts => email.repository.ts} | 4 +- server/src/repositories/index.ts | 4 +- server/src/services/base.service.ts | 4 +- .../src/services/notification.service.spec.ts | 88 +++++++++---------- server/src/services/notification.service.ts | 24 ++--- server/test/medium.factory.ts | 2 +- server/test/utils.ts | 8 +- 13 files changed, 74 insertions(+), 78 deletions(-) rename server/src/repositories/{notification.repository.spec.ts => email.repository.spec.ts} (87%) rename server/src/repositories/{notification.repository.ts => email.repository.ts} (97%) diff --git a/server/src/controllers/notification-admin.controller.ts b/server/src/controllers/notification-admin.controller.ts index f3ce4cdac9..937244fc56 100644 --- a/server/src/controllers/notification-admin.controller.ts +++ b/server/src/controllers/notification-admin.controller.ts @@ -4,7 +4,7 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { TemplateDto, TemplateResponseDto, TestEmailResponseDto } from 'src/dtos/notification.dto'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; -import { EmailTemplate } from 'src/repositories/notification.repository'; +import { EmailTemplate } from 'src/repositories/email.repository'; import { NotificationService } from 'src/services/notification.service'; @ApiTags('Notifications (Admin)') diff --git a/server/src/emails/album-invite.email.tsx b/server/src/emails/album-invite.email.tsx index 4bd7abc305..fdc189af97 100644 --- a/server/src/emails/album-invite.email.tsx +++ b/server/src/emails/album-invite.email.tsx @@ -2,7 +2,7 @@ import { Img, Link, Section, Text } from '@react-email/components'; import * as React from 'react'; import { ImmichButton } from 'src/emails/components/button.component'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { AlbumInviteEmailProps } from 'src/repositories/notification.repository'; +import { AlbumInviteEmailProps } from 'src/repositories/email.repository'; import { replaceTemplateTags } from 'src/utils/replace-template-tags'; export const AlbumInviteEmail = ({ diff --git a/server/src/emails/album-update.email.tsx b/server/src/emails/album-update.email.tsx index 2311e896e1..3bed3a5b36 100644 --- a/server/src/emails/album-update.email.tsx +++ b/server/src/emails/album-update.email.tsx @@ -2,7 +2,7 @@ import { Img, Link, Section, Text } from '@react-email/components'; import * as React from 'react'; import { ImmichButton } from 'src/emails/components/button.component'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { AlbumUpdateEmailProps } from 'src/repositories/notification.repository'; +import { AlbumUpdateEmailProps } from 'src/repositories/email.repository'; import { replaceTemplateTags } from 'src/utils/replace-template-tags'; export const AlbumUpdateEmail = ({ diff --git a/server/src/emails/test.email.tsx b/server/src/emails/test.email.tsx index ac9bdbe0ea..0d87307080 100644 --- a/server/src/emails/test.email.tsx +++ b/server/src/emails/test.email.tsx @@ -1,7 +1,7 @@ import { Link, Row, Text } from '@react-email/components'; import * as React from 'react'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { TestEmailProps } from 'src/repositories/notification.repository'; +import { TestEmailProps } from 'src/repositories/email.repository'; export const TestEmail = ({ baseUrl, displayName }: TestEmailProps) => ( diff --git a/server/src/emails/welcome.email.tsx b/server/src/emails/welcome.email.tsx index 11a6602711..57e86ab252 100644 --- a/server/src/emails/welcome.email.tsx +++ b/server/src/emails/welcome.email.tsx @@ -2,7 +2,7 @@ import { Link, Section, Text } from '@react-email/components'; import * as React from 'react'; import { ImmichButton } from 'src/emails/components/button.component'; import ImmichLayout from 'src/emails/components/immich.layout'; -import { WelcomeEmailProps } from 'src/repositories/notification.repository'; +import { WelcomeEmailProps } from 'src/repositories/email.repository'; import { replaceTemplateTags } from 'src/utils/replace-template-tags'; export const WelcomeEmail = ({ baseUrl, displayName, username, password, customTemplate }: WelcomeEmailProps) => { diff --git a/server/src/repositories/notification.repository.spec.ts b/server/src/repositories/email.repository.spec.ts similarity index 87% rename from server/src/repositories/notification.repository.spec.ts rename to server/src/repositories/email.repository.spec.ts index 1d0770af6b..5640b26bf6 100644 --- a/server/src/repositories/notification.repository.spec.ts +++ b/server/src/repositories/email.repository.spec.ts @@ -1,13 +1,13 @@ +import { EmailRenderRequest, EmailRepository, EmailTemplate } from 'src/repositories/email.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; -import { EmailRenderRequest, EmailTemplate, NotificationRepository } from 'src/repositories/notification.repository'; import { automock } from 'test/utils'; -describe(NotificationRepository.name, () => { - let sut: NotificationRepository; +describe(EmailRepository.name, () => { + let sut: EmailRepository; beforeEach(() => { // eslint-disable-next-line no-sparse-arrays - sut = new NotificationRepository(automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false })); + sut = new EmailRepository(automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false })); }); describe('renderEmail', () => { diff --git a/server/src/repositories/notification.repository.ts b/server/src/repositories/email.repository.ts similarity index 97% rename from server/src/repositories/notification.repository.ts rename to server/src/repositories/email.repository.ts index 91f03b928b..78c89b4a9d 100644 --- a/server/src/repositories/notification.repository.ts +++ b/server/src/repositories/email.repository.ts @@ -98,9 +98,9 @@ export type SendEmailResponse = { }; @Injectable() -export class NotificationRepository { +export class EmailRepository { constructor(private logger: LoggingRepository) { - this.logger.setContext(NotificationRepository.name); + this.logger.setContext(EmailRepository.name); } verifySmtp(options: SmtpOptions): Promise { diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index ef36a2b3f8..bd2e5c6774 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -11,6 +11,7 @@ import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { DownloadRepository } from 'src/repositories/download.repository'; +import { EmailRepository } from 'src/repositories/email.repository'; import { EventRepository } from 'src/repositories/event.repository'; import { JobRepository } from 'src/repositories/job.repository'; import { LibraryRepository } from 'src/repositories/library.repository'; @@ -21,7 +22,6 @@ import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; import { MetadataRepository } from 'src/repositories/metadata.repository'; import { MoveRepository } from 'src/repositories/move.repository'; -import { NotificationRepository } from 'src/repositories/notification.repository'; import { OAuthRepository } from 'src/repositories/oauth.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; import { PersonRepository } from 'src/repositories/person.repository'; @@ -65,7 +65,7 @@ export const repositories = [ MemoryRepository, MetadataRepository, MoveRepository, - NotificationRepository, + EmailRepository, OAuthRepository, PartnerRepository, PersonRepository, diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 2fbdd6e4c0..23ddb1b63e 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -18,6 +18,7 @@ import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { DownloadRepository } from 'src/repositories/download.repository'; +import { EmailRepository } from 'src/repositories/email.repository'; import { EventRepository } from 'src/repositories/event.repository'; import { JobRepository } from 'src/repositories/job.repository'; import { LibraryRepository } from 'src/repositories/library.repository'; @@ -28,7 +29,6 @@ import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; import { MetadataRepository } from 'src/repositories/metadata.repository'; import { MoveRepository } from 'src/repositories/move.repository'; -import { NotificationRepository } from 'src/repositories/notification.repository'; import { OAuthRepository } from 'src/repositories/oauth.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; import { PersonRepository } from 'src/repositories/person.repository'; @@ -70,6 +70,7 @@ export class BaseService { protected cryptoRepository: CryptoRepository, protected databaseRepository: DatabaseRepository, protected downloadRepository: DownloadRepository, + protected emailRepository: EmailRepository, protected eventRepository: EventRepository, protected jobRepository: JobRepository, protected libraryRepository: LibraryRepository, @@ -79,7 +80,6 @@ export class BaseService { protected memoryRepository: MemoryRepository, protected metadataRepository: MetadataRepository, protected moveRepository: MoveRepository, - protected notificationRepository: NotificationRepository, protected oauthRepository: OAuthRepository, protected partnerRepository: PartnerRepository, protected personRepository: PersonRepository, diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 85e425b11f..5830260753 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -3,7 +3,7 @@ import { defaults, SystemConfig } from 'src/config'; import { AlbumUser } from 'src/database'; import { SystemConfigDto } from 'src/dtos/system-config.dto'; import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum'; -import { EmailTemplate } from 'src/repositories/notification.repository'; +import { EmailTemplate } from 'src/repositories/email.repository'; import { NotificationService } from 'src/services/notification.service'; import { INotifyAlbumUpdateJob } from 'src/types'; import { albumStub } from 'test/fixtures/album.stub'; @@ -74,18 +74,18 @@ describe(NotificationService.name, () => { const oldConfig = configs.smtpDisabled; const newConfig = configs.smtpEnabled; - mocks.notification.verifySmtp.mockResolvedValue(true); + mocks.email.verifySmtp.mockResolvedValue(true); await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); - expect(mocks.notification.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport); + expect(mocks.email.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport); }); it('validates smtp config when transport changes', async () => { const oldConfig = configs.smtpEnabled; const newConfig = configs.smtpTransport; - mocks.notification.verifySmtp.mockResolvedValue(true); + mocks.email.verifySmtp.mockResolvedValue(true); await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); - expect(mocks.notification.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport); + expect(mocks.email.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport); }); it('skips smtp validation when there are no changes', async () => { @@ -93,7 +93,7 @@ describe(NotificationService.name, () => { const newConfig = { ...configs.smtpEnabled }; await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); - expect(mocks.notification.verifySmtp).not.toHaveBeenCalled(); + expect(mocks.email.verifySmtp).not.toHaveBeenCalled(); }); it('skips smtp validation with DTO when there are no changes', async () => { @@ -101,7 +101,7 @@ describe(NotificationService.name, () => { const newConfig = plainToInstance(SystemConfigDto, configs.smtpEnabled); await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); - expect(mocks.notification.verifySmtp).not.toHaveBeenCalled(); + expect(mocks.email.verifySmtp).not.toHaveBeenCalled(); }); it('skips smtp validation when smtp is disabled', async () => { @@ -109,14 +109,14 @@ describe(NotificationService.name, () => { const newConfig = { ...configs.smtpDisabled }; await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); - expect(mocks.notification.verifySmtp).not.toHaveBeenCalled(); + expect(mocks.email.verifySmtp).not.toHaveBeenCalled(); }); it('should fail if smtp configuration is invalid', async () => { const oldConfig = configs.smtpDisabled; const newConfig = configs.smtpEnabled; - mocks.notification.verifySmtp.mockRejectedValue(new Error('Failed validating smtp')); + mocks.email.verifySmtp.mockRejectedValue(new Error('Failed validating smtp')); await expect(sut.onConfigValidate({ oldConfig, newConfig })).rejects.toBeInstanceOf(Error); }); }); @@ -248,7 +248,7 @@ describe(NotificationService.name, () => { it('should throw error if smtp validation fails', async () => { mocks.user.get.mockResolvedValue(userStub.admin); - mocks.notification.verifySmtp.mockRejectedValue(''); + mocks.email.verifySmtp.mockRejectedValue(''); await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).rejects.toThrow( 'Failed to verify SMTP configuration', @@ -257,16 +257,16 @@ describe(NotificationService.name, () => { it('should send email to default domain', async () => { mocks.user.get.mockResolvedValue(userStub.admin); - mocks.notification.verifySmtp.mockResolvedValue(true); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.notification.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); + mocks.email.verifySmtp.mockResolvedValue(true); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow(); - expect(mocks.notification.renderEmail).toHaveBeenCalledWith({ + expect(mocks.email.renderEmail).toHaveBeenCalledWith({ template: EmailTemplate.TEST_EMAIL, data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name }, }); - expect(mocks.notification.sendEmail).toHaveBeenCalledWith( + expect(mocks.email.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ subject: 'Test email from Immich', smtp: configs.smtpTransport.notifications.smtp.transport, @@ -276,17 +276,17 @@ describe(NotificationService.name, () => { it('should send email to external domain', async () => { mocks.user.get.mockResolvedValue(userStub.admin); - mocks.notification.verifySmtp.mockResolvedValue(true); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.verifySmtp.mockResolvedValue(true); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.systemMetadata.get.mockResolvedValue({ server: { externalDomain: 'https://demo.immich.app' } }); - mocks.notification.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); + mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow(); - expect(mocks.notification.renderEmail).toHaveBeenCalledWith({ + expect(mocks.email.renderEmail).toHaveBeenCalledWith({ template: EmailTemplate.TEST_EMAIL, data: { baseUrl: 'https://demo.immich.app', displayName: userStub.admin.name }, }); - expect(mocks.notification.sendEmail).toHaveBeenCalledWith( + expect(mocks.email.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ subject: 'Test email from Immich', smtp: configs.smtpTransport.notifications.smtp.transport, @@ -296,18 +296,18 @@ describe(NotificationService.name, () => { it('should send email with replyTo', async () => { mocks.user.get.mockResolvedValue(userStub.admin); - mocks.notification.verifySmtp.mockResolvedValue(true); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.notification.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); + mocks.email.verifySmtp.mockResolvedValue(true); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); await expect( sut.sendTestEmail('', { ...configs.smtpTransport.notifications.smtp, replyTo: 'demo@immich.app' }), ).resolves.not.toThrow(); - expect(mocks.notification.renderEmail).toHaveBeenCalledWith({ + expect(mocks.email.renderEmail).toHaveBeenCalledWith({ template: EmailTemplate.TEST_EMAIL, data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name }, }); - expect(mocks.notification.sendEmail).toHaveBeenCalledWith( + expect(mocks.email.sendEmail).toHaveBeenCalledWith( expect.objectContaining({ subject: 'Test email from Immich', smtp: configs.smtpTransport.notifications.smtp.transport, @@ -325,7 +325,7 @@ describe(NotificationService.name, () => { it('should be successful', async () => { mocks.user.get.mockResolvedValue(userStub.admin); mocks.systemMetadata.get.mockResolvedValue({ server: {} }); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); await expect(sut.handleUserSignup({ id: '' })).resolves.toBe(JobStatus.SUCCESS); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -390,7 +390,7 @@ describe(NotificationService.name, () => { ], }); mocks.systemMetadata.get.mockResolvedValue({ server: {} }); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -411,7 +411,7 @@ describe(NotificationService.name, () => { ], }); mocks.systemMetadata.get.mockResolvedValue({ server: {} }); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); @@ -440,7 +440,7 @@ describe(NotificationService.name, () => { ], }); mocks.systemMetadata.get.mockResolvedValue({ server: {} }); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([ { id: '1', type: AssetFileType.THUMBNAIL, path: 'path-to-thumb.jpg' }, ]); @@ -471,7 +471,7 @@ describe(NotificationService.name, () => { ], }); mocks.systemMetadata.get.mockResolvedValue({ server: {} }); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetStub.image.files[2]]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); @@ -508,12 +508,12 @@ describe(NotificationService.name, () => { albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], }); mocks.user.get.mockResolvedValueOnce(userStub.user1); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] }); expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); - expect(mocks.notification.renderEmail).not.toHaveBeenCalled(); + expect(mocks.email.renderEmail).not.toHaveBeenCalled(); }); it('should skip recipient with disabled email notifications', async () => { @@ -530,12 +530,12 @@ describe(NotificationService.name, () => { }, ], }); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] }); expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); - expect(mocks.notification.renderEmail).not.toHaveBeenCalled(); + expect(mocks.email.renderEmail).not.toHaveBeenCalled(); }); it('should skip recipient with disabled email notifications for the album update event', async () => { @@ -552,12 +552,12 @@ describe(NotificationService.name, () => { }, ], }); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] }); expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); - expect(mocks.notification.renderEmail).not.toHaveBeenCalled(); + expect(mocks.email.renderEmail).not.toHaveBeenCalled(); }); it('should send email', async () => { @@ -566,12 +566,12 @@ describe(NotificationService.name, () => { albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], }); mocks.user.get.mockResolvedValue(userStub.user1); - mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] }); expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); - expect(mocks.notification.renderEmail).toHaveBeenCalled(); + expect(mocks.email.renderEmail).toHaveBeenCalled(); expect(mocks.job.queue).toHaveBeenCalled(); }); @@ -599,24 +599,20 @@ describe(NotificationService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ notifications: { smtp: { enabled: true, from: 'test@immich.app' } }, }); - mocks.notification.sendEmail.mockResolvedValue({ messageId: '', response: '' }); + mocks.email.sendEmail.mockResolvedValue({ messageId: '', response: '' }); await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(mocks.notification.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ replyTo: 'test@immich.app' }), - ); + expect(mocks.email.sendEmail).toHaveBeenCalledWith(expect.objectContaining({ replyTo: 'test@immich.app' })); }); it('should send mail with replyTo successfully', async () => { mocks.systemMetadata.get.mockResolvedValue({ notifications: { smtp: { enabled: true, from: 'test@immich.app', replyTo: 'demo@immich.app' } }, }); - mocks.notification.sendEmail.mockResolvedValue({ messageId: '', response: '' }); + mocks.email.sendEmail.mockResolvedValue({ messageId: '', response: '' }); await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(mocks.notification.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ replyTo: 'demo@immich.app' }), - ); + expect(mocks.email.sendEmail).toHaveBeenCalledWith(expect.objectContaining({ replyTo: 'demo@immich.app' })); }); }); }); diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 2c4cc76756..2e456718ca 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -2,8 +2,8 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { OnEvent, OnJob } from 'src/decorators'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { AssetFileType, JobName, JobStatus, QueueName } from 'src/enum'; +import { EmailTemplate } from 'src/repositories/email.repository'; import { ArgOf } from 'src/repositories/event.repository'; -import { EmailTemplate } from 'src/repositories/notification.repository'; import { BaseService } from 'src/services/base.service'; import { EmailImageAttachment, IEntityJob, INotifyAlbumUpdateJob, JobItem, JobOf } from 'src/types'; import { getFilenameExtension } from 'src/utils/file'; @@ -28,7 +28,7 @@ export class NotificationService extends BaseService { newConfig.notifications.smtp.enabled && !isEqualObject(oldConfig.notifications.smtp, newConfig.notifications.smtp) ) { - await this.notificationRepository.verifySmtp(newConfig.notifications.smtp.transport); + await this.emailRepository.verifySmtp(newConfig.notifications.smtp.transport); } } catch (error: Error | any) { this.logger.error(`Failed to validate SMTP configuration: ${error}`, error?.stack); @@ -138,13 +138,13 @@ export class NotificationService extends BaseService { } try { - await this.notificationRepository.verifySmtp(dto.transport); + await this.emailRepository.verifySmtp(dto.transport); } catch (error) { throw new BadRequestException('Failed to verify SMTP configuration', { cause: error }); } const { server } = await this.getConfig({ withCache: false }); - const { html, text } = await this.notificationRepository.renderEmail({ + const { html, text } = await this.emailRepository.renderEmail({ template: EmailTemplate.TEST_EMAIL, data: { baseUrl: getExternalDomain(server), @@ -152,7 +152,7 @@ export class NotificationService extends BaseService { }, customTemplate: tempTemplate!, }); - const { messageId } = await this.notificationRepository.sendEmail({ + const { messageId } = await this.emailRepository.sendEmail({ to: user.email, subject: 'Test email from Immich', html, @@ -172,7 +172,7 @@ export class NotificationService extends BaseService { switch (name) { case EmailTemplate.WELCOME: { - const { html: _welcomeHtml } = await this.notificationRepository.renderEmail({ + const { html: _welcomeHtml } = await this.emailRepository.renderEmail({ template: EmailTemplate.WELCOME, data: { baseUrl: getExternalDomain(server), @@ -187,7 +187,7 @@ export class NotificationService extends BaseService { break; } case EmailTemplate.ALBUM_UPDATE: { - const { html: _updateAlbumHtml } = await this.notificationRepository.renderEmail({ + const { html: _updateAlbumHtml } = await this.emailRepository.renderEmail({ template: EmailTemplate.ALBUM_UPDATE, data: { baseUrl: getExternalDomain(server), @@ -203,7 +203,7 @@ export class NotificationService extends BaseService { } case EmailTemplate.ALBUM_INVITE: { - const { html } = await this.notificationRepository.renderEmail({ + const { html } = await this.emailRepository.renderEmail({ template: EmailTemplate.ALBUM_INVITE, data: { baseUrl: getExternalDomain(server), @@ -235,7 +235,7 @@ export class NotificationService extends BaseService { } const { server, templates } = await this.getConfig({ withCache: true }); - const { html, text } = await this.notificationRepository.renderEmail({ + const { html, text } = await this.emailRepository.renderEmail({ template: EmailTemplate.WELCOME, data: { baseUrl: getExternalDomain(server), @@ -280,7 +280,7 @@ export class NotificationService extends BaseService { const attachment = await this.getAlbumThumbnailAttachment(album); const { server, templates } = await this.getConfig({ withCache: false }); - const { html, text } = await this.notificationRepository.renderEmail({ + const { html, text } = await this.emailRepository.renderEmail({ template: EmailTemplate.ALBUM_INVITE, data: { baseUrl: getExternalDomain(server), @@ -339,7 +339,7 @@ export class NotificationService extends BaseService { continue; } - const { html, text } = await this.notificationRepository.renderEmail({ + const { html, text } = await this.emailRepository.renderEmail({ template: EmailTemplate.ALBUM_UPDATE, data: { baseUrl: getExternalDomain(server), @@ -374,7 +374,7 @@ export class NotificationService extends BaseService { } const { to, subject, html, text: plain } = data; - const response = await this.notificationRepository.sendEmail({ + const response = await this.emailRepository.sendEmail({ to, subject, html, diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index d3ab876e07..671a8a50ca 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -284,6 +284,7 @@ export const asDeps = (repositories: ServiceOverrides) => { repositories.crypto || getRepositoryMock('crypto'), repositories.database || getRepositoryMock('database'), repositories.downloadRepository, + repositories.email, repositories.event, repositories.job || getRepositoryMock('job'), repositories.library, @@ -293,7 +294,6 @@ export const asDeps = (repositories: ServiceOverrides) => { repositories.memory || getRepositoryMock('memory'), repositories.metadata, repositories.move, - repositories.notification, repositories.oauth, repositories.partner || getRepositoryMock('partner'), repositories.person || getRepositoryMock('person'), diff --git a/server/test/utils.ts b/server/test/utils.ts index 52984d97a2..e1d979fbfe 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -18,6 +18,7 @@ import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { DownloadRepository } from 'src/repositories/download.repository'; +import { EmailRepository } from 'src/repositories/email.repository'; import { EventRepository } from 'src/repositories/event.repository'; import { JobRepository } from 'src/repositories/job.repository'; import { LibraryRepository } from 'src/repositories/library.repository'; @@ -28,7 +29,6 @@ import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; import { MetadataRepository } from 'src/repositories/metadata.repository'; import { MoveRepository } from 'src/repositories/move.repository'; -import { NotificationRepository } from 'src/repositories/notification.repository'; import { OAuthRepository } from 'src/repositories/oauth.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; import { PersonRepository } from 'src/repositories/person.repository'; @@ -124,6 +124,7 @@ export type ServiceOverrides = { crypto: CryptoRepository; database: DatabaseRepository; downloadRepository: DownloadRepository; + email: EmailRepository; event: EventRepository; job: JobRepository; library: LibraryRepository; @@ -134,7 +135,6 @@ export type ServiceOverrides = { memory: MemoryRepository; metadata: MetadataRepository; move: MoveRepository; - notification: NotificationRepository; oauth: OAuthRepository; partner: PartnerRepository; person: PersonRepository; @@ -190,6 +190,7 @@ export const newTestService = ( config: newConfigRepositoryMock(), database: newDatabaseRepositoryMock(), downloadRepository: automock(DownloadRepository, { strict: false }), + email: automock(EmailRepository, { args: [loggerMock] }), // eslint-disable-next-line no-sparse-arrays event: automock(EventRepository, { args: [, , loggerMock], strict: false }), job: newJobRepositoryMock(), @@ -201,7 +202,6 @@ export const newTestService = ( memory: automock(MemoryRepository), metadata: newMetadataRepositoryMock(), move: automock(MoveRepository, { strict: false }), - notification: automock(NotificationRepository, { args: [loggerMock] }), oauth: automock(OAuthRepository, { args: [loggerMock] }), partner: automock(PartnerRepository, { strict: false }), person: newPersonRepositoryMock(), @@ -240,6 +240,7 @@ export const newTestService = ( overrides.crypto || (mocks.crypto as As), overrides.database || (mocks.database as As), overrides.downloadRepository || (mocks.downloadRepository as As), + overrides.email || (mocks.email as As), overrides.event || (mocks.event as As), overrides.job || (mocks.job as As), overrides.library || (mocks.library as As), @@ -249,7 +250,6 @@ export const newTestService = ( overrides.memory || (mocks.memory as As), overrides.metadata || (mocks.metadata as As), overrides.move || (mocks.move as As), - overrides.notification || (mocks.notification as As), overrides.oauth || (mocks.oauth as As), overrides.partner || (mocks.partner as As), overrides.person || (mocks.person as As), From b71039e83c2497f9829f1592573dbc2e85bb2f2c Mon Sep 17 00:00:00 2001 From: Toni <51962051+EinToni@users.noreply.github.com> Date: Mon, 21 Apr 2025 19:51:37 +0200 Subject: [PATCH 018/356] perf(mobile): remove small thumbnail and cache generated thumbnails (#17682) * Remove small thumbnail and cache generated thumbnails * Creating the small thumbnails takes quite some time, which should not be underestimated. * The time needed to generate the small or big thumbnail is not too different from each other. Therefore there is no real benefit of the small thumbnail and it only adds frustration to the end user experience. That is because the image appeared to have loaded (the visual move from blur to something better) but it's still so bad that it is basically a blur. The better solution is therefore to stay at the blur until the actual thumbnail has loaded. * Additionaly to the faster generation of the thumbnail, it now also gets cached similarly to the remote thumbnail which already gets cached. This further speeds up the all over usage of the app and prevents a repeatet thumbnail generation when opening the app. * Decrease quality and use try catch * Decreased the quality from the default 95 to 80 to provide similar quality with much reduces thumbnail size. * Use try catch around the read of the cache file. * Replace ImmutableBuffer.fromUint8List with ImmutableBuffer.fromFilePath * Removed unnecessary comment * Replace debugPrint with log.severe for catch of error --------- Co-authored-by: Alex --- .../immich_local_thumbnail_provider.dart | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart index 69cdb105c0..93245bd78e 100644 --- a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart +++ b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart @@ -2,11 +2,14 @@ import 'dart:async'; import 'dart:ui' as ui; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:photo_manager/photo_manager.dart' show ThumbnailSize; +import 'package:logging/logging.dart'; /// The local image provider for an asset /// Only viable @@ -15,11 +18,14 @@ class ImmichLocalThumbnailProvider final Asset asset; final int height; final int width; + final CacheManager? cacheManager; + final Logger log = Logger("ImmichLocalThumbnailProvider"); ImmichLocalThumbnailProvider({ required this.asset, this.height = 256, this.width = 256, + this.cacheManager, }) : assert(asset.local != null, 'Only usable when asset.local is set'); /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key @@ -36,11 +42,10 @@ class ImmichLocalThumbnailProvider ImmichLocalThumbnailProvider key, ImageDecoderCallback decode, ) { - final chunkEvents = StreamController(); + final cache = cacheManager ?? ThumbnailImageCacheManager(); return MultiImageStreamCompleter( - codec: _codec(key.asset, decode, chunkEvents), + codec: _codec(key.asset, cache, decode), scale: 1.0, - chunkEvents: chunkEvents.stream, informationCollector: () sync* { yield ErrorDescription(asset.fileName); }, @@ -50,34 +55,36 @@ class ImmichLocalThumbnailProvider // Streams in each stage of the image as we ask for it Stream _codec( Asset key, + CacheManager cache, ImageDecoderCallback decode, - StreamController chunkEvents, ) async* { - // Load a small thumbnail - final thumbBytes = await asset.local?.thumbnailDataWithSize( - const ThumbnailSize.square(32), - quality: 75, - ); - if (thumbBytes != null) { - final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); - final codec = await decode(buffer); - yield codec; - } else { - debugPrint("Loading thumb for ${asset.fileName} failed"); + final cacheKey = '${key.id}_${width}x$height'; + final fileFromCache = await cache.getFileFromCache(cacheKey); + if (fileFromCache != null) { + try { + final buffer = + await ui.ImmutableBuffer.fromFilePath(fileFromCache.file.path); + final codec = await decode(buffer); + yield codec; + return; + } catch (error) { + log.severe('Found thumbnail in cache, but loading it failed', error); + } } - final normalThumbBytes = - await asset.local?.thumbnailDataWithSize(ThumbnailSize(width, height)); - if (normalThumbBytes == null) { + final thumbnailBytes = await asset.local?.thumbnailDataWithSize( + ThumbnailSize(width, height), + quality: 80, + ); + if (thumbnailBytes == null) { throw StateError( "Loading thumb for local photo ${asset.fileName} failed", ); } - final buffer = await ui.ImmutableBuffer.fromUint8List(normalThumbBytes); + final buffer = await ui.ImmutableBuffer.fromUint8List(thumbnailBytes); final codec = await decode(buffer); yield codec; - - chunkEvents.close(); + await cache.putFile(cacheKey, thumbnailBytes); } @override From 010b144754680f39979a65668311b5a5ffee2416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wawrzyk?= Date: Mon, 21 Apr 2025 20:00:46 +0200 Subject: [PATCH 019/356] fix(mobile): use immutable cache keys for local images (#17736) fix(mobile): usse immutable cache keys for local images --- .../lib/providers/image/immich_local_image_provider.dart | 4 ++-- .../providers/image/immich_local_thumbnail_provider.dart | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/mobile/lib/providers/image/immich_local_image_provider.dart b/mobile/lib/providers/image/immich_local_image_provider.dart index 36fd3334b9..34c3c94ded 100644 --- a/mobile/lib/providers/image/immich_local_image_provider.dart +++ b/mobile/lib/providers/image/immich_local_image_provider.dart @@ -106,12 +106,12 @@ class ImmichLocalImageProvider extends ImageProvider { bool operator ==(Object other) { if (identical(this, other)) return true; if (other is ImmichLocalImageProvider) { - return asset == other.asset; + return asset.id == other.asset.id; } return false; } @override - int get hashCode => asset.hashCode; + int get hashCode => asset.id.hashCode; } diff --git a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart index 93245bd78e..9ccb127626 100644 --- a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart +++ b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart @@ -89,11 +89,14 @@ class ImmichLocalThumbnailProvider @override bool operator ==(Object other) { - if (other is! ImmichLocalThumbnailProvider) return false; if (identical(this, other)) return true; - return asset == other.asset; + if (other is ImmichLocalThumbnailProvider) { + return asset.id == other.asset.id; + } + + return false; } @override - int get hashCode => asset.hashCode; + int get hashCode => asset.id.hashCode; } From c70140e7078e585db8fe65e5dbbb92d19f11c2c4 Mon Sep 17 00:00:00 2001 From: Yaros Date: Mon, 21 Apr 2025 20:01:38 +0200 Subject: [PATCH 020/356] fix(web): map marker positioning in details pane (#17754) fix: map marker positioning in details pane --- web/src/lib/components/shared-components/map/map.svelte | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/src/lib/components/shared-components/map/map.svelte b/web/src/lib/components/shared-components/map/map.svelte index dc594bae3f..e21a73ab43 100644 --- a/web/src/lib/components/shared-components/map/map.svelte +++ b/web/src/lib/components/shared-components/map/map.svelte @@ -249,7 +249,11 @@ > {#snippet children({ feature }: { feature: Feature })} {#if useLocationPin} - + {:else} Date: Mon, 21 Apr 2025 16:54:33 -0700 Subject: [PATCH 021/356] feat: add album start and end dates for storage template (#17188) --- .../services/storage-template.service.spec.ts | 58 +++++++++++++++++++ .../src/services/storage-template.service.ts | 35 ++++++++++- .../storage-template-settings.svelte | 4 ++ .../supported-variables-panel.svelte | 8 +++ 4 files changed, 102 insertions(+), 3 deletions(-) diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index 971a9e8302..d9ac89952e 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -69,6 +69,7 @@ describe(StorageTemplateService.name, () => { '{{y}}/{{MMMM}}-{{dd}}/{{filename}}', '{{y}}/{{MM}}/{{filename}}', '{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}', + '{{#if album}}{{album-startDate-y}}/{{album}}{{else}}{{y}}/Other/{{MM}}{{/if}}/{{filename}}', '{{y}}/{{MMM}}/{{filename}}', '{{y}}/{{MMMM}}/{{filename}}', '{{y}}/{{MM}}/{{dd}}/{{filename}}', @@ -182,6 +183,63 @@ describe(StorageTemplateService.name, () => { }); }); + it('should handle album startDate', async () => { + const asset = assetStub.storageAsset(); + const user = userStub.user1; + const album = albumStub.oneAsset; + const config = structuredClone(defaults); + config.storageTemplate.template = + '{{#if album}}{{album-startDate-y}}/{{album-startDate-MM}} - {{album}}{{else}}{{y}}/{{MM}}/{{/if}}/{{filename}}'; + + sut.onConfigInit({ newConfig: config }); + + mocks.user.get.mockResolvedValue(user); + mocks.asset.getStorageTemplateAsset.mockResolvedValueOnce(asset); + mocks.album.getByAssetId.mockResolvedValueOnce([album]); + mocks.album.getMetadataForIds.mockResolvedValueOnce([ + { + startDate: asset.fileCreatedAt, + endDate: asset.fileCreatedAt, + albumId: album.id, + assetCount: 1, + lastModifiedAssetTimestamp: null, + }, + ]); + + expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS); + + const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0'); + expect(mocks.move.create).toHaveBeenCalledWith({ + entityId: asset.id, + newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month} - ${album.albumName}/${asset.originalFileName}`, + oldPath: asset.originalPath, + pathType: AssetPathType.ORIGINAL, + }); + }); + + it('should handle else condition from album startDate', async () => { + const asset = assetStub.storageAsset(); + const user = userStub.user1; + const config = structuredClone(defaults); + config.storageTemplate.template = + '{{#if album}}{{album-startDate-y}}/{{album-startDate-MM}} - {{album}}{{else}}{{y}}/{{MM}}/{{/if}}/{{filename}}'; + + sut.onConfigInit({ newConfig: config }); + + mocks.user.get.mockResolvedValue(user); + mocks.asset.getStorageTemplateAsset.mockResolvedValueOnce(asset); + + expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS); + + const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0'); + expect(mocks.move.create).toHaveBeenCalledWith({ + entityId: asset.id, + newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${month}/${asset.originalFileName}`, + oldPath: asset.originalPath, + pathType: AssetPathType.ORIGINAL, + }); + }); + it('should migrate previously failed move from original path when it still exists', async () => { mocks.user.get.mockResolvedValue(userStub.user1); diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 71a0160ee2..542633a03f 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -28,6 +28,7 @@ const storagePresets = [ '{{y}}/{{MMMM}}-{{dd}}/{{filename}}', '{{y}}/{{MM}}/{{filename}}', '{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}', + '{{#if album}}{{album-startDate-y}}/{{album}}{{else}}{{y}}/Other/{{MM}}{{/if}}/{{filename}}', '{{y}}/{{MMM}}/{{filename}}', '{{y}}/{{MMMM}}/{{filename}}', '{{y}}/{{MM}}/{{dd}}/{{filename}}', @@ -54,6 +55,8 @@ interface RenderMetadata { filename: string; extension: string; albumName: string | null; + albumStartDate: Date | null; + albumEndDate: Date | null; } @Injectable() @@ -62,6 +65,7 @@ export class StorageTemplateService extends BaseService { compiled: HandlebarsTemplateDelegate; raw: string; needsAlbum: boolean; + needsAlbumMetadata: boolean; } | null = null; private get template() { @@ -99,6 +103,8 @@ export class StorageTemplateService extends BaseService { filename: 'IMG_123', extension: 'jpg', albumName: 'album', + albumStartDate: new Date(), + albumEndDate: new Date(), }); } catch (error) { this.logger.warn(`Storage template validation failed: ${JSON.stringify(error)}`); @@ -255,9 +261,20 @@ export class StorageTemplateService extends BaseService { } let albumName = null; + let albumStartDate = null; + let albumEndDate = null; if (this.template.needsAlbum) { const albums = await this.albumRepository.getByAssetId(asset.ownerId, asset.id); - albumName = albums?.[0]?.albumName || null; + const album = albums?.[0]; + if (album) { + albumName = album.albumName || null; + + if (this.template.needsAlbumMetadata) { + const [metadata] = await this.albumRepository.getMetadataForIds([album.id]); + albumStartDate = metadata?.startDate || null; + albumEndDate = metadata?.endDate || null; + } + } } const storagePath = this.render(this.template.compiled, { @@ -265,6 +282,8 @@ export class StorageTemplateService extends BaseService { filename: sanitized, extension, albumName, + albumStartDate, + albumEndDate, }); const fullPath = path.normalize(path.join(rootPath, storagePath)); let destination = `${fullPath}.${extension}`; @@ -323,12 +342,13 @@ export class StorageTemplateService extends BaseService { return { raw: template, compiled: handlebar.compile(template, { knownHelpers: undefined, strict: true }), - needsAlbum: template.includes('{{album}}'), + needsAlbum: template.includes('album'), + needsAlbumMetadata: template.includes('album-startDate') || template.includes('album-endDate'), }; } private render(template: HandlebarsTemplateDelegate, options: RenderMetadata) { - const { filename, extension, asset, albumName } = options; + const { filename, extension, asset, albumName, albumStartDate, albumEndDate } = options; const substitutions: Record = { filename, ext: extension, @@ -346,6 +366,15 @@ export class StorageTemplateService extends BaseService { for (const token of Object.values(storageTokens).flat()) { substitutions[token] = dt.toFormat(token); + if (albumName) { + // Use system time zone for album dates to ensure all assets get the exact same date. + substitutions['album-startDate-' + token] = albumStartDate + ? DateTime.fromJSDate(albumStartDate, { zone: systemTimeZone }).toFormat(token) + : ''; + substitutions['album-endDate-' + token] = albumEndDate + ? DateTime.fromJSDate(albumEndDate, { zone: systemTimeZone }).toFormat(token) + : ''; + } } return template(substitutions).replaceAll(/\/{2,}/gm, '/'); diff --git a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte index 9b4aa5e934..67299d8f6b 100644 --- a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte +++ b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte @@ -78,6 +78,8 @@ }; const dt = luxon.DateTime.fromISO(new Date('2022-02-03T04:56:05.250').toISOString()); + const albumStartTime = luxon.DateTime.fromISO(new Date('2021-12-31T05:32:41.750').toISOString()); + const albumEndTime = luxon.DateTime.fromISO(new Date('2023-05-06T09:15:17.100').toISOString()); const dateTokens = [ ...templateOptions.yearOptions, @@ -91,6 +93,8 @@ for (const token of dateTokens) { substitutions[token] = dt.toFormat(token); + substitutions['album-startDate-' + token] = albumStartTime.toFormat(token); + substitutions['album-endDate-' + token] = albumEndTime.toFormat(token); } return template(substitutions); diff --git a/web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte b/web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte index fc8f913281..c1255c252d 100644 --- a/web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte +++ b/web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte @@ -29,6 +29,14 @@
  • {`{{assetId}}`} - Asset ID
  • {`{{assetIdShort}}`} - Asset ID (last 12 characters)
  • {`{{album}}`} - Album Name
  • +
  • + {`{{album-startDate-x}}`} - Album Start Date and Time (e.g. album-startDate-yy). + {$t('admin.storage_template_date_time_sample', { values: { date: '2021-12-31T05:32:41.750' } })} +
  • +
  • + {`{{album-endDate-x}}`} - Album End Date and Time (e.g. album-endDate-MM). + {$t('admin.storage_template_date_time_sample', { values: { date: '2023-05-06T09:15:17.100' } })} +
  • From ad8511c9788a7c248d6f06186d7090f0978c3ddb Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 21 Apr 2025 23:27:00 -0500 Subject: [PATCH 022/356] feat(docs): APK download button (#17768) --- docs/src/pages/index.tsx | 8 ++++++++ docs/static/img/download-apk-github.svg | 13 +++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 docs/static/img/download-apk-github.svg diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index 2ffe1debc7..db299271aa 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -4,6 +4,7 @@ import Layout from '@theme/Layout'; import { discordPath, discordViewBox } from '@site/src/components/svg-paths'; import ThemedImage from '@theme/ThemedImage'; import Icon from '@mdi/react'; +import { mdiAndroid } from '@mdi/js'; function HomepageHeader() { return (
    @@ -88,11 +89,18 @@ function HomepageHeader() { Get it on Google Play + + +
    + + Download APK + +
    + + + + + + + + + + + + From a8eec92da7c3bb846654553156fa7da6868940c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:18:44 +0000 Subject: [PATCH 023/356] chore(deps): update dependency @types/node to ^22.14.1 (#17770) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel Dietzler --- cli/package-lock.json | 4 ++-- cli/package.json | 2 +- e2e/package-lock.json | 6 +++--- e2e/package.json | 2 +- open-api/typescript-sdk/package-lock.json | 8 ++++---- open-api/typescript-sdk/package.json | 2 +- server/package-lock.json | 2 +- server/package.json | 2 +- server/src/services/storage-template.service.spec.ts | 4 ++-- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 22f8980754..daac54a552 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -27,7 +27,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.14.0", + "@types/node": "^22.14.1", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -61,7 +61,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.14.0", + "@types/node": "^22.14.1", "typescript": "^5.3.3" } }, diff --git a/cli/package.json b/cli/package.json index 304c2acfbd..0759eb13ee 100644 --- a/cli/package.json +++ b/cli/package.json @@ -21,7 +21,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.14.0", + "@types/node": "^22.14.1", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index d09a7e9701..07234a770f 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -15,7 +15,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^22.14.0", + "@types/node": "^22.14.1", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", @@ -66,7 +66,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.14.0", + "@types/node": "^22.14.1", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -100,7 +100,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.14.0", + "@types/node": "^22.14.1", "typescript": "^5.3.3" } }, diff --git a/e2e/package.json b/e2e/package.json index f141430c97..8027cb420e 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -25,7 +25,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^22.14.0", + "@types/node": "^22.14.1", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 761a228de0..1fe3ea7587 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -12,7 +12,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.14.0", + "@types/node": "^22.14.1", "typescript": "^5.3.3" } }, @@ -23,9 +23,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.14.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", - "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 29fe50dcd9..a3d5d9d224 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.14.0", + "@types/node": "^22.14.1", "typescript": "^5.3.3" }, "repository": { diff --git a/server/package-lock.json b/server/package-lock.json index 8045976b3c..85edb9426b 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -90,7 +90,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^22.14.0", + "@types/node": "^22.14.1", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/pngjs": "^6.0.5", diff --git a/server/package.json b/server/package.json index 8e149d961e..a641c3b29a 100644 --- a/server/package.json +++ b/server/package.json @@ -115,7 +115,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^22.14.0", + "@types/node": "^22.14.1", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/pngjs": "^6.0.5", diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index d9ac89952e..9c4fe02f3e 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -194,7 +194,7 @@ describe(StorageTemplateService.name, () => { sut.onConfigInit({ newConfig: config }); mocks.user.get.mockResolvedValue(user); - mocks.asset.getStorageTemplateAsset.mockResolvedValueOnce(asset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset); mocks.album.getByAssetId.mockResolvedValueOnce([album]); mocks.album.getMetadataForIds.mockResolvedValueOnce([ { @@ -227,7 +227,7 @@ describe(StorageTemplateService.name, () => { sut.onConfigInit({ newConfig: config }); mocks.user.get.mockResolvedValue(user); - mocks.asset.getStorageTemplateAsset.mockResolvedValueOnce(asset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset); expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS); From fda68f972ff1f0e1f010f7083de2a4a75b59bce0 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 22 Apr 2025 08:25:27 -0500 Subject: [PATCH 024/356] fix(web): forceDark control app bar doesn't work (#17759) --- .../lib/components/shared-components/control-app-bar.svelte | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/lib/components/shared-components/control-app-bar.svelte b/web/src/lib/components/shared-components/control-app-bar.svelte index 6b52adff10..7c630875c0 100644 --- a/web/src/lib/components/shared-components/control-app-bar.svelte +++ b/web/src/lib/components/shared-components/control-app-bar.svelte @@ -77,8 +77,7 @@ appBarBorder, 'mx-2 my-2 place-items-center rounded-lg p-2 max-md:p-0 transition-all', tailwindClasses, - 'bg-immich-gray dark:bg-immich-dark-gray', - forceDark && 'bg-immich-dark-gray text-white', + forceDark ? 'bg-immich-dark-gray text-white' : 'bg-immich-gray dark:bg-immich-dark-gray', ]} >
    From af36eaa61bfffe5f83b47877c5a66feb66dd2492 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 22 Apr 2025 10:51:20 -0500 Subject: [PATCH 025/356] fix(mobile): video player initialization (#17778) * fix(mobile): video player initialization * nit --- mobile/lib/pages/common/gallery_viewer.page.dart | 11 +++++++---- mobile/lib/widgets/common/immich_logo.dart | 13 +++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index 7392a4d340..420b699730 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -63,9 +63,12 @@ class GalleryViewerPage extends HookConsumerWidget { final loadAsset = renderList.loadAsset; final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider); - // This key is to prevent the video player from being re-initialized during - // hero animation or device rotation. - final videoPlayerKey = useMemoized(() => GlobalKey()); + final videoPlayerKeys = useRef>({}); + + GlobalKey getVideoPlayerKey(int id) { + videoPlayerKeys.value.putIfAbsent(id, () => GlobalKey()); + return videoPlayerKeys.value[id]!; + } Future precacheNextImage(int index) async { if (!context.mounted) { @@ -243,7 +246,7 @@ class GalleryViewerPage extends HookConsumerWidget { width: context.width, height: context.height, child: NativeVideoViewerPage( - key: videoPlayerKey, + key: getVideoPlayerKey(asset.id), asset: asset, image: Image( key: ValueKey(asset), diff --git a/mobile/lib/widgets/common/immich_logo.dart b/mobile/lib/widgets/common/immich_logo.dart index 9f7725aa12..43987878cb 100644 --- a/mobile/lib/widgets/common/immich_logo.dart +++ b/mobile/lib/widgets/common/immich_logo.dart @@ -12,14 +12,11 @@ class ImmichLogo extends StatelessWidget { @override Widget build(BuildContext context) { - return Hero( - tag: heroTag, - child: Image( - image: const AssetImage('assets/immich-logo.png'), - width: size, - filterQuality: FilterQuality.high, - isAntiAlias: true, - ), + return Image( + image: const AssetImage('assets/immich-logo.png'), + width: size, + filterQuality: FilterQuality.high, + isAntiAlias: true, ); } } From 0986a71ce3dfc78c547a3cd99c12da0a7f9d96e8 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 22 Apr 2025 12:15:54 -0500 Subject: [PATCH 026/356] fix(mobile): revert cache fixes (#17786) * Revert "fix(mobile): use immutable cache keys for local images (#17736)" This reverts commit 010b144754680f39979a65668311b5a5ffee2416. * Revert "perf(mobile): remove small thumbnail and cache generated thumbnails (#17682)" This reverts commit b71039e83c2497f9829f1592573dbc2e85bb2f2c. --- .../image/immich_local_image_provider.dart | 4 +- .../immich_local_thumbnail_provider.dart | 58 ++++++++----------- 2 files changed, 26 insertions(+), 36 deletions(-) diff --git a/mobile/lib/providers/image/immich_local_image_provider.dart b/mobile/lib/providers/image/immich_local_image_provider.dart index 34c3c94ded..36fd3334b9 100644 --- a/mobile/lib/providers/image/immich_local_image_provider.dart +++ b/mobile/lib/providers/image/immich_local_image_provider.dart @@ -106,12 +106,12 @@ class ImmichLocalImageProvider extends ImageProvider { bool operator ==(Object other) { if (identical(this, other)) return true; if (other is ImmichLocalImageProvider) { - return asset.id == other.asset.id; + return asset == other.asset; } return false; } @override - int get hashCode => asset.id.hashCode; + int get hashCode => asset.hashCode; } diff --git a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart index 9ccb127626..69cdb105c0 100644 --- a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart +++ b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart @@ -2,14 +2,11 @@ import 'dart:async'; import 'dart:ui' as ui; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:photo_manager/photo_manager.dart' show ThumbnailSize; -import 'package:logging/logging.dart'; /// The local image provider for an asset /// Only viable @@ -18,14 +15,11 @@ class ImmichLocalThumbnailProvider final Asset asset; final int height; final int width; - final CacheManager? cacheManager; - final Logger log = Logger("ImmichLocalThumbnailProvider"); ImmichLocalThumbnailProvider({ required this.asset, this.height = 256, this.width = 256, - this.cacheManager, }) : assert(asset.local != null, 'Only usable when asset.local is set'); /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key @@ -42,10 +36,11 @@ class ImmichLocalThumbnailProvider ImmichLocalThumbnailProvider key, ImageDecoderCallback decode, ) { - final cache = cacheManager ?? ThumbnailImageCacheManager(); + final chunkEvents = StreamController(); return MultiImageStreamCompleter( - codec: _codec(key.asset, cache, decode), + codec: _codec(key.asset, decode, chunkEvents), scale: 1.0, + chunkEvents: chunkEvents.stream, informationCollector: () sync* { yield ErrorDescription(asset.fileName); }, @@ -55,48 +50,43 @@ class ImmichLocalThumbnailProvider // Streams in each stage of the image as we ask for it Stream _codec( Asset key, - CacheManager cache, ImageDecoderCallback decode, + StreamController chunkEvents, ) async* { - final cacheKey = '${key.id}_${width}x$height'; - final fileFromCache = await cache.getFileFromCache(cacheKey); - if (fileFromCache != null) { - try { - final buffer = - await ui.ImmutableBuffer.fromFilePath(fileFromCache.file.path); - final codec = await decode(buffer); - yield codec; - return; - } catch (error) { - log.severe('Found thumbnail in cache, but loading it failed', error); - } + // Load a small thumbnail + final thumbBytes = await asset.local?.thumbnailDataWithSize( + const ThumbnailSize.square(32), + quality: 75, + ); + if (thumbBytes != null) { + final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); + final codec = await decode(buffer); + yield codec; + } else { + debugPrint("Loading thumb for ${asset.fileName} failed"); } - final thumbnailBytes = await asset.local?.thumbnailDataWithSize( - ThumbnailSize(width, height), - quality: 80, - ); - if (thumbnailBytes == null) { + final normalThumbBytes = + await asset.local?.thumbnailDataWithSize(ThumbnailSize(width, height)); + if (normalThumbBytes == null) { throw StateError( "Loading thumb for local photo ${asset.fileName} failed", ); } - final buffer = await ui.ImmutableBuffer.fromUint8List(thumbnailBytes); + final buffer = await ui.ImmutableBuffer.fromUint8List(normalThumbBytes); final codec = await decode(buffer); yield codec; - await cache.putFile(cacheKey, thumbnailBytes); + + chunkEvents.close(); } @override bool operator ==(Object other) { + if (other is! ImmichLocalThumbnailProvider) return false; if (identical(this, other)) return true; - if (other is ImmichLocalThumbnailProvider) { - return asset.id == other.asset.id; - } - - return false; + return asset == other.asset; } @override - int get hashCode => asset.id.hashCode; + int get hashCode => asset.hashCode; } From ee017803bf3b01ba27935b740bab09360efa868d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wawrzyk?= Date: Wed, 23 Apr 2025 04:32:03 +0200 Subject: [PATCH 027/356] fix(mobile): use immutable cache keys for local images (#17794) --- .../lib/providers/image/immich_local_image_provider.dart | 5 ++--- .../providers/image/immich_local_thumbnail_provider.dart | 8 +++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/mobile/lib/providers/image/immich_local_image_provider.dart b/mobile/lib/providers/image/immich_local_image_provider.dart index 36fd3334b9..c152934333 100644 --- a/mobile/lib/providers/image/immich_local_image_provider.dart +++ b/mobile/lib/providers/image/immich_local_image_provider.dart @@ -106,12 +106,11 @@ class ImmichLocalImageProvider extends ImageProvider { bool operator ==(Object other) { if (identical(this, other)) return true; if (other is ImmichLocalImageProvider) { - return asset == other.asset; + return asset.id == other.asset.id && asset.localId == other.asset.localId; } - return false; } @override - int get hashCode => asset.hashCode; + int get hashCode => Object.hash(asset.id, asset.localId); } diff --git a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart index 69cdb105c0..54dfd97983 100644 --- a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart +++ b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart @@ -82,11 +82,13 @@ class ImmichLocalThumbnailProvider @override bool operator ==(Object other) { - if (other is! ImmichLocalThumbnailProvider) return false; if (identical(this, other)) return true; - return asset == other.asset; + if (other is ImmichLocalThumbnailProvider) { + return asset.id == other.asset.id && asset.localId == other.asset.localId; + } + return false; } @override - int get hashCode => asset.hashCode; + int get hashCode => Object.hash(asset.id, asset.localId); } From 2a95eccf6ae32a69114633f604ea5ae91411cadd Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Tue, 22 Apr 2025 23:01:22 -0400 Subject: [PATCH 028/356] fix: vscode vitest ext - missing jsdom dev dependency (#17799) --- server/package-lock.json | 509 +++++++++++++++++++++++++++++++++++++++ server/package.json | 5 +- 2 files changed, 512 insertions(+), 2 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 85edb9426b..ca0fc8b1fd 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -105,6 +105,7 @@ "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^57.0.0", "globals": "^16.0.0", + "jsdom": "^26.1.0", "mock-fs": "^5.2.0", "node-addon-api": "^8.3.0", "patch-package": "^8.0.0", @@ -435,6 +436,27 @@ "node": ">= 8" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.3.tgz", + "integrity": "sha512-u25AyjuNrRFGb1O7KmWEu0ExN6iJMlUmDSlOPW/11JF8khOrIGG6oCoYpC+4mZlthNVhFUahk68lNrNI91f6Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -720,6 +742,121 @@ "node": ">=0.1.90" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.3.tgz", + "integrity": "sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.9.tgz", + "integrity": "sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", + "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", + "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@emnapi/runtime": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.2.tgz", @@ -8311,6 +8448,20 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.1.tgz", + "integrity": "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.1.2", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -8318,6 +8469,57 @@ "dev": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -8353,6 +8555,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -10406,6 +10615,19 @@ "dev": true, "license": "ISC" }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -10464,6 +10686,30 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -10829,6 +11075,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -11081,6 +11334,129 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -12293,6 +12669,13 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -12573,6 +12956,19 @@ "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", "license": "MIT" }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseley": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", @@ -14156,6 +14552,13 @@ "node": ">= 18" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -14246,6 +14649,19 @@ "postcss": "^8.3.11" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -15320,6 +15736,13 @@ "node": ">=0.10" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/synckit": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", @@ -15921,6 +16344,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -15963,6 +16406,19 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -17324,6 +17780,19 @@ } } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/watchpack": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", @@ -17549,6 +18018,29 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -17717,6 +18209,23 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/server/package.json b/server/package.json index a641c3b29a..4373e4abb7 100644 --- a/server/package.json +++ b/server/package.json @@ -147,9 +147,10 @@ "unplugin-swc": "^1.4.5", "utimes": "^5.2.1", "vite-tsconfig-paths": "^5.0.0", - "vitest": "^3.0.0" + "vitest": "^3.0.0", + "jsdom": "^26.1.0" }, "volta": { "node": "22.14.0" } -} +} \ No newline at end of file From 92ac1193e62e1b5708a5f557348eed580d9ae552 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Wed, 23 Apr 2025 07:03:28 -0400 Subject: [PATCH 029/356] fix(server): queue android motion assets for transcoding (#17781) --- server/src/services/job.service.spec.ts | 28 +++++++++++--------- server/src/services/job.service.ts | 2 -- server/src/services/metadata.service.spec.ts | 12 +++++++++ server/src/services/metadata.service.ts | 1 + 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index baac0af428..9acc81ceb7 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -230,7 +230,7 @@ describe(JobService.name, () => { expect(mocks.logger.error).not.toHaveBeenCalled(); }); - const tests: Array<{ item: JobItem; jobs: JobName[] }> = [ + const tests: Array<{ item: JobItem; jobs: JobName[]; stub?: any }> = [ { item: { name: JobName.SIDECAR_SYNC, data: { id: 'asset-1' } }, jobs: [JobName.METADATA_EXTRACTION], @@ -258,14 +258,22 @@ describe(JobService.name, () => { { item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1' } }, jobs: [], + stub: [assetStub.image], + }, + { + item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1' } }, + jobs: [], + stub: [assetStub.video], + }, + { + item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1', source: 'upload' } }, + jobs: [JobName.SMART_SEARCH, JobName.FACE_DETECTION], + stub: [assetStub.livePhotoStillAsset], }, { item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1', source: 'upload' } }, jobs: [JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION], - }, - { - item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-live-image', source: 'upload' } }, - jobs: [JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION], + stub: [assetStub.video], }, { item: { name: JobName.SMART_SEARCH, data: { id: 'asset-1' } }, @@ -281,14 +289,10 @@ describe(JobService.name, () => { }, ]; - for (const { item, jobs } of tests) { + for (const { item, jobs, stub } of tests) { it(`should queue ${jobs.length} jobs when a ${item.name} job finishes successfully`, async () => { - if (item.name === JobName.GENERATE_THUMBNAILS && item.data.source === 'upload') { - if (item.data.id === 'asset-live-image') { - mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([assetStub.livePhotoStillAsset as any]); - } else { - mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([assetStub.livePhotoMotionAsset as any]); - } + if (stub) { + mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue(stub); } mocks.job.run.mockResolvedValue(JobStatus.SUCCESS); diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index edd018d7b1..f8298336a8 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -297,8 +297,6 @@ export class JobService extends BaseService { if (asset.type === AssetType.VIDEO) { jobs.push({ name: JobName.VIDEO_CONVERSION, data: item.data }); - } else if (asset.livePhotoVideoId) { - jobs.push({ name: JobName.VIDEO_CONVERSION, data: { id: asset.livePhotoVideoId } }); } await this.jobRepository.queueAll(jobs); diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index e412b1c31f..4fbb2cc48c 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -598,6 +598,10 @@ describe(MetadataService.name, () => { livePhotoVideoId: fileStub.livePhotoMotion.uuid, }); expect(mocks.asset.update).toHaveBeenCalledTimes(3); + expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({ + name: JobName.VIDEO_CONVERSION, + data: { id: assetStub.livePhotoMotionAsset.id }, + }); }); it('should extract the EmbeddedVideo tag from Samsung JPEG motion photos', async () => { @@ -652,6 +656,10 @@ describe(MetadataService.name, () => { livePhotoVideoId: fileStub.livePhotoMotion.uuid, }); expect(mocks.asset.update).toHaveBeenCalledTimes(3); + expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({ + name: JobName.VIDEO_CONVERSION, + data: { id: assetStub.livePhotoMotionAsset.id }, + }); }); it('should extract the motion photo video from the XMP directory entry ', async () => { @@ -706,6 +714,10 @@ describe(MetadataService.name, () => { livePhotoVideoId: fileStub.livePhotoMotion.uuid, }); expect(mocks.asset.update).toHaveBeenCalledTimes(3); + expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({ + name: JobName.VIDEO_CONVERSION, + data: { id: assetStub.livePhotoMotionAsset.id }, + }); }); it('should delete old motion photo video assets if they do not match what is extracted', async () => { diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index faf146a2be..71b8e2de47 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -576,6 +576,7 @@ export class MetadataService extends BaseService { this.logger.log(`Wrote motion photo video to ${motionAsset.originalPath}`); await this.handleMetadataExtraction({ id: motionAsset.id }); + await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: { id: motionAsset.id } }); } this.logger.debug(`Finished motion photo video extraction for asset ${asset.id}: ${asset.originalPath}`); From 550c1c0a1063c2f6630076d287f3f2a183e327b1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 12:04:33 +0100 Subject: [PATCH 030/356] chore(deps): update prom/prometheus docker digest to 339ce86 (#17767) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index f4a57ecbb9..04ddbccbd8 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -90,7 +90,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:502ad90314c7485892ce696cb14a99fceab9fc27af29f4b427f41bd39701a199 + image: prom/prometheus@sha256:339ce86a59413be18d0e445472891d022725b4803fab609069110205e79fb2f1 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus From ca12aff3a4891742bb67b02103038bb2816b2258 Mon Sep 17 00:00:00 2001 From: Bastian Machek <16717398+bmachek@users.noreply.github.com> Date: Wed, 23 Apr 2025 13:11:42 +0200 Subject: [PATCH 031/356] docs: updated community-projects.tsx: lrc-immich-plugin (#17801) --- docs/src/components/community-projects.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/src/components/community-projects.tsx b/docs/src/components/community-projects.tsx index b30544d461..e70b5af50f 100644 --- a/docs/src/components/community-projects.tsx +++ b/docs/src/components/community-projects.tsx @@ -40,8 +40,9 @@ const projects: CommunityProjectProps[] = [ }, { title: 'Lightroom Immich Plugin: lrc-immich-plugin', - description: 'Another Lightroom plugin to publish or export photos from Lightroom to Immich.', - url: 'https://github.com/bmachek/lrc-immich-plugin', + description: + 'Lightroom plugin to publish, export photos from Lightroom to Immich. Import from Immich to Lightroom is also supported.', + url: 'https://blog.fokuspunk.de/lrc-immich-plugin/', }, { title: 'Immich Duplicate Finder', From a774153f677b2f33042c90d9189bf39fa0db36aa Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Wed, 23 Apr 2025 13:30:38 +0200 Subject: [PATCH 032/356] chore(web): update translations (#17627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aleksander Vae Haaland Co-authored-by: Bezruchenko Simon Co-authored-by: Bonov Co-authored-by: Bruno López Barcia Co-authored-by: Chris Axell Co-authored-by: Dymitr Co-authored-by: Florian Ostertag Co-authored-by: GiannosOB Co-authored-by: Happy Co-authored-by: Hurricane-32 Co-authored-by: Indrek Haav Co-authored-by: Jane Co-authored-by: Junghyuk Kwon Co-authored-by: Leo Bottaro Co-authored-by: Linerly Co-authored-by: MannyLama Co-authored-by: Matjaž T Co-authored-by: Miki Mrvos Co-authored-by: RWDai <869759838@qq.com> Co-authored-by: Roi Gabay Co-authored-by: Runskrift Co-authored-by: Sebastian Co-authored-by: Shawn Co-authored-by: Sidewave Tech Co-authored-by: Sylvain Pichon Co-authored-by: Temuri Doghonadze Co-authored-by: Xo Co-authored-by: Zvonimir Co-authored-by: adri1m64 Co-authored-by: catelixor Co-authored-by: eav5jhl0 Co-authored-by: kiwinho Co-authored-by: millallo Co-authored-by: pyccl Co-authored-by: stanciupaul Co-authored-by: thehijacker Co-authored-by: waclaw66 Co-authored-by: xuars Co-authored-by: Вячеслав Лукьяненко Co-authored-by: 灯笼 --- i18n/be.json | 6 +- i18n/ca.json | 2 +- i18n/cs.json | 34 +- i18n/da.json | 5 +- i18n/de.json | 154 +-- i18n/el.json | 24 +- i18n/en.json | 4 +- i18n/es.json | 168 +-- i18n/et.json | 220 +++- i18n/fi.json | 4 +- i18n/fr.json | 85 +- i18n/gl.json | 2394 ++++++++++++++++++++++++++++++--------- i18n/he.json | 443 ++++---- i18n/hi.json | 2 +- i18n/hr.json | 177 ++- i18n/hu.json | 6 +- i18n/id.json | 49 +- i18n/it.json | 234 ++-- i18n/ja.json | 8 +- i18n/ka.json | 10 +- i18n/kk.json | 17 +- i18n/ko.json | 382 ++++--- i18n/lv.json | 4 +- i18n/nb_NO.json | 14 +- i18n/nl.json | 3 + i18n/nn.json | 185 ++- i18n/pl.json | 8 +- i18n/pt.json | 94 +- i18n/pt_BR.json | 2 + i18n/ro.json | 42 +- i18n/ru.json | 114 +- i18n/sl.json | 220 ++-- i18n/sr_Cyrl.json | 48 +- i18n/sr_Latn.json | 26 +- i18n/sv.json | 10 +- i18n/th.json | 4 +- i18n/tr.json | 6 +- i18n/uk.json | 68 +- i18n/vi.json | 4 +- i18n/zh_Hant.json | 70 +- i18n/zh_SIMPLIFIED.json | 106 +- 41 files changed, 3719 insertions(+), 1737 deletions(-) diff --git a/i18n/be.json b/i18n/be.json index 8377ec5383..eea566df6a 100644 --- a/i18n/be.json +++ b/i18n/be.json @@ -4,6 +4,7 @@ "account_settings": "Налады ўліковага запісу", "acknowledge": "Пацвердзіць", "action": "Дзеянне", + "action_common_update": "Абнавіць", "actions": "Дзеянні", "active": "Актыўны", "activity": "Актыўнасць", @@ -20,8 +21,10 @@ "add_partner": "Дадаць партнёра", "add_path": "Дадаць шлях", "add_photos": "Дадаць фота", - "add_to": "Дадаць у...", + "add_to": "Дадаць у…", "add_to_album": "Дадаць у альбом", + "add_to_album_bottom_sheet_added": "Дададзена да {album}", + "add_to_album_bottom_sheet_already_exists": "Ужо знаходзіцца ў {album}", "add_to_shared_album": "Дадаць у агульны альбом", "add_url": "Дадаць URL", "added_to_archive": "Дададзена ў архіў", @@ -41,6 +44,7 @@ "backup_settings": "Налады рэзервовага капіявання", "backup_settings_description": "Кіраванне наладкамі рэзервовага капіявання базы даных", "check_all": "Праверыць усе", + "cleanup": "Ачыстка", "cleared_jobs": "Ачышчаны заданні для: {job}", "config_set_by_file": "Канфігурацыя ў зараз усталявана праз файл канфігурацыі", "confirm_delete_library": "Вы ўпэўнены што жадаеце выдаліць {library} бібліятэку?", diff --git a/i18n/ca.json b/i18n/ca.json index 52a47a83d5..c2482f3ddd 100644 --- a/i18n/ca.json +++ b/i18n/ca.json @@ -533,7 +533,7 @@ "backup_controller_page_backup_sub": "Fotografies i vídeos copiats", "backup_controller_page_created": "Creat el: {}", "backup_controller_page_desc_backup": "Activeu la còpia de seguretat per pujar automàticament els nous elements al servidor en obrir l'aplicació.", - "backup_controller_page_excluded": "Exclosos:", + "backup_controller_page_excluded": "Exclosos: ", "backup_controller_page_failed": "Fallats ({})", "backup_controller_page_filename": "Nom de l'arxiu: {} [{}]", "backup_controller_page_id": "ID: {}", diff --git a/i18n/cs.json b/i18n/cs.json index f3373f06a1..46cde0affd 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "Opravdu chcete zakázat všechny metody přihlášení? Přihlašování bude úplně zakázáno.", "authentication_settings_reenable": "Pro opětovné povolení použijte příkaz Příkaz serveru.", "background_task_job": "Úkoly na pozadí", - "backup_database": "Zálohování databáze", - "backup_database_enable_description": "Povolit zálohování databáze", - "backup_keep_last_amount": "Počet předchozích záloh k uchování", - "backup_settings": "Nastavení zálohování", - "backup_settings_description": "Správa nastavení zálohování databáze", + "backup_database": "Vytvořit výpis databáze", + "backup_database_enable_description": "Povolit výpisy z databáze", + "backup_keep_last_amount": "Počet předchozích výpisů, které se mají ponechat", + "backup_settings": "Nastavení výpisu databáze", + "backup_settings_description": "Správa nastavení výpisu databáze. Poznámka: Tyto úlohy nejsou monitorovány a nebudete upozorněni na jejich selhání.", "check_all": "Vše zkontrolovat", "cleanup": "Vyčištění", "cleared_jobs": "Hotové úlohy pro: {job}", @@ -371,6 +371,8 @@ "admin_password": "Heslo správce", "administration": "Administrace", "advanced": "Pokročilé", + "advanced_settings_enable_alternate_media_filter_subtitle": "Tuto možnost použijte k filtrování médií během synchronizace na základě alternativních kritérií. Tuto možnost vyzkoušejte pouze v případě, že máte problémy s detekcí všech alb v aplikaci.", + "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTÁLNÍ] Použít alternativní filtr pro synchronizaci alb zařízení", "advanced_settings_log_level_title": "Úroveň protokolování: {}", "advanced_settings_prefer_remote_subtitle": "U některých zařízení je načítání miniatur z prostředků v zařízení velmi pomalé. Aktivujte toto nastavení, aby se místo toho načítaly vzdálené obrázky.", "advanced_settings_prefer_remote_title": "Preferovat vzdálené obrázky", @@ -378,6 +380,8 @@ "advanced_settings_proxy_headers_title": "Proxy hlavičky", "advanced_settings_self_signed_ssl_subtitle": "Vynechá ověření SSL certifikátu serveru. Vyžadováno pro self-signed certifikáty.", "advanced_settings_self_signed_ssl_title": "Povolit self-signed SSL certifikáty", + "advanced_settings_sync_remote_deletions_subtitle": "Automaticky odstranit nebo obnovit položku v tomto zařízení, když je tato akce provedena na webu", + "advanced_settings_sync_remote_deletions_title": "Synchronizace vzdáleného mazání [EXPERIMENTÁLNÍ]", "advanced_settings_tile_subtitle": "Pokročilé uživatelské nastavení", "advanced_settings_troubleshooting_subtitle": "Zobrazit dodatečné vlastnosti pro řešení problémů", "advanced_settings_troubleshooting_title": "Řešení problémů", @@ -410,7 +414,7 @@ "album_viewer_appbar_delete_confirm": "Opravdu chcete toto album odstranit ze svého účtu?", "album_viewer_appbar_share_err_delete": "Nepodařilo se smazat album", "album_viewer_appbar_share_err_leave": "Nepodařilo se opustit album", - "album_viewer_appbar_share_err_remove": "Při odstraňování položek z alba se vyskytly problémy.", + "album_viewer_appbar_share_err_remove": "Při odstraňování položek z alba se vyskytly problémy", "album_viewer_appbar_share_err_title": "Nepodařilo se změnit název alba", "album_viewer_appbar_share_leave": "Opustit album", "album_viewer_appbar_share_to": "Sdílet na", @@ -504,16 +508,16 @@ "backup_album_selection_page_selection_info": "Informace o výběru", "backup_album_selection_page_total_assets": "Celkový počet jedinečných položek", "backup_all": "Vše", - "backup_background_service_backup_failed_message": "Zálohování médií selhalo. Zkouším to znovu...", - "backup_background_service_connection_failed_message": "Nepodařilo se připojit k serveru. Zkouším to znovu...", + "backup_background_service_backup_failed_message": "Zálohování médií selhalo. Zkouším to znovu…", + "backup_background_service_connection_failed_message": "Nepodařilo se připojit k serveru. Zkouším to znovu…", "backup_background_service_current_upload_notification": "Nahrávání {}", "backup_background_service_default_notification": "Kontrola nových médií…", "backup_background_service_error_title": "Chyba zálohování", - "backup_background_service_in_progress_notification": "Zálohování vašich médií...", + "backup_background_service_in_progress_notification": "Zálohování vašich médií…", "backup_background_service_upload_failure_notification": "Nepodařilo se nahrát {}", "backup_controller_page_albums": "Zálohovaná alba", "backup_controller_page_background_app_refresh_disabled_content": "Povolte obnovení aplikace na pozadí v Nastavení > Obecné > Obnovení aplikace na pozadí, abyste mohli používat zálohování na pozadí.", - "backup_controller_page_background_app_refresh_disabled_title": " Obnovování aplikací na pozadí je vypnuté", + "backup_controller_page_background_app_refresh_disabled_title": "Obnovování aplikací na pozadí je vypnuté", "backup_controller_page_background_app_refresh_enable_button_text": "Přejít do nastavení", "backup_controller_page_background_battery_info_link": "Ukaž mi jak", "backup_controller_page_background_battery_info_message": "Chcete-li dosáhnout nejlepších výsledků při zálohování na pozadí, vypněte všechny optimalizace baterie, které omezují aktivitu na pozadí pro Immich ve vašem zařízení. \n\nJelikož je to závislé na typu zařízení, vyhledejte požadované informace pro výrobce vašeho zařízení.", @@ -721,7 +725,7 @@ "delete_dialog_alert": "Tyto položky budou trvale smazány z aplikace Immich i z vašeho zařízení", "delete_dialog_alert_local": "Tyto položky budou z vašeho zařízení trvale smazány, ale budou stále k dispozici na Immich serveru", "delete_dialog_alert_local_non_backed_up": "Některé položky nejsou zálohovány na Immich server a budou ze zařízení trvale smazány", - "delete_dialog_alert_remote": "Tyto položky budou trvale smazány z Immich serveru ", + "delete_dialog_alert_remote": "Tyto položky budou trvale smazány z Immich serveru", "delete_dialog_ok_force": "Přesto smazat", "delete_dialog_title": "Smazat trvale", "delete_duplicates_confirmation": "Opravdu chcete tyto duplicity trvale odstranit?", @@ -992,6 +996,7 @@ "filetype": "Typ souboru", "filter": "Filtr", "filter_people": "Filtrovat lidi", + "filter_places": "Filtrovat místa", "find_them_fast": "Najděte je rychle vyhledáním jejich jména", "fix_incorrect_match": "Opravit nesprávnou shodu", "folder": "Složka", @@ -1040,7 +1045,7 @@ "home_page_delete_remote_err_local": "Místní položky ve vzdáleném výběru pro smazání, přeskakuji", "home_page_favorite_err_local": "Zatím není možné zařadit lokální média mezi oblíbená, přeskakuji", "home_page_favorite_err_partner": "Položky partnera nelze označit jako oblíbené, přeskakuji", - "home_page_first_time_notice": "Pokud aplikaci používáte poprvé, nezapomeňte si vybrat zálohovaná alba, aby se na časové ose mohly nacházet fotografie a videa z vybraných alb.", + "home_page_first_time_notice": "Pokud aplikaci používáte poprvé, nezapomeňte si vybrat zálohovaná alba, aby se na časové ose mohly nacházet fotografie a videa z vybraných alb", "home_page_share_err_local": "Nelze sdílet místní položky prostřednictvím odkazu, přeskakuji", "home_page_upload_err_limit": "Lze nahrát nejvýše 30 položek najednou, přeskakuji", "host": "Hostitel", @@ -1144,7 +1149,7 @@ "login_form_err_trailing_whitespace": "Koncová mezera", "login_form_failed_get_oauth_server_config": "Chyba přihlášení pomocí OAuth, zkontrolujte adresu URL serveru", "login_form_failed_get_oauth_server_disable": "Funkce OAuth není na tomto serveru dostupná", - "login_form_failed_login": "Chyba přihlášení, zkontrolujte URL adresu serveru, e-mail a heslo.", + "login_form_failed_login": "Chyba přihlášení, zkontrolujte URL adresu serveru, e-mail a heslo", "login_form_handshake_exception": "Došlo k výjimce Handshake se serverem. Pokud používáte self-signed certifikát, povolte v nastavení podporu self-signed certifikátu.", "login_form_password_hint": "heslo", "login_form_save_login": "Zůstat přihlášen", @@ -1282,6 +1287,7 @@ "onboarding_welcome_user": "Vítej, {user}", "online": "Online", "only_favorites": "Pouze oblíbené", + "open": "Otevřít", "open_in_map_view": "Otevřít v zobrazení mapy", "open_in_openstreetmap": "Otevřít v OpenStreetMap", "open_the_search_filters": "Otevřít vyhledávací filtry", @@ -1759,7 +1765,7 @@ "theme_setting_system_primary_color_title": "Použití systémové barvy", "theme_setting_system_theme_switch": "Automaticky (podle systemového nastavení)", "theme_setting_theme_subtitle": "Vyberte nastavení tématu aplikace", - "theme_setting_three_stage_loading_subtitle": "Třístupňové načítání může zvýšit výkonnost načítání, ale vede k výrazně vyššímu zatížení sítě.", + "theme_setting_three_stage_loading_subtitle": "Třístupňové načítání může zvýšit výkonnost načítání, ale vede k výrazně vyššímu zatížení sítě", "theme_setting_three_stage_loading_title": "Povolení třístupňového načítání", "they_will_be_merged_together": "Budou sloučeny dohromady", "third_party_resources": "Zdroje třetích stran", diff --git a/i18n/da.json b/i18n/da.json index 086b97f15a..a9aaef523c 100644 --- a/i18n/da.json +++ b/i18n/da.json @@ -70,8 +70,10 @@ "forcing_refresh_library_files": "Tvinger genopfriskning af alle biblioteksfiler", "image_format": "Format", "image_format_description": "WebP producerer mindre filer end JPEG, men er langsommere at komprimere.", + "image_fullsize_description": "Fuld størrelses billede uden metadata, brugt når zoomet ind", + "image_fullsize_enabled": "Aktiver fuld størrelses billede generering", "image_prefer_embedded_preview": "Foretræk indlejret forhåndsvisning", - "image_prefer_embedded_preview_setting_description": "Brug indlejrede forhåndsvisninger i RAW fotos som input til billedbehandling, når det er tilgængeligt. Dette kan give mere nøjagtige farver for nogle billeder, men kvaliteten af forhåndsvisningen er kameraafhængig, og billedet kan have flere komprimeringsartefakter.", + "image_prefer_embedded_preview_setting_description": "Brug indlejrede forhåndsvisninger i RAW fotos som input til billedbehandling og når det er tilgængeligt. Dette kan give mere nøjagtige farver for nogle billeder, men kvaliteten af forhåndsvisningen er kameraafhængig, og billedet kan have flere komprimeringsartefakter.", "image_prefer_wide_gamut": "Foretrækker bred farveskala", "image_prefer_wide_gamut_setting_description": "Brug Display P3 til miniaturebilleder. Dette bevarer billeder med brede farveskalaers dynamik bedre, men billeder kan komme til at se anderledes ud på gamle enheder med en gammel browserversion. sRGB-billeder bliver beholdt som sRGB for at undgå farveskift.", "image_preview_description": "Mellemstørrelse billede med fjernet metadata, der bruges, når du ser en enkelt mediefil og til machine learning", @@ -366,6 +368,7 @@ "admin_password": "Administratoradgangskode", "administration": "Administration", "advanced": "Avanceret", + "advanced_settings_enable_alternate_media_filter_subtitle": "Brug denne valgmulighed for at filtrere media under synkronisering baseret på alternative kriterier. Prøv kun denne hvis du har problemer med at appen ikke opdager alle albums.", "advanced_settings_log_level_title": "Logniveau: {}", "advanced_settings_prefer_remote_subtitle": "Nogle enheder tager meget lang tid om at indlæse miniaturebilleder af elementer på enheden. Aktiver denne indstilling for i stedetat indlæse elementer fra serveren.", "advanced_settings_prefer_remote_title": "Foretræk elementer på serveren", diff --git a/i18n/de.json b/i18n/de.json index bc4bc28575..b0649474fd 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "Bist du sicher, dass du alle Anmeldemethoden deaktivieren willst? Die Anmeldung wird vollständig deaktiviert.", "authentication_settings_reenable": "Nutze einen Server-Befehl zur Reaktivierung.", "background_task_job": "Hintergrundaufgaben", - "backup_database": "Datenbank sichern", - "backup_database_enable_description": "Sicherung der Datenbank aktivieren", - "backup_keep_last_amount": "Anzahl der aufzubewahrenden früheren Sicherungen", - "backup_settings": "Datensicherungs-Einstellungen", - "backup_settings_description": "Datensicherungs-Einstellungen verwalten", + "backup_database": "Datenbankabbild erstellen", + "backup_database_enable_description": "Erstellen von Datenbankabbildern aktivieren", + "backup_keep_last_amount": "Anzahl der aufzubewahrenden früheren Abbilder", + "backup_settings": "Datenbankabbild-Einstellungen", + "backup_settings_description": "Einstellungen zum Datenbankabbild verwalten. Hinweis: Diese Jobs werden nicht überwacht und du wirst nicht über Fehlschläge informiert.", "check_all": "Alle überprüfen", "cleanup": "Aufräumen", "cleared_jobs": "Folgende Aufgaben zurückgesetzt: {job}", @@ -371,13 +371,17 @@ "admin_password": "Administrator Passwort", "administration": "Verwaltung", "advanced": "Erweitert", - "advanced_settings_log_level_title": "Log-Level: {}", + "advanced_settings_enable_alternate_media_filter_subtitle": "Verwende diese Option, um Medien während der Synchronisierung nach anderen Kriterien zu filtern. Versuchen dies nur, wenn Probleme mit der Erkennung aller Alben durch die App auftreten.", + "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTELL] Benutze alternativen Filter für Synchronisierung der Gerätealben", + "advanced_settings_log_level_title": "Log-Level: {name}", "advanced_settings_prefer_remote_subtitle": "Einige Geräte sind sehr langsam beim Laden von Miniaturbildern direkt aus dem Gerät. Aktivieren Sie diese Einstellung, um stattdessen die Server-Bilder zu laden.", "advanced_settings_prefer_remote_title": "Server-Bilder bevorzugen", "advanced_settings_proxy_headers_subtitle": "Definiere einen Proxy-Header, den Immich bei jeder Netzwerkanfrage mitschicken soll", "advanced_settings_proxy_headers_title": "Proxy-Headers", "advanced_settings_self_signed_ssl_subtitle": "Verifizierung von SSL-Zertifikaten vom Server überspringen. Notwendig bei selbstsignierten Zertifikaten.", "advanced_settings_self_signed_ssl_title": "Selbstsignierte SSL-Zertifikate erlauben", + "advanced_settings_sync_remote_deletions_subtitle": "Automatisches Löschen oder Wiederherstellen einer Datei auf diesem Gerät, wenn diese Aktion im Web durchgeführt wird", + "advanced_settings_sync_remote_deletions_title": "Synchrone Remote-Löschungen [Experimentell]", "advanced_settings_tile_subtitle": "Erweiterte Benutzereinstellungen", "advanced_settings_troubleshooting_subtitle": "Erweiterte Funktionen zur Fehlersuche aktivieren", "advanced_settings_troubleshooting_title": "Fehlersuche", @@ -447,8 +451,8 @@ "archived_count": "{count, plural, other {# archiviert}}", "are_these_the_same_person": "Ist das dieselbe Person?", "are_you_sure_to_do_this": "Bist du sicher, dass du das tun willst?", - "asset_action_delete_err_read_only": "Schreibgeschützte Inhalte können nicht gelöscht werden, überspringen...", - "asset_action_share_err_offline": "Die Offline-Inhalte konnten nicht gelesen werden, überspringen...", + "asset_action_delete_err_read_only": "Schreibgeschützte Inhalte können nicht gelöscht werden, überspringen", + "asset_action_share_err_offline": "Die Offline-Inhalte konnten nicht gelesen werden, überspringen", "asset_added_to_album": "Zum Album hinzugefügt", "asset_adding_to_album": "Hinzufügen zum Album…", "asset_description_updated": "Die Beschreibung der Datei wurde aktualisiert", @@ -491,29 +495,29 @@ "assets_trashed_from_server": "{} Datei/en vom Immich-Server gelöscht", "assets_were_part_of_album_count": "{count, plural, one {# Datei ist} other {# Dateien sind}} bereits im Album vorhanden", "authorized_devices": "Verwendete Geräte", - "automatic_endpoint_switching_subtitle": "Verbinden Sie sich lokal über ein bestimmtes WLAN, wenn es verfügbar ist, und verwenden Sie andere Verbindungsmöglichkeiten anderswo.", + "automatic_endpoint_switching_subtitle": "Verbinden Sie sich lokal über ein bestimmtes WLAN, wenn es verfügbar ist, und verwenden Sie andere Verbindungsmöglichkeiten anderswo", "automatic_endpoint_switching_title": "Automatische URL-Umschaltung", "back": "Zurück", "back_close_deselect": "Zurück, Schließen oder Abwählen", "background_location_permission": "Hintergrund Standortfreigabe", "background_location_permission_content": "Um im Hintergrund zwischen den Netzwerken wechseln zu können, muss Immich *immer* Zugriff auf den genauen Standort haben, damit die App den Namen des WLAN-Netzwerks ermitteln kann", "backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})", - "backup_album_selection_page_albums_tap": "Einmalig das Album antippen um es zu sichern, doppelt antippen um es nicht mehr zu sichern.", + "backup_album_selection_page_albums_tap": "Einmalig das Album antippen um es zu sichern, doppelt antippen um es nicht mehr zu sichern", "backup_album_selection_page_assets_scatter": "Elemente (Fotos / Videos) können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden.", "backup_album_selection_page_select_albums": "Alben auswählen", "backup_album_selection_page_selection_info": "Information", "backup_album_selection_page_total_assets": "Elemente", "backup_all": "Alle", - "backup_background_service_backup_failed_message": "Es trat ein Fehler bei der Sicherung auf. Erneuter Versuch...", - "backup_background_service_connection_failed_message": "Es konnte keine Verbindung zum Server hergestellt werden. Erneuter Versuch...", + "backup_background_service_backup_failed_message": "Es trat ein Fehler bei der Sicherung auf. Erneuter Versuch…", + "backup_background_service_connection_failed_message": "Es konnte keine Verbindung zum Server hergestellt werden. Erneuter Versuch…", "backup_background_service_current_upload_notification": "Lädt {} hoch", "backup_background_service_default_notification": "Suche nach neuen Elementen…", "backup_background_service_error_title": "Fehler bei der Sicherung", - "backup_background_service_in_progress_notification": "Elemente werden gesichert...", + "backup_background_service_in_progress_notification": "Elemente werden gesichert…", "backup_background_service_upload_failure_notification": "Konnte {} nicht hochladen", "backup_controller_page_albums": "Gesicherte Alben", - "backup_controller_page_background_app_refresh_disabled_content": "Aktiviere Hintergrundaktualisierungen in Einstellungen -> Allgemein -> Hintergrundaktualisierungen um Sicherungen im Hintergrund zu ermöglichen. ", - "backup_controller_page_background_app_refresh_disabled_title": "Hintergrundaktualisierungen sind deaktiviert.", + "backup_controller_page_background_app_refresh_disabled_content": "Aktiviere Hintergrundaktualisierungen in Einstellungen -> Allgemein -> Hintergrundaktualisierungen um Sicherungen im Hintergrund zu ermöglichen.", + "backup_controller_page_background_app_refresh_disabled_title": "Hintergrundaktualisierungen sind deaktiviert", "backup_controller_page_background_app_refresh_enable_button_text": "Gehe zu Einstellungen", "backup_controller_page_background_battery_info_link": "Zeige mir wie", "backup_controller_page_background_battery_info_message": "Für die besten Ergebnisse für Sicherungen im Hintergrund, deaktiviere alle Batterieoptimierungen und Einschränkungen für die Hintergrundaktivitäten von Immich.\n\nDa dies gerätespezifisch ist, schlage diese Informationen für deinen Gerätehersteller nach.", @@ -554,7 +558,7 @@ "backup_err_only_album": "Das einzige Album kann nicht entfernt werden", "backup_info_card_assets": "Elemente", "backup_manual_cancelled": "Abgebrochen", - "backup_manual_in_progress": "Sicherung läuft bereits. Bitte versuche es später erneut.", + "backup_manual_in_progress": "Sicherung läuft bereits. Bitte versuche es später erneut", "backup_manual_success": "Erfolgreich", "backup_manual_title": "Sicherungsstatus", "backup_options_page_title": "Sicherungsoptionen", @@ -630,8 +634,8 @@ "client_cert_import_success_msg": "Client Zertifikat wurde importiert", "client_cert_invalid_msg": "Ungültige Zertifikatsdatei oder falsches Passwort", "client_cert_remove_msg": "Client Zertifikat wurde entfernt", - "client_cert_subtitle": "Unterstützt nur das PKCS12 (.p12, .pfx) Format. Zertifikatsimporte oder -entfernungen sind nur vor dem Login möglich.", - "client_cert_title": "SSL-Client-Zertifikat ", + "client_cert_subtitle": "Unterstützt nur das PKCS12 (.p12, .pfx) Format. Zertifikatsimporte oder -entfernungen sind nur vor dem Login möglich", + "client_cert_title": "SSL-Client-Zertifikat", "clockwise": "Im Uhrzeigersinn", "close": "Schließen", "collapse": "Zusammenklappen", @@ -644,7 +648,7 @@ "comments_are_disabled": "Kommentare sind deaktiviert", "common_create_new_album": "Neues Album erstellen", "common_server_error": "Bitte überprüfe Deine Netzwerkverbindung und stelle sicher, dass die App und Server Versionen kompatibel sind.", - "completed": "Fertig\n", + "completed": "Fertig", "confirm": "Bestätigen", "confirm_admin_password": "Administrator Passwort bestätigen", "confirm_delete_face": "Bist du sicher dass du das Gesicht von {name} aus der Datei entfernen willst?", @@ -719,9 +723,9 @@ "delete_album": "Album löschen", "delete_api_key_prompt": "Bist du sicher, dass du diesen API-Schlüssel löschen willst?", "delete_dialog_alert": "Diese Elemente werden unwiderruflich von Immich und dem Gerät entfernt", - "delete_dialog_alert_local": "Diese Inhalte werden vom Gerät gelöscht, bleiben aber auf dem Immich-Server.", - "delete_dialog_alert_local_non_backed_up": "Einige Inhalte sind nicht in Immich gesichert und werden dauerhaft vom Gerät gelöscht.", - "delete_dialog_alert_remote": "Diese Inhalte werden dauerhaft vom Immich-Server gelöscht.", + "delete_dialog_alert_local": "Diese Inhalte werden vom Gerät gelöscht, bleiben aber auf dem Immich-Server", + "delete_dialog_alert_local_non_backed_up": "Einige Inhalte sind nicht in Immich gesichert und werden dauerhaft vom Gerät gelöscht", + "delete_dialog_alert_remote": "Diese Inhalte werden dauerhaft vom Immich-Server gelöscht", "delete_dialog_ok_force": "Trotzdem löschen", "delete_dialog_title": "Endgültig löschen", "delete_duplicates_confirmation": "Bist du sicher, dass du diese Duplikate endgültig löschen willst?", @@ -741,7 +745,7 @@ "deletes_missing_assets": "Löscht Dateien, die auf der Festplatte fehlen", "description": "Beschreibung", "description_input_hint_text": "Beschreibung hinzufügen...", - "description_input_submit_error": "Beschreibung konnte nicht geändert werden, bitte im Log für mehr Details nachsehen.", + "description_input_submit_error": "Beschreibung konnte nicht geändert werden, bitte im Log für mehr Details nachsehen", "details": "Details", "direction": "Richtung", "disabled": "Deaktiviert", @@ -758,23 +762,23 @@ "documentation": "Dokumentation", "done": "Fertig", "download": "Herunterladen", - "download_canceled": "Download abgebrochen!", - "download_complete": "Download vollständig!", - "download_enqueue": "Download in die Warteschlange gesetzt!", + "download_canceled": "Download abgebrochen", + "download_complete": "Download vollständig", + "download_enqueue": "Download in die Warteschlange gesetzt", "download_error": "Download fehlerhaft", - "download_failed": "Download fehlerhaft!", + "download_failed": "Download fehlerhaft", "download_filename": "Datei: {}", "download_finished": "Download abgeschlossen", "download_include_embedded_motion_videos": "Eingebettete Videos", "download_include_embedded_motion_videos_description": "Videos, die in Bewegungsfotos eingebettet sind, als separate Datei einfügen", - "download_notfound": "Download nicht gefunden!", - "download_paused": "Download pausiert!", + "download_notfound": "Download nicht gefunden", + "download_paused": "Download pausiert", "download_settings": "Download", "download_settings_description": "Einstellungen für das Herunterladen von Dateien verwalten", "download_started": "Download gestartet", "download_sucess": "Download erfolgreich", "download_sucess_android": "Die Datei wurde nach DCIM/Immich heruntergeladen", - "download_waiting_to_retry": "Warte auf erneuten Versuch...", + "download_waiting_to_retry": "Warte auf erneuten Versuch", "downloading": "Herunterladen", "downloading_asset_filename": "Datei {filename} wird heruntergeladen", "downloading_media": "Medien werden heruntergeladen", @@ -954,9 +958,9 @@ "exif_bottom_sheet_people": "PERSONEN", "exif_bottom_sheet_person_add_person": "Namen hinzufügen", "exif_bottom_sheet_person_age": "Alter {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", + "exif_bottom_sheet_person_age_months": "{} Monate alt", + "exif_bottom_sheet_person_age_year_months": "1 Jahr, {} Monate alt", + "exif_bottom_sheet_person_age_years": "{} alt", "exit_slideshow": "Diashow beenden", "expand_all": "Alle aufklappen", "experimental_settings_new_asset_list_subtitle": "In Arbeit", @@ -992,6 +996,7 @@ "filetype": "Dateityp", "filter": "Filter", "filter_people": "Personen filtern", + "filter_places": "Orte filtern", "find_them_fast": "Finde sie schneller mit der Suche nach Namen", "fix_incorrect_match": "Fehlerhafte Übereinstimmung beheben", "folder": "Ordner", @@ -1001,7 +1006,7 @@ "forward": "Vorwärts", "general": "Allgemein", "get_help": "Hilfe erhalten", - "get_wifiname_error": "WLAN-Name konnte nicht ermittelt werden. Vergewissere dich, dass die erforderlichen Berechtigungen erteilt wurden und du mit einem WLAN-Netzwerk verbunden bist.\n", + "get_wifiname_error": "WLAN-Name konnte nicht ermittelt werden. Vergewissere dich, dass die erforderlichen Berechtigungen erteilt wurden und du mit einem WLAN-Netzwerk verbunden bist", "getting_started": "Erste Schritte", "go_back": "Zurück", "go_to_folder": "Gehe zu Ordner", @@ -1030,23 +1035,23 @@ "hide_person": "Person verbergen", "hide_unnamed_people": "Unbenannte Personen verbergen", "home_page_add_to_album_conflicts": "{added} Elemente zu {album} hinzugefügt. {failed} Elemente sind bereits vorhanden.", - "home_page_add_to_album_err_local": "Es können lokale Elemente noch nicht zu Alben hinzugefügt werden, überspringen...", + "home_page_add_to_album_err_local": "Es können lokale Elemente noch nicht zu Alben hinzugefügt werden, überspringen", "home_page_add_to_album_success": "{added} Elemente zu {album} hinzugefügt.", - "home_page_album_err_partner": "Inhalte von Partnern können derzeit nicht zu Alben hinzugefügt werden!", - "home_page_archive_err_local": "Kann lokale Elemente nicht archvieren, überspringen...", - "home_page_archive_err_partner": "Inhalte von Partnern können nicht archiviert werden!", - "home_page_building_timeline": "Zeitachse wird erstellt.", - "home_page_delete_err_partner": "Inhalte von Partnern können nicht gelöscht werden!", - "home_page_delete_remote_err_local": "Lokale Inhalte in der Auswahl, überspringen...", - "home_page_favorite_err_local": "Kann lokale Elemente noch nicht favorisieren, überspringen...", - "home_page_favorite_err_partner": "Inhalte von Partnern können nicht favorisiert werden!", - "home_page_first_time_notice": "Wenn dies das erste Mal ist dass Du Immich nutzt, stelle bitte sicher, dass mindestens ein Album zur Sicherung ausgewählt ist, sodass die Zeitachse mit Fotos und Videos gefüllt werden kann.", + "home_page_album_err_partner": "Inhalte von Partnern können derzeit nicht zu Alben hinzugefügt werden", + "home_page_archive_err_local": "Kann lokale Elemente nicht archvieren, überspringen", + "home_page_archive_err_partner": "Inhalte von Partnern können nicht archiviert werden", + "home_page_building_timeline": "Zeitachse wird erstellt", + "home_page_delete_err_partner": "Inhalte von Partnern können nicht gelöscht werden, überspringe", + "home_page_delete_remote_err_local": "Lokale Inhalte in der Auswahl, überspringen", + "home_page_favorite_err_local": "Kann lokale Elemente noch nicht favorisieren, überspringen", + "home_page_favorite_err_partner": "Inhalte von Partnern können nicht favorisiert werden, überspringe", + "home_page_first_time_notice": "Wenn dies das erste Mal ist dass Du Immich nutzt, stelle bitte sicher, dass mindestens ein Album zur Sicherung ausgewählt ist, sodass die Zeitachse mit Fotos und Videos gefüllt werden kann", "home_page_share_err_local": "Lokale Inhalte können nicht per Link geteilt werden, überspringe", - "home_page_upload_err_limit": "Es können max. 30 Elemente gleichzeitig hochgeladen werden, überspringen...", + "home_page_upload_err_limit": "Es können max. 30 Elemente gleichzeitig hochgeladen werden, überspringen", "host": "Host", "hour": "Stunde", "ignore_icloud_photos": "iCloud Fotos ignorieren", - "ignore_icloud_photos_description": "Fotos, die in der iCloud gespeichert sind, werden nicht auf den immich Server hochgeladen", + "ignore_icloud_photos_description": "Fotos, die in der iCloud gespeichert sind, werden nicht auf den immich Server hochgeladen", "image": "Bild", "image_alt_text_date": "{isVideo, select, true {Video} other {Bild}} aufgenommen am {date}", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Bild}} aufgenommen mit {person1} am {date}", @@ -1080,7 +1085,7 @@ "night_at_midnight": "Täglich um Mitternacht", "night_at_twoam": "Täglich nachts um 2:00 Uhr" }, - "invalid_date": "Ungültiges Datum ", + "invalid_date": "Ungültiges Datum", "invalid_date_format": "Ungültiges Datumsformat", "invite_people": "Personen einladen", "invite_to_album": "Zum Album einladen", @@ -1143,7 +1148,7 @@ "login_form_err_leading_whitespace": "Leerzeichen am Anfang", "login_form_err_trailing_whitespace": "Leerzeichen am Ende", "login_form_failed_get_oauth_server_config": "Fehler beim Login per OAuth, bitte Server-URL überprüfen", - "login_form_failed_get_oauth_server_disable": "Die OAuth-Funktion ist auf diesem Server nicht verfügbar.", + "login_form_failed_get_oauth_server_disable": "Die OAuth-Funktion ist auf diesem Server nicht verfügbar", "login_form_failed_login": "Fehler beim Login, bitte überprüfe die Server-URL, deine E-Mail oder das Passwort", "login_form_handshake_exception": "Fehler beim Verbindungsaufbau mit dem Server. Falls du ein selbstsigniertes Zertifikat verwendest, aktiviere die Unterstützung dafür in den Einstellungen.", "login_form_password_hint": "Passwort", @@ -1151,8 +1156,8 @@ "login_form_server_empty": "Serveradresse eingeben.", "login_form_server_error": "Es Konnte sich nicht mit dem Server verbunden werden.", "login_has_been_disabled": "Die Anmeldung wurde deaktiviert.", - "login_password_changed_error": "Fehler beim Passwort ändern!", - "login_password_changed_success": "Passwort erfolgreich geändert.", + "login_password_changed_error": "Fehler beim Ändern deines Passwort", + "login_password_changed_success": "Passwort erfolgreich geändert", "logout_all_device_confirmation": "Bist du sicher, dass du alle Geräte abmelden willst?", "logout_this_device_confirmation": "Bist du sicher, dass du dieses Gerät abmelden willst?", "longitude": "Längengrad", @@ -1172,7 +1177,7 @@ "map": "Karte", "map_assets_in_bound": "{} Foto", "map_assets_in_bounds": "{} Fotos", - "map_cannot_get_user_location": "Standort konnte nicht ermittelt werden!", + "map_cannot_get_user_location": "Standort konnte nicht ermittelt werden", "map_location_dialog_yes": "Ja", "map_location_picker_page_use_location": "Aufnahmeort verwenden", "map_location_service_disabled_content": "Ortungsdienste müssen aktiviert sein, um Inhalte am aktuellen Standort anzuzeigen. Willst du die Ortungsdienste jetzt aktivieren?", @@ -1181,7 +1186,7 @@ "map_marker_with_image": "Kartenmarkierung mit Bild", "map_no_assets_in_bounds": "Keine Fotos in dieser Gegend", "map_no_location_permission_content": "Ortungsdienste müssen aktiviert sein, um Inhalte am aktuellen Standort anzuzeigen. Willst du die Ortungsdienste jetzt aktivieren?", - "map_no_location_permission_title": "Kein Zugriff auf den Standort!", + "map_no_location_permission_title": "Kein Zugriff auf den Standort", "map_settings": "Karteneinstellungen", "map_settings_dark_mode": "Dunkler Modus", "map_settings_date_range_option_day": "Letzte 24 Stunden", @@ -1198,7 +1203,7 @@ "media_type": "Medientyp", "memories": "Erinnerungen", "memories_all_caught_up": "Alles aufgeholt", - "memories_check_back_tomorrow": "Schau morgen wieder vorbei für weitere Erinnerungen!", + "memories_check_back_tomorrow": "Schau morgen wieder vorbei für weitere Erinnerungen", "memories_setting_description": "Verwalte, was du in deinen Erinnerungen siehst", "memories_start_over": "Erneut beginnen", "memories_swipe_to_close": "Nach oben Wischen zum schließen", @@ -1221,8 +1226,8 @@ "monthly_title_text_date_format": "MMMM y", "more": "Mehr", "moved_to_trash": "In den Papierkorb verschoben", - "multiselect_grid_edit_date_time_err_read_only": "Das Datum und die Uhrzeit von schreibgeschützten Inhalten kann nicht verändert werden, überspringen...", - "multiselect_grid_edit_gps_err_read_only": "Der Aufnahmeort von schreibgeschützten Inhalten kann nicht verändert werden, überspringen...", + "multiselect_grid_edit_date_time_err_read_only": "Das Datum und die Uhrzeit von schreibgeschützten Inhalten kann nicht verändert werden, überspringen", + "multiselect_grid_edit_gps_err_read_only": "Der Aufnahmeort von schreibgeschützten Inhalten kann nicht verändert werden, überspringen", "mute_memories": "Erinnerungen stumm schalten", "my_albums": "Meine Alben", "name": "Name", @@ -1260,8 +1265,8 @@ "not_selected": "Nicht ausgewählt", "note_apply_storage_label_to_previously_uploaded assets": "Hinweis: Um eine Speicherpfadbezeichnung anzuwenden, starte den", "notes": "Notizen", - "notification_permission_dialog_content": "Um Benachrichtigungen zu aktivieren, navigiere zu Einstellungen und klicke \"Erlauben\"", - "notification_permission_list_tile_content": "Erlaube Berechtigung für Benachrichtigungen", + "notification_permission_dialog_content": "Um Benachrichtigungen zu aktivieren, navigiere zu Einstellungen und klicke \"Erlauben\".", + "notification_permission_list_tile_content": "Erlaube Berechtigung für Benachrichtigungen.", "notification_permission_list_tile_enable_button": "Aktiviere Benachrichtigungen", "notification_permission_list_tile_title": "Benachrichtigungs-Berechtigung", "notification_toggle_setting_description": "E-Mail-Benachrichtigungen aktivieren", @@ -1282,6 +1287,7 @@ "onboarding_welcome_user": "Willkommen, {user}", "online": "Online", "only_favorites": "Nur Favoriten", + "open": "Öffnen", "open_in_map_view": "In Kartenansicht öffnen", "open_in_openstreetmap": "In OpenStreetMap öffnen", "open_the_search_filters": "Die Suchfilter öffnen", @@ -1371,7 +1377,7 @@ "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Major-Version.", "profile_drawer_client_out_of_date_minor": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Minor-Version.", - "profile_drawer_client_server_up_to_date": "Die App-Version / Server-Version sind aktuell.", + "profile_drawer_client_server_up_to_date": "Die App-Version / Server-Version sind aktuell", "profile_drawer_github": "GitHub", "profile_drawer_server_out_of_date_major": "Server-Version ist veraltet. Bitte aktualisiere auf die neueste Major-Version.", "profile_drawer_server_out_of_date_minor": "Server-Version ist veraltet. Bitte aktualisiere auf die neueste Minor-Version.", @@ -1504,7 +1510,7 @@ "search_city": "Suche nach Stadt...", "search_country": "Suche nach Land...", "search_filter_apply": "Filter anwenden", - "search_filter_camera_title": "Kameratyp auswählen ", + "search_filter_camera_title": "Kameratyp auswählen", "search_filter_date": "Datum", "search_filter_date_interval": "{start} bis {end}", "search_filter_date_title": "Wähle einen Zeitraum", @@ -1512,10 +1518,10 @@ "search_filter_display_options": "Anzeigeeinstellungen", "search_filter_filename": "Suche nach Dateiname", "search_filter_location": "Ort", - "search_filter_location_title": "Ort auswählen ", + "search_filter_location_title": "Ort auswählen", "search_filter_media_type": "Medientyp", - "search_filter_media_type_title": "Medientyp auswählen ", - "search_filter_people_title": "Personen auswählen ", + "search_filter_media_type_title": "Medientyp auswählen", + "search_filter_people_title": "Personen auswählen", "search_for": "Suche nach", "search_for_existing_person": "Suche nach vorhandener Person", "search_no_more_result": "Keine weiteren Ergebnisse", @@ -1596,7 +1602,7 @@ "setting_notifications_notify_minutes": "{} Minuten", "setting_notifications_notify_never": "niemals", "setting_notifications_notify_seconds": "{} Sekunden", - "setting_notifications_single_progress_subtitle": "Detaillierter Upload-Fortschritt für jedes Element.", + "setting_notifications_single_progress_subtitle": "Detaillierter Upload-Fortschritt für jedes Element", "setting_notifications_single_progress_title": "Zeige den detaillierten Fortschritt der Hintergrundsicherung", "setting_notifications_subtitle": "Benachrichtigungen anpassen", "setting_notifications_total_progress_subtitle": "Gesamter Upload-Fortschritt (abgeschlossen/Anzahl Elemente)", @@ -1605,14 +1611,14 @@ "setting_video_viewer_original_video_subtitle": "Beim Streaming eines Videos vom Server wird das Original abgespielt, auch wenn eine Transkodierung verfügbar ist. Kann zu Pufferung führen. Lokal verfügbare Videos werden unabhängig von dieser Einstellung in Originalqualität wiedergegeben.", "setting_video_viewer_original_video_title": "Originalvideo erzwingen", "settings": "Einstellungen", - "settings_require_restart": "Bitte starte Immich neu, um diese Einstellung anzuwenden.", + "settings_require_restart": "Bitte starte Immich neu, um diese Einstellung anzuwenden", "settings_saved": "Einstellungen gespeichert", "share": "Teilen", "share_add_photos": "Fotos hinzufügen", "share_assets_selected": "{} ausgewählt", "share_dialog_preparing": "Vorbereiten...", "shared": "Geteilt", - "shared_album_activities_input_disable": "Kommentare sind deaktiviert.", + "shared_album_activities_input_disable": "Kommentare sind deaktiviert", "shared_album_activity_remove_content": "Möchtest du diese Aktivität entfernen?", "shared_album_activity_remove_title": "Aktivität entfernen", "shared_album_section_people_action_error": "Fehler beim Verlassen oder Entfernen aus dem Album", @@ -1635,20 +1641,20 @@ "shared_link_edit_expire_after_option_hours": "{} Stunden", "shared_link_edit_expire_after_option_minute": "1 Minute", "shared_link_edit_expire_after_option_minutes": "{} Minuten", - "shared_link_edit_expire_after_option_months": "{} Monat/en", - "shared_link_edit_expire_after_option_year": "{} Jahr/en", + "shared_link_edit_expire_after_option_months": "{} Monate", + "shared_link_edit_expire_after_option_year": "{} Jahr", "shared_link_edit_password_hint": "Passwort eingeben", "shared_link_edit_submit_button": "Link aktualisieren", "shared_link_error_server_url_fetch": "Fehler beim Ermitteln der Server-URL", "shared_link_expires_day": "Verfällt in {} Tag", - "shared_link_expires_days": "Verfällt in {} Tag/en", + "shared_link_expires_days": "Verfällt in {} Tagen", "shared_link_expires_hour": "Verfällt in {} Stunde", - "shared_link_expires_hours": "Verfällt in {} Stunde/n", + "shared_link_expires_hours": "Verfällt in {} Stunden", "shared_link_expires_minute": "Verfällt in {} Minute", - "shared_link_expires_minutes": "Verfällt in {} Minute/n", + "shared_link_expires_minutes": "Verfällt in {} Minuten", "shared_link_expires_never": "Läuft nie ab", "shared_link_expires_second": "Verfällt in {} Sekunde", - "shared_link_expires_seconds": "Verfällt in {} Sekunde/n", + "shared_link_expires_seconds": "Verfällt in {} Sekunden", "shared_link_individual_shared": "Individuell geteilt", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Geteilte Links verwalten", @@ -1750,11 +1756,11 @@ "theme_selection_description": "Automatische Einstellung des Themes auf Hell oder Dunkel, je nach Systemeinstellung des Browsers", "theme_setting_asset_list_storage_indicator_title": "Forschrittsbalken der Sicherung auf dem Vorschaubild", "theme_setting_asset_list_tiles_per_row_title": "Anzahl der Elemente pro Reihe ({})", - "theme_setting_colorful_interface_subtitle": "Primärfarbe auf App-Hintergrund anwenden", + "theme_setting_colorful_interface_subtitle": "Primärfarbe auf App-Hintergrund anwenden.", "theme_setting_colorful_interface_title": "Farbige UI-Oberfläche", "theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters", "theme_setting_image_viewer_quality_title": "Qualität des Bildbetrachters", - "theme_setting_primary_color_subtitle": "Farbauswahl für primäre Aktionen und Akzente", + "theme_setting_primary_color_subtitle": "Farbauswahl für primäre Aktionen und Akzente.", "theme_setting_primary_color_title": "Primärfarbe", "theme_setting_system_primary_color_title": "Systemfarbe verwenden", "theme_setting_system_theme_switch": "Automatisch (Systemeinstellung)", @@ -1784,7 +1790,7 @@ "trash_no_results_message": "Gelöschte Fotos und Videos werden hier angezeigt.", "trash_page_delete_all": "Alle löschen", "trash_page_empty_trash_dialog_content": "Elemente im Papierkorb löschen? Diese Elemente werden dauerhaft aus Immich entfernt", - "trash_page_info": "Elemente im Papierkorb werden nach {} Tagen endgültig gelöscht.", + "trash_page_info": "Elemente im Papierkorb werden nach {} Tagen endgültig gelöscht", "trash_page_no_assets": "Es gibt keine Daten im Papierkorb", "trash_page_restore_all": "Alle wiederherstellen", "trash_page_select_assets_btn": "Elemente auswählen", diff --git a/i18n/el.json b/i18n/el.json index 52efcccd50..a8a56f5122 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "Είστε βέβαιοι ότι θέλετε να απενεργοποιήσετε όλες τις μεθόδους σύνδεσης; Η σύνδεση θα απενεργοποιηθεί πλήρως.", "authentication_settings_reenable": "Για επανενεργοποίηση, χρησιμοποιήστε μία Εντολή Διακομιστή.", "background_task_job": "Εργασίες Παρασκηνίου", - "backup_database": "Δημιουργία Αντιγράφου Ασφαλείας της Βάσης Δεδομένων", - "backup_database_enable_description": "Ενεργοποίηση αντιγράφων ασφαλείας της βάσης δεδομένων", - "backup_keep_last_amount": "Αριθμός προηγούμενων αντιγράφων ασφαλείας για διατήρηση", - "backup_settings": "Ρυθμίσεις Αντιγράφων Ασφαλείας", - "backup_settings_description": "Διαχείρηση ρυθμίσεων των αντιγράφων ασφαλείας της βάσης δεδομένων", + "backup_database": "Δημιουργία Dump βάσης δεδομένων", + "backup_database_enable_description": "Ενεργοποίηση dumps βάσης δεδομένων", + "backup_keep_last_amount": "Ποσότητα προηγούμενων dumps που πρέπει να διατηρηθούν", + "backup_settings": "Ρυθμίσεις dump βάσης δεδομένων", + "backup_settings_description": "Διαχείριση ρυθμίσεων dump της βάσης δεδομένων. Σημείωση: Αυτές οι εργασίες δεν παρακολουθούνται και δεν θα ειδοποιηθείτε για αποτυχία.", "check_all": "Έλεγχος Όλων", "cleanup": "Εκκαθάριση", "cleared_jobs": "Εκκαθαρίστηκαν οι εργασίες για: {job}", @@ -371,13 +371,17 @@ "admin_password": "Κωδικός πρόσβασης Διαχειριστή", "administration": "Διαχείριση", "advanced": "Για προχωρημένους", - "advanced_settings_log_level_title": "Επίπεδο καταγραφής: {}", + "advanced_settings_enable_alternate_media_filter_subtitle": "Χρησιμοποιήστε αυτήν την επιλογή για να φιλτράρετε τα μέσα ενημέρωσης κατά τον συγχρονισμό με βάση εναλλακτικά κριτήρια. Δοκιμάστε αυτή τη δυνατότητα μόνο αν έχετε προβλήματα με την εφαρμογή που εντοπίζει όλα τα άλμπουμ.", + "advanced_settings_enable_alternate_media_filter_title": "[ΠΕΙΡΑΜΑΤΙΚΟ] Χρήση εναλλακτικού φίλτρου συγχρονισμού άλμπουμ συσκευής", + "advanced_settings_log_level_title": "Επίπεδο σύνδεσης: {}", "advanced_settings_prefer_remote_subtitle": "Μερικές συσκευές αργούν πολύ να φορτώσουν μικρογραφίες από αρχεία στη συσκευή. Ενεργοποιήστε αυτήν τη ρύθμιση για να φορτώνονται αντί αυτού απομακρυσμένες εικόνες.", "advanced_settings_prefer_remote_title": "Προτίμηση απομακρυσμένων εικόνων.", "advanced_settings_proxy_headers_subtitle": "Καθορισμός κεφαλίδων διακομιστή μεσολάβησης που το Immich πρέπει να στέλνει με κάθε αίτημα δικτύου", "advanced_settings_proxy_headers_title": "Κεφαλίδες διακομιστή μεσολάβησης", "advanced_settings_self_signed_ssl_subtitle": "Παρακάμπτει τον έλεγχο πιστοποιητικού SSL του διακομιστή. Απαραίτητο για αυτο-υπογεγραμμένα πιστοποιητικά.", "advanced_settings_self_signed_ssl_title": "Να επιτρέπονται αυτο-υπογεγραμμένα πιστοποιητικά SSL", + "advanced_settings_sync_remote_deletions_subtitle": "Αυτόματη διαγραφή ή επαναφορά ενός περιουσιακού στοιχείου σε αυτή τη συσκευή, όταν η ενέργεια αυτή πραγματοποιείται στο διαδίκτυο", + "advanced_settings_sync_remote_deletions_title": "Συγχρονισμός απομακρυσμένων διαγραφών [ΠΕΙΡΑΜΑΤΙΚΟ]", "advanced_settings_tile_subtitle": "Ρυθμίσεις προχωρημένου χρήστη", "advanced_settings_troubleshooting_subtitle": "Ενεργοποίηση πρόσθετων χαρακτηριστικών για αντιμετώπιση προβλημάτων", "advanced_settings_troubleshooting_title": "Αντιμετώπιση προβλημάτων", @@ -477,8 +481,8 @@ "assets_added_to_album_count": "Προστέθηκε {count, plural, one {# αρχείο} other {# αρχεία}} στο άλμπουμ", "assets_added_to_name_count": "Προστέθηκε {count, plural, one {# αρχείο} other {# αρχεία}} στο {hasName, select, true {{name}} other {νέο άλμπουμ}}", "assets_count": "{count, plural, one {# αρχείο} other {# αρχεία}}", - "assets_deleted_permanently": "{} στοιχείο(α) διαγράφηκαν οριστικά", - "assets_deleted_permanently_from_server": "{} στοιχείο(α) διαγράφηκαν οριστικά από τον διακομιστή Immich", + "assets_deleted_permanently": "{} στοιχείο(-α) διαγράφηκε(-αν) οριστικά", + "assets_deleted_permanently_from_server": "{} στοιχείο(α) διαγράφηκε(-αν) οριστικά από τον διακομιστή Immich", "assets_moved_to_trash_count": "Μετακινήθηκε/καν {count, plural, one {# αρχείο} other {# αρχεία}} στον κάδο απορριμμάτων", "assets_permanently_deleted_count": "Διαγράφηκε/καν μόνιμα {count, plural, one {# αρχείο} other {# αρχεία}}", "assets_removed_count": "Αφαιρέθηκαν {count, plural, one {# αρχείο} other {# αρχεία}}", @@ -529,11 +533,11 @@ "backup_controller_page_background_turn_on": "Ενεργοποίηση υπηρεσίας παρασκηνίου", "backup_controller_page_background_wifi": "Μόνο σε σύνδεση WiFi", "backup_controller_page_backup": "Αντίγραφα ασφαλείας", - "backup_controller_page_backup_selected": "Επιλεγμένα:", + "backup_controller_page_backup_selected": "Επιλεγμένα: ", "backup_controller_page_backup_sub": "Φωτογραφίες και βίντεο για τα οποία έχουν δημιουργηθεί αντίγραφα ασφαλείας", "backup_controller_page_created": "Δημιουργήθηκε στις: {}", "backup_controller_page_desc_backup": "Ενεργοποιήστε την δημιουργία αντιγράφων ασφαλείας στο προσκήνιο για αυτόματη μεταφόρτωση νέων στοιχείων στον διακομιστή όταν ανοίγετε την εφαρμογή.", - "backup_controller_page_excluded": "Εξαιρούμενα:", + "backup_controller_page_excluded": "Εξαιρούμενα: ", "backup_controller_page_failed": "Αποτυχημένα ({})", "backup_controller_page_filename": "Όνομα αρχείου: {} [{}]", "backup_controller_page_id": "ID: {}", diff --git a/i18n/en.json b/i18n/en.json index 9951717de6..883b69dff5 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -978,7 +978,7 @@ "external": "External", "external_libraries": "External Libraries", "external_network": "External network", - "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", + "external_network_sheet_info": "When not on the preferred Wi-Fi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "face_unassigned": "Unassigned", "failed": "Failed", "failed_to_load_assets": "Failed to load assets", @@ -1125,7 +1125,7 @@ "local_network": "Local network", "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", "location_permission": "Location permission", - "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", + "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current Wi-Fi network's name", "location_picker_choose_on_map": "Choose on map", "location_picker_latitude_error": "Enter a valid latitude", "location_picker_latitude_hint": "Enter your latitude here", diff --git a/i18n/es.json b/i18n/es.json index 0fe78eb66f..02cd6ab840 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -14,7 +14,7 @@ "add_a_location": "Agregar ubicación", "add_a_name": "Agregar nombre", "add_a_title": "Agregar título", - "add_endpoint": "Add endpoint", + "add_endpoint": "Añadir endpoint", "add_exclusion_pattern": "Agregar patrón de exclusión", "add_import_path": "Agregar ruta de importación", "add_location": "Agregar ubicación", @@ -371,19 +371,23 @@ "admin_password": "Contraseña del Administrador", "administration": "Administración", "advanced": "Avanzada", + "advanced_settings_enable_alternate_media_filter_subtitle": "Usa esta opción para filtrar medios durante la sincronización según criterios alternativos. Intenta esto solo si tienes problemas con que la aplicación detecte todos los álbumes.", + "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Usar filtro alternativo de sincronización de álbumes del dispositivo", "advanced_settings_log_level_title": "Nivel de registro: {}", "advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de los elementos encontrados en el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.", "advanced_settings_prefer_remote_title": "Preferir imágenes remotas", "advanced_settings_proxy_headers_subtitle": "Configura headers HTTP que Immich incluirá en cada petición de red", - "advanced_settings_proxy_headers_title": "Proxy Headers", - "advanced_settings_self_signed_ssl_subtitle": "Omitir verificación del certificado SSL del servidor. Requerido para certificados autofirmados", + "advanced_settings_proxy_headers_title": "Cabeceras Proxy", + "advanced_settings_self_signed_ssl_subtitle": "Omitir verificación del certificado SSL del servidor. Requerido para certificados autofirmados.", "advanced_settings_self_signed_ssl_title": "Permitir certificados autofirmados", + "advanced_settings_sync_remote_deletions_subtitle": "Eliminar o restaurar automáticamente un recurso en este dispositivo cuando se realice esa acción en la web", + "advanced_settings_sync_remote_deletions_title": "Sincronizar eliminaciones remotas [EXPERIMENTAL]", "advanced_settings_tile_subtitle": "Configuraciones avanzadas del usuario", "advanced_settings_troubleshooting_subtitle": "Habilitar funciones adicionales para solución de problemas", "advanced_settings_troubleshooting_title": "Solución de problemas", "age_months": "Tiempo {months, plural, one {# mes} other {# meses}}", "age_year_months": "1 año, {months, plural, one {# mes} other {# meses}}", - "age_years": "Edad {years, plural, one {# año} other {# años}}", + "age_years": "Antigüedad {years, plural, one {# año} other {# años}}", "album_added": "Álbum añadido", "album_added_notification_setting_description": "Reciba una notificación por correo electrónico cuando lo agreguen a un álbum compartido", "album_cover_updated": "Portada del álbum actualizada", @@ -401,7 +405,7 @@ "album_share_no_users": "Parece que has compartido este álbum con todos los usuarios o no tienes ningún usuario con quien compartirlo.", "album_thumbnail_card_item": "1 elemento", "album_thumbnail_card_items": "{} elementos", - "album_thumbnail_card_shared": "Compartido", + "album_thumbnail_card_shared": " · Compartido", "album_thumbnail_shared_by": "Compartido por {}", "album_updated": "Album actualizado", "album_updated_setting_description": "Reciba una notificación por correo electrónico cuando un álbum compartido tenga nuevos archivos", @@ -411,8 +415,8 @@ "album_viewer_appbar_share_err_delete": "No ha podido eliminar el álbum", "album_viewer_appbar_share_err_leave": "No se ha podido abandonar el álbum", "album_viewer_appbar_share_err_remove": "Hay problemas para eliminar los elementos del álbum", - "album_viewer_appbar_share_err_title": "Error al cambiar el título del álbum ", - "album_viewer_appbar_share_leave": "Abandonar álbum ", + "album_viewer_appbar_share_err_title": "Error al cambiar el título del álbum", + "album_viewer_appbar_share_leave": "Abandonar álbum", "album_viewer_appbar_share_to": "Compartir Con", "album_viewer_page_share_add_users": "Agregar usuarios", "album_with_link_access": "Permita que cualquier persona con el enlace vea fotos y personas en este álbum.", @@ -469,7 +473,7 @@ "asset_skipped": "Omitido", "asset_skipped_in_trash": "En la papelera", "asset_uploaded": "Subido", - "asset_uploading": "Subiendo…", + "asset_uploading": "Cargando…", "asset_viewer_settings_subtitle": "Administra las configuracioens de tu visor de fotos", "asset_viewer_settings_title": "Visor de Archivos", "assets": "elementos", @@ -477,8 +481,8 @@ "assets_added_to_album_count": "Añadido {count, plural, one {# asset} other {# assets}} al álbum", "assets_added_to_name_count": "Añadido {count, plural, one {# asset} other {# assets}} a {hasName, select, true {{name}} other {new album}}", "assets_count": "{count, plural, one {# activo} other {# activos}}", - "assets_deleted_permanently": "\n{} elementos(s) eliminado(s) permanentemente", - "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_deleted_permanently": "{} elementos(s) eliminado(s) permanentemente", + "assets_deleted_permanently_from_server": "{} recurso(s) eliminados de forma permanente del servidor de Immich", "assets_moved_to_trash_count": "{count, plural, one {# elemento movido} other {# elementos movidos}} a la papelera", "assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# elemento} other {# elementos}}", "assets_removed_count": "Eliminado {count, plural, one {# elemento} other {# elementos}}", @@ -488,15 +492,15 @@ "assets_restored_successfully": "{} elemento(s) restaurado(s) exitosamente", "assets_trashed": "{} elemento(s) eliminado(s)", "assets_trashed_count": "Borrado {count, plural, one {# elemento} other {# elementos}}", - "assets_trashed_from_server": "{} elemento(s) movido a la papelera en Immich", + "assets_trashed_from_server": "{} recurso(s) enviados a la papelera desde el servidor de Immich", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} ya forma parte del álbum", "authorized_devices": "Dispositivos Autorizados", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", + "automatic_endpoint_switching_subtitle": "Conectarse localmente a través de la Wi-Fi designada cuando esté disponible y usar conexiones alternativas en otros lugares", + "automatic_endpoint_switching_title": "Cambio automático de URL", "back": "Atrás", "back_close_deselect": "Atrás, cerrar o anular la selección", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", + "background_location_permission": "Permiso de ubicación en segundo plano", + "background_location_permission_content": "Para poder cambiar de red mientras se ejecuta en segundo plano, Immich debe tener *siempre* acceso a la ubicación precisa para que la aplicación pueda leer el nombre de la red Wi-Fi", "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})", "backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir", "backup_album_selection_page_assets_scatter": "Los elementos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.", @@ -504,12 +508,12 @@ "backup_album_selection_page_selection_info": "Información sobre la Selección", "backup_album_selection_page_total_assets": "Total de elementos únicos", "backup_all": "Todos", - "backup_background_service_backup_failed_message": "Error al copiar elementos. Reintentando...", - "backup_background_service_connection_failed_message": "Error al conectar con el servidor. Reintentando...", + "backup_background_service_backup_failed_message": "Error al copiar elementos. Reintentando…", + "backup_background_service_connection_failed_message": "Error al conectar con el servidor. Reintentando…", "backup_background_service_current_upload_notification": "Cargando {}", - "backup_background_service_default_notification": "Verificando si hay nuevos elementos", + "backup_background_service_default_notification": "Comprobando nuevos elementos…", "backup_background_service_error_title": "Error de copia de seguridad", - "backup_background_service_in_progress_notification": "Creando copia de seguridad de tus elementos...", + "backup_background_service_in_progress_notification": "Creando copia de seguridad de tus elementos…", "backup_background_service_upload_failure_notification": "Error al cargar {}", "backup_controller_page_albums": "Álbumes de copia de seguridad", "backup_controller_page_background_app_refresh_disabled_content": "Activa la actualización en segundo plano de la aplicación en Configuración > General > Actualización en segundo plano para usar la copia de seguridad en segundo plano.", @@ -521,19 +525,19 @@ "backup_controller_page_background_battery_info_title": "Optimizaciones de batería", "backup_controller_page_background_charging": "Solo mientras se carga", "backup_controller_page_background_configure_error": "Error al configurar el servicio en segundo plano", - "backup_controller_page_background_delay": "Retraso en la copia de seguridad de nuevos elementos: {}", - "backup_controller_page_background_description": "Activa el servicio en segundo plano para copiar automáticamente cualquier nuevos elementos sin necesidad de abrir la aplicación.", + "backup_controller_page_background_delay": "Retrasar la copia de seguridad de los nuevos elementos: {}", + "backup_controller_page_background_description": "Activa el servicio en segundo plano para copiar automáticamente cualquier nuevos elementos sin necesidad de abrir la aplicación", "backup_controller_page_background_is_off": "La copia de seguridad en segundo plano automática está desactivada", "backup_controller_page_background_is_on": "La copia de seguridad en segundo plano automática está activada", "backup_controller_page_background_turn_off": "Desactivar el servicio en segundo plano", "backup_controller_page_background_turn_on": "Activar el servicio en segundo plano", "backup_controller_page_background_wifi": "Solo en WiFi", "backup_controller_page_backup": "Copia de Seguridad", - "backup_controller_page_backup_selected": "Seleccionado:", + "backup_controller_page_backup_selected": "Seleccionado: ", "backup_controller_page_backup_sub": "Fotos y videos respaldados", "backup_controller_page_created": "Creado el: {}", "backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos elementos al servidor.", - "backup_controller_page_excluded": "Excluido:", + "backup_controller_page_excluded": "Excluido: ", "backup_controller_page_failed": "Fallidos ({})", "backup_controller_page_filename": "Nombre del archivo: {} [{}]", "backup_controller_page_id": "ID: {}", @@ -593,12 +597,12 @@ "camera_model": "Modelo de cámara", "cancel": "Cancelar", "cancel_search": "Cancelar búsqueda", - "canceled": "Canceled", + "canceled": "Cancelado", "cannot_merge_people": "No se pueden fusionar personas", "cannot_undo_this_action": "¡No puedes deshacer esta acción!", "cannot_update_the_description": "No se puede actualizar la descripción", "change_date": "Cambiar fecha", - "change_display_order": "Change display order", + "change_display_order": "Cambiar orden de visualización", "change_expiration_time": "Cambiar fecha de caducidad", "change_location": "Cambiar ubicación", "change_name": "Cambiar nombre", @@ -613,9 +617,9 @@ "change_your_password": "Cambia tu contraseña", "changed_visibility_successfully": "Visibilidad cambiada correctamente", "check_all": "Comprobar todo", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", + "check_corrupt_asset_backup": "Comprobar copias de seguridad de archivos corruptos", + "check_corrupt_asset_backup_button": "Realizar comprobación", + "check_corrupt_asset_backup_description": "Ejecutar esta comprobación solo por Wi-Fi y una vez que todos los archivos hayan sido respaldados. El procedimiento puede tardar unos minutos.", "check_logs": "Comprobar Registros", "choose_matching_people_to_merge": "Elija personas similares para fusionar", "city": "Ciudad", @@ -627,11 +631,11 @@ "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Introduzca contraseña", "client_cert_import": "Importar", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove_msg": "Client certificate is removed", - "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", + "client_cert_import_success_msg": "El certificado de cliente está importado", + "client_cert_invalid_msg": "Archivo de certificado no válido o contraseña incorrecta", + "client_cert_remove_msg": "El certificado de cliente se ha eliminado", + "client_cert_subtitle": "Solo se admite el formato PKCS12 (.p12, .pfx). La importación/eliminación de certificados solo está disponible antes de iniciar sesión", + "client_cert_title": "Certificado de cliente SSL", "clockwise": "En el sentido de las agujas del reloj", "close": "Cerrar", "collapse": "Agrupar", @@ -644,7 +648,7 @@ "comments_are_disabled": "Los comentarios están deshabilitados", "common_create_new_album": "Crear nuevo álbum", "common_server_error": "Por favor, verifica tu conexión de red, asegúrate de que el servidor esté accesible y las versiones de la aplicación y del servidor sean compatibles.", - "completed": "Completed", + "completed": "Completado", "confirm": "Confirmar", "confirm_admin_password": "Confirmar Contraseña de Administrador", "confirm_delete_face": "¿Estás seguro que deseas eliminar la cara de {name} del archivo?", @@ -660,7 +664,7 @@ "control_bottom_app_bar_delete_from_local": "Borrar del dispositivo", "control_bottom_app_bar_edit_location": "Editar ubicación", "control_bottom_app_bar_edit_time": "Editar fecha y hora", - "control_bottom_app_bar_share_link": "Share Link", + "control_bottom_app_bar_share_link": "Enlace para compartir", "control_bottom_app_bar_share_to": "Enviar", "control_bottom_app_bar_trash_from_immich": "Mover a la papelera", "copied_image_to_clipboard": "Imagen copiada al portapapeles.", @@ -695,7 +699,7 @@ "crop": "Recortar", "curated_object_page_title": "Objetos", "current_device": "Dispositivo actual", - "current_server_address": "Current server address", + "current_server_address": "Dirección actual del servidor", "custom_locale": "Configuración regional personalizada", "custom_locale_description": "Formatear fechas y números según el idioma y la región", "daily_title_text_date": "E dd, MMM", @@ -746,7 +750,7 @@ "direction": "Dirección", "disabled": "Deshabilitado", "disallow_edits": "Bloquear edición", - "discord": "", + "discord": "Discord", "discover": "Descubrir", "dismiss_all_errors": "Descartar todos los errores", "dismiss_error": "Descartar error", @@ -807,16 +811,16 @@ "editor_crop_tool_h2_aspect_ratios": "Proporciones del aspecto", "editor_crop_tool_h2_rotation": "Rotación", "email": "Correo", - "empty_folder": "This folder is empty", + "empty_folder": "Esta carpeta está vacía", "empty_trash": "Vaciar papelera", "empty_trash_confirmation": "¿Estás seguro de que quieres vaciar la papelera? Esto eliminará permanentemente todos los archivos de la basura de Immich.\n¡No puedes deshacer esta acción!", "enable": "Habilitar", "enabled": "Habilitado", "end_date": "Fecha final", - "enqueued": "Enqueued", + "enqueued": "Añadido a la cola", "enter_wifi_name": "Enter WiFi name", "error": "Error", - "error_change_sort_album": "Failed to change album sort order", + "error_change_sort_album": "No se pudo cambiar el orden de visualización del álbum", "error_delete_face": "Error al eliminar la cara del archivo", "error_loading_image": "Error al cargar la imagen", "error_saving_image": "Error: {}", @@ -953,15 +957,15 @@ "exif_bottom_sheet_location": "UBICACIÓN", "exif_bottom_sheet_people": "PERSONAS", "exif_bottom_sheet_person_add_person": "Añadir nombre", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", + "exif_bottom_sheet_person_age": "Antigüedad {}", + "exif_bottom_sheet_person_age_months": "Antigüedad {} meses", + "exif_bottom_sheet_person_age_year_months": "Antigüedad 1 año, {} meses", + "exif_bottom_sheet_person_age_years": "Antigüedad {}", "exit_slideshow": "Salir de la presentación", "expand_all": "Expandir todo", "experimental_settings_new_asset_list_subtitle": "Trabajo en progreso", "experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental", - "experimental_settings_subtitle": "Úsalo bajo tu responsabilidad", + "experimental_settings_subtitle": "¡Úsalo bajo tu propia responsabilidad!", "experimental_settings_title": "Experimental", "expire_after": "Expirar después de", "expired": "Caducado", @@ -973,12 +977,12 @@ "extension": "Extension", "external": "Externo", "external_libraries": "Bibliotecas Externas", - "external_network": "External network", - "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", + "external_network": "Red externa", + "external_network_sheet_info": "Cuando no estés conectado a la red WiFi preferida, la aplicación se conectará al servidor utilizando la primera de las siguientes URLs a la que pueda acceder, comenzando desde la parte superior de la lista hacia abajo", "face_unassigned": "Sin asignar", - "failed": "Failed", + "failed": "Fallido", "failed_to_load_assets": "Error al cargar los activos", - "failed_to_load_folder": "Failed to load folder", + "failed_to_load_folder": "No se pudo cargar la carpeta", "favorite": "Favorito", "favorite_or_unfavorite_photo": "Foto favorita o no favorita", "favorites": "Favoritos", @@ -992,21 +996,22 @@ "filetype": "Tipo de archivo", "filter": "Filtrar", "filter_people": "Filtrar personas", + "filter_places": "Filtrar lugares", "find_them_fast": "Encuéntrelos rápidamente por nombre con la búsqueda", "fix_incorrect_match": "Corregir coincidencia incorrecta", - "folder": "Folder", - "folder_not_found": "Folder not found", + "folder": "Carpeta", + "folder_not_found": "Carpeta no encontrada", "folders": "Carpetas", "folders_feature_description": "Explorar la vista de carpetas para las fotos y los videos en el sistema de archivos", "forward": "Reenviar", "general": "General", "get_help": "Solicitar ayuda", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "get_wifiname_error": "No se pudo obtener el nombre de la red Wi-Fi. Asegúrate de haber concedido los permisos necesarios y de estar conectado a una red Wi-Fi", "getting_started": "Comenzamos", "go_back": "Volver atrás", "go_to_folder": "Ir al directorio", "go_to_search": "Ir a búsqueda", - "grant_permission": "Grant permission", + "grant_permission": "Conceder permiso", "group_albums_by": "Agrupar albums por...", "group_country": "Agrupar por país", "group_no": "Sin agrupación", @@ -1031,7 +1036,7 @@ "hide_unnamed_people": "Ocultar personas anónimas", "home_page_add_to_album_conflicts": "{added} elementos agregados al álbum {album}.{failed} elementos ya existen en el álbum.", "home_page_add_to_album_err_local": "Aún no se pueden agregar elementos locales a álbumes, omitiendo", - "home_page_add_to_album_success": "{added} elementos agregados al álbum {album}. ", + "home_page_add_to_album_success": "Se añadieron {added} elementos al álbum {album}.", "home_page_album_err_partner": "Aún no se pueden agregar elementos a un álbum de un compañero, omitiendo", "home_page_archive_err_local": "Los elementos locales no pueden ser archivados, omitiendo", "home_page_archive_err_partner": "No se pueden archivar elementos de un compañero, omitiendo", @@ -1040,7 +1045,7 @@ "home_page_delete_remote_err_local": "Elementos locales en la selección de eliminación remota, omitiendo", "home_page_favorite_err_local": "Aún no se pueden archivar elementos locales, omitiendo", "home_page_favorite_err_partner": "Aún no se pueden marcar elementos de compañeros como favoritos, omitiendo", - "home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.", + "home_page_first_time_notice": "Si es la primera vez que usas la aplicación, asegúrate de elegir un álbum de copia de seguridad para que la línea de tiempo pueda mostrar fotos y vídeos en él", "home_page_share_err_local": "No se pueden compartir elementos locales a través de un enlace, omitiendo", "home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultáneamente, omitiendo", "host": "Host", @@ -1118,9 +1123,9 @@ "loading": "Cargando", "loading_search_results_failed": "Error al cargar los resultados de la búsqueda", "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", - "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", + "local_network_sheet_info": "La aplicación se conectará al servidor a través de esta URL cuando utilice la red Wi-Fi especificada", + "location_permission": "Permiso de ubicación", + "location_permission_content": "Para usar la función de cambio automático, Immich necesita permiso de ubicación precisa para poder leer el nombre de la red WiFi actual", "location_picker_choose_on_map": "Elegir en el mapa", "location_picker_latitude_error": "Introduce una latitud válida", "location_picker_latitude_hint": "Introduce tu latitud aquí", @@ -1145,7 +1150,7 @@ "login_form_failed_get_oauth_server_config": "Error al iniciar sesión con OAuth, verifica la URL del servidor", "login_form_failed_get_oauth_server_disable": "La función de OAuth no está disponible en este servidor", "login_form_failed_login": "Error al iniciar sesión, comprueba la URL del servidor, el correo electrónico y la contraseña", - "login_form_handshake_exception": "Hubo un error de verificación del certificado del servidor. Activa el soporte para certificados autofirmados en las preferencias si estás usando un certificado autofirmado", + "login_form_handshake_exception": "Hubo una excepción de handshake con el servidor. Activa la compatibilidad con certificados autofirmados en la configuración si estás utilizando un certificado autofirmado.", "login_form_password_hint": "contraseña", "login_form_save_login": "Mantener la sesión iniciada", "login_form_server_empty": "Agrega la URL del servidor.", @@ -1222,7 +1227,7 @@ "more": "Mas", "moved_to_trash": "Movido a la papelera", "multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha del archivo(s) de solo lectura, omitiendo", - "multiselect_grid_edit_gps_err_read_only": "No se puede cambiar la localización de archivos de solo lectura. Saltando.", + "multiselect_grid_edit_gps_err_read_only": "No se puede editar la ubicación de activos de solo lectura, omitiendo", "mute_memories": "Silenciar Recuerdos", "my_albums": "Mis albums", "name": "Nombre", @@ -1257,7 +1262,7 @@ "no_results_description": "Pruebe con un sinónimo o una palabra clave más general", "no_shared_albums_message": "Crea un álbum para compartir fotos y vídeos con personas de tu red", "not_in_any_album": "Sin álbum", - "not_selected": "Not selected", + "not_selected": "No seleccionado", "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar la etiqueta de almacenamiento a los archivos cargados previamente, ejecute el", "notes": "Notas", "notification_permission_dialog_content": "Para activar las notificaciones, ve a Configuración y selecciona permitir.", @@ -1282,6 +1287,7 @@ "onboarding_welcome_user": "Bienvenido, {user}", "online": "En línea", "only_favorites": "Solo favoritos", + "open": "Abierto", "open_in_map_view": "Abrir en la vista del mapa", "open_in_openstreetmap": "Abrir en OpenStreetMap", "open_the_search_filters": "Abre los filtros de búsqueda", @@ -1302,10 +1308,10 @@ "partner_list_view_all": "Ver todas", "partner_page_empty_message": "Tus fotos aún no se han compartido con ningún compañero.", "partner_page_no_more_users": "No hay más usuarios para agregar", - "partner_page_partner_add_failed": "Compañero no pudo ser agregado ", + "partner_page_partner_add_failed": "No se pudo añadir el socio", "partner_page_select_partner": "Seleccionar compañero", "partner_page_shared_to_title": "Compartido con", - "partner_page_stop_sharing_content": "{} ya no podrá acceder a tus fotos", + "partner_page_stop_sharing_content": "{} ya no podrá acceder a tus fotos.", "partner_sharing": "Compartir con invitados", "partners": "Invitados", "password": "Contraseña", @@ -1509,8 +1515,8 @@ "search_filter_date_interval": "{start} al {end}", "search_filter_date_title": "Selecciona un intervalo de fechas", "search_filter_display_option_not_in_album": "No en álbum", - "search_filter_display_options": "Display Options", - "search_filter_filename": "Search by file name", + "search_filter_display_options": "Opciones de visualización", + "search_filter_filename": "Buscar por nombre de archivo", "search_filter_location": "Ubicación", "search_filter_location_title": "Seleccionar una ubicación", "search_filter_media_type": "Tipo de archivo", @@ -1518,10 +1524,10 @@ "search_filter_people_title": "Seleccionar personas", "search_for": "Buscar", "search_for_existing_person": "Buscar persona existente", - "search_no_more_result": "No more results", + "search_no_more_result": "No hay más resultados", "search_no_people": "Ninguna persona", "search_no_people_named": "Ninguna persona llamada \"{name}\"", - "search_no_result": "No results found, try a different search term or combination", + "search_no_result": "No se encontraron resultados, prueba con un término o combinación de búsqueda diferente", "search_options": "Opciones de búsqueda", "search_page_categories": "Categorías", "search_page_motion_photos": "Foto en Movimiento", @@ -1567,7 +1573,7 @@ "selected_count": "{count, plural, one {# seleccionado} other {# seleccionados}}", "send_message": "Enviar mensaje", "send_welcome_email": "Enviar correo de bienvenida", - "server_endpoint": "Server Endpoint", + "server_endpoint": "Punto final del servidor", "server_info_box_app_version": "Versión de la Aplicación", "server_info_box_server_url": "URL del servidor", "server_offline": "Servidor desconectado", @@ -1602,8 +1608,8 @@ "setting_notifications_total_progress_subtitle": "Progreso general de subida (elementos completados/total)", "setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano", "setting_video_viewer_looping_title": "Bucle", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", + "setting_video_viewer_original_video_subtitle": "Al reproducir un video en streaming desde el servidor, reproducir el original incluso cuando haya una transcodificación disponible. Puede causar buffering. Los videos disponibles localmente se reproducen en calidad original independientemente de esta configuración.", + "setting_video_viewer_original_video_title": "Forzar vídeo original", "settings": "Ajustes", "settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste", "settings_saved": "Ajustes guardados", @@ -1623,7 +1629,7 @@ "shared_by_user": "Compartido por {user}", "shared_by_you": "Compartido por ti", "shared_from_partner": "Fotos de {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", + "shared_intent_upload_button_progress_text": "{} / {} Cargados", "shared_link_app_bar_title": "Enlaces compartidos", "shared_link_clipboard_copied_massage": "Copiado al portapapeles", "shared_link_clipboard_text": "Enlace: {}\nContraseña: {}", @@ -1734,7 +1740,7 @@ "sync": "Sincronizar", "sync_albums": "Sincronizar álbumes", "sync_albums_manual_subtitle": "Sincroniza todos los videos y fotos subidos con los álbumes seleccionados a respaldar", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", + "sync_upload_album_setting_subtitle": "Crea y sube tus fotos y videos a los álbumes seleccionados en Immich", "tag": "Etiqueta", "tag_assets": "Etiquetar activos", "tag_created": "Etiqueta creada: {tag}", @@ -1750,12 +1756,12 @@ "theme_selection_description": "Establece el tema automáticamente como \"claro\" u \"oscuro\" según las preferencias del sistema/navegador", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamiento en las miniaturas de los archivos", "theme_setting_asset_list_tiles_per_row_title": "Número de elementos por fila ({})", - "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", - "theme_setting_colorful_interface_title": "Colorful interface", + "theme_setting_colorful_interface_subtitle": "Aplicar el color primario a las superficies de fondo.", + "theme_setting_colorful_interface_title": "Color de Interfaz", "theme_setting_image_viewer_quality_subtitle": "Ajustar la calidad del visor de detalles de imágenes", "theme_setting_image_viewer_quality_title": "Calidad del visor de imágenes", - "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", - "theme_setting_primary_color_title": "Primary color", + "theme_setting_primary_color_subtitle": "Elige un color para las acciones principales y los acentos.", + "theme_setting_primary_color_title": "Color primario", "theme_setting_system_primary_color_title": "Usar color del sistema", "theme_setting_system_theme_switch": "Automático (seguir ajuste del sistema)", "theme_setting_theme_subtitle": "Elige la configuración del tema de la aplicación", @@ -1826,11 +1832,11 @@ "upload_status_errors": "Errores", "upload_status_uploaded": "Subido", "upload_success": "Carga realizada correctamente, actualice la página para ver los nuevos recursos de carga.", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", + "upload_to_immich": "Subir a Immich ({})", + "uploading": "Cargando", "url": "URL", "usage": "Uso", - "use_current_connection": "use current connection", + "use_current_connection": "Usar conexión actual", "use_custom_date_range": "Usa un intervalo de fechas personalizado", "user": "Usuario", "user_id": "ID de usuario", @@ -1845,7 +1851,7 @@ "users": "Usuarios", "utilities": "Utilidades", "validate": "Validar", - "validate_endpoint_error": "Please enter a valid URL", + "validate_endpoint_error": "Por favor, introduce una URL válida", "variables": "Variables", "version": "Versión", "version_announcement_closing": "Tu amigo, Alex", @@ -1888,6 +1894,6 @@ "years_ago": "Hace {years, plural, one {# año} other {# años}}", "yes": "Sí", "you_dont_have_any_shared_links": "No tienes ningún enlace compartido", - "your_wifi_name": "Your WiFi name", + "your_wifi_name": "El nombre de tu WiFi", "zoom_image": "Acercar Imagen" } diff --git a/i18n/et.json b/i18n/et.json index 673d2ea63a..98b829bc97 100644 --- a/i18n/et.json +++ b/i18n/et.json @@ -4,6 +4,7 @@ "account_settings": "Konto seaded", "acknowledge": "Sain aru", "action": "Tegevus", + "action_common_update": "Uuenda", "actions": "Tegevused", "active": "Aktiivne", "activity": "Aktiivsus", @@ -13,6 +14,7 @@ "add_a_location": "Lisa asukoht", "add_a_name": "Lisa nimi", "add_a_title": "Lisa pealkiri", + "add_endpoint": "Lisa lõpp-punkt", "add_exclusion_pattern": "Lisa välistamismuster", "add_import_path": "Lisa imporditee", "add_location": "Lisa asukoht", @@ -22,6 +24,8 @@ "add_photos": "Lisa fotosid", "add_to": "Lisa kohta…", "add_to_album": "Lisa albumisse", + "add_to_album_bottom_sheet_added": "Lisatud albumisse {album}", + "add_to_album_bottom_sheet_already_exists": "On juba albumis {album}", "add_to_shared_album": "Lisa jagatud albumisse", "add_url": "Lisa URL", "added_to_archive": "Lisatud arhiivi", @@ -35,11 +39,11 @@ "authentication_settings_disable_all": "Kas oled kindel, et soovid kõik sisselogimismeetodid välja lülitada? Sisselogimine lülitatakse täielikult välja.", "authentication_settings_reenable": "Et taas lubada, kasuta serveri käsku.", "background_task_job": "Tausttegumid", - "backup_database": "Varunda andmebaas", - "backup_database_enable_description": "Luba andmebaasi varundamine", - "backup_keep_last_amount": "Varukoopiate arv, mida alles hoida", - "backup_settings": "Varundamise seaded", - "backup_settings_description": "Halda andmebaasi varundamise seadeid", + "backup_database": "Loo andmebaasi tõmmis", + "backup_database_enable_description": "Luba andmebaasi tõmmised", + "backup_keep_last_amount": "Eelmiste tõmmiste arv, mida alles hoida", + "backup_settings": "Andmebaasi tõmmiste seaded", + "backup_settings_description": "Halda andmebaasi tõmmiste seadeid. Märkus: Neid tööteid ei jälgita ning ebaõnnestumisest ei hoiatata.", "check_all": "Märgi kõik", "cleanup": "Koristus", "cleared_jobs": "Tööted eemaldatud: {job}", @@ -367,6 +371,10 @@ "admin_password": "Administraatori parool", "administration": "Administratsioon", "advanced": "Täpsemad valikud", + "advanced_settings_log_level_title": "Logimistase: {}", + "advanced_settings_proxy_headers_title": "Vaheserveri päised", + "advanced_settings_self_signed_ssl_title": "Luba endasigneeritud SSL-sertifikaadid", + "advanced_settings_troubleshooting_title": "Tõrkeotsing", "age_months": "Vanus {months, plural, one {# kuu} other {# kuud}}", "age_year_months": "Vanus 1 aasta, {months, plural, one {# kuu} other {# kuud}}", "age_years": "{years, plural, other {Vanus #}}", @@ -375,6 +383,8 @@ "album_cover_updated": "Albumi kaanepilt muudetud", "album_delete_confirmation": "Kas oled kindel, et soovid albumi {album} kustutada?", "album_delete_confirmation_description": "Kui see album on jagatud, ei pääse teised kasutajad sellele enam ligi.", + "album_info_card_backup_album_excluded": "VÄLJA JÄETUD", + "album_info_card_backup_album_included": "LISATUD", "album_info_updated": "Albumi info muudetud", "album_leave": "Lahku albumist?", "album_leave_confirmation": "Kas oled kindel, et soovid albumist {album} lahkuda?", @@ -383,10 +393,21 @@ "album_remove_user": "Eemalda kasutaja?", "album_remove_user_confirmation": "Kas oled kindel, et soovid kasutaja {user} eemaldada?", "album_share_no_users": "Paistab, et oled seda albumit kõikide kasutajatega jaganud, või pole ühtegi kasutajat, kellega jagada.", + "album_thumbnail_card_item": "1 üksus", + "album_thumbnail_card_items": "{} üksust", + "album_thumbnail_card_shared": " · Jagatud", + "album_thumbnail_shared_by": "Jagas {}", "album_updated": "Album muudetud", "album_updated_setting_description": "Saa teavitus e-posti teel, kui jagatud albumis on uusi üksuseid", "album_user_left": "Lahkutud albumist {album}", "album_user_removed": "Kasutaja {user} eemaldatud", + "album_viewer_appbar_delete_confirm": "Kas oled kindel, et soovid selle albumi oma kontolt kustutada?", + "album_viewer_appbar_share_err_delete": "Albumi kustutamine ebaõnnestus", + "album_viewer_appbar_share_err_leave": "Albumist lahkumine ebaõnnestus", + "album_viewer_appbar_share_err_remove": "Üksuste albumist eemaldamisel tekkis probleeme", + "album_viewer_appbar_share_err_title": "Albumi pealkirja muutmine ebaõnnestus", + "album_viewer_appbar_share_leave": "Lahku albumist", + "album_viewer_page_share_add_users": "Lisa kasutajaid", "album_with_link_access": "Luba kõigil, kellel on link, näha selle albumi fotosid ja isikuid.", "albums": "Albumid", "albums_count": "{count, plural, one {{count, number} album} other {{count, number} albumit}}", @@ -404,23 +425,36 @@ "api_key_description": "Seda väärtust kuvatakse ainult üks kord. Kopeeri see enne akna sulgemist.", "api_key_empty": "Su API võtme nimi ei tohiks olla tühi", "api_keys": "API võtmed", + "app_bar_signout_dialog_content": "Kas oled kindel, et soovid välja logida?", + "app_bar_signout_dialog_ok": "Jah", + "app_bar_signout_dialog_title": "Logi välja", "app_settings": "Rakenduse seaded", "appears_in": "Albumid", "archive": "Arhiiv", "archive_or_unarchive_photo": "Arhiveeri või taasta foto", + "archive_page_no_archived_assets": "Arhiveeritud üksuseid ei leitud", "archive_size": "Arhiivi suurus", "archive_size_description": "Seadista arhiivi suurus allalaadimiseks (GiB)", + "archived": "Arhiveeritud", "archived_count": "{count, plural, other {# arhiveeritud}}", "are_these_the_same_person": "Kas need on sama isik?", "are_you_sure_to_do_this": "Kas oled kindel, et soovid seda teha?", + "asset_action_delete_err_read_only": "Kirjutuskaitstud üksuseid ei saa kustutada, jätan vahele", "asset_added_to_album": "Lisatud albumisse", "asset_adding_to_album": "Albumisse lisamine…", "asset_description_updated": "Üksuse kirjeldus on muudetud", "asset_filename_is_offline": "Üksus {filename} ei ole kättesaadav", "asset_has_unassigned_faces": "Üksusel on seostamata nägusid", "asset_hashing": "Räsimine…", + "asset_list_group_by_sub_title": "Grupeeri", + "asset_list_layout_settings_dynamic_layout_title": "Dünaamiline asetus", + "asset_list_layout_settings_group_automatically": "Automaatne", + "asset_list_layout_settings_group_by": "Grupeeri üksused", + "asset_list_layout_settings_group_by_month_day": "Kuu + päev", + "asset_list_layout_sub_title": "Asetus", "asset_offline": "Üksus pole kättesaadav", "asset_offline_description": "Seda välise kogu üksust ei leitud kettalt. Abi saamiseks palun võta ühendust oma Immich'i administraatoriga.", + "asset_restored_successfully": "Üksus edukalt taastatud", "asset_skipped": "Vahele jäetud", "asset_skipped_in_trash": "Prügikastis", "asset_uploaded": "Üleslaaditud", @@ -438,8 +472,26 @@ "assets_trashed_count": "{count, plural, one {# üksus} other {# üksust}} liigutatud prügikasti", "assets_were_part_of_album_count": "{count, plural, one {Üksus oli} other {Üksused olid}} juba osa albumist", "authorized_devices": "Autoriseeritud seadmed", + "automatic_endpoint_switching_subtitle": "Ühendu lokaalselt üle valitud WiFi-võrgu, kui see on saadaval, ja kasuta mujal alternatiivseid ühendusi", "back": "Tagasi", "back_close_deselect": "Tagasi, sulge või tühista valik", + "backup_album_selection_page_select_albums": "Vali albumid", + "backup_album_selection_page_selection_info": "Valiku info", + "backup_album_selection_page_total_assets": "Unikaalseid üksuseid kokku", + "backup_all": "Kõik", + "backup_background_service_default_notification": "Uute üksuste kontrollimine…", + "backup_background_service_error_title": "Varundamise viga", + "backup_controller_page_background_battery_info_ok": "OK", + "backup_controller_page_background_wifi": "Ainult WiFi-võrgus", + "backup_controller_page_backup_sub": "Varundatud fotod ja videod", + "backup_controller_page_desc_backup": "Lülita sisse esiplaanil varundamine, et rakenduse avamisel uued üksused automaatselt serverisse üles laadida.", + "backup_controller_page_to_backup": "Albumid, mida varundada", + "backup_controller_page_total_sub": "Kõik unikaalsed fotod ja videod valitud albumitest", + "backup_err_only_album": "Ei saa ainsat albumit eemaldada", + "backup_info_card_assets": "üksused", + "backup_manual_cancelled": "Tühistatud", + "backup_manual_title": "Üleslaadimise staatus", + "backup_setting_subtitle": "Halda taustal ja esiplaanil üleslaadimise seadeid", "backward": "Tagasi", "birthdate_saved": "Sünnikuupäev salvestatud", "birthdate_set_description": "Sünnikuupäeva kasutatakse isiku vanuse arvutamiseks foto tegemise hetkel.", @@ -451,11 +503,14 @@ "bulk_keep_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} alles jätta? Sellega märgitakse kõik duplikaadigrupid lahendatuks ilma midagi kustutamata.", "bulk_trash_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} masskustutada? Sellega jäetakse alles iga grupi suurim üksus ning duplikaadid liigutatakse prügikasti.", "buy": "Osta Immich", + "cache_settings_clear_cache_button": "Tühjenda puhver", + "cache_settings_statistics_title": "Puhvri kasutus", "camera": "Kaamera", "camera_brand": "Kaamera mark", "camera_model": "Kaamera mudel", "cancel": "Katkesta", "cancel_search": "Katkesta otsing", + "canceled": "Tühistatud", "cannot_merge_people": "Ei saa isikuid ühendada", "cannot_undo_this_action": "Sa ei saa seda tagasi võtta!", "cannot_update_the_description": "Kirjelduse muutmine ebaõnnestus", @@ -466,6 +521,10 @@ "change_name_successfully": "Nimi edukalt muudetud", "change_password": "Parooli muutmine", "change_password_description": "See on su esimene kord süsteemi siseneda, või on tehtud taotlus parooli muutmiseks. Palun sisesta allpool uus parool.", + "change_password_form_confirm_password": "Kinnita parool", + "change_password_form_new_password": "Uus parool", + "change_password_form_password_mismatch": "Paroolid ei klapi", + "change_password_form_reenter_new_password": "Korda uut parooli", "change_your_password": "Muuda oma parooli", "changed_visibility_successfully": "Nähtavus muudetud", "check_all": "Märgi kõik", @@ -477,6 +536,14 @@ "clear_all_recent_searches": "Tühjenda hiljutised otsingud", "clear_message": "Tühjenda sõnum", "clear_value": "Tühjenda väärtus", + "client_cert_dialog_msg_confirm": "OK", + "client_cert_enter_password": "Sisesta parool", + "client_cert_import": "Impordi", + "client_cert_import_success_msg": "Klientsertifikaat on imporditud", + "client_cert_invalid_msg": "Vigane sertifikaadi fail või vale parool", + "client_cert_remove_msg": "Klientsertifikaat on eemaldatud", + "client_cert_subtitle": "Toetab ainult PKCS12 (.p12, .pfx) formaati. Sertifikaadi importimine/eemaldamine on saadaval ainult enne sisselogimist", + "client_cert_title": "SSL klientsertifikaat", "clockwise": "Päripäeva", "close": "Sulge", "collapse": "Peida", @@ -487,6 +554,8 @@ "comment_options": "Kommentaari valikud", "comments_and_likes": "Kommentaarid ja meeldimised", "comments_are_disabled": "Kommentaarid on keelatud", + "common_create_new_album": "Lisa uus album", + "completed": "Lõpetatud", "confirm": "Kinnita", "confirm_admin_password": "Kinnita administraatori parool", "confirm_delete_face": "Kas oled kindel, et soovid isiku {name} näo üksuselt kustutada?", @@ -496,6 +565,10 @@ "contain": "Mahuta ära", "context": "Kontekst", "continue": "Jätka", + "control_bottom_app_bar_create_new_album": "Lisa uus album", + "control_bottom_app_bar_delete_from_local": "Kustuta seadmest", + "control_bottom_app_bar_edit_location": "Muuda asukohta", + "control_bottom_app_bar_edit_time": "Muuda kuupäeva ja aega", "copied_image_to_clipboard": "Pilt kopeeritud lõikelauale.", "copied_to_clipboard": "Kopeeritud lõikelauale!", "copy_error": "Kopeeri viga", @@ -517,6 +590,7 @@ "create_new_person": "Lisa uus isik", "create_new_person_hint": "Seosta valitud üksused uue isikuga", "create_new_user": "Lisa uus kasutaja", + "create_shared_album_page_share_select_photos": "Vali fotod", "create_tag": "Lisa silt", "create_tag_description": "Lisa uus silt. Pesastatud siltide jaoks sisesta täielik tee koos kaldkriipsudega.", "create_user": "Lisa kasutaja", @@ -541,19 +615,23 @@ "delete": "Kustuta", "delete_album": "Kustuta album", "delete_api_key_prompt": "Kas oled kindel, et soovid selle API võtme kustutada?", + "delete_dialog_title": "Kustuta jäädavalt", "delete_duplicates_confirmation": "Kas oled kindel, et soovid need duplikaadid jäädavalt kustutada?", "delete_face": "Kustuta nägu", "delete_key": "Kustuta võti", "delete_library": "Kustuta kogu", "delete_link": "Kustuta link", + "delete_local_dialog_ok_backed_up_only": "Kustuta ainult varundatud", "delete_others": "Kustuta teised", "delete_shared_link": "Kustuta jagatud link", + "delete_shared_link_dialog_title": "Kustuta jagatud link", "delete_tag": "Kustuta silt", "delete_tag_confirmation_prompt": "Kas oled kindel, et soovid sildi {tagName} kustutada?", "delete_user": "Kustuta kasutaja", "deleted_shared_link": "Jagatud link kustutatud", "deletes_missing_assets": "Kustutab üksused, mis on kettalt puudu", "description": "Kirjeldus", + "description_input_hint_text": "Lisa kirjeldus...", "details": "Üksikasjad", "direction": "Suund", "disabled": "Välja lülitatud", @@ -570,10 +648,20 @@ "documentation": "Dokumentatsioon", "done": "Tehtud", "download": "Laadi alla", + "download_canceled": "Allalaadimine katkestatud", + "download_complete": "Allalaadimine lõpetatud", + "download_enqueue": "Allalaadimine ootel", + "download_error": "Allalaadimise viga", + "download_failed": "Allalaadimine ebaõnnestus", + "download_finished": "Allalaadimine lõpetatud", "download_include_embedded_motion_videos": "Manustatud videod", "download_include_embedded_motion_videos_description": "Lisa liikuvatesse fotodesse manustatud videod eraldi failidena", + "download_paused": "Allalaadimine peatatud", "download_settings": "Allalaadimine", "download_settings_description": "Halda üksuste allalaadimise seadeid", + "download_started": "Allalaadimine alustatud", + "download_sucess": "Allalaadimine õnnestus", + "download_sucess_android": "Meediumid laaditi alla kataloogi DCIM/Immich", "downloading": "Allalaadimine", "downloading_asset_filename": "Üksuse {filename} allalaadimine", "drop_files_to_upload": "Failide üleslaadimiseks sikuta need ükskõik kuhu", @@ -592,6 +680,7 @@ "edit_key": "Muuda võtit", "edit_link": "Muuda linki", "edit_location": "Muuda asukohta", + "edit_location_dialog_title": "Asukoht", "edit_name": "Muuda nime", "edit_people": "Muuda isikuid", "edit_tag": "Muuda silti", @@ -604,12 +693,15 @@ "editor_crop_tool_h2_aspect_ratios": "Kuvasuhted", "editor_crop_tool_h2_rotation": "Pööre", "email": "E-post", + "empty_folder": "See kaust on tühi", "empty_trash": "Tühjenda prügikast", "empty_trash_confirmation": "Kas oled kindel, et soovid prügikasti tühjendada? See eemaldab kõik seal olevad üksused Immich'ist jäädavalt.\nSeda tegevust ei saa tagasi võtta!", "enable": "Luba", "enabled": "Lubatud", "end_date": "Lõppkuupäev", + "enter_wifi_name": "Sisesta WiFi-võrgu nimi", "error": "Viga", + "error_change_sort_album": "Albumi sorteerimisjärjestuse muutmine ebaõnnestus", "error_delete_face": "Viga näo kustutamisel", "error_loading_image": "Viga pildi laadimisel", "error_title": "Viga - midagi läks valesti", @@ -740,8 +832,14 @@ "unable_to_upload_file": "Faili üleslaadimine ebaõnnestus" }, "exif": "Exif", + "exif_bottom_sheet_description": "Lisa kirjeldus...", + "exif_bottom_sheet_details": "ÜKSIKASJAD", + "exif_bottom_sheet_location": "ASUKOHT", + "exif_bottom_sheet_people": "ISIKUD", + "exif_bottom_sheet_person_add_person": "Lisa nimi", "exit_slideshow": "Sulge slaidiesitlus", "expand_all": "Näita kõik", + "experimental_settings_title": "Eksperimentaalne", "expire_after": "Aegub", "expired": "Aegunud", "expires_date": "Aegub {date}", @@ -752,6 +850,7 @@ "extension": "Laiend", "external": "Väline", "external_libraries": "Välised kogud", + "external_network_sheet_info": "Kui seade ei ole eelistatud WiFi-võrgus, ühendub rakendus serveriga allolevatest URL-idest esimese kättesaadava kaudu, alustades ülevalt", "face_unassigned": "Seostamata", "failed_to_load_assets": "Üksuste laadimine ebaõnnestus", "favorite": "Lemmik", @@ -767,6 +866,8 @@ "filter_people": "Filtreeri isikuid", "find_them_fast": "Leia teda kiiresti nime järgi otsides", "fix_incorrect_match": "Paranda ebaõige vaste", + "folder": "Kaust", + "folder_not_found": "Kausta ei leitud", "folders": "Kaustad", "folders_feature_description": "Kaustavaate abil failisüsteemis olevate fotode ja videote sirvimine", "forward": "Edasi", @@ -783,6 +884,10 @@ "group_places_by": "Grupeeri kohad...", "group_year": "Grupeeri aasta kaupa", "has_quota": "On kvoot", + "header_settings_add_header_tip": "Lisa päis", + "header_settings_field_validator_msg": "Väärtus ei saa olla tühi", + "header_settings_header_name_input": "Päise nimi", + "header_settings_header_value_input": "Päise väärtus", "hi_user": "Tere {name} ({email})", "hide_all_people": "Peida kõik isikud", "hide_gallery": "Peida galerii", @@ -790,8 +895,20 @@ "hide_password": "Peida parool", "hide_person": "Peida isik", "hide_unnamed_people": "Peida nimetud isikud", + "home_page_add_to_album_conflicts": "{added} üksust lisati albumisse {album}. {failed} üksust oli juba albumis.", + "home_page_add_to_album_err_local": "Lokaalseid üksuseid ei saa veel albumisse lisada, jätan vahele", + "home_page_add_to_album_success": "{added} üksust lisati albumisse {album}.", + "home_page_album_err_partner": "Partneri üksuseid ei saa veel albumisse lisada, jätan vahele", + "home_page_archive_err_local": "Lokaalseid üksuseid ei saa veel arhiveerida, jätan vahele", + "home_page_archive_err_partner": "Partneri üksuseid ei saa arhiveerida, jätan vahele", + "home_page_building_timeline": "Ajajoone koostamine", + "home_page_delete_err_partner": "Partneri üksuseid ei saa kustutada, jätan vahele", + "home_page_favorite_err_local": "Lokaalseid üksuseid ei saa lemmikuks märkida, jätan vahele", + "home_page_favorite_err_partner": "Partneri üksuseid ei saa lemmikuks märkida, jätan vahele", + "home_page_share_err_local": "Lokaalseid üksuseid ei saa lingiga jagada, jätan vahele", "host": "Host", "hour": "Tund", + "ignore_icloud_photos": "Ignoreeri iCloud fotosid", "image": "Pilt", "image_alt_text_date": "{isVideo, select, true {Video} other {Pilt}} tehtud {date}", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} koos isikuga {person1}", @@ -803,6 +920,8 @@ "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos isikutega {person1} ja {person2}", "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos isikutega {person1}, {person2} ja {person3}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos {person1}, {person2} ja veel {additionalCount, number} isikuga", + "image_viewer_page_state_provider_download_started": "Allalaadimine alustatud", + "image_viewer_page_state_provider_download_success": "Allalaadimine õnnestus", "immich_logo": "Immich'i logo", "immich_web_interface": "Immich'i veebiliides", "import_from_json": "Impordi JSON-formaadist", @@ -821,6 +940,8 @@ "night_at_midnight": "Iga päev keskööl", "night_at_twoam": "Iga öö kell 2" }, + "invalid_date": "Vigane kuupäev", + "invalid_date_format": "Vigane kuupäevaformaat", "invite_people": "Kutsu inimesi", "invite_to_album": "Kutsu albumisse", "items_count": "{count, plural, one {# üksus} other {# üksust}}", @@ -841,6 +962,9 @@ "level": "Tase", "library": "Kogu", "library_options": "Kogu seaded", + "library_page_new_album": "Uus album", + "library_page_sort_asset_count": "Üksuste arv", + "library_page_sort_title": "Albumi pealkiri", "light": "Hele", "like_deleted": "Meeldimine kustutatud", "link_motion_video": "Lingi liikuv video", @@ -850,12 +974,24 @@ "list": "Loend", "loading": "Laadimine", "loading_search_results_failed": "Otsitulemuste laadimine ebaõnnestus", + "local_network_sheet_info": "Rakendus ühendub valitud Wi-Fi võrgus olles serveriga selle URL-i kaudu", + "location_permission_content": "Automaatseks ümberlülitumiseks vajab Immich täpse asukoha luba, et saaks lugeda aktiivse WiFi-võrgu nime", + "location_picker_choose_on_map": "Vali kaardil", "log_out": "Logi välja", "log_out_all_devices": "Logi kõigist seadmetest välja", "logged_out_all_devices": "Kõigist seadmetest välja logitud", "logged_out_device": "Seadmest välja logitud", "login": "Logi sisse", + "login_form_back_button_text": "Tagasi", + "login_form_email_hint": "sinunimi@email.com", + "login_form_endpoint_hint": "http://serveri-ip:port", + "login_form_endpoint_url": "Serveri lõpp-punkti URL", + "login_form_err_http": "Palun täpsusta http:// või https://", + "login_form_err_invalid_email": "Vigane e-posti aadress", + "login_form_err_invalid_url": "Vigane URL", + "login_form_password_hint": "parool", "login_has_been_disabled": "Sisselogimine on keelatud.", + "login_password_changed_success": "Parool edukalt uuendatud", "logout_all_device_confirmation": "Kas oled kindel, et soovid kõigist seadmetest välja logida?", "logout_this_device_confirmation": "Kas oled kindel, et soovid sellest seadmest välja logida?", "longitude": "Pikkuskraad", @@ -876,10 +1012,14 @@ "map_marker_for_images": "Kaardimarker kohas {city}, {country} tehtud piltide jaoks", "map_marker_with_image": "Kaardimarker pildiga", "map_settings": "Kaardi seaded", + "map_settings_date_range_option_day": "Viimased 24 tundi", + "map_settings_date_range_option_year": "Viimane aasta", + "map_settings_dialog_title": "Kaardi seaded", "matches": "Ühtivad failid", - "media_type": "Meedia tüüp", + "media_type": "Meediumi tüüp", "memories": "Mälestused", "memories_setting_description": "Halda, mida sa oma mälestustes näed", + "memories_year_ago": "Aasta tagasi", "memory": "Mälestus", "memory_lane_title": "Mälestus {title}", "menu": "Menüü", @@ -896,10 +1036,14 @@ "month": "Kuu", "more": "Rohkem", "moved_to_trash": "Liigutatud prügikasti", + "multiselect_grid_edit_date_time_err_read_only": "Kirjutuskaitsega üksus(t)e kuupäeva ei saa muuta, jätan vahele", + "multiselect_grid_edit_gps_err_read_only": "Kirjutuskaitsega üksus(t)e asukohta ei saa muuta, jätan vahele", "mute_memories": "Vaigista mälestused", "my_albums": "Minu albumid", "name": "Nimi", "name_or_nickname": "Nimi või hüüdnimi", + "networking_settings": "Võrguühendus", + "networking_subtitle": "Halda serveri lõpp-punkti seadeid", "never": "Mitte kunagi", "new_album": "Uus album", "new_api_key": "Uus API võti", @@ -962,6 +1106,9 @@ "partner_can_access": "{partner} pääseb ligi", "partner_can_access_assets": "Kõik su fotod ja videod, välja arvatud arhiveeritud ja kustutatud", "partner_can_access_location": "Asukohad, kus su fotod tehti", + "partner_list_view_all": "Vaata kõiki", + "partner_page_partner_add_failed": "Partneri lisamine ebaõnnestus", + "partner_page_select_partner": "Vali partner", "partner_sharing": "Partneriga jagamine", "partners": "Partnerid", "password": "Parool", @@ -990,6 +1137,7 @@ "permanently_delete_assets_prompt": "Kas oled kindel, et soovid {count, plural, one {selle üksuse} other {need # üksust}} jäädavalt kustutada? Sellega eemaldatakse {count, plural, one {see} other {need}} ka oma albumi(te)st.", "permanently_deleted_asset": "Üksus jäädavalt kustutatud", "permanently_deleted_assets_count": "{count, plural, one {# üksus} other {# üksust}} jäädavalt kustutatud", + "permission_onboarding_back": "Tagasi", "person": "Isik", "person_birthdate": "Sündinud {date}", "person_hidden": "{name}{hidden, select, true { (peidetud)} other {}}", @@ -1007,6 +1155,7 @@ "play_motion_photo": "Esita liikuv foto", "play_or_pause_video": "Esita või peata video", "port": "Port", + "preferences_settings_title": "Eelistused", "preset": "Eelseadistus", "preview": "Eelvaade", "previous": "Eelmine", @@ -1014,6 +1163,8 @@ "previous_or_next_photo": "Eelmine või järgmine foto", "primary": "Peamine", "privacy": "Privaatsus", + "profile_drawer_app_logs": "Logid", + "profile_drawer_github": "GitHub", "profile_image_of_user": "Kasutaja {user} profiilipilt", "profile_picture_set": "Profiilipilt määratud.", "public_album": "Avalik album", @@ -1063,6 +1214,8 @@ "recent": "Hiljutine", "recent-albums": "Hiljutised albumid", "recent_searches": "Hiljutised otsingud", + "recently_added": "Hiljuti lisatud", + "recently_added_page_title": "Hiljuti lisatud", "refresh": "Värskenda", "refresh_encoded_videos": "Värskenda kodeeritud videod", "refresh_faces": "Värskenda näod", @@ -1119,6 +1272,7 @@ "role_editor": "Muutja", "role_viewer": "Vaataja", "save": "Salvesta", + "save_to_gallery": "Salvesta galeriisse", "saved_api_key": "API võti salvestatud", "saved_profile": "Profiil salvestatud", "saved_settings": "Seaded salvestatud", @@ -1138,14 +1292,32 @@ "search_camera_model": "Otsi kaamera mudelit...", "search_city": "Otsi linna...", "search_country": "Otsi riiki...", + "search_filter_camera_title": "Vali kaamera tüüp", + "search_filter_date": "Kuupäev", + "search_filter_date_interval": "{start} kuni {end}", + "search_filter_date_title": "Vali kuupäevavahemik", + "search_filter_display_options": "Kuva valikud", + "search_filter_filename": "Otsi failinime alusel", + "search_filter_location": "Asukoht", + "search_filter_location_title": "Vali asukoht", + "search_filter_media_type": "Meediumi tüüp", + "search_filter_media_type_title": "Vali meediumi tüüp", + "search_filter_people_title": "Vali isikud", "search_for": "Otsi", "search_for_existing_person": "Otsi olemasolevat isikut", "search_no_people": "Isikuid ei ole", "search_no_people_named": "Ei ole isikuid nimega \"{name}\"", "search_options": "Otsingu valikud", + "search_page_categories": "Kategooriad", + "search_page_screenshots": "Ekraanipildid", + "search_page_search_photos_videos": "Otsi oma fotosid ja videosid", + "search_page_selfies": "Selfid", + "search_page_things": "Asjad", + "search_page_view_all_button": "Vaata kõiki", "search_people": "Otsi inimesi", "search_places": "Otsi kohti", "search_rating": "Otsi hinnangu järgi...", + "search_result_page_new_search_hint": "Uus otsing", "search_settings": "Otsi seadeid", "search_state": "Otsi osariiki...", "search_tags": "Otsi silte...", @@ -1168,10 +1340,14 @@ "select_new_face": "Vali uus nägu", "select_photos": "Vali fotod", "select_trash_all": "Vali kõik prügikasti", + "select_user_for_sharing_page_err_album": "Albumi lisamine ebaõnnestus", "selected": "Valitud", "selected_count": "{count, plural, other {# valitud}}", "send_message": "Saada sõnum", "send_welcome_email": "Saada tervituskiri", + "server_endpoint": "Serveri lõpp-punkt", + "server_info_box_app_version": "Rakenduse versioon", + "server_info_box_server_url": "Serveri URL", "server_offline": "Serveriga ühendus puudub", "server_online": "Server ühendatud", "server_stats": "Serveri statistika", @@ -1183,22 +1359,42 @@ "set_date_of_birth": "Määra sünnikuupäev", "set_profile_picture": "Sea profiilipilt", "set_slideshow_to_fullscreen": "Kuva slaidiesitlus täisekraanil", + "setting_languages_apply": "Rakenda", + "setting_languages_title": "Keeled", + "setting_notifications_notify_immediately": "kohe", + "setting_notifications_notify_never": "mitte kunagi", "settings": "Seaded", "settings_saved": "Seaded salvestatud", "share": "Jaga", "shared": "Jagatud", + "shared_album_section_people_action_error": "Viga albumist eemaldamisel/lahkumisel", + "shared_album_section_people_action_leave": "Eemalda kasutaja albumist", + "shared_album_section_people_action_remove_user": "Eemalda kasutaja albumist", + "shared_album_section_people_title": "ISIKUD", "shared_by": "Jagas", "shared_by_user": "Jagas {user}", "shared_by_you": "Jagasid sina", "shared_from_partner": "Fotod partnerilt {partner}", + "shared_link_app_bar_title": "Jagatud lingid", + "shared_link_clipboard_copied_massage": "Kopeeritud lõikelauale", + "shared_link_create_error": "Viga jagatud lingi loomisel", + "shared_link_edit_expire_after_option_day": "1 päev", + "shared_link_edit_expire_after_option_hour": "1 tund", + "shared_link_edit_expire_after_option_minute": "1 minut", + "shared_link_info_chip_metadata": "EXIF", + "shared_link_manage_links": "Halda jagatud linke", "shared_link_options": "Jagatud lingi valikud", "shared_links": "Jagatud lingid", "shared_links_description": "Jaga fotosid ja videosid lingiga", "shared_photos_and_videos_count": "{assetCount, plural, other {# jagatud fotot ja videot.}}", + "shared_with_me": "Minuga jagatud", "shared_with_partner": "Jagatud partneriga {partner}", "sharing": "Jagamine", "sharing_enter_password": "Palun sisesta selle lehe vaatamiseks salasõna.", + "sharing_page_album": "Jagatud albumid", "sharing_sidebar_description": "Kuva külgmenüüs Jagamise linki", + "sharing_silver_appbar_create_shared_album": "Uus jagatud album", + "sharing_silver_appbar_share_partner": "Jaga partneriga", "shift_to_permanent_delete": "vajuta ⇧, et üksus jäädavalt kustutada", "show_album_options": "Näita albumi valikuid", "show_albums": "Näita albumeid", @@ -1265,6 +1461,7 @@ "support_third_party_description": "Sinu Immich'i install on kolmanda osapoole pakendatud. Probleemid, mida täheldad, võivad olla põhjustatud selle pakendamise poolt, seega võta esmajärjekorras nendega ühendust, kasutades allolevaid linke.", "swap_merge_direction": "Muuda ühendamise suunda", "sync": "Sünkrooni", + "sync_albums": "Sünkrooni albumid", "tag": "Silt", "tag_assets": "Sildista üksuseid", "tag_created": "Lisatud silt: {tag}", @@ -1278,6 +1475,9 @@ "theme": "Teema", "theme_selection": "Teema valik", "theme_selection_description": "Sea automaatselt hele või tume teema vastavalt veebilehitseja eelistustele", + "theme_setting_primary_color_title": "Põhivärv", + "theme_setting_system_primary_color_title": "Kasuta süsteemset värvi", + "theme_setting_system_theme_switch": "Automaatne (järgi süsteemi seadet)", "they_will_be_merged_together": "Nad ühendatakse kokku", "third_party_resources": "Kolmanda osapoole ressursid", "time_based_memories": "Ajapõhised mälestused", @@ -1298,6 +1498,9 @@ "trash_count": "Liiguta {count, number} prügikasti", "trash_delete_asset": "Kustuta üksus", "trash_no_results_message": "Siia ilmuvad prügikasti liigutatud fotod ja videod.", + "trash_page_delete_all": "Kustuta kõik", + "trash_page_restore_all": "Taasta kõik", + "trash_page_select_assets_btn": "Vali üksused", "trashed_items_will_be_permanently_deleted_after": "Prügikasti tõstetud üksused kustutatakse jäädavalt {days, plural, one {# päeva} other {# päeva}} pärast.", "type": "Tüüp", "unarchive": "Taasta arhiivist", @@ -1333,6 +1536,7 @@ "upload_status_errors": "Vead", "upload_status_uploaded": "Üleslaaditud", "upload_success": "Üleslaadimine õnnestus, uute üksuste nägemiseks värskenda lehte.", + "uploading": "Üleslaadimine", "url": "URL", "usage": "Kasutus", "use_custom_date_range": "Kasuta kohandatud kuupäevavahemikku", @@ -1372,15 +1576,19 @@ "view_previous_asset": "Vaata eelmist üksust", "view_qr_code": "Vaata QR-koodi", "view_stack": "Vaata virna", + "viewer_remove_from_stack": "Eemalda virnast", + "viewer_unstack": "Eralda", "visibility_changed": "{count, plural, one {# isiku} other {# isiku}} nähtavus muudetud", "waiting": "Ootel", "warning": "Hoiatus", "week": "Nädal", "welcome": "Tere tulemast", "welcome_to_immich": "Tere tulemast Immich'isse", + "wifi_name": "WiFi-võrgu nimi", "year": "Aasta", "years_ago": "{years, plural, one {# aasta} other {# aastat}} tagasi", "yes": "Jah", "you_dont_have_any_shared_links": "Sul pole ühtegi jagatud linki", + "your_wifi_name": "Sinu WiFi-võrgu nimi", "zoom_image": "Suumi pilti" } diff --git a/i18n/fi.json b/i18n/fi.json index 8ca468fbab..8e64c372de 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -520,11 +520,11 @@ "backup_controller_page_background_turn_on": "Kytke taustapalvelu päälle", "backup_controller_page_background_wifi": "Vain WiFi-verkossa", "backup_controller_page_backup": "Varmuuskopiointi", - "backup_controller_page_backup_selected": "Valittu:", + "backup_controller_page_backup_selected": "Valittu: ", "backup_controller_page_backup_sub": "Varmuuskopioidut kuvat ja videot", "backup_controller_page_created": "Luotu: {}", "backup_controller_page_desc_backup": "Kytke varmuuskopiointi päälle lähettääksesi uudet kohteet palvelimelle automaattisesti.", - "backup_controller_page_excluded": "Jätetty pois:", + "backup_controller_page_excluded": "Jätetty pois: ", "backup_controller_page_failed": "Epäonnistui ({})", "backup_controller_page_filename": "Tiedoston nimi: {} [{}]", "backup_controller_page_id": "ID: {}", diff --git a/i18n/fr.json b/i18n/fr.json index c116ee261a..00a4a281b5 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -87,9 +87,9 @@ "image_resolution_description": "Les résolutions plus élevées permettent de préserver davantage de détails, mais l'encodage est plus long, les fichiers sont plus volumineux et la réactivité de l'application peut s'en trouver réduite.", "image_settings": "Paramètres d'image", "image_settings_description": "Gestion de la qualité et résolution des images générées", - "image_thumbnail_description": "Petite vignette avec métadonnées retirées, utilisée lors de la visualisation de groupes de photos comme sur la vue chronologique principale", - "image_thumbnail_quality_description": "Qualité des vignettes : de 1 à 100. Une valeur élevée produit de meilleurs résultats, mais elle produit des fichiers plus volumineux et peut réduire la réactivité de l'application.", - "image_thumbnail_title": "Paramètres des vignettes", + "image_thumbnail_description": "Petite miniature avec les métadonnées retirées, utilisée lors de la visualisation de groupes de photos comme sur la vue chronologique principale", + "image_thumbnail_quality_description": "Qualité des miniatures : de 1 à 100. Une valeur élevée produit de meilleurs résultats, mais elle produit des fichiers plus volumineux et peut réduire la réactivité de l'application.", + "image_thumbnail_title": "Paramètres des miniatures", "job_concurrency": "{job} : nombre de tâches simultanées", "job_created": "Tâche créée", "job_not_concurrency_safe": "Cette tâche ne peut pas être exécutée en multitâche de façon sûre.", @@ -345,7 +345,7 @@ "trash_settings": "Corbeille", "trash_settings_description": "Gérer les paramètres de la corbeille", "untracked_files": "Fichiers non suivis", - "untracked_files_description": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat d'erreurs de déplacement, d'envois interrompus, ou d'abandons en raison d'un bug", + "untracked_files_description": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat d'échecs de déplacement, d'envois interrompus, ou d'abandons en raison d'un bug", "user_cleanup_job": "Nettoyage des utilisateurs", "user_delete_delay": "La suppression définitive du compte et des médias de {user} sera programmée dans {delay, plural, one {# jour} other {# jours}}.", "user_delete_delay_settings": "Délai de suppression", @@ -371,13 +371,17 @@ "admin_password": "Mot de passe Admin", "administration": "Administration", "advanced": "Avancé", - "advanced_settings_log_level_title": "Niveau de log : {}", - "advanced_settings_prefer_remote_subtitle": "Certains appareils sont très lents à charger des vignettes à partir de ressources présentes sur l'appareil. Activez ce paramètre pour charger des images externes à la place.", + "advanced_settings_enable_alternate_media_filter_subtitle": "Utilisez cette option pour filtrer les média durant la synchronisation avec des critères alternatifs. N'utilisez cela que lorsque l'application n'arrive pas à détecter tout les albums.", + "advanced_settings_enable_alternate_media_filter_title": "[EXEPRIMENTAL] Utiliser le filtre de synchronisation d'album alternatif", + "advanced_settings_log_level_title": "Niveau de journalisation : {}", + "advanced_settings_prefer_remote_subtitle": "Certains appareils sont très lents à charger des miniatures à partir de ressources présentes sur l'appareil. Activez ce paramètre pour charger des images externes à la place.", "advanced_settings_prefer_remote_title": "Préférer les images externes", "advanced_settings_proxy_headers_subtitle": "Ajoutez des en-têtes personnalisés à chaque requête réseau", "advanced_settings_proxy_headers_title": "En-têtes de proxy", "advanced_settings_self_signed_ssl_subtitle": "Permet d'ignorer la vérification du certificat SSL pour le point d'accès du serveur. Requis pour les certificats auto-signés.", "advanced_settings_self_signed_ssl_title": "Autoriser les certificats SSL auto-signés", + "advanced_settings_sync_remote_deletions_subtitle": "Supprimer ou restaurer automatiquement un média sur cet appareil lorsqu'une action a été faite sur le web", + "advanced_settings_sync_remote_deletions_title": "Synchroniser les suppressions depuis le serveur [EXPERIMENTAL]", "advanced_settings_tile_subtitle": "Paramètres d'utilisateur avancés", "advanced_settings_troubleshooting_subtitle": "Activer des fonctions supplémentaires pour le dépannage", "advanced_settings_troubleshooting_title": "Dépannage", @@ -506,11 +510,11 @@ "backup_all": "Tout", "backup_background_service_backup_failed_message": "Échec de la sauvegarde des éléments. Nouvelle tentative...", "backup_background_service_connection_failed_message": "Impossible de se connecter au serveur. Nouvelle tentative...", - "backup_background_service_current_upload_notification": "Transfert {}", + "backup_background_service_current_upload_notification": "Envoi {}", "backup_background_service_default_notification": "Recherche de nouveaux éléments...", "backup_background_service_error_title": "Erreur de sauvegarde", "backup_background_service_in_progress_notification": "Sauvegarde de vos éléments...", - "backup_background_service_upload_failure_notification": "Impossible de transférer {}", + "backup_background_service_upload_failure_notification": "Échec lors de l'envoi {}", "backup_controller_page_albums": "Sauvegarder les albums", "backup_controller_page_background_app_refresh_disabled_content": "Activez le rafraîchissement de l'application en arrière-plan dans Paramètres > Général > Rafraîchissement de l'application en arrière-plan afin d'utiliser la sauvegarde en arrière-plan.", "backup_controller_page_background_app_refresh_disabled_title": "Rafraîchissement de l'application en arrière-plan désactivé", @@ -521,7 +525,7 @@ "backup_controller_page_background_battery_info_title": "Optimisation de la batterie", "backup_controller_page_background_charging": "Seulement pendant la charge", "backup_controller_page_background_configure_error": "Échec de la configuration du service d'arrière-plan", - "backup_controller_page_background_delay": "Retarder la sauvegarde des nouveaux éléments d'actif: {}", + "backup_controller_page_background_delay": "Retarder la sauvegarde des nouveaux éléments : {}", "backup_controller_page_background_description": "Activez le service d'arrière-plan pour sauvegarder automatiquement tous les nouveaux éléments sans avoir à ouvrir l'application.", "backup_controller_page_background_is_off": "La sauvegarde automatique en arrière-plan est désactivée", "backup_controller_page_background_is_on": "La sauvegarde automatique en arrière-plan est activée", @@ -531,12 +535,12 @@ "backup_controller_page_backup": "Sauvegardé", "backup_controller_page_backup_selected": "Sélectionné: ", "backup_controller_page_backup_sub": "Photos et vidéos sauvegardées", - "backup_controller_page_created": "Créé le: {}", + "backup_controller_page_created": "Créé le : {}", "backup_controller_page_desc_backup": "Activez la sauvegarde pour envoyer automatiquement les nouveaux éléments sur le serveur.", "backup_controller_page_excluded": "Exclus: ", "backup_controller_page_failed": "Échec de l'opération ({})", - "backup_controller_page_filename": "Nom du fichier: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_filename": "Nom du fichier : {} [{}]", + "backup_controller_page_id": "ID : {}", "backup_controller_page_info": "Informations de sauvegarde", "backup_controller_page_none_selected": "Aucune sélection", "backup_controller_page_remainder": "Restant", @@ -545,7 +549,7 @@ "backup_controller_page_start_backup": "Démarrer la sauvegarde", "backup_controller_page_status_off": "La sauvegarde est désactivée", "backup_controller_page_status_on": "La sauvegarde est activée", - "backup_controller_page_storage_format": "{} de {} utilisé", + "backup_controller_page_storage_format": "{} sur {} utilisés", "backup_controller_page_to_backup": "Albums à sauvegarder", "backup_controller_page_total_sub": "Toutes les photos et vidéos uniques des albums sélectionnés", "backup_controller_page_turn_off": "Désactiver la sauvegarde", @@ -570,21 +574,21 @@ "bulk_keep_duplicates_confirmation": "Êtes-vous sûr de vouloir conserver {count, plural, one {# doublon} other {# doublons}} ? Cela résoudra tous les groupes de doublons sans rien supprimer.", "bulk_trash_duplicates_confirmation": "Êtes-vous sûr de vouloir mettre à la corbeille {count, plural, one {# doublon} other {# doublons}} ? Cette opération permet de conserver le plus grand média de chaque groupe et de mettre à la corbeille tous les autres doublons.", "buy": "Acheter Immich", - "cache_settings_album_thumbnails": "vignettes de la page bibliothèque ({} éléments)", + "cache_settings_album_thumbnails": "Page des miniatures de la bibliothèque ({} éléments)", "cache_settings_clear_cache_button": "Effacer le cache", "cache_settings_clear_cache_button_title": "Efface le cache de l'application. Cela aura un impact significatif sur les performances de l'application jusqu'à ce que le cache soit reconstruit.", "cache_settings_duplicated_assets_clear_button": "EFFACER", "cache_settings_duplicated_assets_subtitle": "Photos et vidéos qui sont exclues par l'application", "cache_settings_duplicated_assets_title": "Éléments dupliqués ({})", "cache_settings_image_cache_size": "Taille du cache des images ({} éléments)", - "cache_settings_statistics_album": "vignettes de la bibliothèque", + "cache_settings_statistics_album": "Miniatures de la bibliothèque", "cache_settings_statistics_assets": "{} éléments ({})", "cache_settings_statistics_full": "Images complètes", - "cache_settings_statistics_shared": "vignettes d'albums partagés", - "cache_settings_statistics_thumbnail": "vignettes", + "cache_settings_statistics_shared": "Miniatures de l'album partagé", + "cache_settings_statistics_thumbnail": "Miniatures", "cache_settings_statistics_title": "Utilisation du cache", "cache_settings_subtitle": "Contrôler le comportement de mise en cache de l'application mobile Immich", - "cache_settings_thumbnail_size": "Taille du cache des vignettes ({} éléments)", + "cache_settings_thumbnail_size": "Taille du cache des miniatures ({} éléments)", "cache_settings_tile_subtitle": "Contrôler le comportement du stockage local", "cache_settings_tile_title": "Stockage local", "cache_settings_title": "Paramètres de mise en cache", @@ -654,7 +658,7 @@ "contain": "Contenu", "context": "Contexte", "continue": "Continuer", - "control_bottom_app_bar_album_info_shared": "{} éléments - Partagés", + "control_bottom_app_bar_album_info_shared": "{} éléments · Partagés", "control_bottom_app_bar_create_new_album": "Créer un nouvel album", "control_bottom_app_bar_delete_from_immich": "Supprimer de Immich", "control_bottom_app_bar_delete_from_local": "Supprimer de l'appareil", @@ -763,7 +767,7 @@ "download_enqueue": "Téléchargement en attente", "download_error": "Erreur de téléchargement", "download_failed": "Téléchargement échoué", - "download_filename": "fichier : {}", + "download_filename": "fichier : {}", "download_finished": "Téléchargement terminé", "download_include_embedded_motion_videos": "Vidéos intégrées", "download_include_embedded_motion_videos_description": "Inclure des vidéos intégrées dans les photos de mouvement comme un fichier séparé", @@ -819,7 +823,7 @@ "error_change_sort_album": "Impossible de modifier l'ordre de tri des albums", "error_delete_face": "Erreur lors de la suppression du visage pour le média", "error_loading_image": "Erreur de chargement de l'image", - "error_saving_image": "Erreur : {}", + "error_saving_image": "Erreur : {}", "error_title": "Erreur - Quelque chose s'est mal passé", "errors": { "cannot_navigate_next_asset": "Impossible de naviguer jusqu'au prochain média", @@ -907,7 +911,7 @@ "unable_to_log_out_all_devices": "Incapable de déconnecter tous les appareils", "unable_to_log_out_device": "Impossible de déconnecter l'appareil", "unable_to_login_with_oauth": "Impossible de se connecter avec OAuth", - "unable_to_play_video": "Impossible de jouer la vidéo", + "unable_to_play_video": "Impossible de lancer la vidéo", "unable_to_reassign_assets_existing_person": "Impossible de réattribuer les médias à {name, select, null {une personne existante} other {{name}}}", "unable_to_reassign_assets_new_person": "Impossible de réattribuer les médias à une nouvelle personne", "unable_to_refresh_user": "Impossible d'actualiser l'utilisateur", @@ -954,9 +958,9 @@ "exif_bottom_sheet_people": "PERSONNES", "exif_bottom_sheet_person_add_person": "Ajouter un nom", "exif_bottom_sheet_person_age": "Âge {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", + "exif_bottom_sheet_person_age_months": "Âge {} mois", + "exif_bottom_sheet_person_age_year_months": "Âge 1 an, {} mois", + "exif_bottom_sheet_person_age_years": "Âge {}", "exit_slideshow": "Quitter le diaporama", "expand_all": "Tout développer", "experimental_settings_new_asset_list_subtitle": "En cours de développement", @@ -978,7 +982,7 @@ "face_unassigned": "Non attribué", "failed": "Échec", "failed_to_load_assets": "Échec du chargement des ressources", - "failed_to_load_folder": "Impossible d'ouvrir le dossier", + "failed_to_load_folder": "Échec de chargement du dossier", "favorite": "Favori", "favorite_or_unfavorite_photo": "Ajouter ou supprimer des favoris", "favorites": "Favoris", @@ -1282,6 +1286,7 @@ "onboarding_welcome_user": "Bienvenue {user}", "online": "En ligne", "only_favorites": "Uniquement les favoris", + "open": "Ouvert", "open_in_map_view": "Montrer sur la carte", "open_in_openstreetmap": "Ouvrir dans OpenStreetMap", "open_the_search_filters": "Ouvrir les filtres de recherche", @@ -1354,10 +1359,10 @@ "place": "Lieu", "places": "Lieux", "places_count": "{count, plural, one {{count, number} Lieu} other {{count, number} Lieux}}", - "play": "Jouer", + "play": "Lancer", "play_memories": "Lancer les souvenirs", "play_motion_photo": "Jouer la photo animée", - "play_or_pause_video": "Jouer ou mettre en pause la vidéo", + "play_or_pause_video": "Lancer ou mettre en pause la vidéo", "port": "Port", "preferences_settings_subtitle": "Gérer les préférences de l'application", "preferences_settings_title": "Préférences", @@ -1390,7 +1395,7 @@ "purchase_button_reminder": "Me le rappeler dans 30 jours", "purchase_button_remove_key": "Supprimer la clé", "purchase_button_select": "Sélectionner", - "purchase_failed_activation": "Erreur à l'activation. Veuillez vérifier votre courriel pour obtenir la clé du produit correcte !", + "purchase_failed_activation": "Échec de l'activation. Veuillez vérifier votre courriel pour obtenir la clé correcte du produit !", "purchase_individual_description_1": "Pour un utilisateur", "purchase_individual_description_2": "Statut de contributeur", "purchase_individual_title": "Utilisateur", @@ -1430,13 +1435,13 @@ "refresh_encoded_videos": "Actualiser les vidéos encodées", "refresh_faces": "Actualiser les visages", "refresh_metadata": "Actualiser les métadonnées", - "refresh_thumbnails": "Actualiser les vignettes", + "refresh_thumbnails": "Actualiser les miniatures", "refreshed": "Actualisé", "refreshes_every_file": "Actualise tous les fichiers (existants et nouveaux)", "refreshing_encoded_video": "Actualisation de la vidéo encodée", "refreshing_faces": "Actualisation des visages", "refreshing_metadata": "Actualisation des métadonnées", - "regenerating_thumbnails": "Regénération des vignettes", + "regenerating_thumbnails": "Regénération des miniatures", "remove": "Supprimer", "remove_assets_album_confirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, one {# média} other {# médias}} de l'album ?", "remove_assets_shared_link_confirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, one {# média} other {# médias}} de ce lien partagé ?", @@ -1581,16 +1586,16 @@ "set_date_of_birth": "Changer la date de naissance", "set_profile_picture": "Définir la photo de profil", "set_slideshow_to_fullscreen": "Afficher le diaporama en plein écran", - "setting_image_viewer_help": "Le visualiseur de détails charge d'abord la petite vignette, puis l'aperçu de taille moyenne (s'il est activé), enfin l'original (s'il est activé).", + "setting_image_viewer_help": "Le visualiseur de détails charge d'abord la petite miniature, puis l'aperçu de taille moyenne (s'il est activé), enfin l'original (s'il est activé).", "setting_image_viewer_original_subtitle": "Activez cette option pour charger l'image en résolution originale (volumineux!). Désactiver pour réduire l'utilisation des données (réseau et cache de l'appareil).", "setting_image_viewer_original_title": "Charger l'image originale", - "setting_image_viewer_preview_subtitle": "Activer pour charger une image de résolution moyenne. Désactiver pour charger directement l'original ou utiliser uniquement la vignette.", + "setting_image_viewer_preview_subtitle": "Activer pour charger une image de résolution moyenne. Désactiver pour charger directement l'original ou utiliser uniquement la miniature.", "setting_image_viewer_preview_title": "Charger l'image d'aperçu", "setting_image_viewer_title": "Images", "setting_languages_apply": "Appliquer", "setting_languages_subtitle": "Changer la langue de l'application", "setting_languages_title": "Langues", - "setting_notifications_notify_failures_grace_period": "Notifier les échecs de la sauvegarde en arrière-plan: {}", + "setting_notifications_notify_failures_grace_period": "Notifier les échecs de sauvegarde en arrière-plan : {}", "setting_notifications_notify_hours": "{} heures", "setting_notifications_notify_immediately": "immédiatement", "setting_notifications_notify_minutes": "{} minutes", @@ -1609,7 +1614,7 @@ "settings_saved": "Paramètres sauvegardés", "share": "Partager", "share_add_photos": "Ajouter des photos", - "share_assets_selected": "{} séléctionné(s)", + "share_assets_selected": "{} sélectionné(s)", "share_dialog_preparing": "Préparation...", "shared": "Partagé", "shared_album_activities_input_disable": "Les commentaires sont désactivés", @@ -1623,10 +1628,10 @@ "shared_by_user": "Partagé par {user}", "shared_by_you": "Partagé par vous", "shared_from_partner": "Photos de {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Téléversé", + "shared_intent_upload_button_progress_text": "{} / {} Envoyés", "shared_link_app_bar_title": "Liens partagés", "shared_link_clipboard_copied_massage": "Copié dans le presse-papier\n", - "shared_link_clipboard_text": "\nLien : {}\nMot de passe : {}", + "shared_link_clipboard_text": "Lien : {}\nMot de passe : {}", "shared_link_create_error": "Erreur pendant la création du lien partagé", "shared_link_edit_description_hint": "Saisir la description du partage", "shared_link_edit_expire_after_option_day": "1 jour", @@ -1636,7 +1641,7 @@ "shared_link_edit_expire_after_option_minute": "1 minute", "shared_link_edit_expire_after_option_minutes": "{} minutes", "shared_link_edit_expire_after_option_months": "{} mois", - "shared_link_edit_expire_after_option_year": "{} ans", + "shared_link_edit_expire_after_option_year": "{} années", "shared_link_edit_password_hint": "Saisir le mot de passe de partage", "shared_link_edit_submit_button": "Mettre à jour le lien", "shared_link_error_server_url_fetch": "Impossible de récupérer l'url du serveur", @@ -1826,7 +1831,7 @@ "upload_status_errors": "Erreurs", "upload_status_uploaded": "Envoyé", "upload_success": "Envoi réussi. Rafraîchir la page pour voir les nouveaux médias envoyés.", - "upload_to_immich": "Téléverser vers Immich ({})", + "upload_to_immich": "Envoyer vers Immich ({})", "uploading": "Téléversement en cours", "url": "URL", "usage": "Utilisation", @@ -1859,7 +1864,7 @@ "version_history_item": "Version {version} installée le {date}", "video": "Vidéo", "video_hover_setting": "Lire la miniature des vidéos au survol", - "video_hover_setting_description": "Jouer la prévisualisation vidéo au survol. Si désactivé, la lecture peut quand même être démarrée en survolant le bouton Play.", + "video_hover_setting_description": "Lancer la prévisualisation vidéo au survol. Si désactivé, la lecture peut quand même être démarrée en survolant le bouton Play.", "videos": "Vidéos", "videos_count": "{count, plural, one {# Vidéo} other {# Vidéos}}", "view": "Voir", diff --git a/i18n/gl.json b/i18n/gl.json index 72cf8d540a..d03294c778 100644 --- a/i18n/gl.json +++ b/i18n/gl.json @@ -4,576 +4,1896 @@ "account_settings": "Configuración da conta", "acknowledge": "De acordo", "action": "Acción", - "action_common_update": "Update", + "action_common_update": "Actualizar", "actions": "Accións", "active": "Activo", "activity": "Actividade", - "activity_changed": "A actividade está {enabled, select, true {habilitada} other {deshabilitada}}", + "activity_changed": "A actividade está {enabled, select, true {activada} other {desactivada}}", "add": "Engadir", "add_a_description": "Engadir unha descrición", - "add_a_location": "Engadir unha localización", + "add_a_location": "Engadir unha ubicación", "add_a_name": "Engadir un nome", "add_a_title": "Engadir un título", - "add_endpoint": "Add endpoint", + "add_endpoint": "Engadir endpoint", "add_exclusion_pattern": "Engadir patrón de exclusión", "add_import_path": "Engadir ruta de importación", - "add_location": "Engadir localización", + "add_location": "Engadir ubicación", "add_more_users": "Engadir máis usuarios", - "add_partner": "Engadir compañeiro", + "add_partner": "Engadir compañeiro/a", "add_path": "Engadir ruta", "add_photos": "Engadir fotos", "add_to": "Engadir a…", "add_to_album": "Engadir ao álbum", - "add_to_album_bottom_sheet_added": "Added to {album}", - "add_to_album_bottom_sheet_already_exists": "Already in {album}", + "add_to_album_bottom_sheet_added": "Engadido a {album}", + "add_to_album_bottom_sheet_already_exists": "Xa está en {album}", "add_to_shared_album": "Engadir ao álbum compartido", "add_url": "Engadir URL", "added_to_archive": "Engadido ao arquivo", "added_to_favorites": "Engadido a favoritos", - "added_to_favorites_count": "Engadidos {count, number} a favoritos", + "added_to_favorites_count": "Engadido {count, number} a favoritos", "admin": { + "add_exclusion_pattern_description": "Engadir patróns de exclusión. Admítense caracteres comodín usando *, ** e ?. Para ignorar todos os ficheiros en calquera directorio chamado \"Raw\", emprega \"**/Raw/**\". Para ignorar todos os ficheiros que rematen en \".tif\", usa \"**/*.tif\". Para ignorar unha ruta absoluta, emprega \"/ruta/a/ignorar/**\".", + "asset_offline_description": "Este activo da biblioteca externa xa non se atopa no disco e moveuse ao lixo. Se o ficheiro se moveu dentro da biblioteca, comproba a túa liña de tempo para o novo activo correspondente. Para restaurar este activo, asegúrate de que Immich poida acceder á ruta do ficheiro a continuación e escanee a biblioteca.", "authentication_settings": "Configuración de autenticación", - "authentication_settings_description": "Xestionar contrasinal, OAuth e outros parámetros de autenticación", - "authentication_settings_disable_all": "Estás seguro de deshabilitar todos os métodos de inicio de sesión? Iniciar a sesión quedará completamente deshabilitado.", - "authentication_settings_reenable": "Para rehabilitala, usa un Comando do servidor.", + "authentication_settings_description": "Xestionar contrasinal, OAuth e outras configuracións de autenticación", + "authentication_settings_disable_all": "Estás seguro de que queres desactivar todos os métodos de inicio de sesión? O inicio de sesión desactivarase completamente.", + "authentication_settings_reenable": "Para reactivalo, use un Comando de servidor.", "background_task_job": "Tarefas en segundo plano", - "backup_database": "Respaldo da base de datos", - "backup_database_enable_description": "Habilitar as copias de seguridade da base de datos", - "backup_keep_last_amount": "Cantidade de copias de seguridade previas a manter", - "backup_settings": "Configuración de copias de seguridade", - "backup_settings_description": "Xestionar a configuración das copias de seguridade da base de datos", - "check_all": "Comprobar todo", + "backup_database": "Copia de seguridade da base de datos", + "backup_database_enable_description": "Activar copias de seguridade da base de datos", + "backup_keep_last_amount": "Cantidade de copias de seguridade anteriores a conservar", + "backup_settings": "Configuración da copia de seguridade", + "backup_settings_description": "Xestionar a configuración da copia de seguridade da base de datos", + "check_all": "Marcar todo", + "cleanup": "Limpeza", "cleared_jobs": "Traballos borrados para: {job}", - "config_set_by_file": "As configuracións están actualmente seleccionadas por un ficheiro de configuracións", + "config_set_by_file": "A configuración establécese actualmente mediante un ficheiro de configuración", "confirm_delete_library": "Estás seguro de que queres eliminar a biblioteca {library}?", - "exclusion_pattern_description": "Os patróns de exclusión permítenche ignorar ficheiros e cartafoles ao escanear a túa biblioteca. Isto é útil se tes cartafoles que conteñen ficheiros que non queres importar, coma ficheiros RAW.", + "confirm_delete_library_assets": "Estás seguro de que queres eliminar esta biblioteca? Isto eliminará {count, plural, one {# activo contido} other {todos os # activos contidos}} de Immich e non se pode desfacer. Os ficheiros permanecerán no disco.", + "confirm_email_below": "Para confirmar, escriba \"{email}\" a continuación", + "confirm_reprocess_all_faces": "Estás seguro de que queres reprocesar todas as caras? Isto tamén borrará as persoas nomeadas.", + "confirm_user_password_reset": "Estás seguro de que queres restablecer o contrasinal de {user}?", + "create_job": "Crear traballo", + "cron_expression": "Expresión Cron", + "cron_expression_description": "Estableza o intervalo de escaneo usando o formato cron. Para obter máis información, consulte por exemplo Crontab Guru", + "cron_expression_presets": "Preaxustes de expresión Cron", + "disable_login": "Desactivar inicio de sesión", + "duplicate_detection_job_description": "Executar aprendizaxe automática nos activos para detectar imaxes similares. Depende da Busca Intelixente", + "exclusion_pattern_description": "Os patróns de exclusión permítenche ignorar ficheiros e cartafoles ao escanear a túa biblioteca. Isto é útil se tes cartafoles que conteñen ficheiros que non queres importar, como ficheiros RAW.", "external_library_created_at": "Biblioteca externa (creada o {date})", - "external_library_management": "Xestión de bibliotecas externas", + "external_library_management": "Xestión da biblioteca externa", "face_detection": "Detección de caras", - "job_settings": "Configuración de tarefas", - "job_settings_description": "Administrar tarefas simultáneas", - "job_status": "Estado da tarefa", - "jobs_failed": "{jobCount, one {# errado}, plural, other {# errados}}", - "note_cannot_be_changed_later": "NOTA: Non editable posteriormente!", - "notification_email_host_description": "Host do servidor de correo electrónico (p. ex.: smtp.immich.app)", + "face_detection_description": "Detectar as caras nos activos usando aprendizaxe automática. Para vídeos, só se considera a miniatura. \"Actualizar\" (re)procesa todos os activos. \"Restablecer\" ademais borra todos os datos de caras actuais. \"Faltantes\" pon en cola os activos que aínda non foron procesados. As caras detectadas poranse en cola para o Recoñecemento Facial despois de completar a Detección de Caras, agrupándoas en persoas existentes ou novas.", + "facial_recognition_job_description": "Agrupar caras detectadas en persoas. Este paso execútase despois de completar a Detección de Caras. \"Restablecer\" (re)agrupa todas as caras. \"Faltantes\" pon en cola as caras que non teñen unha persoa asignada.", + "failed_job_command": "O comando {command} fallou para o traballo: {job}", + "force_delete_user_warning": "AVISO: Isto eliminará inmediatamente o usuario e todos os activos. Isto non se pode desfacer e os ficheiros non se poden recuperar.", + "forcing_refresh_library_files": "Forzando a actualización de todos os ficheiros da biblioteca", + "image_format": "Formato", + "image_format_description": "WebP produce ficheiros máis pequenos que JPEG, pero é máis lento de codificar.", + "image_fullsize_description": "Imaxe a tamaño completo con metadatos eliminados, usada ao facer zoom", + "image_fullsize_enabled": "Activar a xeración de imaxes a tamaño completo", + "image_fullsize_enabled_description": "Xerar imaxe a tamaño completo para formatos non compatibles coa web. Cando \"Preferir vista previa incrustada\" está activado, as vistas previas incrustadas utilízanse directamente sen conversión. Non afecta a formatos compatibles coa web como JPEG.", + "image_fullsize_quality_description": "Calidade da imaxe a tamaño completo de 1 a 100. Máis alto é mellor, pero produce ficheiros máis grandes.", + "image_fullsize_title": "Configuración da imaxe a tamaño completo", + "image_prefer_embedded_preview": "Preferir vista previa incrustada", + "image_prefer_embedded_preview_setting_description": "Usar vistas previas incrustadas en fotos RAW como entrada para o procesamento de imaxes e cando estean dispoñibles. Isto pode producir cores máis precisas para algunhas imaxes, pero a calidade da vista previa depende da cámara e a imaxe pode ter máis artefactos de compresión.", + "image_prefer_wide_gamut": "Preferir gama ampla", + "image_prefer_wide_gamut_setting_description": "Usar Display P3 para as miniaturas. Isto preserva mellor a viveza das imaxes con espazos de cor amplos, pero as imaxes poden aparecer de forma diferente en dispositivos antigos cunha versión de navegador antiga. As imaxes sRGB mantéñense como sRGB para evitar cambios de cor.", + "image_preview_description": "Imaxe de tamaño medio con metadatos eliminados, usada ao ver un único activo e para aprendizaxe automática", + "image_preview_quality_description": "Calidade da vista previa de 1 a 100. Máis alto é mellor, pero produce ficheiros máis grandes e pode reducir a capacidade de resposta da aplicación. Establecer un valor baixo pode afectar á calidade da aprendizaxe automática.", + "image_preview_title": "Configuración da vista previa", + "image_quality": "Calidade", + "image_resolution": "Resolución", + "image_resolution_description": "Resolucións máis altas poden preservar máis detalles pero tardan máis en codificarse, teñen tamaños de ficheiro máis grandes e poden reducir a capacidade de resposta da aplicación.", + "image_settings": "Configuración da imaxe", + "image_settings_description": "Xestionar a calidade e resolución das imaxes xeradas", + "image_thumbnail_description": "Miniatura pequena con metadatos eliminados, usada ao ver grupos de fotos como a liña de tempo principal", + "image_thumbnail_quality_description": "Calidade da miniatura de 1 a 100. Máis alto é mellor, pero produce ficheiros máis grandes e pode reducir a capacidade de resposta da aplicación.", + "image_thumbnail_title": "Configuración da miniatura", + "job_concurrency": "concorrencia de {job}", + "job_created": "Traballo creado", + "job_not_concurrency_safe": "Este traballo non é seguro para concorrencia.", + "job_settings": "Configuración de traballos", + "job_settings_description": "Xestionar a concorrencia de traballos", + "job_status": "Estado do traballo", + "jobs_delayed": "{jobCount, plural, other {# atrasados}}", + "jobs_failed": "{jobCount, plural, other {# fallados}}", + "library_created": "Biblioteca creada: {library}", + "library_deleted": "Biblioteca eliminada", + "library_import_path_description": "Especifique un cartafol para importar. Este cartafol, incluídos os subcartafoles, escanearase en busca de imaxes e vídeos.", + "library_scanning": "Escaneo periódico", + "library_scanning_description": "Configurar o escaneo periódico da biblioteca", + "library_scanning_enable_description": "Activar o escaneo periódico da biblioteca", + "library_settings": "Biblioteca externa", + "library_settings_description": "Xestionar a configuración da biblioteca externa", + "library_tasks_description": "Escanear bibliotecas externas en busca de activos novos e/ou modificados", + "library_watching_enable_description": "Vixiar bibliotecas externas para cambios nos ficheiros", + "library_watching_settings": "Vixilancia da biblioteca (EXPERIMENTAL)", + "library_watching_settings_description": "Vixiar automaticamente os ficheiros modificados", + "logging_enable_description": "Activar rexistro", + "logging_level_description": "Cando estea activado, que nivel de rexistro usar.", + "logging_settings": "Rexistro", + "machine_learning_clip_model": "Modelo CLIP", + "machine_learning_clip_model_description": "O nome dun modelo CLIP listado aquí. Ten en conta que debe volver executar o traballo 'Busca Intelixente' para todas as imaxes ao cambiar un modelo.", + "machine_learning_duplicate_detection": "Detección de duplicados", + "machine_learning_duplicate_detection_enabled": "Activar detección de duplicados", + "machine_learning_duplicate_detection_enabled_description": "Se está desactivado, os activos exactamente idénticos aínda se eliminarán duplicados.", + "machine_learning_duplicate_detection_setting_description": "Usar incrustacións CLIP para atopar posibles duplicados", + "machine_learning_enabled": "Activar aprendizaxe automática", + "machine_learning_enabled_description": "Se está desactivado, todas as funcións de ML desactivaranse independentemente da configuración a continuación.", + "machine_learning_facial_recognition": "Recoñecemento facial", + "machine_learning_facial_recognition_description": "Detectar, recoñecer e agrupar caras en imaxes", + "machine_learning_facial_recognition_model": "Modelo de recoñecemento facial", + "machine_learning_facial_recognition_model_description": "Os modelos están listados en orde descendente de tamaño. Os modelos máis grandes son máis lentos e usan máis memoria, pero producen mellores resultados. Teña en conta que debes volver executar o traballo de Detección de Caras para todas as imaxes ao cambiar un modelo.", + "machine_learning_facial_recognition_setting": "Activar recoñecemento facial", + "machine_learning_facial_recognition_setting_description": "Se está desactivado, as imaxes non se codificarán para o recoñecemento facial e non encherán a sección Persoas na páxina Explorar.", + "machine_learning_max_detection_distance": "Distancia máxima de detección", + "machine_learning_max_detection_distance_description": "Distancia máxima entre dúas imaxes para consideralas duplicadas, variando de 0.001 a 0.1. Valores máis altos detectarán máis duplicados, pero poden resultar en falsos positivos.", + "machine_learning_max_recognition_distance": "Distancia máxima de recoñecemento", + "machine_learning_max_recognition_distance_description": "Distancia máxima entre dúas caras para ser consideradas a mesma persoa, variando de 0 a 2. Baixar isto pode evitar etiquetar dúas persoas como a mesma persoa, mentres que subilo pode evitar etiquetar a mesma persoa como dúas persoas diferentes. Teña en conta que é máis fácil fusionar dúas persoas que dividir unha persoa en dúas, así que erre polo lado dun limiar máis baixo cando sexa posible.", + "machine_learning_min_detection_score": "Puntuación mínima de detección", + "machine_learning_min_detection_score_description": "Puntuación mínima de confianza para que unha cara sexa detectada, de 0 a 1. Valores máis baixos detectarán máis caras pero poden resultar en falsos positivos.", + "machine_learning_min_recognized_faces": "Mínimo de caras recoñecidas", + "machine_learning_min_recognized_faces_description": "O número mínimo de caras recoñecidas para que se cree unha persoa. Aumentar isto fai que o Recoñecemento Facial sexa máis preciso a costa de aumentar a posibilidade de que unha cara non se asigne a unha persoa.", + "machine_learning_settings": "Configuración da Aprendizaxe Automática", + "machine_learning_settings_description": "Xestionar funcións e configuracións da aprendizaxe automática", + "machine_learning_smart_search": "Busca Intelixente", + "machine_learning_smart_search_description": "Buscar imaxes semanticamente usando incrustacións CLIP", + "machine_learning_smart_search_enabled": "Activar busca intelixente", + "machine_learning_smart_search_enabled_description": "Se está desactivado, as imaxes non se codificarán para a busca intelixente.", + "machine_learning_url_description": "A URL do servidor de aprendizaxe automática. Se se proporciona máis dunha URL, intentarase con cada servidor un por un ata que un responda correctamente, en orde do primeiro ao último. Os servidores que non respondan ignoraranse temporalmente ata que volvan estar en liña.", + "manage_concurrency": "Xestionar Concorrencia", + "manage_log_settings": "Xestionar configuración de rexistro", + "map_dark_style": "Estilo escuro", + "map_enable_description": "Activar funcións do mapa", + "map_gps_settings": "Configuración do Mapa e GPS", + "map_gps_settings_description": "Xestionar a configuración do Mapa e GPS (Xeocodificación Inversa)", + "map_implications": "A función do mapa depende dun servizo de teselas externo (tiles.immich.cloud)", + "map_light_style": "Estilo claro", + "map_manage_reverse_geocoding_settings": "Xestionar configuración de Xeocodificación Inversa", + "map_reverse_geocoding": "Xeocodificación Inversa", + "map_reverse_geocoding_enable_description": "Activar xeocodificación inversa", + "map_reverse_geocoding_settings": "Configuración da Xeocodificación Inversa", + "map_settings": "Mapa", + "map_settings_description": "Xestionar a configuración do mapa", + "map_style_description": "URL a un tema de mapa style.json", + "memory_cleanup_job": "Limpeza de recordos", + "memory_generate_job": "Xeración de recordos", + "metadata_extraction_job": "Extraer metadatos", + "metadata_extraction_job_description": "Extraer información de metadatos de cada activo, como GPS, caras e resolución", + "metadata_faces_import_setting": "Activar importación de caras", + "metadata_faces_import_setting_description": "Importar caras dos datos EXIF da imaxe e ficheiros sidecar", + "metadata_settings": "Configuración de Metadatos", + "metadata_settings_description": "Xestionar a configuración de metadatos", + "migration_job": "Migración", + "migration_job_description": "Migrar miniaturas de activos e caras á última estrutura de cartafoles", + "no_paths_added": "Non se engadiron rutas", + "no_pattern_added": "Non se engadiu ningún padrón", + "note_apply_storage_label_previous_assets": "Nota: Para aplicar a Etiqueta de Almacenamento a activos cargados previamente, execute o", + "note_cannot_be_changed_later": "NOTA: Isto non se pode cambiar máis tarde!", + "notification_email_from_address": "Enderezo do remitente", + "notification_email_from_address_description": "Enderezo de correo electrónico do remitente, por exemplo: \"Servidor de Fotos Immich \"", + "notification_email_host_description": "Host do servidor de correo electrónico (p. ex. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorar erros de certificado", - "notification_email_ignore_certificate_errors_description": "Ignorar erros de validación de certificados TLS (non recomendado)", - "notification_settings": "Configuración de notificacións" + "notification_email_ignore_certificate_errors_description": "Ignorar erros de validación do certificado TLS (non recomendado)", + "notification_email_password_description": "Contrasinal a usar ao autenticarse co servidor de correo electrónico", + "notification_email_port_description": "Porto do servidor de correo electrónico (p. ex. 25, 465 ou 587)", + "notification_email_sent_test_email_button": "Enviar correo de proba e gardar", + "notification_email_setting_description": "Configuración para enviar notificacións por correo electrónico", + "notification_email_test_email": "Enviar correo de proba", + "notification_email_test_email_failed": "Erro ao enviar correo de proba, comproba os teus valores", + "notification_email_test_email_sent": "Enviouse un correo electrónico de proba a {email}. Por favor, comproba a túa caixa de entrada.", + "notification_email_username_description": "Nome de usuario a usar ao autenticarse co servidor de correo electrónico", + "notification_enable_email_notifications": "Activar notificacións por correo electrónico", + "notification_settings": "Configuración de Notificacións", + "notification_settings_description": "Xestionar a configuración de notificacións, incluído o correo electrónico", + "oauth_auto_launch": "Lanzamento automático", + "oauth_auto_launch_description": "Iniciar o fluxo de inicio de sesión OAuth automaticamente ao navegar á páxina de inicio de sesión", + "oauth_auto_register": "Rexistro automático", + "oauth_auto_register_description": "Rexistrar automaticamente novos usuarios despois de iniciar sesión con OAuth", + "oauth_button_text": "Texto do botón", + "oauth_client_id": "ID de cliente", + "oauth_client_secret": "Segredo do cliente", + "oauth_enable_description": "Iniciar sesión con OAuth", + "oauth_issuer_url": "URL do emisor", + "oauth_mobile_redirect_uri": "URI de redirección móbil", + "oauth_mobile_redirect_uri_override": "Substitución de URI de redirección móbil", + "oauth_mobile_redirect_uri_override_description": "Activar cando o provedor OAuth non permite un URI móbil, como '{callback}'", + "oauth_profile_signing_algorithm": "Algoritmo de sinatura do perfil", + "oauth_profile_signing_algorithm_description": "Algoritmo usado para asinar o perfil do usuario.", + "oauth_scope": "Ámbito", + "oauth_settings": "OAuth", + "oauth_settings_description": "Xestionar a configuración de inicio de sesión OAuth", + "oauth_settings_more_details": "Para máis detalles sobre esta función, consulte a documentación.", + "oauth_signing_algorithm": "Algoritmo de sinatura", + "oauth_storage_label_claim": "Declaración de etiqueta de almacenamento", + "oauth_storage_label_claim_description": "Establecer automaticamente a etiqueta de almacenamento do usuario ao valor desta declaración.", + "oauth_storage_quota_claim": "Declaración de cota de almacenamento", + "oauth_storage_quota_claim_description": "Establecer automaticamente a cota de almacenamento do usuario ao valor desta declaración.", + "oauth_storage_quota_default": "Cota de almacenamento predeterminada (GiB)", + "oauth_storage_quota_default_description": "Cota en GiB a usar cando non se proporciona ningunha declaración (Introduza 0 para cota ilimitada).", + "offline_paths": "Rutas fóra de liña", + "offline_paths_description": "Estes resultados poden deberse á eliminación manual de ficheiros que non forman parte dunha biblioteca externa.", + "password_enable_description": "Iniciar sesión con correo electrónico e contrasinal", + "password_settings": "Inicio de sesión con contrasinal", + "password_settings_description": "Xestionar a configuración de inicio de sesión con contrasinal", + "paths_validated_successfully": "Todas as rutas validadas correctamente", + "person_cleanup_job": "Limpeza de persoas", + "quota_size_gib": "Tamaño da cota (GiB)", + "refreshing_all_libraries": "Actualizando todas as bibliotecas", + "registration": "Rexistro do administrador", + "registration_description": "Dado que ti es o primeiro usuario no sistema, asignarásete como Administrador e serás responsable das tarefas administrativas, e os usuarios adicionais serán creados por ti.", + "repair_all": "Reparar todo", + "repair_matched_items": "Coincidiron {count, plural, one {# elemento} other {# elementos}}", + "repaired_items": "Reparáronse {count, plural, one {# elemento} other {# elementos}}", + "require_password_change_on_login": "Requirir que o usuario cambie o contrasinal no primeiro inicio de sesión", + "reset_settings_to_default": "Restablecer a configuración aos valores predeterminados", + "reset_settings_to_recent_saved": "Restablecer a configuración á configuración gardada recentemente", + "scanning_library": "Escaneando biblioteca", + "search_jobs": "Buscar traballos…", + "send_welcome_email": "Enviar correo electrónico de benvida", + "server_external_domain_settings": "Dominio externo", + "server_external_domain_settings_description": "Dominio para ligazóns públicas compartidas, incluíndo http(s)://", + "server_public_users": "Usuarios públicos", + "server_public_users_description": "Todos os usuarios (nome e correo electrónico) listanse ao engadir un usuario a álbums compartidos. Cando está desactivado, a lista de usuarios só estará dispoñible para os usuarios administradores.", + "server_settings": "Configuración do servidor", + "server_settings_description": "Xestionar a configuración do servidor", + "server_welcome_message": "Mensaxe de benvida", + "server_welcome_message_description": "Unha mensaxe que se mostra na páxina de inicio de sesión.", + "sidecar_job": "Metadatos Sidecar", + "sidecar_job_description": "Descubrir ou sincronizar metadatos sidecar desde o sistema de ficheiros", + "slideshow_duration_description": "Número de segundos para mostrar cada imaxe", + "smart_search_job_description": "Executar aprendizaxe automática nos activos para soportar a busca intelixente", + "storage_template_date_time_description": "A marca de tempo de creación do activo úsase para a información de data e hora", + "storage_template_date_time_sample": "Tempo de mostra {date}", + "storage_template_enable_description": "Activar o motor de modelos de almacenamento", + "storage_template_hash_verification_enabled": "Verificación de hash activada", + "storage_template_hash_verification_enabled_description": "Activa a verificación de hash, non desactives isto a menos que esteas seguro das implicacións", + "storage_template_migration": "Migración do modelo de almacenamento", + "storage_template_migration_description": "Aplicar o {template} actual aos activos cargados previamente", + "storage_template_migration_info": "O modelo de almacenamento converterá todas as extensións a minúsculas. Os cambios no modelo só se aplicarán aos activos novos. Para aplicar retroactivamente o modelo aos activos cargados previamente, execute o {job}.", + "storage_template_migration_job": "Traballo de Migración do Modelo de Almacenamento", + "storage_template_more_details": "Para máis detalles sobre esta función, consulte o Modelo de Almacenamento e as súas implicacións", + "storage_template_onboarding_description": "Cando estea activada, esta función autoorganizará os ficheiros baseándose nun modelo definido polo usuario. Debido a problemas de estabilidade, a función desactivouse por defecto. Para obter máis información, consulte a documentación.", + "storage_template_path_length": "Límite aproximado da lonxitude da ruta: {length, number}/{limit, number}", + "storage_template_settings": "Modelo de Almacenamento", + "storage_template_settings_description": "Xestionar a estrutura de cartafoles e o nome de ficheiro do activo cargado", + "storage_template_user_label": "{label} é a Etiqueta de Almacenamento do usuario", + "system_settings": "Configuración do Sistema", + "tag_cleanup_job": "Limpeza de etiquetas", + "template_email_available_tags": "Podes usar as seguintes variables no teu modelo: {tags}", + "template_email_if_empty": "Se o modelo está baleiro, usarase o correo electrónico predeterminado.", + "template_email_invite_album": "Modelo de Invitación a Álbum", + "template_email_preview": "Vista previa", + "template_email_settings": "Modelos de Correo Electrónico", + "template_email_settings_description": "Xestionar modelos personalizados de notificación por correo electrónico", + "template_email_update_album": "Modelo de Actualización de Álbum", + "template_email_welcome": "Modelo de correo electrónico de benvida", + "template_settings": "Modelos de Notificación", + "template_settings_description": "Xestionar modelos personalizados para notificacións.", + "theme_custom_css_settings": "CSS Personalizado", + "theme_custom_css_settings_description": "As Follas de Estilo en Cascada permiten personalizar o deseño de Immich.", + "theme_settings": "Configuración do Tema", + "theme_settings_description": "Xestionar a personalización da interface web de Immich", + "these_files_matched_by_checksum": "Estes ficheiros coinciden polas súas sumas de verificación", + "thumbnail_generation_job": "Xerar Miniaturas", + "thumbnail_generation_job_description": "Xerar miniaturas grandes, pequenas e borrosas para cada activo, así como miniaturas para cada persoa", + "transcoding_acceleration_api": "API de aceleración", + "transcoding_acceleration_api_description": "A API que interactuará co teu dispositivo para acelerar a transcodificación. Esta configuración é de 'mellor esforzo': recurrirá á transcodificación por software en caso de fallo. VP9 pode funcionar ou non dependendo do teu hardware.", + "transcoding_acceleration_nvenc": "NVENC (require GPU NVIDIA)", + "transcoding_acceleration_qsv": "Quick Sync (require CPU Intel de 7ª xeración ou posterior)", + "transcoding_acceleration_rkmpp": "RKMPP (só en SOCs Rockchip)", + "transcoding_acceleration_vaapi": "VAAPI", + "transcoding_accepted_audio_codecs": "Códecs de audio aceptados", + "transcoding_accepted_audio_codecs_description": "Seleccione que códecs de audio non necesitan ser transcodificados. Só se usa para certas políticas de transcodificación.", + "transcoding_accepted_containers": "Contedores aceptados", + "transcoding_accepted_containers_description": "Seleccione que formatos de contedor non necesitan ser remuxados a MP4. Só se usa para certas políticas de transcodificación.", + "transcoding_accepted_video_codecs": "Códecs de vídeo aceptados", + "transcoding_accepted_video_codecs_description": "Seleccione que códecs de vídeo non necesitan ser transcodificados. Só se usa para certas políticas de transcodificación.", + "transcoding_advanced_options_description": "Opcións que a maioría dos usuarios non deberían necesitar cambiar", + "transcoding_audio_codec": "Códec de audio", + "transcoding_audio_codec_description": "Opus é a opción de maior calidade, pero ten menor compatibilidade con dispositivos ou software antigos.", + "transcoding_bitrate_description": "Vídeos cun bitrate superior ao máximo ou que non estean nun formato aceptado", + "transcoding_codecs_learn_more": "Para saber máis sobre a terminoloxía usada aquí, consulte a documentación de FFmpeg para códec H.264, códec HEVC e códec VP9.", + "transcoding_constant_quality_mode": "Modo de calidade constante", + "transcoding_constant_quality_mode_description": "ICQ é mellor que CQP, pero algúns dispositivos de aceleración por hardware non admiten este modo. Establecer esta opción preferirá o modo especificado ao usar codificación baseada na calidade. Ignorado por NVENC xa que non admite ICQ.", + "transcoding_constant_rate_factor": "Factor de taxa constante (-crf)", + "transcoding_constant_rate_factor_description": "Nivel de calidade do vídeo. Valores típicos son 23 para H.264, 28 para HEVC, 31 para VP9 e 35 para AV1. Máis baixo é mellor, pero produce ficheiros máis grandes.", + "transcoding_disabled_description": "Non transcodificar ningún vídeo, pode romper a reprodución nalgúns clientes", + "transcoding_encoding_options": "Opcións de Codificación", + "transcoding_encoding_options_description": "Establecer códecs, resolución, calidade e outras opcións para os vídeos codificados", + "transcoding_hardware_acceleration": "Aceleración por Hardware", + "transcoding_hardware_acceleration_description": "Experimental; moito máis rápido, pero terá menor calidade co mesmo bitrate", + "transcoding_hardware_decoding": "Decodificación por hardware", + "transcoding_hardware_decoding_setting_description": "Activa a aceleración de extremo a extremo en lugar de só acelerar a codificación. Pode non funcionar en todos os vídeos.", + "transcoding_hevc_codec": "Códec HEVC", + "transcoding_max_b_frames": "Máximo de B-frames", + "transcoding_max_b_frames_description": "Valores máis altos melloran a eficiencia da compresión, pero ralentizan a codificación. Pode non ser compatible coa aceleración por hardware en dispositivos máis antigos. 0 desactiva os B-frames, mentres que -1 establece este valor automaticamente.", + "transcoding_max_bitrate": "Bitrate máximo", + "transcoding_max_bitrate_description": "Establecer un bitrate máximo pode facer que os tamaños dos ficheiros sexan máis predicibles a un custo menor para a calidade. A 720p, os valores típicos son 2600 kbit/s para VP9 ou HEVC, ou 4500 kbit/s para H.264. Desactivado se se establece en 0.", + "transcoding_max_keyframe_interval": "Intervalo máximo de fotogramas clave", + "transcoding_max_keyframe_interval_description": "Establece a distancia máxima de fotogramas entre fotogramas clave. Valores máis baixos empeoran a eficiencia da compresión, pero melloran os tempos de busca e poden mellorar a calidade en escenas con movemento rápido. 0 establece este valor automaticamente.", + "transcoding_optimal_description": "Vídeos cunha resolución superior á obxectivo ou que non estean nun formato aceptado", + "transcoding_policy": "Política de Transcodificación", + "transcoding_policy_description": "Establecer cando se transcodificará un vídeo", + "transcoding_preferred_hardware_device": "Dispositivo de hardware preferido", + "transcoding_preferred_hardware_device_description": "Aplícase só a VAAPI e QSV. Establece o nodo dri usado para a transcodificación por hardware.", + "transcoding_preset_preset": "Preaxuste (-preset)", + "transcoding_preset_preset_description": "Velocidade de compresión. Preaxustes máis lentos producen ficheiros máis pequenos e aumentan a calidade ao apuntar a un certo bitrate. VP9 ignora velocidades superiores a 'faster'.", + "transcoding_reference_frames": "Fotogramas de referencia", + "transcoding_reference_frames_description": "O número de fotogramas aos que facer referencia ao comprimir un fotograma dado. Valores máis altos melloran a eficiencia da compresión, pero ralentizan a codificación. 0 establece este valor automaticamente.", + "transcoding_required_description": "Só vídeos que non estean nun formato aceptado", + "transcoding_settings": "Configuración da Transcodificación de Vídeo", + "transcoding_settings_description": "Xestionar que vídeos transcodificar e como procesalos", + "transcoding_target_resolution": "Resolución obxectivo", + "transcoding_target_resolution_description": "Resolucións máis altas poden preservar máis detalles pero tardan máis en codificarse, teñen tamaños de ficheiro máis grandes e poden reducir a capacidade de resposta da aplicación.", + "transcoding_temporal_aq": "AQ Temporal", + "transcoding_temporal_aq_description": "Aplícase só a NVENC. Aumenta a calidade de escenas de alto detalle e baixo movemento. Pode non ser compatible con dispositivos máis antigos.", + "transcoding_threads": "Fíos", + "transcoding_threads_description": "Valores máis altos levan a unha codificación máis rápida, pero deixan menos marxe para que o servidor procese outras tarefas mentres está activo. Este valor non debería ser maior que o número de núcleos da CPU. Maximiza a utilización se se establece en 0.", + "transcoding_tone_mapping": "Mapeo de tons", + "transcoding_tone_mapping_description": "Intenta preservar a aparencia dos vídeos HDR cando se converten a SDR. Cada algoritmo fai diferentes compromisos para cor, detalle e brillo. Hable preserva o detalle, Mobius preserva a cor e Reinhard preserva o brillo.", + "transcoding_transcode_policy": "Política de transcodificación", + "transcoding_transcode_policy_description": "Política para cando un vídeo debe ser transcodificado. Os vídeos HDR sempre serán transcodificados (excepto se a transcodificación está desactivada).", + "transcoding_two_pass_encoding": "Codificación en dous pasos", + "transcoding_two_pass_encoding_setting_description": "Transcodificar en dous pasos para producir vídeos codificados mellor. Cando o bitrate máximo está activado (requirido para que funcione con H.264 e HEVC), este modo usa un rango de bitrate baseado no bitrate máximo e ignora CRF. Para VP9, pódese usar CRF se o bitrate máximo está desactivado.", + "transcoding_video_codec": "Códec de vídeo", + "transcoding_video_codec_description": "VP9 ten alta eficiencia e compatibilidade web, pero tarda máis en transcodificarse. HEVC ten un rendemento similar, pero ten menor compatibilidade web. H.264 é amplamente compatible e rápido de transcodificar, pero produce ficheiros moito máis grandes. AV1 é o códec máis eficiente pero carece de soporte en dispositivos máis antigos.", + "trash_enabled_description": "Activar funcións do Lixo", + "trash_number_of_days": "Número de días", + "trash_number_of_days_description": "Número de días para manter os activos no lixo antes de eliminalos permanentemente", + "trash_settings": "Configuración do Lixo", + "trash_settings_description": "Xestionar a configuración do lixo", + "untracked_files": "Ficheiros non rastrexados", + "untracked_files_description": "Estes ficheiros non son rastrexados pola aplicación. Poden ser o resultado de movementos fallidos, cargas interrompidas ou deixados atrás debido a un erro", + "user_cleanup_job": "Limpeza de usuarios", + "user_delete_delay": "A conta e os activos de {user} programaranse para a súa eliminación permanente en {delay, plural, one {# día} other {# días}}.", + "user_delete_delay_settings": "Atraso na eliminación", + "user_delete_delay_settings_description": "Número de días despois da eliminación para eliminar permanentemente a conta e os activos dun usuario. O traballo de eliminación de usuarios execútase á medianoite para comprobar os usuarios que están listos para a eliminación. Os cambios nesta configuración avaliaranse na próxima execución.", + "user_delete_immediately": "A conta e os activos de {user} poranse en cola para a súa eliminación permanente inmediatamente.", + "user_delete_immediately_checkbox": "Poñer en cola o usuario e os activos para a súa eliminación inmediata", + "user_management": "Xestión de Usuarios", + "user_password_has_been_reset": "Restableceuse o contrasinal do usuario:", + "user_password_reset_description": "Proporcione o contrasinal temporal ao usuario e infórmelle de que necesitará cambiar o contrasinal no seu próximo inicio de sesión.", + "user_restore_description": "Restaurarase a conta de {user}.", + "user_restore_scheduled_removal": "Restaurar usuario - eliminación programada o {date, date, long}", + "user_settings": "Configuración do Usuario", + "user_settings_description": "Xestionar a configuración do usuario", + "user_successfully_removed": "Usuario {email} eliminado correctamente.", + "version_check_enabled_description": "Activar comprobación de versión", + "version_check_implications": "A función de comprobación de versión depende da comunicación periódica con github.com", + "version_check_settings": "Comprobación de Versión", + "version_check_settings_description": "Activar/desactivar a notificación de nova versión", + "video_conversion_job": "Transcodificar vídeos", + "video_conversion_job_description": "Transcodificar vídeos para unha maior compatibilidade con navegadores e dispositivos" }, - "advanced_settings_log_level_title": "Log level: {}", - "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", - "advanced_settings_prefer_remote_title": "Prefer remote images", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates (EXPERIMENTAL)", - "advanced_settings_tile_subtitle": "Advanced user's settings", - "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", - "advanced_settings_troubleshooting_title": "Troubleshooting", - "album_info_card_backup_album_excluded": "EXCLUDED", - "album_info_card_backup_album_included": "INCLUDED", - "album_thumbnail_card_item": "1 item", - "album_thumbnail_card_items": "{} items", - "album_thumbnail_card_shared": " · Shared", - "album_thumbnail_shared_by": "Shared by {}", - "album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?", - "album_viewer_appbar_share_err_delete": "Failed to delete album", - "album_viewer_appbar_share_err_leave": "Failed to leave album", - "album_viewer_appbar_share_err_remove": "There are problems in removing assets from album", - "album_viewer_appbar_share_err_title": "Failed to change album title", - "album_viewer_appbar_share_leave": "Leave album", - "album_viewer_appbar_share_to": "Share To", - "album_viewer_page_share_add_users": "Add users", - "albums": "Albums", - "all": "All", - "app_bar_signout_dialog_content": "Are you sure you want to sign out?", - "app_bar_signout_dialog_ok": "Yes", - "app_bar_signout_dialog_title": "Sign out", - "archive_page_no_archived_assets": "No archived assets found", - "archive_page_title": "Archive ({})", - "archived": "Archived", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", - "asset_list_group_by_sub_title": "Group by", - "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", - "asset_list_layout_settings_group_automatically": "Automatic", - "asset_list_layout_settings_group_by": "Group assets by", - "asset_list_layout_settings_group_by_month_day": "Month + day", - "asset_list_layout_sub_title": "Layout", - "asset_list_settings_subtitle": "Photo grid layout settings", - "asset_list_settings_title": "Photo Grid", - "asset_restored_successfully": "Asset restored successfully", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", - "asset_viewer_settings_title": "Asset Viewer", - "assets_deleted_permanently": "{} asset(s) deleted permanently", - "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", - "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", - "assets_restored_successfully": "{} asset(s) restored successfully", - "assets_trashed": "{} asset(s) trashed", - "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", - "backup_album_selection_page_albums_device": "Albums on device ({})", - "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", - "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", - "backup_album_selection_page_select_albums": "Select albums", - "backup_album_selection_page_selection_info": "Selection Info", - "backup_album_selection_page_total_assets": "Total unique assets", - "backup_all": "All", - "backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…", - "backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…", - "backup_background_service_current_upload_notification": "Uploading {}", - "backup_background_service_default_notification": "Checking for new assets…", - "backup_background_service_error_title": "Backup error", - "backup_background_service_in_progress_notification": "Backing up your assets…", - "backup_background_service_upload_failure_notification": "Failed to upload {}", - "backup_controller_page_albums": "Backup Albums", - "backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.", - "backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled", - "backup_controller_page_background_app_refresh_enable_button_text": "Go to settings", - "backup_controller_page_background_battery_info_link": "Show me how", - "backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.", - "backup_controller_page_background_battery_info_ok": "OK", - "backup_controller_page_background_battery_info_title": "Battery optimizations", - "backup_controller_page_background_charging": "Only while charging", - "backup_controller_page_background_configure_error": "Failed to configure the background service", - "backup_controller_page_background_delay": "Delay new assets backup: {}", - "backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app", - "backup_controller_page_background_is_off": "Automatic background backup is off", - "backup_controller_page_background_is_on": "Automatic background backup is on", - "backup_controller_page_background_turn_off": "Turn off background service", - "backup_controller_page_background_turn_on": "Turn on background service", - "backup_controller_page_background_wifi": "Only on WiFi", - "backup_controller_page_backup": "Backup", - "backup_controller_page_backup_selected": "Selected: ", - "backup_controller_page_backup_sub": "Backed up photos and videos", - "backup_controller_page_created": "Created on: {}", - "backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.", - "backup_controller_page_excluded": "Excluded: ", - "backup_controller_page_failed": "Failed ({})", - "backup_controller_page_filename": "File name: {} [{}]", + "admin_email": "Correo electrónico do administrador", + "admin_password": "Contrasinal do administrador", + "administration": "Administración", + "advanced": "Avanzado", + "advanced_settings_enable_alternate_media_filter_subtitle": "Usa esta opción para filtrar medios durante a sincronización baseándose en criterios alternativos. Só proba isto se tes problemas coa aplicación detectando todos os álbums.", + "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Usar filtro alternativo de sincronización de álbums do dispositivo", + "advanced_settings_log_level_title": "Nivel de rexistro: {}", + "advanced_settings_prefer_remote_subtitle": "Algúns dispositivos son extremadamente lentos para cargar miniaturas de activos no dispositivo. Active esta configuración para cargar imaxes remotas no seu lugar.", + "advanced_settings_prefer_remote_title": "Preferir imaxes remotas", + "advanced_settings_proxy_headers_subtitle": "Definir cabeceiras de proxy que Immich debería enviar con cada solicitude de rede", + "advanced_settings_proxy_headers_title": "Cabeceiras de Proxy", + "advanced_settings_self_signed_ssl_subtitle": "Omite a verificación do certificado SSL para o punto final do servidor. Requirido para certificados autofirmados.", + "advanced_settings_self_signed_ssl_title": "Permitir certificados SSL autofirmados", + "advanced_settings_sync_remote_deletions_subtitle": "Eliminar ou restaurar automaticamente un activo neste dispositivo cando esa acción se realiza na web", + "advanced_settings_sync_remote_deletions_title": "Sincronizar eliminacións remotas [EXPERIMENTAL]", + "advanced_settings_tile_subtitle": "Configuración de usuario avanzado", + "advanced_settings_troubleshooting_subtitle": "Activar funcións adicionais para a resolución de problemas", + "advanced_settings_troubleshooting_title": "Resolución de problemas", + "age_months": "Idade {months, plural, one {# mes} other {# meses}}", + "age_year_months": "Idade 1 ano, {months, plural, one {# mes} other {# meses}}", + "age_years": "{years, plural, other {Idade #}}", + "album_added": "Álbum engadido", + "album_added_notification_setting_description": "Recibir unha notificación por correo electrónico cando sexas engadido a un álbum compartido", + "album_cover_updated": "Portada do álbum actualizada", + "album_delete_confirmation": "Estás seguro de que queres eliminar o álbum {album}?", + "album_delete_confirmation_description": "Se este álbum está compartido, outros usuarios non poderán acceder a el.", + "album_info_card_backup_album_excluded": "EXCLUÍDO", + "album_info_card_backup_album_included": "INCLUÍDO", + "album_info_updated": "Información do álbum actualizada", + "album_leave": "Saír do álbum?", + "album_leave_confirmation": "Estás seguro de que queres saír de {album}?", + "album_name": "Nome do Álbum", + "album_options": "Opcións do álbum", + "album_remove_user": "Eliminar usuario?", + "album_remove_user_confirmation": "Estás seguro de que queres eliminar a {user}?", + "album_share_no_users": "Parece que compartiches este álbum con todos os usuarios ou non tes ningún usuario co que compartir.", + "album_thumbnail_card_item": "1 elemento", + "album_thumbnail_card_items": "{} elementos", + "album_thumbnail_card_shared": " · Compartido", + "album_thumbnail_shared_by": "Compartido por {}", + "album_updated": "Álbum actualizado", + "album_updated_setting_description": "Recibir unha notificación por correo electrónico cando un álbum compartido teña novos activos", + "album_user_left": "Saíu de {album}", + "album_user_removed": "Eliminado {user}", + "album_viewer_appbar_delete_confirm": "Estás seguro de que queres eliminar este álbum da túa conta?", + "album_viewer_appbar_share_err_delete": "Erro ao eliminar o álbum", + "album_viewer_appbar_share_err_leave": "Erro ao saír do álbum", + "album_viewer_appbar_share_err_remove": "Hai problemas ao eliminar activos do álbum", + "album_viewer_appbar_share_err_title": "Erro ao cambiar o título do álbum", + "album_viewer_appbar_share_leave": "Saír do álbum", + "album_viewer_appbar_share_to": "Compartir con", + "album_viewer_page_share_add_users": "Engadir usuarios", + "album_with_link_access": "Permitir que calquera persoa coa ligazón vexa fotos e persoas neste álbum.", + "albums": "Álbums", + "albums_count": "{count, plural, one {{count, number} Álbum} other {{count, number} Álbums}}", + "all": "Todo", + "all_albums": "Todos os álbums", + "all_people": "Todas as persoas", + "all_videos": "Todos os vídeos", + "allow_dark_mode": "Permitir modo escuro", + "allow_edits": "Permitir edicións", + "allow_public_user_to_download": "Permitir que o usuario público descargue", + "allow_public_user_to_upload": "Permitir que o usuario público cargue", + "alt_text_qr_code": "Imaxe de código QR", + "anti_clockwise": "Sentido antihorario", + "api_key": "Chave API", + "api_key_description": "Este valor só se mostrará unha vez. Asegúrese de copialo antes de pechar a xanela.", + "api_key_empty": "O nome da súa chave API non pode estar baleiro", + "api_keys": "Chaves API", + "app_bar_signout_dialog_content": "Estás seguro de que queres pechar sesión?", + "app_bar_signout_dialog_ok": "Si", + "app_bar_signout_dialog_title": "Pechar sesión", + "app_settings": "Configuración da Aplicación", + "appears_in": "Aparece en", + "archive": "Arquivo", + "archive_or_unarchive_photo": "Arquivar ou desarquivar foto", + "archive_page_no_archived_assets": "Non se atoparon activos arquivados", + "archive_page_title": "Arquivo ({})", + "archive_size": "Tamaño do arquivo", + "archive_size_description": "Configurar o tamaño do arquivo para descargas (en GiB)", + "archived": "Arquivado", + "archived_count": "{count, plural, other {Arquivados #}}", + "are_these_the_same_person": "Son estas a mesma persoa?", + "are_you_sure_to_do_this": "Estás seguro de que queres facer isto?", + "asset_action_delete_err_read_only": "Non se poden eliminar activo(s) de só lectura, omitindo", + "asset_action_share_err_offline": "Non se poden obter activo(s) fóra de liña, omitindo", + "asset_added_to_album": "Engadido ao álbum", + "asset_adding_to_album": "Engadindo ao álbum…", + "asset_description_updated": "A descrición do activo actualizouse", + "asset_filename_is_offline": "O activo {filename} está fóra de liña", + "asset_has_unassigned_faces": "O activo ten caras non asignadas", + "asset_hashing": "Calculando hash…", + "asset_list_group_by_sub_title": "Agrupar por", + "asset_list_layout_settings_dynamic_layout_title": "Deseño dinámico", + "asset_list_layout_settings_group_automatically": "Automático", + "asset_list_layout_settings_group_by": "Agrupar activos por", + "asset_list_layout_settings_group_by_month_day": "Mes + día", + "asset_list_layout_sub_title": "Deseño", + "asset_list_settings_subtitle": "Configuración do deseño da grella de fotos", + "asset_list_settings_title": "Grella de Fotos", + "asset_offline": "Activo Fóra de Liña", + "asset_offline_description": "Este activo externo xa non se atopa no disco. Por favor, contacta co teu administrador de Immich para obter axuda.", + "asset_restored_successfully": "Activo restaurado correctamente", + "asset_skipped": "Omitido", + "asset_skipped_in_trash": "No lixo", + "asset_uploaded": "Subido", + "asset_uploading": "Subindo…", + "asset_viewer_settings_subtitle": "Xestionar a túa configuración do visor da galería", + "asset_viewer_settings_title": "Visor de Activos", + "assets": "Activos", + "assets_added_count": "Engadido {count, plural, one {# activo} other {# activos}}", + "assets_added_to_album_count": "Engadido {count, plural, one {# activo} other {# activos}} ao álbum", + "assets_added_to_name_count": "Engadido {count, plural, one {# activo} other {# activos}} a {hasName, select, true {{name}} other {novo álbum}}", + "assets_count": "{count, plural, one {# activo} other {# activos}}", + "assets_deleted_permanently": "{} activo(s) eliminado(s) permanentemente", + "assets_deleted_permanently_from_server": "{} activo(s) eliminado(s) permanentemente do servidor Immich", + "assets_moved_to_trash_count": "Movido {count, plural, one {# activo} other {# activos}} ao lixo", + "assets_permanently_deleted_count": "Eliminados permanentemente {count, plural, one {# activo} other {# activos}}", + "assets_removed_count": "Eliminados {count, plural, one {# activo} other {# activos}}", + "assets_removed_permanently_from_device": "{} activo(s) eliminado(s) permanentemente do teu dispositivo", + "assets_restore_confirmation": "Estás seguro de que queres restaurar todos os seus activos no lixo? Non podes desfacer esta acción! Ten en conta que calquera activo fóra de liña non pode ser restaurado desta maneira.", + "assets_restored_count": "Restaurados {count, plural, one {# activo} other {# activos}}", + "assets_restored_successfully": "{} activo(s) restaurado(s) correctamente", + "assets_trashed": "{} activo(s) movido(s) ao lixo", + "assets_trashed_count": "Movido {count, plural, one {# activo} other {# activos}} ao lixo", + "assets_trashed_from_server": "{} activo(s) movido(s) ao lixo desde o servidor Immich", + "assets_were_part_of_album_count": "{count, plural, one {O activo xa era} other {Os activos xa eran}} parte do álbum", + "authorized_devices": "Dispositivos Autorizados", + "automatic_endpoint_switching_subtitle": "Conectar localmente a través de Wi-Fi designada cando estea dispoñible e usar conexións alternativas noutros lugares", + "automatic_endpoint_switching_title": "Cambio automático de URL", + "back": "Atrás", + "back_close_deselect": "Atrás, pechar ou deseleccionar", + "background_location_permission": "Permiso de ubicación en segundo plano", + "background_location_permission_content": "Para cambiar de rede cando se executa en segundo plano, Immich debe ter *sempre* acceso á ubicación precisa para que a aplicación poida ler o nome da rede Wi-Fi", + "backup_album_selection_page_albums_device": "Álbums no dispositivo ({})", + "backup_album_selection_page_albums_tap": "Tocar para incluír, dobre toque para excluír", + "backup_album_selection_page_assets_scatter": "Os activos poden dispersarse por varios álbums. Polo tanto, os álbums poden incluírse ou excluírse durante o proceso de copia de seguridade.", + "backup_album_selection_page_select_albums": "Seleccionar álbums", + "backup_album_selection_page_selection_info": "Información da selección", + "backup_album_selection_page_total_assets": "Total de activos únicos", + "backup_all": "Todo", + "backup_background_service_backup_failed_message": "Erro ao facer copia de seguridade dos activos. Reintentando…", + "backup_background_service_connection_failed_message": "Erro ao conectar co servidor. Reintentando…", + "backup_background_service_current_upload_notification": "Subindo {}", + "backup_background_service_default_notification": "Comprobando novos activos…", + "backup_background_service_error_title": "Erro na copia de seguridade", + "backup_background_service_in_progress_notification": "Facendo copia de seguridade dos teus activos…", + "backup_background_service_upload_failure_notification": "Erro ao subir {}", + "backup_controller_page_albums": "Álbums da Copia de Seguridade", + "backup_controller_page_background_app_refresh_disabled_content": "Active a actualización de aplicacións en segundo plano en Axustes > Xeral > Actualización en segundo plano para usar a copia de seguridade en segundo plano.", + "backup_controller_page_background_app_refresh_disabled_title": "Actualización de aplicacións en segundo plano desactivada", + "backup_controller_page_background_app_refresh_enable_button_text": "Ir a axustes", + "backup_controller_page_background_battery_info_link": "Móstrame como", + "backup_controller_page_background_battery_info_message": "Para a mellor experiencia de copia de seguridade en segundo plano, desactiva calquera optimización de batería que restrinxa a actividade en segundo plano para Immich.\n\nDado que isto é específico do dispositivo, busque a información requirida para o fabricante do teu dispositivo.", + "backup_controller_page_background_battery_info_ok": "Aceptar", + "backup_controller_page_background_battery_info_title": "Optimizacións da batería", + "backup_controller_page_background_charging": "Só mentres se carga", + "backup_controller_page_background_configure_error": "Erro ao configurar o servizo en segundo plano", + "backup_controller_page_background_delay": "Atrasar copia de seguridade de novos activos: {}", + "backup_controller_page_background_description": "Active o servizo en segundo plano para facer copia de seguridade automaticamente de calquera activo novo sen necesidade de abrir a aplicación", + "backup_controller_page_background_is_off": "A copia de seguridade automática en segundo plano está desactivada", + "backup_controller_page_background_is_on": "A copia de seguridade automática en segundo plano está activada", + "backup_controller_page_background_turn_off": "Desactivar servizo en segundo plano", + "backup_controller_page_background_turn_on": "Activar servizo en segundo plano", + "backup_controller_page_background_wifi": "Só con WiFi", + "backup_controller_page_backup": "Copia de Seguridade", + "backup_controller_page_backup_selected": "Seleccionado: ", + "backup_controller_page_backup_sub": "Fotos e vídeos con copia de seguridade", + "backup_controller_page_created": "Creado o: {}", + "backup_controller_page_desc_backup": "Active a copia de seguridade en primeiro plano para cargar automaticamente novos activos ao servidor ao abrir a aplicación.", + "backup_controller_page_excluded": "Excluído: ", + "backup_controller_page_failed": "Fallado ({})", + "backup_controller_page_filename": "Nome do ficheiro: {} [{}]", "backup_controller_page_id": "ID: {}", - "backup_controller_page_info": "Backup Information", - "backup_controller_page_none_selected": "None selected", - "backup_controller_page_remainder": "Remainder", - "backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection", - "backup_controller_page_server_storage": "Server Storage", - "backup_controller_page_start_backup": "Start Backup", - "backup_controller_page_status_off": "Automatic foreground backup is off", - "backup_controller_page_status_on": "Automatic foreground backup is on", - "backup_controller_page_storage_format": "{} of {} used", - "backup_controller_page_to_backup": "Albums to be backed up", - "backup_controller_page_total_sub": "All unique photos and videos from selected albums", - "backup_controller_page_turn_off": "Turn off foreground backup", - "backup_controller_page_turn_on": "Turn on foreground backup", - "backup_controller_page_uploading_file_info": "Uploading file info", - "backup_err_only_album": "Cannot remove the only album", - "backup_info_card_assets": "assets", - "backup_manual_cancelled": "Cancelled", - "backup_manual_in_progress": "Upload already in progress. Try after some time", - "backup_manual_success": "Success", - "backup_manual_title": "Upload status", - "backup_options_page_title": "Backup options", - "backup_setting_subtitle": "Manage background and foreground upload settings", - "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", - "cache_settings_clear_cache_button": "Clear cache", - "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", - "cache_settings_duplicated_assets_clear_button": "CLEAR", - "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", - "cache_settings_duplicated_assets_title": "Duplicated Assets ({})", - "cache_settings_image_cache_size": "Image cache size ({} assets)", - "cache_settings_statistics_album": "Library thumbnails", - "cache_settings_statistics_assets": "{} assets ({})", - "cache_settings_statistics_full": "Full images", - "cache_settings_statistics_shared": "Shared album thumbnails", - "cache_settings_statistics_thumbnail": "Thumbnails", - "cache_settings_statistics_title": "Cache usage", - "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", - "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", - "cache_settings_tile_subtitle": "Control the local storage behaviour", - "cache_settings_tile_title": "Local Storage", - "cache_settings_title": "Caching Settings", - "cancel": "Cancel", - "canceled": "Canceled", - "change_display_order": "Change display order", - "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", - "change_password_form_new_password": "New Password", - "change_password_form_password_mismatch": "Passwords do not match", - "change_password_form_reenter_new_password": "Re-enter New Password", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove_msg": "Client certificate is removed", - "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate (EXPERIMENTAL)", - "common_create_new_album": "Create new album", - "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.", - "completed": "Completed", - "control_bottom_app_bar_album_info_shared": "{} items · Shared", - "control_bottom_app_bar_create_new_album": "Create new album", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", - "control_bottom_app_bar_edit_location": "Edit Location", - "control_bottom_app_bar_edit_time": "Edit Date & Time", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", - "create_album": "Create album", - "create_album_page_untitled": "Untitled", - "create_new": "CREATE NEW", - "create_shared_album_page_share_add_assets": "ADD ASSETS", - "create_shared_album_page_share_select_photos": "Select Photos", - "crop": "Crop", - "curated_object_page_title": "Things", - "current_server_address": "Current server address", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", - "date_format": "E, LLL d, y • h:mm a", - "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", - "delete_dialog_ok_force": "Delete Anyway", - "delete_dialog_title": "Delete Permanently", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", - "delete_shared_link_dialog_title": "Delete Shared Link", - "description_input_hint_text": "Add description...", - "description_input_submit_error": "Error updating description, check the log for more details", - "download_canceled": "Download canceled", - "download_complete": "Download complete", - "download_enqueue": "Download enqueued", - "download_error": "Download Error", - "download_failed": "Download failed", - "download_filename": "file: {}", - "download_finished": "Download finished", - "download_notfound": "Download not found", - "download_paused": "Download paused", - "download_started": "Download started", - "download_sucess": "Download success", - "download_sucess_android": "The media has been downloaded to DCIM/Immich", - "download_waiting_to_retry": "Waiting to retry", - "downloading": "Downloading...", - "downloading_media": "Downloading media", - "edit_location_dialog_title": "Location", - "end_date": "End date", - "enqueued": "Enqueued", - "enter_wifi_name": "Enter WiFi name", - "error_change_sort_album": "Failed to change album sort order", - "error_saving_image": "Error: {}", - "exif_bottom_sheet_description": "Add Description...", - "exif_bottom_sheet_details": "DETAILS", - "exif_bottom_sheet_location": "LOCATION", - "exif_bottom_sheet_people": "PEOPLE", - "exif_bottom_sheet_person_add_person": "Add name", - "experimental_settings_new_asset_list_subtitle": "Work in progress", - "experimental_settings_new_asset_list_title": "Enable experimental photo grid", - "experimental_settings_subtitle": "Use at your own risk!", + "backup_controller_page_info": "Información da Copia de Seguridade", + "backup_controller_page_none_selected": "Ningún seleccionado", + "backup_controller_page_remainder": "Restante", + "backup_controller_page_remainder_sub": "Fotos e vídeos restantes para facer copia de seguridade da selección", + "backup_controller_page_server_storage": "Almacenamento do Servidor", + "backup_controller_page_start_backup": "Iniciar Copia de Seguridade", + "backup_controller_page_status_off": "A copia de seguridade automática en primeiro plano está desactivada", + "backup_controller_page_status_on": "A copia de seguridade automática en primeiro plano está activada", + "backup_controller_page_storage_format": "{} de {} usado", + "backup_controller_page_to_backup": "Álbums para facer copia de seguridade", + "backup_controller_page_total_sub": "Todas as fotos e vídeos únicos dos álbums seleccionados", + "backup_controller_page_turn_off": "Desactivar copia de seguridade en primeiro plano", + "backup_controller_page_turn_on": "Activar copia de seguridade en primeiro plano", + "backup_controller_page_uploading_file_info": "Subindo información do ficheiro", + "backup_err_only_album": "Non se pode eliminar o único álbum", + "backup_info_card_assets": "activos", + "backup_manual_cancelled": "Cancelado", + "backup_manual_in_progress": "Subida xa en progreso. Intenta despois dun tempo", + "backup_manual_success": "Éxito", + "backup_manual_title": "Estado da subida", + "backup_options_page_title": "Opcións da copia de seguridade", + "backup_setting_subtitle": "Xestionar a configuración de carga en segundo plano e primeiro plano", + "backward": "Atrás", + "birthdate_saved": "Data de nacemento gardada correctamente", + "birthdate_set_description": "A data de nacemento úsase para calcular a idade desta persoa no momento dunha foto.", + "blurred_background": "Fondo borroso", + "bugs_and_feature_requests": "Erros e Solicitudes de Funcións", + "build": "Compilación", + "build_image": "Construír Imaxe", + "bulk_delete_duplicates_confirmation": "Estás seguro de que queres eliminar masivamente {count, plural, one {# activo duplicado} other {# activos duplicados}}? Isto conservará o activo máis grande de cada grupo e eliminará permanentemente todos os demais duplicados. Non pode desfacer esta acción!", + "bulk_keep_duplicates_confirmation": "Estás seguro de que queres conservar {count, plural, one {# activo duplicado} other {# activos duplicados}}? Isto resolverá todos os grupos duplicados sen eliminar nada.", + "bulk_trash_duplicates_confirmation": "Estás seguro de que queres mover masivamente ao lixo {count, plural, one {# activo duplicado} other {# activos duplicados}}? Isto conservará o activo máis grande de cada grupo e moverá ao lixo todos os demais duplicados.", + "buy": "Comprar Immich", + "cache_settings_album_thumbnails": "Miniaturas da páxina da biblioteca ({} activos)", + "cache_settings_clear_cache_button": "Borrar caché", + "cache_settings_clear_cache_button_title": "Borra a caché da aplicación. Isto afectará significativamente o rendemento da aplicación ata que a caché se reconstruíu.", + "cache_settings_duplicated_assets_clear_button": "BORRAR", + "cache_settings_duplicated_assets_subtitle": "Fotos e vídeos que están na lista negra da aplicación", + "cache_settings_duplicated_assets_title": "Activos Duplicados ({})", + "cache_settings_image_cache_size": "Tamaño da caché de imaxes ({} activos)", + "cache_settings_statistics_album": "Miniaturas da biblioteca", + "cache_settings_statistics_assets": "{} activos ({})", + "cache_settings_statistics_full": "Imaxes completas", + "cache_settings_statistics_shared": "Miniaturas de álbums compartidos", + "cache_settings_statistics_thumbnail": "Miniaturas", + "cache_settings_statistics_title": "Uso da caché", + "cache_settings_subtitle": "Controlar o comportamento da caché da aplicación móbil Immich", + "cache_settings_thumbnail_size": "Tamaño da caché de miniaturas ({} activos)", + "cache_settings_tile_subtitle": "Controlar o comportamento do almacenamento local", + "cache_settings_tile_title": "Almacenamento Local", + "cache_settings_title": "Configuración da Caché", + "camera": "Cámara", + "camera_brand": "Marca da cámara", + "camera_model": "Modelo da cámara", + "cancel": "Cancelar", + "cancel_search": "Cancelar busca", + "canceled": "Cancelado", + "cannot_merge_people": "Non se poden fusionar persoas", + "cannot_undo_this_action": "Non pode desfacer esta acción!", + "cannot_update_the_description": "Non se pode actualizar a descrición", + "change_date": "Cambiar data", + "change_display_order": "Cambiar orde de visualización", + "change_expiration_time": "Cambiar hora de caducidade", + "change_location": "Cambiar ubicación", + "change_name": "Cambiar nome", + "change_name_successfully": "Nome cambiado correctamente", + "change_password": "Cambiar Contrasinal", + "change_password_description": "Esta é a primeira vez que inicias sesión no sistema ou solicitouse un cambio do teu contrasinal. Introduza o novo contrasinal a continuación.", + "change_password_form_confirm_password": "Confirmar Contrasinal", + "change_password_form_description": "Ola {name},\n\nEsta é a primeira vez que inicias sesión no sistema ou solicitouse un cambio do teu contrasinal. Introduza o novo contrasinal a continuación.", + "change_password_form_new_password": "Novo Contrasinal", + "change_password_form_password_mismatch": "Os contrasinais non coinciden", + "change_password_form_reenter_new_password": "Reintroducir Novo Contrasinal", + "change_your_password": "Cambiar o teu contrasinal", + "changed_visibility_successfully": "Visibilidade cambiada correctamente", + "check_all": "Marcar todo", + "check_corrupt_asset_backup": "Comprobar copias de seguridade de activos corruptos", + "check_corrupt_asset_backup_button": "Realizar comprobación", + "check_corrupt_asset_backup_description": "Execute esta comprobación só a través de Wi-Fi e unha vez que todos os activos teñan copia de seguridade. O procedemento pode tardar uns minutos.", + "check_logs": "Comprobar Rexistros", + "choose_matching_people_to_merge": "Elixir persoas coincidentes para fusionar", + "city": "Cidade", + "clear": "Limpar", + "clear_all": "Limpar todo", + "clear_all_recent_searches": "Limpar todas as buscas recentes", + "clear_message": "Limpar mensaxe", + "clear_value": "Limpar valor", + "client_cert_dialog_msg_confirm": "Aceptar", + "client_cert_enter_password": "Introducir Contrasinal", + "client_cert_import": "Importar", + "client_cert_import_success_msg": "Certificado de cliente importado", + "client_cert_invalid_msg": "Ficheiro de certificado inválido ou contrasinal incorrecto", + "client_cert_remove_msg": "Certificado de cliente eliminado", + "client_cert_subtitle": "Só admite o formato PKCS12 (.p12, .pfx). A importación/eliminación de certificados só está dispoñible antes de iniciar sesión", + "client_cert_title": "Certificado de Cliente SSL", + "clockwise": "Sentido horario", + "close": "Pechar", + "collapse": "Contraer", + "collapse_all": "Contraer todo", + "color": "Cor", + "color_theme": "Tema de cor", + "comment_deleted": "Comentario eliminado", + "comment_options": "Opcións de comentario", + "comments_and_likes": "Comentarios e Gústames", + "comments_are_disabled": "Os comentarios están desactivados", + "common_create_new_album": "Crear novo álbum", + "common_server_error": "Por favor, comprobe a túa conexión de rede, asegúrache de que o servidor sexa accesible e que as versións da aplicación/servidor sexan compatibles.", + "completed": "Completado", + "confirm": "Confirmar", + "confirm_admin_password": "Confirmar Contrasinal do Administrador", + "confirm_delete_face": "Estás seguro de que queres eliminar a cara de {name} do activo?", + "confirm_delete_shared_link": "Estás seguro de que queres eliminar esta ligazón compartida?", + "confirm_keep_this_delete_others": "Todos os demais activos na pila eliminaranse excepto este activo. Estás seguro de que queres continuar?", + "confirm_password": "Confirmar contrasinal", + "contain": "Conter", + "context": "Contexto", + "continue": "Continuar", + "control_bottom_app_bar_album_info_shared": "{} elementos · Compartidos", + "control_bottom_app_bar_create_new_album": "Crear novo álbum", + "control_bottom_app_bar_delete_from_immich": "Eliminar de Immich", + "control_bottom_app_bar_delete_from_local": "Eliminar do dispositivo", + "control_bottom_app_bar_edit_location": "Editar ubicación", + "control_bottom_app_bar_edit_time": "Editar Data e Hora", + "control_bottom_app_bar_share_link": "Compartir Ligazón", + "control_bottom_app_bar_share_to": "Compartir Con", + "control_bottom_app_bar_trash_from_immich": "Mover ao Lixo", + "copied_image_to_clipboard": "Imaxe copiada ao portapapeis.", + "copied_to_clipboard": "Copiado ao portapapeis!", + "copy_error": "Erro ao copiar", + "copy_file_path": "Copiar ruta do ficheiro", + "copy_image": "Copiar Imaxe", + "copy_link": "Copiar ligazón", + "copy_link_to_clipboard": "Copiar ligazón ao portapapeis", + "copy_password": "Copiar contrasinal", + "copy_to_clipboard": "Copiar ao Portapapeis", + "country": "País", + "cover": "Portada", + "covers": "Portadas", + "create": "Crear", + "create_album": "Crear álbum", + "create_album_page_untitled": "Sen título", + "create_library": "Crear Biblioteca", + "create_link": "Crear ligazón", + "create_link_to_share": "Crear ligazón para compartir", + "create_link_to_share_description": "Permitir que calquera persoa coa ligazón vexa a(s) foto(s) seleccionada(s)", + "create_new": "CREAR NOVO", + "create_new_person": "Crear nova persoa", + "create_new_person_hint": "Asignar activos seleccionados a unha nova persoa", + "create_new_user": "Crear novo usuario", + "create_shared_album_page_share_add_assets": "ENGADIR ACTIVOS", + "create_shared_album_page_share_select_photos": "Seleccionar Fotos", + "create_tag": "Crear etiqueta", + "create_tag_description": "Crear unha nova etiqueta. Para etiquetas aniñadas, introduza a ruta completa da etiqueta incluíndo barras inclinadas.", + "create_user": "Crear usuario", + "created": "Creado", + "crop": "Recortar", + "curated_object_page_title": "Cousas", + "current_device": "Dispositivo actual", + "current_server_address": "Enderezo do servidor actual", + "custom_locale": "Configuración Rexional Personalizada", + "custom_locale_description": "Formatar datas e números baseándose na lingua e a rexión", + "daily_title_text_date": "E, dd MMM", + "daily_title_text_date_year": "E, dd MMM, yyyy", + "dark": "Escuro", + "date_after": "Data posterior a", + "date_and_time": "Data e Hora", + "date_before": "Data anterior a", + "date_format": "E, d LLL, y • H:mm", + "date_of_birth_saved": "Data de nacemento gardada correctamente", + "date_range": "Rango de datas", + "day": "Día", + "deduplicate_all": "Eliminar Duplicados Todos", + "deduplication_criteria_1": "Tamaño da imaxe en bytes", + "deduplication_criteria_2": "Reconto de datos EXIF", + "deduplication_info": "Información de Deduplicación", + "deduplication_info_description": "Para preseleccionar automaticamente activos e eliminar duplicados masivamente, miramos:", + "default_locale": "Configuración Rexional Predeterminada", + "default_locale_description": "Formatar datas e números baseándose na configuración rexional do teu navegador", + "delete": "Eliminar", + "delete_album": "Eliminar álbum", + "delete_api_key_prompt": "Estás seguro de que queres eliminar esta chave API?", + "delete_dialog_alert": "Estes elementos eliminaranse permanentemente de Immich e do teu dispositivo", + "delete_dialog_alert_local": "Estes elementos eliminaranse permanentemente do teu dispositivo pero aínda estarán dispoñibles no servidor Immich", + "delete_dialog_alert_local_non_backed_up": "Algúns dos elementos non teñen copia de seguridade en Immich e eliminaranse permanentemente do teu dispositivo", + "delete_dialog_alert_remote": "Estes elementos eliminaranse permanentemente do servidor Immich", + "delete_dialog_ok_force": "Eliminar Igualmente", + "delete_dialog_title": "Eliminar Permanentemente", + "delete_duplicates_confirmation": "Estás seguro de que queres eliminar permanentemente estes duplicados?", + "delete_face": "Eliminar cara", + "delete_key": "Eliminar chave", + "delete_library": "Eliminar Biblioteca", + "delete_link": "Eliminar ligazón", + "delete_local_dialog_ok_backed_up_only": "Eliminar Só con Copia de Seguridade", + "delete_local_dialog_ok_force": "Eliminar Igualmente", + "delete_others": "Eliminar outros", + "delete_shared_link": "Eliminar ligazón compartida", + "delete_shared_link_dialog_title": "Eliminar Ligazón Compartida", + "delete_tag": "Eliminar etiqueta", + "delete_tag_confirmation_prompt": "Estás seguro de que queres eliminar a etiqueta {tagName}?", + "delete_user": "Eliminar usuario", + "deleted_shared_link": "Ligazón compartida eliminada", + "deletes_missing_assets": "Elimina activos que faltan no disco", + "description": "Descrición", + "description_input_hint_text": "Engadir descrición...", + "description_input_submit_error": "Erro ao actualizar a descrición, comprobe o rexistro para máis detalles", + "details": "Detalles", + "direction": "Dirección", + "disabled": "Desactivado", + "disallow_edits": "Non permitir edicións", + "discord": "Discord", + "discover": "Descubrir", + "dismiss_all_errors": "Descartar todos os erros", + "dismiss_error": "Descartar erro", + "display_options": "Opcións de visualización", + "display_order": "Orde de visualización", + "display_original_photos": "Mostrar fotos orixinais", + "display_original_photos_setting_description": "Preferir mostrar a foto orixinal ao ver un activo en lugar de miniaturas cando o activo orixinal é compatible coa web. Isto pode resultar en velocidades de visualización de fotos máis lentas.", + "do_not_show_again": "Non mostrar esta mensaxe de novo", + "documentation": "Documentación", + "done": "Feito", + "download": "Descargar", + "download_canceled": "Descarga cancelada", + "download_complete": "Descarga completada", + "download_enqueue": "Descarga en cola", + "download_error": "Erro na Descarga", + "download_failed": "Descarga fallada", + "download_filename": "ficheiro: {}", + "download_finished": "Descarga finalizada", + "download_include_embedded_motion_videos": "Vídeos incrustados", + "download_include_embedded_motion_videos_description": "Incluír vídeos incrustados en fotos en movemento como un ficheiro separado", + "download_notfound": "Descarga non atopada", + "download_paused": "Descarga pausada", + "download_settings": "Descarga", + "download_settings_description": "Xestionar configuracións relacionadas coa descarga de activos", + "download_started": "Descarga iniciada", + "download_sucess": "Descarga exitosa", + "download_sucess_android": "Os medios descargáronse en DCIM/Immich", + "download_waiting_to_retry": "Agardando para reintentar", + "downloading": "Descargando", + "downloading_asset_filename": "Descargando activo {filename}", + "downloading_media": "Descargando medios", + "drop_files_to_upload": "Solte ficheiros en calquera lugar para cargar", + "duplicates": "Duplicados", + "duplicates_description": "Resolve cada grupo indicando cales, se os houber, son duplicados", + "duration": "Duración", + "edit": "Editar", + "edit_album": "Editar álbum", + "edit_avatar": "Editar avatar", + "edit_date": "Editar data", + "edit_date_and_time": "Editar data e hora", + "edit_exclusion_pattern": "Editar padrón de exclusión", + "edit_faces": "Editar caras", + "edit_import_path": "Editar ruta de importación", + "edit_import_paths": "Editar Rutas de Importación", + "edit_key": "Editar chave", + "edit_link": "Editar ligazón", + "edit_location": "Editar ubicación", + "edit_location_dialog_title": "Ubicación", + "edit_name": "Editar nome", + "edit_people": "Editar persoas", + "edit_tag": "Editar etiqueta", + "edit_title": "Editar Título", + "edit_user": "Editar usuario", + "edited": "Editado", + "editor": "Editor", + "editor_close_without_save_prompt": "Os cambios non se gardarán", + "editor_close_without_save_title": "Pechar editor?", + "editor_crop_tool_h2_aspect_ratios": "Proporcións de aspecto", + "editor_crop_tool_h2_rotation": "Rotación", + "email": "Correo electrónico", + "empty_folder": "Este cartafol está baleiro", + "empty_trash": "Baleirar lixo", + "empty_trash_confirmation": "Estás seguro de que queres baleirar o lixo? Isto eliminará permanentemente todos os activos no lixo de Immich. Non podes desfacer esta acción!", + "enable": "Activar", + "enabled": "Activado", + "end_date": "Data de fin", + "enqueued": "En cola", + "enter_wifi_name": "Introducir nome da WiFi", + "error": "Erro", + "error_change_sort_album": "Erro ao cambiar a orde de clasificación do álbum", + "error_delete_face": "Erro ao eliminar a cara do activo", + "error_loading_image": "Erro ao cargar a imaxe", + "error_saving_image": "Erro: {}", + "error_title": "Erro - Algo saíu mal", + "errors": { + "cannot_navigate_next_asset": "Non se pode navegar ao seguinte activo", + "cannot_navigate_previous_asset": "Non se pode navegar ao activo anterior", + "cant_apply_changes": "Non se poden aplicar os cambios", + "cant_change_activity": "Non se pode {enabled, select, true {desactivar} other {activar}} a actividade", + "cant_change_asset_favorite": "Non se pode cambiar o favorito do activo", + "cant_change_metadata_assets_count": "Non se poden cambiar os metadatos de {count, plural, one {# activo} other {# activos}}", + "cant_get_faces": "Non se poden obter caras", + "cant_get_number_of_comments": "Non se pode obter o número de comentarios", + "cant_search_people": "Non se poden buscar persoas", + "cant_search_places": "Non se poden buscar lugares", + "cleared_jobs": "Traballos borrados para: {job}", + "error_adding_assets_to_album": "Erro ao engadir activos ao álbum", + "error_adding_users_to_album": "Erro ao engadir usuarios ao álbum", + "error_deleting_shared_user": "Erro ao eliminar o usuario compartido", + "error_downloading": "Erro ao descargar {filename}", + "error_hiding_buy_button": "Erro ao ocultar o botón de compra", + "error_removing_assets_from_album": "Erro ao eliminar activos do álbum, comprobe a consola para máis detalles", + "error_selecting_all_assets": "Erro ao seleccionar todos os activos", + "exclusion_pattern_already_exists": "Este padrón de exclusión xa existe.", + "failed_job_command": "O comando {command} fallou para o traballo: {job}", + "failed_to_create_album": "Erro ao crear o álbum", + "failed_to_create_shared_link": "Erro ao crear a ligazón compartida", + "failed_to_edit_shared_link": "Erro ao editar a ligazón compartida", + "failed_to_get_people": "Erro ao obter persoas", + "failed_to_keep_this_delete_others": "Erro ao conservar este activo e eliminar os outros activos", + "failed_to_load_asset": "Erro ao cargar o activo", + "failed_to_load_assets": "Erro ao cargar activos", + "failed_to_load_people": "Erro ao cargar persoas", + "failed_to_remove_product_key": "Erro ao eliminar a chave do produto", + "failed_to_stack_assets": "Erro ao apilar activos", + "failed_to_unstack_assets": "Erro ao desapilar activos", + "import_path_already_exists": "Esta ruta de importación xa existe.", + "incorrect_email_or_password": "Correo electrónico ou contrasinal incorrectos", + "paths_validation_failed": "{paths, plural, one {# ruta fallou} other {# rutas fallaron}} na validación", + "profile_picture_transparent_pixels": "As imaxes de perfil non poden ter píxeles transparentes. Por favor, faga zoom e/ou mova a imaxe.", + "quota_higher_than_disk_size": "Estableceu unha cota superior ao tamaño do disco", + "repair_unable_to_check_items": "Non se puideron comprobar {count, select, one {elemento} other {elementos}}", + "unable_to_add_album_users": "Non se puideron engadir usuarios ao álbum", + "unable_to_add_assets_to_shared_link": "Non se puideron engadir activos á ligazón compartida", + "unable_to_add_comment": "Non se puido engadir o comentario", + "unable_to_add_exclusion_pattern": "Non se puido engadir o padrón de exclusión", + "unable_to_add_import_path": "Non se puido engadir a ruta de importación", + "unable_to_add_partners": "Non se puideron engadir compañeiros/as", + "unable_to_add_remove_archive": "Non se puido {archived, select, true {eliminar activo do} other {engadir activo ao}} arquivo", + "unable_to_add_remove_favorites": "Non se puido {favorite, select, true {engadir activo a} other {eliminar activo de}} favoritos", + "unable_to_archive_unarchive": "Non se puido {archived, select, true {arquivar} other {desarquivar}}", + "unable_to_change_album_user_role": "Non se puido cambiar o rol do usuario do álbum", + "unable_to_change_date": "Non se puido cambiar a data", + "unable_to_change_favorite": "Non se puido cambiar o favorito do activo", + "unable_to_change_location": "Non se puido cambiar a ubicación", + "unable_to_change_password": "Non se puido cambiar o contrasinal", + "unable_to_change_visibility": "Non se puido cambiar a visibilidade para {count, plural, one {# persoa} other {# persoas}}", + "unable_to_complete_oauth_login": "Non se puido completar o inicio de sesión OAuth", + "unable_to_connect": "Non se puido conectar", + "unable_to_connect_to_server": "Non se puido conectar ao servidor", + "unable_to_copy_to_clipboard": "Non se puido copiar ao portapapeis, asegúrate de acceder á páxina a través de https", + "unable_to_create_admin_account": "Non se puido crear a conta de administrador", + "unable_to_create_api_key": "Non se puido crear unha nova Chave API", + "unable_to_create_library": "Non se puido crear a biblioteca", + "unable_to_create_user": "Non se puido crear o usuario", + "unable_to_delete_album": "Non se puido eliminar o álbum", + "unable_to_delete_asset": "Non se puido eliminar o activo", + "unable_to_delete_assets": "Erro ao eliminar activos", + "unable_to_delete_exclusion_pattern": "Non se puido eliminar o padrón de exclusión", + "unable_to_delete_import_path": "Non se puido eliminar a ruta de importación", + "unable_to_delete_shared_link": "Non se puido eliminar a ligazón compartida", + "unable_to_delete_user": "Non se puido eliminar o usuario", + "unable_to_download_files": "Non se puideron descargar os ficheiros", + "unable_to_edit_exclusion_pattern": "Non se puido editar o padrón de exclusión", + "unable_to_edit_import_path": "Non se puido editar a ruta de importación", + "unable_to_empty_trash": "Non se puido baleirar o lixo", + "unable_to_enter_fullscreen": "Non se puido entrar en pantalla completa", + "unable_to_exit_fullscreen": "Non se puido saír da pantalla completa", + "unable_to_get_comments_number": "Non se puido obter o número de comentarios", + "unable_to_get_shared_link": "Erro ao obter a ligazón compartida", + "unable_to_hide_person": "Non se puido ocultar a persoa", + "unable_to_link_motion_video": "Non se puido ligar o vídeo en movemento", + "unable_to_link_oauth_account": "Non se puido ligar a conta OAuth", + "unable_to_load_album": "Non se puido cargar o álbum", + "unable_to_load_asset_activity": "Non se puido cargar a actividade do activo", + "unable_to_load_items": "Non se puideron cargar os elementos", + "unable_to_load_liked_status": "Non se puido cargar o estado de gustar", + "unable_to_log_out_all_devices": "Non se puido pechar sesión en todos os dispositivos", + "unable_to_log_out_device": "Non se puido pechar sesión no dispositivo", + "unable_to_login_with_oauth": "Non se puido iniciar sesión con OAuth", + "unable_to_play_video": "Non se puido reproducir o vídeo", + "unable_to_reassign_assets_existing_person": "Non se puideron reasignar activos a {name, select, null {unha persoa existente} other {{name}}}", + "unable_to_reassign_assets_new_person": "Non se puideron reasignar activos a unha nova persoa", + "unable_to_refresh_user": "Non se puido actualizar o usuario", + "unable_to_remove_album_users": "Non se puideron eliminar usuarios do álbum", + "unable_to_remove_api_key": "Non se puido eliminar a Chave API", + "unable_to_remove_assets_from_shared_link": "Non se puideron eliminar activos da ligazón compartida", + "unable_to_remove_deleted_assets": "Non se puideron eliminar ficheiros fóra de liña", + "unable_to_remove_library": "Non se puido eliminar a biblioteca", + "unable_to_remove_partner": "Non se puido eliminar o/a compañeiro/a", + "unable_to_remove_reaction": "Non se puido eliminar a reacción", + "unable_to_repair_items": "Non se puideron reparar os elementos", + "unable_to_reset_password": "Non se puido restablecer o contrasinal", + "unable_to_resolve_duplicate": "Non se puido resolver o duplicado", + "unable_to_restore_assets": "Non se puideron restaurar os activos", + "unable_to_restore_trash": "Non se puido restaurar o lixo", + "unable_to_restore_user": "Non se puido restaurar o usuario", + "unable_to_save_album": "Non se puido gardar o álbum", + "unable_to_save_api_key": "Non se puido gardar a Chave API", + "unable_to_save_date_of_birth": "Non se puido gardar a data de nacemento", + "unable_to_save_name": "Non se puido gardar o nome", + "unable_to_save_profile": "Non se puido gardar o perfil", + "unable_to_save_settings": "Non se puido gardar a configuración", + "unable_to_scan_libraries": "Non se puideron escanear as bibliotecas", + "unable_to_scan_library": "Non se puido escanear a biblioteca", + "unable_to_set_feature_photo": "Non se puido establecer a foto destacada", + "unable_to_set_profile_picture": "Non se puido establecer a imaxe de perfil", + "unable_to_submit_job": "Non se puido enviar o traballo", + "unable_to_trash_asset": "Non se puido mover o activo ao lixo", + "unable_to_unlink_account": "Non se puido desvincular a conta", + "unable_to_unlink_motion_video": "Non se puido desvincular o vídeo en movemento", + "unable_to_update_album_cover": "Non se puido actualizar a portada do álbum", + "unable_to_update_album_info": "Non se puido actualizar a información do álbum", + "unable_to_update_library": "Non se puido actualizar a biblioteca", + "unable_to_update_location": "Non se puido actualizar a ubicación", + "unable_to_update_settings": "Non se puido actualizar a configuración", + "unable_to_update_timeline_display_status": "Non se puido actualizar o estado de visualización da liña de tempo", + "unable_to_update_user": "Non se puido actualizar o usuario", + "unable_to_upload_file": "Non se puido cargar o ficheiro" + }, + "exif": "Exif", + "exif_bottom_sheet_description": "Engadir Descrición...", + "exif_bottom_sheet_details": "DETALLES", + "exif_bottom_sheet_location": "UBICACIÓN", + "exif_bottom_sheet_people": "PERSOAS", + "exif_bottom_sheet_person_add_person": "Engadir nome", + "exif_bottom_sheet_person_age": "Idade {}", + "exif_bottom_sheet_person_age_months": "Idade {} meses", + "exif_bottom_sheet_person_age_year_months": "Idade 1 ano, {} meses", + "exif_bottom_sheet_person_age_years": "Idade {}", + "exit_slideshow": "Saír da Presentación", + "expand_all": "Expandir todo", + "experimental_settings_new_asset_list_subtitle": "Traballo en progreso", + "experimental_settings_new_asset_list_title": "Activar grella de fotos experimental", + "experimental_settings_subtitle": "Use baixo o teu propio risco!", "experimental_settings_title": "Experimental", - "external_network": "External network", - "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", - "failed": "Failed", - "favorites": "Favorites", - "favorites_page_no_favorites": "No favorite assets found", - "filter": "Filter", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", - "grant_permission": "Grant permission", - "haptic_feedback_switch": "Enable haptic feedback", - "haptic_feedback_title": "Haptic Feedback", - "header_settings_add_header_tip": "Add Header", - "header_settings_field_validator_msg": "Value cannot be empty", - "header_settings_header_name_input": "Header name", - "header_settings_header_value_input": "Header value", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", - "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", - "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", - "home_page_add_to_album_success": "Added {added} assets to album {album}.", - "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", - "home_page_archive_err_local": "Can not archive local assets yet, skipping", - "home_page_archive_err_partner": "Can not archive partner assets, skipping", - "home_page_building_timeline": "Building the timeline", - "home_page_delete_err_partner": "Can not delete partner assets, skipping", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", - "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", - "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", - "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", - "home_page_share_err_local": "Can not share local assets via link, skipping", - "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", - "ignore_icloud_photos": "Ignore iCloud photos", - "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", - "image_saved_successfully": "Image saved", - "image_viewer_page_state_provider_download_started": "Download Started", - "image_viewer_page_state_provider_download_success": "Download Success", - "image_viewer_page_state_provider_share_error": "Share Error", - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", - "library": "Library", - "library_page_device_albums": "Albums on Device", - "library_page_new_album": "New album", - "library_page_sort_asset_count": "Number of assets", - "library_page_sort_created": "Created date", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_title": "Album title", - "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", - "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", - "location_picker_choose_on_map": "Choose on map", - "location_picker_latitude_error": "Enter a valid latitude", - "location_picker_latitude_hint": "Enter your latitude here", - "location_picker_longitude_error": "Enter a valid longitude", - "location_picker_longitude_hint": "Enter your longitude here", - "login_disabled": "Login has been disabled", - "login_form_api_exception": "API exception. Please check the server URL and try again.", - "login_form_back_button_text": "Back", - "login_form_email_hint": "youremail@email.com", - "login_form_endpoint_hint": "http://your-server-ip:port", - "login_form_endpoint_url": "Server Endpoint URL", - "login_form_err_http": "Please specify http:// or https://", - "login_form_err_invalid_email": "Invalid Email", - "login_form_err_invalid_url": "Invalid URL", - "login_form_err_leading_whitespace": "Leading whitespace", - "login_form_err_trailing_whitespace": "Trailing whitespace", - "login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL", - "login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server", - "login_form_failed_login": "Error logging you in, check server URL, email and password", - "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", - "login_form_password_hint": "password", - "login_form_save_login": "Stay logged in", - "login_form_server_empty": "Enter a server URL.", - "login_form_server_error": "Could not connect to server.", - "login_password_changed_error": "There was an error updating your password", - "login_password_changed_success": "Password updated successfully", - "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", - "map_cannot_get_user_location": "Cannot get user's location", - "map_location_dialog_yes": "Yes", - "map_location_picker_page_use_location": "Use this location", - "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", - "map_location_service_disabled_title": "Location Service disabled", - "map_no_assets_in_bounds": "No photos in this area", - "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", - "map_no_location_permission_title": "Location Permission denied", - "map_settings_dark_mode": "Dark mode", - "map_settings_date_range_option_day": "Past 24 hours", - "map_settings_date_range_option_days": "Past {} days", - "map_settings_date_range_option_year": "Past year", - "map_settings_date_range_option_years": "Past {} years", - "map_settings_dialog_title": "Map Settings", - "map_settings_include_show_archived": "Include Archived", - "map_settings_include_show_partners": "Include Partners", - "map_settings_only_show_favorites": "Show Favorite Only", - "map_settings_theme_settings": "Map Theme", - "map_zoom_to_see_photos": "Zoom out to see photos", - "memories_all_caught_up": "All caught up", - "memories_check_back_tomorrow": "Check back tomorrow for more memories", - "memories_start_over": "Start Over", - "memories_swipe_to_close": "Swipe up to close", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", + "expire_after": "Caduca despois de", + "expired": "Caducado", + "expires_date": "Caduca o {date}", + "explore": "Explorar", + "explorer": "Explorador", + "export": "Exportar", + "export_as_json": "Exportar como JSON", + "extension": "Extensión", + "external": "Externo", + "external_libraries": "Bibliotecas Externas", + "external_network": "Rede externa", + "external_network_sheet_info": "Cando non estea na rede WiFi preferida, a aplicación conectarase ao servidor a través da primeira das seguintes URLs que poida alcanzar, comezando de arriba a abaixo", + "face_unassigned": "Sen asignar", + "failed": "Fallado", + "failed_to_load_assets": "Erro ao cargar activos", + "failed_to_load_folder": "Erro ao cargar o cartafol", + "favorite": "Favorito", + "favorite_or_unfavorite_photo": "Marcar ou desmarcar como favorito", + "favorites": "Favoritos", + "favorites_page_no_favorites": "Non se atoparon activos favoritos", + "feature_photo_updated": "Foto destacada actualizada", + "features": "Funcións", + "features_setting_description": "Xestionar as funcións da aplicación", + "file_name": "Nome do ficheiro", + "file_name_or_extension": "Nome do ficheiro ou extensión", + "filename": "Nome do ficheiro", + "filetype": "Tipo de ficheiro", + "filter": "Filtro", + "filter_people": "Filtrar persoas", + "filter_places": "Filtrar lugares", + "find_them_fast": "Atópaos rápido por nome coa busca", + "fix_incorrect_match": "Corrixir coincidencia incorrecta", + "folder": "Cartafol", + "folder_not_found": "Cartafol non atopado", + "folders": "Cartafoles", + "folders_feature_description": "Navegar pola vista de cartafoles para as fotos e vídeos no sistema de ficheiros", + "forward": "Adiante", + "general": "Xeral", + "get_help": "Obter Axuda", + "get_wifiname_error": "Non se puido obter o nome da Wi-Fi. Asegúrate de que concedeu os permisos necesarios e está conectado a unha rede Wi-Fi", + "getting_started": "Primeiros Pasos", + "go_back": "Volver", + "go_to_folder": "Ir ao cartafol", + "go_to_search": "Ir á busca", + "grant_permission": "Conceder permiso", + "group_albums_by": "Agrupar álbums por...", + "group_country": "Agrupar por país", + "group_no": "Sen agrupación", + "group_owner": "Agrupar por propietario", + "group_places_by": "Agrupar lugares por...", + "group_year": "Agrupar por ano", + "haptic_feedback_switch": "Activar resposta háptica", + "haptic_feedback_title": "Resposta Háptica", + "has_quota": "Ten cota", + "header_settings_add_header_tip": "Engadir Cabeceira", + "header_settings_field_validator_msg": "O valor non pode estar baleiro", + "header_settings_header_name_input": "Nome da cabeceira", + "header_settings_header_value_input": "Valor da cabeceira", + "headers_settings_tile_subtitle": "Definir cabeceiras de proxy que a aplicación debería enviar con cada solicitude de rede", + "headers_settings_tile_title": "Cabeceiras de proxy personalizadas", + "hi_user": "Ola {name} ({email})", + "hide_all_people": "Ocultar todas as persoas", + "hide_gallery": "Ocultar galería", + "hide_named_person": "Ocultar persoa {name}", + "hide_password": "Ocultar contrasinal", + "hide_person": "Ocultar persoa", + "hide_unnamed_people": "Ocultar persoas sen nome", + "home_page_add_to_album_conflicts": "Engadidos {added} activos ao álbum {album}. {failed} activos xa están no álbum.", + "home_page_add_to_album_err_local": "Non se poden engadir activos locais a álbums aínda, omitindo", + "home_page_add_to_album_success": "Engadidos {added} activos ao álbum {album}.", + "home_page_album_err_partner": "Non se poden engadir activos de compañeiro/a a un álbum aínda, omitindo", + "home_page_archive_err_local": "Non se poden arquivar activos locais aínda, omitindo", + "home_page_archive_err_partner": "Non se poden arquivar activos de compañeiro/a, omitindo", + "home_page_building_timeline": "Construíndo a liña de tempo", + "home_page_delete_err_partner": "Non se poden eliminar activos de compañeiro/a, omitindo", + "home_page_delete_remote_err_local": "Activos locais na selección de eliminación remota, omitindo", + "home_page_favorite_err_local": "Non se poden marcar como favoritos activos locais aínda, omitindo", + "home_page_favorite_err_partner": "Non se poden marcar como favoritos activos de compañeiro/a aínda, omitindo", + "home_page_first_time_notice": "Se esta é a primeira vez que usas a aplicación, asegúrate de elixir un álbum de copia de seguridade para que a liña de tempo poida encherse con fotos e vídeos nel", + "home_page_share_err_local": "Non se poden compartir activos locais mediante ligazón, omitindo", + "home_page_upload_err_limit": "Só se pode cargar un máximo de 30 activos á vez, omitindo", + "host": "Host", + "hour": "Hora", + "ignore_icloud_photos": "Ignorar fotos de iCloud", + "ignore_icloud_photos_description": "As fotos que están almacenadas en iCloud non se cargarán ao servidor Immich", + "image": "Imaxe", + "image_alt_text_date": "{isVideo, select, true {Vídeo} other {Imaxe}} tomado/a o {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Vídeo} other {Imaxe}} tomado/a con {person1} o {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Vídeo} other {Imaxe}} tomado/a con {person1} e {person2} o {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Vídeo} other {Imaxe}} tomado/a con {person1}, {person2} e {person3} o {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Vídeo} other {Imaxe}} tomado/a con {person1}, {person2} e {additionalCount, number} outros/as o {date}", + "image_alt_text_date_place": "{isVideo, select, true {Vídeo} other {Imaxe}} tomado/a en {city}, {country} o {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Vídeo} other {Imaxe}} tomado/a en {city}, {country} con {person1} o {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Vídeo} other {Imaxe}} tomado/a en {city}, {country} con {person1} e {person2} o {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Vídeo} other {Imaxe}} tomado/a en {city}, {country} con {person1}, {person2} e {person3} o {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Vídeo} other {Imaxe}} tomado/a en {city}, {country} con {person1}, {person2} e {additionalCount, number} outros/as o {date}", + "image_saved_successfully": "Imaxe gardada", + "image_viewer_page_state_provider_download_started": "Descarga Iniciada", + "image_viewer_page_state_provider_download_success": "Descarga Exitosa", + "image_viewer_page_state_provider_share_error": "Erro ao Compartir", + "immich_logo": "Logo de Immich", + "immich_web_interface": "Interface Web de Immich", + "import_from_json": "Importar desde JSON", + "import_path": "Ruta de importación", + "in_albums": "En {count, plural, one {# álbum} other {# álbums}}", + "in_archive": "No arquivo", + "include_archived": "Incluír arquivados", + "include_shared_albums": "Incluír álbums compartidos", + "include_shared_partner_assets": "Incluír activos de compañeiro/a compartidos", + "individual_share": "Compartir individual", + "individual_shares": "Compartires individuais", + "info": "Información", + "interval": { + "day_at_onepm": "Todos os días ás 13:00", + "hours": "Cada {hours, plural, one {hora} other {{hours, number} horas}}", + "night_at_midnight": "Todas as noites á medianoite", + "night_at_twoam": "Todas as noites ás 2:00" + }, + "invalid_date": "Data inválida", + "invalid_date_format": "Formato de data inválido", + "invite_people": "Invitar Persoas", + "invite_to_album": "Invitar ao álbum", + "items_count": "{count, plural, one {# elemento} other {# elementos}}", + "jobs": "Traballos", + "keep": "Conservar", + "keep_all": "Conservar Todo", + "keep_this_delete_others": "Conservar este, eliminar outros", + "kept_this_deleted_others": "Conservouse este activo e elimináronse {count, plural, one {# activo} other {# activos}}", + "keyboard_shortcuts": "Atallos de teclado", + "language": "Lingua", + "language_setting_description": "Seleccione a túa lingua preferida", + "last_seen": "Visto por última vez", + "latest_version": "Última Versión", + "latitude": "Latitude", + "leave": "Saír", + "lens_model": "Modelo da lente", + "let_others_respond": "Permitir que outros respondan", + "level": "Nivel", + "library": "Biblioteca", + "library_options": "Opcións da biblioteca", + "library_page_device_albums": "Álbums no Dispositivo", + "library_page_new_album": "Novo álbum", + "library_page_sort_asset_count": "Número de activos", + "library_page_sort_created": "Data de creación", + "library_page_sort_last_modified": "Última modificación", + "library_page_sort_title": "Título do álbum", + "light": "Claro", + "like_deleted": "Gústame eliminado", + "link_motion_video": "Ligar vídeo en movemento", + "link_options": "Opcións da ligazón", + "link_to_oauth": "Ligar a OAuth", + "linked_oauth_account": "Conta OAuth ligada", + "list": "Lista", + "loading": "Cargando", + "loading_search_results_failed": "Erro ao cargar os resultados da busca", + "local_network": "Rede local", + "local_network_sheet_info": "A aplicación conectarase ao servidor a través desta URL cando use a rede Wi-Fi especificada", + "location_permission": "Permiso de ubicación", + "location_permission_content": "Para usar a función de cambio automático, Immich necesita permiso de ubicación precisa para poder ler o nome da rede WiFi actual", + "location_picker_choose_on_map": "Elixir no mapa", + "location_picker_latitude_error": "Introducir unha latitude válida", + "location_picker_latitude_hint": "Introduza a túa latitude aquí", + "location_picker_longitude_error": "Introducir unha lonxitude válida", + "location_picker_longitude_hint": "Introduza a túa lonxitude aquí", + "log_out": "Pechar sesión", + "log_out_all_devices": "Pechar Sesión en Todos os Dispositivos", + "logged_out_all_devices": "Pechouse sesión en todos os dispositivos", + "logged_out_device": "Pechouse sesión no dispositivo", + "login": "Iniciar sesión", + "login_disabled": "O inicio de sesión foi desactivado", + "login_form_api_exception": "Excepción da API. Por favor, comprobe a URL do servidor e inténteo de novo.", + "login_form_back_button_text": "Atrás", + "login_form_email_hint": "oteuemail@email.com", + "login_form_endpoint_hint": "http://ip-do-teu-servidor:porto", + "login_form_endpoint_url": "URL do Punto Final do Servidor", + "login_form_err_http": "Por favor, especifique http:// ou https://", + "login_form_err_invalid_email": "Correo electrónico inválido", + "login_form_err_invalid_url": "URL inválida", + "login_form_err_leading_whitespace": "Espazo en branco inicial", + "login_form_err_trailing_whitespace": "Espazo en branco final", + "login_form_failed_get_oauth_server_config": "Erro ao iniciar sesión usando OAuth, comprobe a URL do servidor", + "login_form_failed_get_oauth_server_disable": "A función OAuth non está dispoñible neste servidor", + "login_form_failed_login": "Erro ao iniciar sesión, comproba a URL do servidor, correo electrónico e contrasinal", + "login_form_handshake_exception": "Houbo unha Excepción de Handshake co servidor. Activa o soporte para certificados autofirmados nas configuracións se estás a usar un certificado autofirmado.", + "login_form_password_hint": "contrasinal", + "login_form_save_login": "Manter sesión iniciada", + "login_form_server_empty": "Introduza unha URL do servidor.", + "login_form_server_error": "Non se puido conectar co servidor.", + "login_has_been_disabled": "O inicio de sesión foi desactivado.", + "login_password_changed_error": "Houbo un erro ao actualizar o teu contrasinal", + "login_password_changed_success": "Contrasinal actualizado correctamente", + "logout_all_device_confirmation": "Estás seguro de que queres pechar sesión en todos os dispositivos?", + "logout_this_device_confirmation": "Estás seguro de que queres pechar sesión neste dispositivo?", + "longitude": "Lonxitude", + "look": "Ollar", + "loop_videos": "Reproducir vídeos en bucle", + "loop_videos_description": "Activar para reproducir automaticamente un vídeo en bucle no visor de detalles.", + "main_branch_warning": "Está a usar unha versión de desenvolvemento; recomendamos encarecidamente usar unha versión de lanzamento!", + "main_menu": "Menú principal", + "make": "Marca", + "manage_shared_links": "Xestionar ligazóns compartidas", + "manage_sharing_with_partners": "Xestionar compartición con compañeiros/as", + "manage_the_app_settings": "Xestionar a configuración da aplicación", + "manage_your_account": "Xestionar a túa conta", + "manage_your_api_keys": "Xestionar as túas claves API", + "manage_your_devices": "Xestionar os teus dispositivos con sesión iniciada", + "manage_your_oauth_connection": "Xestionar a túa conexión OAuth", + "map": "Mapa", + "map_assets_in_bound": "{} foto", + "map_assets_in_bounds": "{} fotos", + "map_cannot_get_user_location": "Non se pode obter a ubicación do usuario", + "map_location_dialog_yes": "Si", + "map_location_picker_page_use_location": "Usar esta ubicación", + "map_location_service_disabled_content": "O servizo de ubicación debe estar activado para mostrar activos da túa ubicación actual. Queres activalo agora?", + "map_location_service_disabled_title": "Servizo de ubicación deshabilitado", + "map_marker_for_images": "Marcador de mapa para imaxes tomadas en {city}, {country}", + "map_marker_with_image": "Marcador de mapa con imaxe", + "map_no_assets_in_bounds": "Non hai fotos nesta área", + "map_no_location_permission_content": "Necesítase permiso de ubicación para mostrar activos da súa ubicación actual. Queres permitilo agora?", + "map_no_location_permission_title": "Permiso de ubicación denegado", + "map_settings": "Configuración do mapa", + "map_settings_dark_mode": "Modo escuro", + "map_settings_date_range_option_day": "Últimas 24 horas", + "map_settings_date_range_option_days": "Últimos {} días", + "map_settings_date_range_option_year": "Último ano", + "map_settings_date_range_option_years": "Últimos {} anos", + "map_settings_dialog_title": "Configuración do Mapa", + "map_settings_include_show_archived": "Incluír Arquivados", + "map_settings_include_show_partners": "Incluír Compañeiros/as", + "map_settings_only_show_favorites": "Mostrar Só Favoritos", + "map_settings_theme_settings": "Tema do Mapa", + "map_zoom_to_see_photos": "Alonxe o zoom para ver fotos", + "matches": "Coincidencias", + "media_type": "Tipo de medio", + "memories": "Recordos", + "memories_all_caught_up": "Todo ao día", + "memories_check_back_tomorrow": "Volva mañá para máis recordos", + "memories_setting_description": "Xestionar o que ves nos teus recordos", + "memories_start_over": "Comezar de novo", + "memories_swipe_to_close": "Deslizar cara arriba para pechar", + "memories_year_ago": "Hai un ano", + "memories_years_ago": "Hai {} anos", + "memory": "Recordo", + "memory_lane_title": "Camiño dos Recordos {title}", + "menu": "Menú", + "merge": "Fusionar", + "merge_people": "Fusionar persoas", + "merge_people_limit": "Só pode fusionar ata 5 caras á vez", + "merge_people_prompt": "Queres fusionar estas persoas? Esta acción é irreversible.", + "merge_people_successfully": "Persoas fusionadas correctamente", + "merged_people_count": "Fusionadas {count, plural, one {# persoa} other {# persoas}}", + "minimize": "Minimizar", + "minute": "Minuto", + "missing": "Faltantes", + "model": "Modelo", + "month": "Mes", "monthly_title_text_date_format": "MMMM y", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", - "my_albums": "My albums", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", - "no_assets_to_show": "No assets to show", - "no_name": "No name", - "not_selected": "Not selected", - "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", - "notification_permission_list_tile_content": "Grant permission to enable notifications.", - "notification_permission_list_tile_enable_button": "Enable Notifications", - "notification_permission_list_tile_title": "Notification Permission", - "on_this_device": "On this device", - "partner_list_user_photos": "{user}'s photos", - "partner_list_view_all": "View all", - "partner_page_empty_message": "Your photos are not yet shared with any partner.", - "partner_page_no_more_users": "No more users to add", - "partner_page_partner_add_failed": "Failed to add partner", - "partner_page_select_partner": "Select partner", - "partner_page_shared_to_title": "Shared to", - "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", - "partners": "Partners", - "paused": "Paused", - "people": "People", - "permission_onboarding_back": "Back", - "permission_onboarding_continue_anyway": "Continue anyway", - "permission_onboarding_get_started": "Get started", - "permission_onboarding_go_to_settings": "Go to settings", - "permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.", - "permission_onboarding_permission_granted": "Permission granted! You are all set.", - "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", - "permission_onboarding_request": "Immich requires permission to view your photos and videos.", - "places": "Places", - "preferences_settings_subtitle": "Manage the app's preferences", - "preferences_settings_title": "Preferences", - "profile_drawer_app_logs": "Logs", - "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", - "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", - "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", + "more": "Máis", + "moved_to_trash": "Movido ao lixo", + "multiselect_grid_edit_date_time_err_read_only": "Non se pode editar a data de activo(s) de só lectura, omitindo", + "multiselect_grid_edit_gps_err_read_only": "Non se pode editar a ubicación de activo(s) de só lectura, omitindo", + "mute_memories": "Silenciar Recordos", + "my_albums": "Os meus álbums", + "name": "Nome", + "name_or_nickname": "Nome ou alcume", + "networking_settings": "Rede", + "networking_subtitle": "Xestionar a configuración do punto final do servidor", + "never": "Nunca", + "new_album": "Novo Álbum", + "new_api_key": "Nova Chave API", + "new_password": "Novo contrasinal", + "new_person": "Nova persoa", + "new_user_created": "Novo usuario creado", + "new_version_available": "NOVA VERSIÓN DISPOÑIBLE", + "newest_first": "Máis recentes primeiro", + "next": "Seguinte", + "next_memory": "Seguinte recordo", + "no": "Non", + "no_albums_message": "Crea un álbum para organizar as túas fotos e vídeos", + "no_albums_with_name_yet": "Parece que aínda non tes ningún álbum con este nome.", + "no_albums_yet": "Parece que aínda non tes ningún álbum.", + "no_archived_assets_message": "Arquiva fotos e vídeos para ocultalos da túa vista de Fotos", + "no_assets_message": "PREMA PARA CARGAR A SÚA PRIMEIRA FOTO", + "no_assets_to_show": "Non hai activos para mostrar", + "no_duplicates_found": "Non se atoparon duplicados.", + "no_exif_info_available": "Non hai información exif dispoñible", + "no_explore_results_message": "Suba máis fotos para explorar a túa colección.", + "no_favorites_message": "Engade favoritos para atopar rapidamente as túas mellores fotos e vídeos", + "no_libraries_message": "Crea unha biblioteca externa para ver as túas fotos e vídeos", + "no_name": "Sen Nome", + "no_places": "Sen lugares", + "no_results": "Sen resultados", + "no_results_description": "Proba cun sinónimo ou palabra chave máis xeral", + "no_shared_albums_message": "Crea un álbum para compartir fotos e vídeos con persoas na túa rede", + "not_in_any_album": "Non está en ningún álbum", + "not_selected": "Non seleccionado", + "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar a Etiqueta de Almacenamento a activos cargados previamente, execute o", + "notes": "Notas", + "notification_permission_dialog_content": "Para activar as notificacións, vaia a Axustes e seleccione permitir.", + "notification_permission_list_tile_content": "Conceda permiso para activar as notificacións.", + "notification_permission_list_tile_enable_button": "Activar Notificacións", + "notification_permission_list_tile_title": "Permiso de Notificación", + "notification_toggle_setting_description": "Activar notificacións por correo electrónico", + "notifications": "Notificacións", + "notifications_setting_description": "Xestionar notificacións", + "oauth": "OAuth", + "official_immich_resources": "Recursos Oficiais de Immich", + "offline": "Fóra de liña", + "offline_paths": "Rutas fóra de liña", + "offline_paths_description": "Estes resultados poden deberse á eliminación manual de ficheiros que non forman parte dunha biblioteca externa.", + "ok": "Aceptar", + "oldest_first": "Máis antigos primeiro", + "on_this_device": "Neste dispositivo", + "onboarding": "Incorporación", + "onboarding_privacy_description": "As seguintes funcións (opcionais) dependen de servizos externos e poden desactivarse en calquera momento na configuración da administración.", + "onboarding_theme_description": "Elixe un tema de cor para a túa instancia. Podes cambialo máis tarde na túa configuración.", + "onboarding_welcome_description": "Imos configurar a túa instancia con algunhas configuracións comúns.", + "onboarding_welcome_user": "Benvido/a, {user}", + "online": "En liña", + "only_favorites": "Só favoritos", + "open": "Abrir", + "open_in_map_view": "Abrir na vista de mapa", + "open_in_openstreetmap": "Abrir en OpenStreetMap", + "open_the_search_filters": "Abrir os filtros de busca", + "options": "Opcións", + "or": "ou", + "organize_your_library": "Organizar a túa biblioteca", + "original": "orixinal", + "other": "Outro", + "other_devices": "Outros dispositivos", + "other_variables": "Outras variables", + "owned": "Propio", + "owner": "Propietario", + "partner": "Compañeiro/a", + "partner_can_access": "{partner} pode acceder a", + "partner_can_access_assets": "Todas as túas fotos e vídeos excepto os de Arquivo e Eliminados", + "partner_can_access_location": "A ubicación onde se tomaron as túas fotos", + "partner_list_user_photos": "Fotos de {user}", + "partner_list_view_all": "Ver todo", + "partner_page_empty_message": "As súas fotos aínda non están compartidas con ningún compañeiro/a.", + "partner_page_no_more_users": "Non hai máis usuarios para engadir", + "partner_page_partner_add_failed": "Erro ao engadir compañeiro/a", + "partner_page_select_partner": "Seleccionar compañeiro/a", + "partner_page_shared_to_title": "Compartido con", + "partner_page_stop_sharing_content": "{} xa non poderás acceder ás túas fotos.", + "partner_sharing": "Compartición con Compañeiro/a", + "partners": "Compañeiros/as", + "password": "Contrasinal", + "password_does_not_match": "O contrasinal non coincide", + "password_required": "Requírese Contrasinal", + "password_reset_success": "Contrasinal restablecido correctamente", + "past_durations": { + "days": "Últimos {days, plural, one {día} other {# días}}", + "hours": "Últimas {hours, plural, one {hora} other {# horas}}", + "years": "Últimos {years, plural, one {ano} other {# anos}}" + }, + "path": "Ruta", + "pattern": "Padrón", + "pause": "Pausa", + "pause_memories": "Pausar recordos", + "paused": "Pausado", + "pending": "Pendente", + "people": "Persoas", + "people_edits_count": "Editadas {count, plural, one {# persoa} other {# persoas}}", + "people_feature_description": "Navegar por fotos e vídeos agrupados por persoas", + "people_sidebar_description": "Mostrar unha ligazón a Persoas na barra lateral", + "permanent_deletion_warning": "Aviso de eliminación permanente", + "permanent_deletion_warning_setting_description": "Mostrar un aviso ao eliminar permanentemente activos", + "permanently_delete": "Eliminar permanentemente", + "permanently_delete_assets_count": "Eliminar permanentemente {count, plural, one {activo} other {activos}}", + "permanently_delete_assets_prompt": "Estás seguro de que queres eliminar permanentemente {count, plural, one {este activo?} other {estes # activos?}} Isto tamén {count, plural, one {o eliminará do teu} other {os eliminará dos teus}} álbum(s).", + "permanently_deleted_asset": "Activo eliminado permanentemente", + "permanently_deleted_assets_count": "Eliminados permanentemente {count, plural, one {# activo} other {# activos}}", + "permission_onboarding_back": "Atrás", + "permission_onboarding_continue_anyway": "Continuar de todos os xeitos", + "permission_onboarding_get_started": "Comezar", + "permission_onboarding_go_to_settings": "Ir a axustes", + "permission_onboarding_permission_denied": "Permiso denegado. Para usar Immich, conceda permisos de fotos e vídeos en Axustes.", + "permission_onboarding_permission_granted": "Permiso concedido! Xa está todo listo.", + "permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich faga copia de seguridade e xestione toda a túa colección da galería, conceda permisos de fotos e vídeos en Configuración.", + "permission_onboarding_request": "Immich require permiso para ver as túas fotos e vídeos.", + "person": "Persoa", + "person_birthdate": "Nacido/a o {date}", + "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}", + "photo_shared_all_users": "Parece que compartiches as túas fotos con todos os usuarios ou non tes ningún usuario co que compartir.", + "photos": "Fotos", + "photos_and_videos": "Fotos e Vídeos", + "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", + "photos_from_previous_years": "Fotos de anos anteriores", + "pick_a_location": "Elixir unha ubicación", + "place": "Lugar", + "places": "Lugares", + "places_count": "{count, plural, one {{count, number} Lugar} other {{count, number} Lugares}}", + "play": "Reproducir", + "play_memories": "Reproducir recordos", + "play_motion_photo": "Reproducir Foto en Movemento", + "play_or_pause_video": "Reproducir ou pausar vídeo", + "port": "Porto", + "preferences_settings_subtitle": "Xestionar as preferencias da aplicación", + "preferences_settings_title": "Preferencias", + "preset": "Preaxuste", + "preview": "Vista previa", + "previous": "Anterior", + "previous_memory": "Recordo anterior", + "previous_or_next_photo": "Foto anterior ou seguinte", + "primary": "Principal", + "privacy": "Privacidade", + "profile_drawer_app_logs": "Rexistros", + "profile_drawer_client_out_of_date_major": "A aplicación móbil está desactualizada. Por favor, actualice á última versión maior.", + "profile_drawer_client_out_of_date_minor": "A aplicación móbil está desactualizada. Por favor, actualice á última versión menor.", + "profile_drawer_client_server_up_to_date": "Cliente e Servidor están actualizados", "profile_drawer_github": "GitHub", - "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.", - "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.", - "recently_added": "Recently added", - "recently_added_page_title": "Recently Added", - "save": "Save", - "save_to_gallery": "Save to gallery", - "scaffold_body_error_occurred": "Error occurred", - "search_albums": "Search albums", - "search_filter_apply": "Apply filter", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", - "search_filter_display_option_not_in_album": "Not in album", - "search_filter_display_options": "Display Options", - "search_filter_location": "Location", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", - "search_filter_media_type_title": "Select media type", - "search_filter_people_title": "Select people", - "search_page_categories": "Categories", - "search_page_motion_photos": "Motion Photos", - "search_page_no_objects": "No Objects Info Available", - "search_page_no_places": "No Places Info Available", - "search_page_screenshots": "Screenshots", - "search_page_search_photos_videos": "Search for your photos and videos", + "profile_drawer_server_out_of_date_major": "O servidor está desactualizado. Por favor, actualice á última versión maior.", + "profile_drawer_server_out_of_date_minor": "O servidor está desactualizado. Por favor, actualice á última versión menor.", + "profile_image_of_user": "Imaxe de perfil de {user}", + "profile_picture_set": "Imaxe de perfil establecida.", + "public_album": "Álbum público", + "public_share": "Compartir Público", + "purchase_account_info": "Seguidor/a", + "purchase_activated_subtitle": "Grazas por apoiar Immich e o software de código aberto", + "purchase_activated_time": "Activado o {date, date}", + "purchase_activated_title": "A súa chave activouse correctamente", + "purchase_button_activate": "Activar", + "purchase_button_buy": "Comprar", + "purchase_button_buy_immich": "Comprar Immich", + "purchase_button_never_show_again": "Non mostrar nunca máis", + "purchase_button_reminder": "Lembrarme en 30 días", + "purchase_button_remove_key": "Eliminar chave", + "purchase_button_select": "Seleccionar", + "purchase_failed_activation": "Erro ao activar! Por favor, comproba o teu correo electrónico para a chave do produto correcta!", + "purchase_individual_description_1": "Para un individuo", + "purchase_individual_description_2": "Estado de seguidor/a", + "purchase_individual_title": "Individual", + "purchase_input_suggestion": "Ten unha chave de produto? Introduza a chave a continuación", + "purchase_license_subtitle": "Compre Immich para apoiar o desenvolvemento continuado do servizo", + "purchase_lifetime_description": "Compra vitalicia", + "purchase_option_title": "OPCIÓNS DE COMPRA", + "purchase_panel_info_1": "Construír Immich leva moito tempo e esforzo, e temos enxeñeiros a tempo completo traballando nel para facelo o mellor posible. A nosa misión é que o software de código aberto e as prácticas comerciais éticas se convertan nunha fonte de ingresos sostible para os desenvolvedores e crear un ecosistema respectuoso coa privacidade con alternativas reais aos servizos na nube explotadores.", + "purchase_panel_info_2": "Como estamos comprometidos a non engadir muros de pago, esta compra non che outorgará ningunha función adicional en Immich. Dependemos de usuarios coma ti para apoiar o desenvolvemento continuo de Immich.", + "purchase_panel_title": "Apoiar o proxecto", + "purchase_per_server": "Por servidor", + "purchase_per_user": "Por usuario", + "purchase_remove_product_key": "Eliminar Chave do Produto", + "purchase_remove_product_key_prompt": "Estás seguro de que queres eliminar a chave do produto?", + "purchase_remove_server_product_key": "Eliminar chave do produto do Servidor", + "purchase_remove_server_product_key_prompt": "Estás seguro de que queres eliminar a chave do produto do Servidor?", + "purchase_server_description_1": "Para todo o servidor", + "purchase_server_description_2": "Estado de seguidor/a", + "purchase_server_title": "Servidor", + "purchase_settings_server_activated": "A chave do produto do servidor é xestionada polo administrador", + "rating": "Clasificación por estrelas", + "rating_clear": "Borrar clasificación", + "rating_count": "{count, plural, one {# estrela} other {# estrelas}}", + "rating_description": "Mostrar a clasificación EXIF no panel de información", + "reaction_options": "Opcións de reacción", + "read_changelog": "Ler Rexistro de Cambios", + "reassign": "Reasignar", + "reassigned_assets_to_existing_person": "Reasignados {count, plural, one {# activo} other {# activos}} a {name, select, null {unha persoa existente} other {{name}}}", + "reassigned_assets_to_new_person": "Reasignados {count, plural, one {# activo} other {# activos}} a unha nova persoa", + "reassing_hint": "Asignar activos seleccionados a unha persoa existente", + "recent": "Recente", + "recent-albums": "Álbums recentes", + "recent_searches": "Buscas recentes", + "recently_added": "Engadido recentemente", + "recently_added_page_title": "Engadido Recentemente", + "refresh": "Actualizar", + "refresh_encoded_videos": "Actualizar vídeos codificados", + "refresh_faces": "Actualizar caras", + "refresh_metadata": "Actualizar metadatos", + "refresh_thumbnails": "Actualizar miniaturas", + "refreshed": "Actualizado", + "refreshes_every_file": "Volve ler todos os ficheiros existentes e novos", + "refreshing_encoded_video": "Actualizando vídeo codificado", + "refreshing_faces": "Actualizando caras", + "refreshing_metadata": "Actualizando metadatos", + "regenerating_thumbnails": "Rexenerando miniaturas", + "remove": "Eliminar", + "remove_assets_album_confirmation": "Estás seguro de que queres eliminar {count, plural, one {# activo} other {# activos}} do álbum?", + "remove_assets_shared_link_confirmation": "Estás seguro de que queres eliminar {count, plural, one {# activo} other {# activos}} desta ligazón compartida?", + "remove_assets_title": "Eliminar activos?", + "remove_custom_date_range": "Eliminar rango de datas personalizado", + "remove_deleted_assets": "Eliminar Activos Eliminados", + "remove_from_album": "Eliminar do álbum", + "remove_from_favorites": "Eliminar de favoritos", + "remove_from_shared_link": "Eliminar da ligazón compartida", + "remove_memory": "Eliminar recordo", + "remove_photo_from_memory": "Eliminar foto deste recordo", + "remove_url": "Eliminar URL", + "remove_user": "Eliminar usuario", + "removed_api_key": "Chave API eliminada: {name}", + "removed_from_archive": "Eliminado do arquivo", + "removed_from_favorites": "Eliminado de favoritos", + "removed_from_favorites_count": "{count, plural, other {Eliminados #}} de favoritos", + "removed_memory": "Recordo eliminado", + "removed_photo_from_memory": "Foto eliminada do recordo", + "removed_tagged_assets": "Etiqueta eliminada de {count, plural, one {# activo} other {# activos}}", + "rename": "Renomear", + "repair": "Reparar", + "repair_no_results_message": "Os ficheiros non rastrexados e faltantes aparecerán aquí", + "replace_with_upload": "Substituír con carga", + "repository": "Repositorio", + "require_password": "Requirir contrasinal", + "require_user_to_change_password_on_first_login": "Requirir que o usuario cambie o contrasinal no primeiro inicio de sesión", + "rescan": "Volver escanear", + "reset": "Restablecer", + "reset_password": "Restablecer contrasinal", + "reset_people_visibility": "Restablecer visibilidade das persoas", + "reset_to_default": "Restablecer ao predeterminado", + "resolve_duplicates": "Resolver duplicados", + "resolved_all_duplicates": "Resolvéronse todos os duplicados", + "restore": "Restaurar", + "restore_all": "Restaurar todo", + "restore_user": "Restaurar usuario", + "restored_asset": "Activo restaurado", + "resume": "Reanudar", + "retry_upload": "Reintentar carga", + "review_duplicates": "Revisar duplicados", + "role": "Rol", + "role_editor": "Editor", + "role_viewer": "Visor", + "save": "Gardar", + "save_to_gallery": "Gardar na galería", + "saved_api_key": "Chave API gardada", + "saved_profile": "Perfil gardado", + "saved_settings": "Configuración gardada", + "say_something": "Dicir algo", + "scaffold_body_error_occurred": "Ocorreu un erro", + "scan_all_libraries": "Escanear Todas as Bibliotecas", + "scan_library": "Escanear", + "scan_settings": "Configuración de Escaneo", + "scanning_for_album": "Escaneando álbum...", + "search": "Buscar", + "search_albums": "Buscar álbums", + "search_by_context": "Buscar por contexto", + "search_by_description": "Buscar por descrición", + "search_by_description_example": "Día de sendeirismo en Sapa", + "search_by_filename": "Buscar por nome de ficheiro ou extensión", + "search_by_filename_example": "p. ex. IMG_1234.JPG ou PNG", + "search_camera_make": "Buscar marca de cámara...", + "search_camera_model": "Buscar modelo de cámara...", + "search_city": "Buscar cidade...", + "search_country": "Buscar país...", + "search_filter_apply": "Aplicar filtro", + "search_filter_camera_title": "Seleccionar tipo de cámara", + "search_filter_date": "Data", + "search_filter_date_interval": "{start} a {end}", + "search_filter_date_title": "Seleccionar un rango de datas", + "search_filter_display_option_not_in_album": "Non nun álbum", + "search_filter_display_options": "Opcións de Visualización", + "search_filter_filename": "Buscar por nome de ficheiro", + "search_filter_location": "Ubicación", + "search_filter_location_title": "Seleccionar ubicación", + "search_filter_media_type": "Tipo de Medio", + "search_filter_media_type_title": "Seleccionar tipo de medio", + "search_filter_people_title": "Seleccionar persoas", + "search_for": "Buscar por", + "search_for_existing_person": "Buscar persoa existente", + "search_no_more_result": "Non hai máis resultados", + "search_no_people": "Sen persoas", + "search_no_people_named": "Sen persoas chamadas \"{name}\"", + "search_no_result": "Non se atoparon resultados, probe cun termo de busca ou combinación diferente", + "search_options": "Opcións de busca", + "search_page_categories": "Categorías", + "search_page_motion_photos": "Fotos en Movemento", + "search_page_no_objects": "Non hai Información de Obxectos Dispoñible", + "search_page_no_places": "Non hai Información de Lugares Dispoñible", + "search_page_screenshots": "Capturas de pantalla", + "search_page_search_photos_videos": "Busca as túas fotos e vídeos", "search_page_selfies": "Selfies", - "search_page_things": "Things", - "search_page_view_all_button": "View all", - "search_page_your_activity": "Your activity", - "search_page_your_map": "Your Map", - "search_result_page_new_search_hint": "New Search", - "search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ", - "search_suggestion_list_smart_search_hint_2": "m:your-search-term", - "select_user_for_sharing_page_err_album": "Failed to create album", - "server_endpoint": "Server Endpoint", - "server_info_box_app_version": "App Version", - "server_info_box_server_url": "Server URL", - "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", - "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", - "setting_image_viewer_original_title": "Load original image", - "setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.", - "setting_image_viewer_preview_title": "Load preview image", - "setting_image_viewer_title": "Images", - "setting_languages_apply": "Apply", - "setting_languages_subtitle": "Change the app's language", - "setting_languages_title": "Languages", - "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", - "setting_notifications_notify_hours": "{} hours", - "setting_notifications_notify_immediately": "immediately", - "setting_notifications_notify_minutes": "{} minutes", - "setting_notifications_notify_never": "never", - "setting_notifications_notify_seconds": "{} seconds", - "setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset", - "setting_notifications_single_progress_title": "Show background backup detail progress", - "setting_notifications_subtitle": "Adjust your notification preferences", - "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", - "setting_notifications_total_progress_title": "Show background backup total progress", - "setting_video_viewer_looping_title": "Looping", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", - "settings_require_restart": "Please restart Immich to apply this setting", - "share_add_photos": "Add photos", - "share_assets_selected": "{} selected", - "share_dialog_preparing": "Preparing...", - "shared_album_activities_input_disable": "Comment is disabled", - "shared_album_activity_remove_content": "Do you want to delete this activity?", - "shared_album_activity_remove_title": "Delete Activity", - "shared_album_section_people_action_error": "Error leaving/removing from album", - "shared_album_section_people_action_leave": "Remove user from album", - "shared_album_section_people_action_remove_user": "Remove user from album", - "shared_album_section_people_title": "PEOPLE", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", - "shared_link_app_bar_title": "Shared Links", - "shared_link_clipboard_copied_massage": "Copied to clipboard", - "shared_link_clipboard_text": "Link: {}\nPassword: {}", - "shared_link_create_error": "Error while creating shared link", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_expire_after_option_day": "1 day", - "shared_link_edit_expire_after_option_days": "{} days", - "shared_link_edit_expire_after_option_hour": "1 hour", - "shared_link_edit_expire_after_option_hours": "{} hours", - "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{} minutes", - "shared_link_edit_expire_after_option_months": "{} months", - "shared_link_edit_expire_after_option_year": "{} year", - "shared_link_edit_password_hint": "Enter the share password", - "shared_link_edit_submit_button": "Update link", - "shared_link_error_server_url_fetch": "Cannot fetch the server url", - "shared_link_expires_day": "Expires in {} day", - "shared_link_expires_days": "Expires in {} days", - "shared_link_expires_hour": "Expires in {} hour", - "shared_link_expires_hours": "Expires in {} hours", - "shared_link_expires_minute": "Expires in {} minute", - "shared_link_expires_minutes": "Expires in {} minutes", - "shared_link_expires_never": "Expires ∞", - "shared_link_expires_second": "Expires in {} second", - "shared_link_expires_seconds": "Expires in {} seconds", - "shared_link_individual_shared": "Individual shared", + "search_page_things": "Cousas", + "search_page_view_all_button": "Ver todo", + "search_page_your_activity": "A túa actividade", + "search_page_your_map": "O teu Mapa", + "search_people": "Buscar persoas", + "search_places": "Buscar lugares", + "search_rating": "Buscar por clasificación...", + "search_result_page_new_search_hint": "Nova Busca", + "search_settings": "Configuración da busca", + "search_state": "Buscar estado...", + "search_suggestion_list_smart_search_hint_1": "A busca intelixente está activada por defecto, para buscar metadatos use a sintaxe ", + "search_suggestion_list_smart_search_hint_2": "m:o-teu-termo-de-busca", + "search_tags": "Buscar etiquetas...", + "search_timezone": "Buscar fuso horario...", + "search_type": "Tipo de busca", + "search_your_photos": "Buscar as túas fotos", + "searching_locales": "Buscando configuracións rexionais...", + "second": "Segundo", + "see_all_people": "Ver todas as persoas", + "select": "Seleccionar", + "select_album_cover": "Seleccionar portada do álbum", + "select_all": "Seleccionar todo", + "select_all_duplicates": "Seleccionar todos os duplicados", + "select_avatar_color": "Seleccionar cor do avatar", + "select_face": "Seleccionar cara", + "select_featured_photo": "Seleccionar foto destacada", + "select_from_computer": "Seleccionar do ordenador", + "select_keep_all": "Seleccionar conservar todo", + "select_library_owner": "Seleccionar propietario da biblioteca", + "select_new_face": "Seleccionar nova cara", + "select_photos": "Seleccionar fotos", + "select_trash_all": "Seleccionar mover todo ao lixo", + "select_user_for_sharing_page_err_album": "Erro ao crear o álbum", + "selected": "Seleccionado", + "selected_count": "{count, plural, other {# seleccionados}}", + "send_message": "Enviar mensaxe", + "send_welcome_email": "Enviar correo electrónico de benvida", + "server_endpoint": "Punto Final do Servidor", + "server_info_box_app_version": "Versión da Aplicación", + "server_info_box_server_url": "URL do Servidor", + "server_offline": "Servidor Fóra de Liña", + "server_online": "Servidor En Liña", + "server_stats": "Estatísticas do Servidor", + "server_version": "Versión do Servidor", + "set": "Establecer", + "set_as_album_cover": "Establecer como portada do álbum", + "set_as_featured_photo": "Establecer como foto destacada", + "set_as_profile_picture": "Establecer como imaxe de perfil", + "set_date_of_birth": "Establecer data de nacemento", + "set_profile_picture": "Establecer imaxe de perfil", + "set_slideshow_to_fullscreen": "Poñer Presentación a pantalla completa", + "setting_image_viewer_help": "O visor de detalles carga primeiro a miniatura pequena, despois carga a vista previa de tamaño medio (se está activada), finalmente carga o orixinal (se está activado).", + "setting_image_viewer_original_subtitle": "Activar para cargar a imaxe orixinal a resolución completa (grande!). Desactivar para reducir o uso de datos (tanto na rede como na caché do dispositivo).", + "setting_image_viewer_original_title": "Cargar imaxe orixinal", + "setting_image_viewer_preview_subtitle": "Activar para cargar unha imaxe de resolución media. Desactivar para cargar directamente o orixinal ou usar só a miniatura.", + "setting_image_viewer_preview_title": "Cargar imaxe de vista previa", + "setting_image_viewer_title": "Imaxes", + "setting_languages_apply": "Aplicar", + "setting_languages_subtitle": "Cambiar a lingua da aplicación", + "setting_languages_title": "Linguas", + "setting_notifications_notify_failures_grace_period": "Notificar fallos da copia de seguridade en segundo plano: {}", + "setting_notifications_notify_hours": "{} horas", + "setting_notifications_notify_immediately": "inmediatamente", + "setting_notifications_notify_minutes": "{} minutos", + "setting_notifications_notify_never": "nunca", + "setting_notifications_notify_seconds": "{} segundos", + "setting_notifications_single_progress_subtitle": "Información detallada do progreso da carga por activo", + "setting_notifications_single_progress_title": "Mostrar progreso detallado da copia de seguridade en segundo plano", + "setting_notifications_subtitle": "Axustar as túas preferencias de notificación", + "setting_notifications_total_progress_subtitle": "Progreso xeral da carga (feitos/total activos)", + "setting_notifications_total_progress_title": "Mostrar progreso total da copia de seguridade en segundo plano", + "setting_video_viewer_looping_title": "Bucle", + "setting_video_viewer_original_video_subtitle": "Ao transmitir un vídeo desde o servidor, reproducir o orixinal aínda que haxa unha transcodificación dispoñible. Pode provocar buffering. Os vídeos dispoñibles localmente repródúcense en calidade orixinal independentemente desta configuración.", + "setting_video_viewer_original_video_title": "Forzar vídeo orixinal", + "settings": "Configuración", + "settings_require_restart": "Por favor, reinicie Immich para aplicar esta configuración", + "settings_saved": "Configuración gardada", + "share": "Compartir", + "share_add_photos": "Engadir fotos", + "share_assets_selected": "{} seleccionados", + "share_dialog_preparing": "Preparando...", + "shared": "Compartido", + "shared_album_activities_input_disable": "O comentario está desactivado", + "shared_album_activity_remove_content": "Queres eliminar esta actividade?", + "shared_album_activity_remove_title": "Eliminar Actividade", + "shared_album_section_people_action_error": "Erro ao saír/eliminar do álbum", + "shared_album_section_people_action_leave": "Eliminar usuario do álbum", + "shared_album_section_people_action_remove_user": "Eliminar usuario do álbum", + "shared_album_section_people_title": "PERSOAS", + "shared_by": "Compartido por", + "shared_by_user": "Compartido por {user}", + "shared_by_you": "Compartido por ti", + "shared_from_partner": "Fotos de {partner}", + "shared_intent_upload_button_progress_text": "{} / {} Subidos", + "shared_link_app_bar_title": "Ligazóns Compartidas", + "shared_link_clipboard_copied_massage": "Copiado ao portapapeis", + "shared_link_clipboard_text": "Ligazón: {}\nContrasinal: {}", + "shared_link_create_error": "Erro ao crear ligazón compartida", + "shared_link_edit_description_hint": "Introduza a descrición da compartición", + "shared_link_edit_expire_after_option_day": "1 día", + "shared_link_edit_expire_after_option_days": "{} días", + "shared_link_edit_expire_after_option_hour": "1 hora", + "shared_link_edit_expire_after_option_hours": "{} horas", + "shared_link_edit_expire_after_option_minute": "1 minuto", + "shared_link_edit_expire_after_option_minutes": "{} minutos", + "shared_link_edit_expire_after_option_months": "{} meses", + "shared_link_edit_expire_after_option_year": "{} ano", + "shared_link_edit_password_hint": "Introduza o contrasinal da compartición", + "shared_link_edit_submit_button": "Actualizar ligazón", + "shared_link_error_server_url_fetch": "Non se pode obter a url do servidor", + "shared_link_expires_day": "Caduca en {} día", + "shared_link_expires_days": "Caduca en {} días", + "shared_link_expires_hour": "Caduca en {} hora", + "shared_link_expires_hours": "Caduca en {} horas", + "shared_link_expires_minute": "Caduca en {} minuto", + "shared_link_expires_minutes": "Caduca en {} minutos", + "shared_link_expires_never": "Caduca ∞", + "shared_link_expires_second": "Caduca en {} segundo", + "shared_link_expires_seconds": "Caduca en {} segundos", + "shared_link_individual_shared": "Compartido individualmente", "shared_link_info_chip_metadata": "EXIF", - "shared_link_manage_links": "Manage Shared links", - "shared_links": "Shared links", - "shared_with_me": "Shared with me", - "sharing_page_album": "Shared albums", - "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", - "sharing_page_empty_list": "EMPTY LIST", - "sharing_silver_appbar_create_shared_album": "New shared album", - "sharing_silver_appbar_share_partner": "Share with partner", - "start_date": "Start date", - "sync": "Sync", - "sync_albums": "Sync albums", - "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", - "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", - "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", - "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", - "theme_setting_colorful_interface_title": "Colorful interface", - "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", - "theme_setting_image_viewer_quality_title": "Image viewer quality", - "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", - "theme_setting_primary_color_title": "Primary color", - "theme_setting_system_primary_color_title": "Use system color", - "theme_setting_system_theme_switch": "Automatic (Follow system setting)", - "theme_setting_theme_subtitle": "Choose the app's theme setting", - "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", - "theme_setting_three_stage_loading_title": "Enable three-stage loading", - "trash": "Trash", - "trash_emptied": "Emptied trash", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_title": "Trash ({})", - "upload": "Upload", - "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", - "upload_dialog_title": "Upload Asset", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", - "use_current_connection": "use current connection", - "validate_endpoint_error": "Please enter a valid URL", - "version_announcement_overlay_release_notes": "release notes", - "version_announcement_overlay_text_1": "Hi friend, there is a new release of", - "version_announcement_overlay_text_2": "please take your time to visit the ", - "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", - "version_announcement_overlay_title": "New Server Version Available 🎉", - "videos": "Videos", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack", - "wifi_name": "WiFi Name", + "shared_link_manage_links": "Xestionar ligazóns Compartidas", + "shared_link_options": "Opcións da ligazón compartida", + "shared_links": "Ligazóns compartidas", + "shared_links_description": "Compartir fotos e vídeos cunha ligazón", + "shared_photos_and_videos_count": "{assetCount, plural, other {# fotos e vídeos compartidos.}}", + "shared_with_me": "Compartido comigo", + "shared_with_partner": "Compartido con {partner}", + "sharing": "Compartir", + "sharing_enter_password": "Por favor, introduza o contrasinal para ver esta páxina.", + "sharing_page_album": "Álbums compartidos", + "sharing_page_description": "Crea álbums compartidos para compartir fotos e vídeos con persoas na túa rede.", + "sharing_page_empty_list": "LISTA BALEIRA", + "sharing_sidebar_description": "Mostrar unha ligazón a Compartir na barra lateral", + "sharing_silver_appbar_create_shared_album": "Novo álbum compartido", + "sharing_silver_appbar_share_partner": "Compartir con compañeiro/a", + "shift_to_permanent_delete": "prema ⇧ para eliminar permanentemente o activo", + "show_album_options": "Mostrar opcións do álbum", + "show_albums": "Mostrar álbums", + "show_all_people": "Mostrar todas as persoas", + "show_and_hide_people": "Mostrar e ocultar persoas", + "show_file_location": "Mostrar ubicación do ficheiro", + "show_gallery": "Mostrar galería", + "show_hidden_people": "Mostrar persoas ocultas", + "show_in_timeline": "Mostrar na liña de tempo", + "show_in_timeline_setting_description": "Mostrar fotos e vídeos deste usuario na túa liña de tempo", + "show_keyboard_shortcuts": "Mostrar atallos de teclado", + "show_metadata": "Mostrar metadatos", + "show_or_hide_info": "Mostrar ou ocultar información", + "show_password": "Mostrar contrasinal", + "show_person_options": "Mostrar opcións da persoa", + "show_progress_bar": "Mostrar Barra de Progreso", + "show_search_options": "Mostrar opcións de busca", + "show_shared_links": "Mostrar ligazóns compartidas", + "show_slideshow_transition": "Mostrar transición da presentación", + "show_supporter_badge": "Insignia de seguidor/a", + "show_supporter_badge_description": "Mostrar unha insignia de seguidor/a", + "shuffle": "Aleatorio", + "sidebar": "Barra lateral", + "sidebar_display_description": "Mostrar unha ligazón á vista na barra lateral", + "sign_out": "Pechar Sesión", + "sign_up": "Rexistrarse", + "size": "Tamaño", + "skip_to_content": "Saltar ao contido", + "skip_to_folders": "Saltar a cartafoles", + "skip_to_tags": "Saltar a etiquetas", + "slideshow": "Presentación", + "slideshow_settings": "Configuración da presentación", + "sort_albums_by": "Ordenar álbums por...", + "sort_created": "Data de creación", + "sort_items": "Número de elementos", + "sort_modified": "Data de modificación", + "sort_oldest": "Foto máis antiga", + "sort_people_by_similarity": "Ordenar persoas por similitude", + "sort_recent": "Foto máis recente", + "sort_title": "Título", + "source": "Fonte", + "stack": "Apilar", + "stack_duplicates": "Apilar duplicados", + "stack_select_one_photo": "Seleccionar unha foto principal para a pila", + "stack_selected_photos": "Apilar fotos seleccionadas", + "stacked_assets_count": "Apilados {count, plural, one {# activo} other {# activos}}", + "stacktrace": "Rastro da Pila", + "start": "Iniciar", + "start_date": "Data de inicio", + "state": "Estado", + "status": "Estado", + "stop_motion_photo": "Deter Foto en Movemento", + "stop_photo_sharing": "Deixar de compartir as túas fotos?", + "stop_photo_sharing_description": "{partner} xa non poderá acceder ás túas fotos.", + "stop_sharing_photos_with_user": "Deixar de compartir as túas fotos con este usuario", + "storage": "Espazo de almacenamento", + "storage_label": "Etiqueta de almacenamento", + "storage_usage": "{used} de {available} usado", + "submit": "Enviar", + "suggestions": "Suxestións", + "sunrise_on_the_beach": "Amencer na praia", + "support": "Soporte", + "support_and_feedback": "Soporte e Comentarios", + "support_third_party_description": "A túa instalación de Immich foi empaquetada por un terceiro. Os problemas que experimente poden ser causados por ese paquete, así que por favor, comunica os problemas con eles en primeira instancia usando as ligazóns a continuación.", + "swap_merge_direction": "Intercambiar dirección de fusión", + "sync": "Sincronizar", + "sync_albums": "Sincronizar álbums", + "sync_albums_manual_subtitle": "Sincronizar todos os vídeos e fotos cargados aos álbums de copia de seguridade seleccionados", + "sync_upload_album_setting_subtitle": "Crear e suba as túas fotos e vídeos aos álbums seleccionados en Immich", + "tag": "Etiqueta", + "tag_assets": "Etiquetar activos", + "tag_created": "Etiqueta creada: {tag}", + "tag_feature_description": "Navegar por fotos e vídeos agrupados por temas de etiquetas lóxicas", + "tag_not_found_question": "Non atopa unha etiqueta? Crear unha nova etiqueta.", + "tag_people": "Etiquetar Persoas", + "tag_updated": "Etiqueta actualizada: {tag}", + "tagged_assets": "Etiquetados {count, plural, one {# activo} other {# activos}}", + "tags": "Etiquetas", + "template": "Modelo", + "theme": "Tema", + "theme_selection": "Selección de tema", + "theme_selection_description": "Establecer automaticamente o tema a claro ou escuro baseándose na preferencia do sistema do teu navegador", + "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamento nas tellas de activos", + "theme_setting_asset_list_tiles_per_row_title": "Número de activos por fila ({})", + "theme_setting_colorful_interface_subtitle": "Aplicar cor primaria ás superficies de fondo.", + "theme_setting_colorful_interface_title": "Interface colorida", + "theme_setting_image_viewer_quality_subtitle": "Axustar a calidade do visor de imaxes de detalle", + "theme_setting_image_viewer_quality_title": "Calidade do visor de imaxes", + "theme_setting_primary_color_subtitle": "Elixa unha cor para accións primarias e acentos.", + "theme_setting_primary_color_title": "Cor primaria", + "theme_setting_system_primary_color_title": "Usar cor do sistema", + "theme_setting_system_theme_switch": "Automático (Seguir configuración do sistema)", + "theme_setting_theme_subtitle": "Elixir a configuración do tema da aplicación", + "theme_setting_three_stage_loading_subtitle": "A carga en tres etapas pode aumentar o rendemento da carga pero causa unha carga de rede significativamente maior", + "theme_setting_three_stage_loading_title": "Activar carga en tres etapas", + "they_will_be_merged_together": "Fusionaranse xuntos", + "third_party_resources": "Recursos de Terceiros", + "time_based_memories": "Recordos baseados no tempo", + "timeline": "Liña de tempo", + "timezone": "Fuso horario", + "to_archive": "Arquivar", + "to_change_password": "Cambiar contrasinal", + "to_favorite": "Favorito", + "to_login": "Iniciar sesión", + "to_parent": "Ir ao pai", + "to_trash": "Lixo", + "toggle_settings": "Alternar configuración", + "toggle_theme": "Alternar tema escuro", + "total": "Total", + "total_usage": "Uso total", + "trash": "Lixo", + "trash_all": "Mover Todo ao Lixo", + "trash_count": "Lixo {count, number}", + "trash_delete_asset": "Mover ao Lixo/Eliminar Activo", + "trash_emptied": "Lixo baleirado", + "trash_no_results_message": "As fotos e vídeos movidos ao lixo aparecerán aquí.", + "trash_page_delete_all": "Eliminar Todo", + "trash_page_empty_trash_dialog_content": "Queres baleirar os teus activos no lixo? Estes elementos eliminaranse permanentemente de Immich", + "trash_page_info": "Os elementos no lixo eliminaranse permanentemente despois de {} días", + "trash_page_no_assets": "Non hai activos no lixo", + "trash_page_restore_all": "Restaurar Todo", + "trash_page_select_assets_btn": "Seleccionar activos", + "trash_page_title": "Lixo ({})", + "trashed_items_will_be_permanently_deleted_after": "Os elementos no lixo eliminaranse permanentemente despois de {days, plural, one {# día} other {# días}}.", + "type": "Tipo", + "unarchive": "Desarquivar", + "unarchived_count": "{count, plural, other {Desarquivados #}}", + "unfavorite": "Desmarcar como favorito", + "unhide_person": "Mostrar persoa", + "unknown": "Descoñecido", + "unknown_country": "País Descoñecido", + "unknown_year": "Ano Descoñecido", + "unlimited": "Ilimitado", + "unlink_motion_video": "Desvincular vídeo en movemento", + "unlink_oauth": "Desvincular OAuth", + "unlinked_oauth_account": "Conta OAuth desvinculada", + "unmute_memories": "Desilenciar Recordos", + "unnamed_album": "Álbum Sen Nome", + "unnamed_album_delete_confirmation": "Estás seguro de que queres eliminar este álbum?", + "unnamed_share": "Compartir Sen Nome", + "unsaved_change": "Cambio sen gardar", + "unselect_all": "Deseleccionar todo", + "unselect_all_duplicates": "Deseleccionar todos os duplicados", + "unstack": "Desapilar", + "unstacked_assets_count": "Desapilados {count, plural, one {# activo} other {# activos}}", + "untracked_files": "Ficheiros non rastrexados", + "untracked_files_decription": "Estes ficheiros non son rastrexados pola aplicación. Poden ser o resultado de movementos fallidos, cargas interrompidas ou deixados atrás debido a un erro", + "up_next": "A continuación", + "updated_password": "Contrasinal actualizado", + "upload": "Subir", + "upload_concurrency": "Concorrencia de subida", + "upload_dialog_info": "Queres facer copia de seguridade do(s) Activo(s) seleccionado(s) no servidor?", + "upload_dialog_title": "Subir Activo", + "upload_errors": "Subida completada con {count, plural, one {# erro} other {# erros}}, actualice a páxina para ver os novos activos subidos.", + "upload_progress": "Restantes {remaining, number} - Procesados {processed, number}/{total, number}", + "upload_skipped_duplicates": "Omitidos {count, plural, one {# activo duplicado} other {# activos duplicados}}", + "upload_status_duplicates": "Duplicados", + "upload_status_errors": "Erros", + "upload_status_uploaded": "Subido", + "upload_success": "Subida exitosa, actualice a páxina para ver os novos activos subidos.", + "upload_to_immich": "Subir a Immich ({})", + "uploading": "Subindo", + "url": "URL", + "usage": "Uso", + "use_current_connection": "usar conexión actual", + "use_custom_date_range": "Usar rango de datas personalizado no seu lugar", + "user": "Usuario", + "user_id": "ID de Usuario", + "user_liked": "A {user} gustoulle {type, select, photo {esta foto} video {este vídeo} asset {este activo} other {isto}}", + "user_purchase_settings": "Compra", + "user_purchase_settings_description": "Xestionar a túa compra", + "user_role_set": "Establecer {user} como {role}", + "user_usage_detail": "Detalle de uso do usuario", + "user_usage_stats": "Estatísticas de uso da conta", + "user_usage_stats_description": "Ver estatísticas de uso da conta", + "username": "Nome de usuario", + "users": "Usuarios", + "utilities": "Utilidades", + "validate": "Validar", + "validate_endpoint_error": "Por favor, introduza unha URL válida", + "variables": "Variables", + "version": "Versión", + "version_announcement_closing": "O seu amigo, Alex", + "version_announcement_message": "Ola! Unha nova versión de Immich está dispoñible. Por favor, toma un tempo para ler as notas de lanzamento para asegurarse de que a túa configuración está actualizada para evitar calquera configuración incorrecta, especialmente se usas WatchTower ou calquera mecanismo que xestione a actualización automática da túa instancia de Immich.", + "version_announcement_overlay_release_notes": "notas de lanzamento", + "version_announcement_overlay_text_1": "Ola amigo/a, hai unha nova versión de", + "version_announcement_overlay_text_2": "por favor, toma o teu tempo para visitar as ", + "version_announcement_overlay_text_3": " e asegúrate de que a túa configuración de docker-compose e .env está actualizada para evitar calquera configuración incorrecta, especialmente se usa WatchTower ou calquera mecanismo que xestione a actualización automática da túa aplicación de servidor.", + "version_announcement_overlay_title": "Nova Versión do Servidor Dispoñible 🎉", + "version_history": "Historial de Versións", + "version_history_item": "Instalado {version} o {date}", + "video": "Vídeo", + "video_hover_setting": "Reproducir miniatura do vídeo ao pasar o rato por riba", + "video_hover_setting_description": "Reproducir miniatura do vídeo cando o rato está sobre o elemento. Mesmo cando está desactivado, a reprodución pode iniciarse pasando o rato sobre a icona de reprodución.", + "videos": "Vídeos", + "videos_count": "{count, plural, one {# Vídeo} other {# Vídeos}}", + "view": "Ver", + "view_album": "Ver Álbum", + "view_all": "Ver Todo", + "view_all_users": "Ver todos os usuarios", + "view_in_timeline": "Ver na liña de tempo", + "view_link": "Ver ligazón", + "view_links": "Ver ligazóns", + "view_name": "Vista", + "view_next_asset": "Ver seguinte activo", + "view_previous_asset": "Ver activo anterior", + "view_qr_code": "Ver código QR", + "view_stack": "Ver Pila", + "viewer_remove_from_stack": "Eliminar da Pila", + "viewer_stack_use_as_main_asset": "Usar como Activo Principal", + "viewer_unstack": "Desapilar", + "visibility_changed": "Visibilidade cambiada para {count, plural, one {# persoa} other {# persoas}}", + "waiting": "Agardando", + "warning": "Aviso", + "week": "Semana", + "welcome": "Benvido/a", + "welcome_to_immich": "Benvido/a a Immich", + "wifi_name": "Nome da WiFi", "year": "Ano", + "years_ago": "Hai {years, plural, one {# ano} other {# anos}}", "yes": "Si", - "your_wifi_name": "Your WiFi name", - "zoom_image": "Acercar imaxe" + "you_dont_have_any_shared_links": "Non tes ningunha ligazón compartida", + "your_wifi_name": "O nome da túa WiFi", + "zoom_image": "Ampliar Imaxe" } diff --git a/i18n/he.json b/i18n/he.json index 6a743ad6f7..927a43f020 100644 --- a/i18n/he.json +++ b/i18n/he.json @@ -33,7 +33,7 @@ "added_to_favorites_count": "{count, number} נוספו למועדפים", "admin": { "add_exclusion_pattern_description": "הוספת דפוסי החרגה. נתמכת התאמת דפוסים באמצעות *, ** ו-?. כדי להתעלם מכל הקבצים בתיקיה כלשהי בשם \"Raw\", יש להשתמש ב \"**/Raw/**\". כדי להתעלם מכל הקבצים המסתיימים ב \"tif.\", יש להשתמש ב \"tif.*/**\". כדי להתעלם מנתיב מוחלט, יש להשתמש ב \"**/נתיב/להתעלמות\".", - "asset_offline_description": "נכס ספרייה חיצונית זה לא נמצא יותר בדיסק והועבר לאשפה. אם הקובץ הועבר מתוך הספרייה, נא לבדוק את ציר הזמן שלך עבור הנכס המקביל החדש. כדי לשחזר נכס זה, נא לוודא ש-Immich יכול לגשת אל נתיב הקובץ למטה ולסרוק מחדש את הספרייה.", + "asset_offline_description": "תמונה מספרייה חיצונית זו לא נמצאת יותר בדיסק והועברה לאשפה. אם הקובץ הועבר מתוך הספרייה, נא לבדוק את ציר הזמן שלך עבור התמונה המקבילה החדש. כדי לשחזר תמונה זו, נא לוודא ש-Immich יכול לגשת אל נתיב הקובץ למטה ולסרוק מחדש את הספרייה.", "authentication_settings": "הגדרות התחברות", "authentication_settings_description": "ניהול סיסמה, OAuth, והגדרות התחברות אחרות", "authentication_settings_disable_all": "האם ברצונך להשבית את כל שיטות ההתחברות? כניסה למערכת תהיה מושבתת לחלוטין.", @@ -49,7 +49,7 @@ "cleared_jobs": "נוקו משימות עבור: {job}", "config_set_by_file": "התצורה מוגדרת כעת על ידי קובץ תצורה", "confirm_delete_library": "האם באמת ברצונך למחוק את הספרייה {library}?", - "confirm_delete_library_assets": "האם באמת ברצונך למחוק את הספרייה הזו? זה ימחק את {count, plural, one {נכס # המוכל} other {כל # הנכסים המוכלים}} בה מ-Immich ואינו ניתן לביטול. קבצים יישארו בדיסק.", + "confirm_delete_library_assets": "האם באמת ברצונך למחוק את הספרייה הזו? זה ימחק את {count, plural, one {תמונה # המוכלת} other {כל # תמונות המוכלים}} בה מ-Immich ואינו ניתן לביטול. קבצים יישארו בדיסק.", "confirm_email_below": "כדי לאשר, יש להקליד \"{email}\" למטה", "confirm_reprocess_all_faces": "האם באמת ברצונך לעבד מחדש את כל הפנים? זה גם ינקה אנשים בעלי שם.", "confirm_user_password_reset": "האם באמת ברצונך לאפס את הסיסמה של המשתמש {user}?", @@ -58,15 +58,15 @@ "cron_expression_description": "הגדר את מרווח הסריקה באמצעות תבנית ה- cron. למידע נוסף נא לפנות למשל אל Crontab Guru", "cron_expression_presets": "הגדרות קבועות מראש של ביטוי cron", "disable_login": "השבת כניסה", - "duplicate_detection_job_description": "הפעל למידת מכונה על נכסים כדי לזהות תמונות דומות. נשען על חיפוש חכם", + "duplicate_detection_job_description": "הפעל למידת מכונה על תמונות כדי לזהות תמונות דומות. נשען על חיפוש חכם", "exclusion_pattern_description": "דפוסי החרגה מאפשרים לך להתעלם מקבצים ומתיקיות בעת סריקת הספרייה שלך. זה שימושי אם יש לך תיקיות המכילות קבצים שאינך רוצה לייבא, כגון קובצי RAW.", "external_library_created_at": "ספרייה חיצונית (נוצרה ב-{date})", "external_library_management": "ניהול ספרייה חיצונית", "face_detection": "איתור פנים", - "face_detection_description": "אתר את הפנים בנכסים באמצעות למידת מכונה. עבור סרטונים, רק התמונה הממוזערת נלקחת בחשבון. \"רענון\" מעבד (מחדש) את כל הנכסים. \"איפוס\" מנקה בנוסף את כל נתוני הפנים הנוכחיים. \"חסרים\" מוסיף לתור נכסים שלא עובדו עדיין. לאחר שאיתור הפנים הושלם, פנים שאותרו יעמדו בתור לזיהוי פנים המשייך אותן לאנשים קיימים או חדשים.", + "face_detection_description": "אתר את הפנים בתמונות באמצעות למידת מכונה. עבור סרטונים, רק התמונה הממוזערת נלקחת בחשבון. \"רענון\" מעבד (מחדש) את כל התמונות. \"איפוס\" מנקה בנוסף את כל נתוני הפנים הנוכחיים. \"חסרים\" מוסיף לתור תמונות שלא עובדו עדיין. לאחר שאיתור הפנים הושלם, פנים שאותרו יעמדו בתור לזיהוי פנים המשייך אותן לאנשים קיימים או חדשים.", "facial_recognition_job_description": "קבץ פנים שאותרו לתוך אנשים. שלב זה מורץ לאחר השלמת איתור פנים. \"איפוס\" מקבץ (מחדש) את כל הפרצופים. \"חסרים\" מוסיף לתור פנים שלא הוקצה להם אדם.", "failed_job_command": "הפקודה {command} נכשלה עבור המשימה: {job}", - "force_delete_user_warning": "אזהרה: פעולה זו תסיר מיד את המשתמש ואת כל הנכסים. לא ניתן לבטל פעולה זו והקבצים לא ניתנים לשחזור.", + "force_delete_user_warning": "אזהרה: פעולה זו תסיר מיד את המשתמש ואת כל התמונות. לא ניתן לבטל פעולה זו והקבצים לא ניתנים לשחזור.", "forcing_refresh_library_files": "כפיית רענון של כל קבצי הספרייה", "image_format": "פורמט", "image_format_description": "WebP מפיק קבצים קטנים יותר מ JPEG, אך הוא איטי יותר לקידוד.", @@ -79,7 +79,7 @@ "image_prefer_embedded_preview_setting_description": "השתמש בתצוגות מקדימות מוטמעות בתמונות RAW כקלט לעיבוד תמונה וכאשר זמינות. זה יכול להפיק צבעים מדויקים יותר עבור תמונות מסוימות, אבל האיכות של התצוגה המקדימה היא תלוית מצלמה ולתמונה עשויים להיות יותר פגמי דחיסה.", "image_prefer_wide_gamut": "העדף סולם צבעים רחב", "image_prefer_wide_gamut_setting_description": "השתמש ב-Display P3 לתמונות ממוזערות. זה משמר טוב יותר את החיוניות של תמונות עם מרחבי צבע רחבים, אבל תמונות עשויות להופיע אחרת במכשירים ישנים עם גרסת דפדפן ישנה. תמונות sRGB נשמרות כ-sRGB כדי למנוע שינויי צבע.", - "image_preview_description": "תמונה בגודל בינוני עם מטא-נתונים שהוסרו, משמשת בעת צפייה בנכס בודד ועבור למידת מכונה", + "image_preview_description": "תמונה בגודל בינוני עם מטא-נתונים שהוסרו, משמשת בעת צפייה בתמונה בודדת ועבור למידת מכונה", "image_preview_quality_description": "איכות תצוגה מקדימה מ-1 עד 100. איכות גבוהה יותר היא טובה יותר, אבל מייצרת קבצים גדולים יותר ויכולה להפחית את תגובתיות היישום. הגדרת ערך נמוך עשויה להשפיע על איכות תוצאות של למידת מכונה.", "image_preview_title": "הגדרות תצוגה מקדימה", "image_quality": "איכות", @@ -106,7 +106,7 @@ "library_scanning_enable_description": "אפשר סריקת ספרייה תקופתית", "library_settings": "ספרייה חיצונית", "library_settings_description": "ניהול הגדרות ספרייה חיצונית", - "library_tasks_description": "סרוק ספריות חיצוניות עבור נכסים חדשים ו/או שהשתנו", + "library_tasks_description": "סרוק ספריות חיצוניות עבור תמונות חדשות ו/או שהשתנו", "library_watching_enable_description": "עקוב אחר שינויי קבצים בספריות חיצוניות", "library_watching_settings": "צפיית ספרייה (ניסיוני)", "library_watching_settings_description": "עקוב אוטומטית אחר שינויי קבצים", @@ -117,7 +117,7 @@ "machine_learning_clip_model_description": "שמו של מודל CLIP רשום כאן. שים לב שעליך להפעיל מחדש את המשימה 'חיפוש חכם' עבור כל התמונות בעת שינוי מודל.", "machine_learning_duplicate_detection": "איתור כפילויות", "machine_learning_duplicate_detection_enabled": "אפשר איתור כפילויות", - "machine_learning_duplicate_detection_enabled_description": "אם מושבת, נכסים זהים בדיוק עדיין יעברו ביטול כפילויות.", + "machine_learning_duplicate_detection_enabled_description": "אם מושבת, תמונות זהות בדיוק עדיין יעברו ביטול כפילויות.", "machine_learning_duplicate_detection_setting_description": "השתמש בהטמעות של CLIP כדי למצוא כפילויות אפשריות", "machine_learning_enabled": "אפשר למידת מכונה", "machine_learning_enabled_description": "אם מושבת, כל תכונות למידת מכונה יהיו מושבתות ללא קשר להגדרות שלהלן.", @@ -160,16 +160,16 @@ "memory_cleanup_job": "ניקוי זיכרון", "memory_generate_job": "יצירת זיכרון", "metadata_extraction_job": "חלץ מטא-נתונים", - "metadata_extraction_job_description": "חלץ מידע מטא-נתונים מכל נכס, כגון GPS, פנים ורזולוציה", + "metadata_extraction_job_description": "חלץ מטא-נתונים מכל תמונה, כגון GPS, פנים ורזולוציה", "metadata_faces_import_setting": "אפשר יבוא פנים", "metadata_faces_import_setting_description": "יבא פנים מנתוני EXIF של תמונה ומקבצים נלווים", "metadata_settings": "הגדרות מטא-נתונים", "metadata_settings_description": "ניהול הגדרות מטא-נתונים", "migration_job": "העברה", - "migration_job_description": "העבר תמונות ממוזערות של נכסים ופנים למבנה התיקיות העדכני ביותר", + "migration_job_description": "העבר תמונות ממוזערות של תמונות ופנים למבנה התיקיות העדכני ביותר", "no_paths_added": "לא נוספו נתיבים", "no_pattern_added": "לא נוספה תבנית", - "note_apply_storage_label_previous_assets": "הערה: כדי להחיל את תווית האחסון על נכסים שהועלו בעבר, הפעל את", + "note_apply_storage_label_previous_assets": "הערה: כדי להחיל את תווית האחסון על תמונות שהועלו בעבר, הפעל את", "note_cannot_be_changed_later": "הערה: אי אפשר לשנות זאת מאוחר יותר!", "notification_email_from_address": "מכתובת", "notification_email_from_address_description": "כתובת דוא\"ל של השולח, לדוגמה: \"Immich שרת תמונות \"", @@ -243,21 +243,21 @@ "sidecar_job": "מטא-נתונים נלווים", "sidecar_job_description": "גלה או סנכרן מטא-נתונים נלווים ממערכת הקבצים", "slideshow_duration_description": "מספר שניות להצגת כל תמונה", - "smart_search_job_description": "הפעל למידת מכונה על נכסים כדי לתמוך בחיפוש חכם", - "storage_template_date_time_description": "חותמת זמן יצירת הנכס משמשת למידע על התאריך והשעה", + "smart_search_job_description": "הפעל למידת מכונה על תמונות כדי לתמוך בחיפוש חכם", + "storage_template_date_time_description": "חותמת זמן יצירת התמונה משמשת למידע על התאריך והשעה", "storage_template_date_time_sample": "זמן לדוגמא {date}", "storage_template_enable_description": "הפעל מנוע תבנית אחסון", "storage_template_hash_verification_enabled": "אימות גיבוב מופעל", "storage_template_hash_verification_enabled_description": "מאפשר אימות גיבוב, אין להשבית זאת אלא אם יש לך ודאות לגבי ההשלכות", "storage_template_migration": "העברת תבנית אחסון", - "storage_template_migration_description": "החל את ה{template} הנוכחית על נכסים שהועלו בעבר", - "storage_template_migration_info": "תבנית האחסון תמיר את כל ההרחבות לאותיות קטנות. שינויים בתבנית יחולו רק על נכסים חדשים. כדי להחיל באופן רטרואקטיבי את התבנית על נכסים שהועלו בעבר, הפעל את {job}.", + "storage_template_migration_description": "החל את ה{template} הנוכחית על תמונות שהועלו בעבר", + "storage_template_migration_info": "תבנית האחסון תמיר את כל ההרחבות לאותיות קטנות. שינויים בתבנית יחולו רק על תמונות חדשות. כדי להחיל באופן רטרואקטיבי את התבנית על תמונות שהועלו בעבר, הפעל את {job}.", "storage_template_migration_job": "משימת העברת תבנית אחסון", "storage_template_more_details": "לפרטים נוספים אודות תכונה זו, עיין בתבנית האחסון ובהשלכותיה", "storage_template_onboarding_description": "כאשר מופעלת, תכונה זו תארגן אוטומטית קבצים בהתבסס על תבנית שהמשתמש הגדיר. עקב בעיות יציבות התכונה כבויה כברירת מחדל. למידע נוסף, נא לראות את התיעוד.", "storage_template_path_length": "מגבלת אורך נתיב משוערת: {length, number}/{limit, number}", "storage_template_settings": "תבנית אחסון", - "storage_template_settings_description": "ניהול מבנה התיקיות ואת שם הקובץ של נכס ההעלאה", + "storage_template_settings_description": "ניהול מבנה התיקיות ואת שם הקובץ של התמונה שהועלתה", "storage_template_user_label": "{label} היא תווית האחסון של המשתמש", "system_settings": "הגדרות מערכת", "tag_cleanup_job": "ניקוי תגים", @@ -277,7 +277,7 @@ "theme_settings_description": "ניהול התאמה אישית של ממשק האינטרנט של Immich", "these_files_matched_by_checksum": "קבצים אלה תואמים לפי סיכומי הביקורת שלהם", "thumbnail_generation_job": "צור תמונות ממוזערות", - "thumbnail_generation_job_description": "יוצר תמונות ממוזערות גדולות, קטנות ומטושטשות עבור כל נכס, כמו גם תמונות ממוזערות עבור כל אדם", + "thumbnail_generation_job_description": "יוצר תמונות ממוזערות גדולות, קטנות ומטושטשות עבור כל תמונה, כמו גם תמונות ממוזערות עבור כל אדם", "transcoding_acceleration_api": "API האצה", "transcoding_acceleration_api_description": "ה-API שייצור אינטראקציה עם המכשיר שלך כדי להאיץ את המרת הקידוד. הגדרה זו היא 'המאמץ הטוב ביותר': היא תחזור לקידוד תוכנה במקרה של כשל. VP9 עשוי לעבוד או לא, תלוי בחומרה שלך.", "transcoding_acceleration_nvenc": "NVENC (דורש כרטיס מסך של NVIDIA)", @@ -341,17 +341,17 @@ "transcoding_video_codec_description": "ל-VP9 יש יעילות גבוהה ותאימות רשת, אבל לוקח יותר זמן להמיר את הקידוד עבורו. HEVC מתפקד באופן דומה, אך בעל תאימות רשת נמוכה יותר. H.264 תואם באופן נרחב ומהיר להמיר את קידודו, אבל הוא מייצר קבצים גדולים בהרבה. AV1 הוא הקידוד היעיל ביותר אך לוקה בתמיכה במכשירים ישנים יותר.", "trash_enabled_description": "הפעל את תכונות האשפה", "trash_number_of_days": "מספר הימים", - "trash_number_of_days_description": "מספר הימים לשמירה על הנכסים באשפה לפני הסרתם לצמיתות", + "trash_number_of_days_description": "מספר הימים לשמירה של תמונות באשפה לפני הסרתם לצמיתות", "trash_settings": "הגדרות האשפה", "trash_settings_description": "ניהול הגדרות האשפה", "untracked_files": "קבצים ללא מעקב", "untracked_files_description": "קבצים אלה אינם נמצאים במעקב של היישום. הם יכולים להיות תוצאות של העברות כושלות, העלאות שנקטעו, או שנותרו מאחור בגלל שיבוש בתוכנה", "user_cleanup_job": "ניקוי משתמשים", - "user_delete_delay": "החשבון והנכסים של {user} יתוזמנו למחיקה לצמיתות בעוד {delay, plural, one {יום #} other {# ימים}}.", + "user_delete_delay": "החשבון והתמונות של {user} יתוזמנו למחיקה לצמיתות בעוד {delay, plural, one {יום #} other {# ימים}}.", "user_delete_delay_settings": "עיכוב מחיקה", - "user_delete_delay_settings_description": "מספר הימים לאחר ההסרה עד מחיקה לצמיתות של החשבון והנכסים של המשתמש. משימת מחיקת המשתמש פועלת בחצות כדי לבדוק אם יש משתמשים שמוכנים למחיקה. שינויים בהגדרה זו יוערכו בביצוע הבא.", - "user_delete_immediately": "החשבון והנכסים של {user} יעמדו בתור למחיקה לצמיתות באופן מיידי.", - "user_delete_immediately_checkbox": "העמד משתמש ונכסים בתור למחיקה מיידית", + "user_delete_delay_settings_description": "מספר הימים לאחר ההסרה עד מחיקה לצמיתות של החשבון והתמונות של המשתמש. משימת מחיקת המשתמש פועלת בחצות כדי לבדוק אם יש משתמשים שמוכנים למחיקה. שינויים בהגדרה זו יוערכו בביצוע הבא.", + "user_delete_immediately": "החשבון והתמונות של {user} יעמדו בתור למחיקה לצמיתות באופן מיידי.", + "user_delete_immediately_checkbox": "הצב משתמש ותמונות בתור למחיקה מיידית", "user_management": "ניהול משתמשים", "user_password_has_been_reset": "סיסמת המשתמש אופסה:", "user_password_reset_description": "אנא ספק את הסיסמה הזמנית למשתמש והודע לו שיש צורך לשנות את הסיסמה בכניסה הבאה שלו.", @@ -371,13 +371,17 @@ "admin_password": "סיסמת מנהל", "administration": "ניהול", "advanced": "מתקדם", - "advanced_settings_log_level_title": "רמת תיעוד אירועים: {}", - "advanced_settings_prefer_remote_subtitle": "חלק מהמכשירים הם איטיים מאד לטעון תמונות ממוזערות מנכסים שבמכשיר. הפעל הגדרה זו כדי לטעון תמונות מרוחקות במקום", + "advanced_settings_enable_alternate_media_filter_subtitle": "השתמש באפשרות זו כדי לסנן מדיה במהלך הסנכרון לפי קריטריונים חלופיים. מומלץ להשתמש בזה רק אם יש בעיה בזיהוי כל האלבומים באפליקציה.", + "advanced_settings_enable_alternate_media_filter_title": "[ניסיוני] השתמש במסנן סנכרון אלבום חלופי שמבכשיר", + "advanced_settings_log_level_title": "רמת רישום ביומן: {}", + "advanced_settings_prefer_remote_subtitle": "חלק מהמכשירים הם איטיים מאד לטעינה של תמונות ממוזערות מתמונות שבמכשיר. הפעל הגדרה זו כדי לטעון תמונות מרוחקות במקום.", "advanced_settings_prefer_remote_title": "העדף תמונות מרוחקות", - "advanced_settings_proxy_headers_subtitle": "הגדר כותרות פרוקסי שהיישום צריך לשלוח עם כל בקשת רשת", + "advanced_settings_proxy_headers_subtitle": "הגדר proxy headers שהיישום צריך לשלוח עם כל בקשת רשת", "advanced_settings_proxy_headers_title": "כותרות פרוקסי", - "advanced_settings_self_signed_ssl_subtitle": "מדלג על אימות תעודת SSL עבור נקודת הקצה של השרת. דרוש עבור תעודות בחתימה עצמית", + "advanced_settings_self_signed_ssl_subtitle": "מדלג על אימות תעודת SSL עבור נקודת הקצה של השרת. דרוש עבור תעודות בחתימה עצמית.", "advanced_settings_self_signed_ssl_title": "התר תעודות SSL בחתימה עצמית", + "advanced_settings_sync_remote_deletions_subtitle": "מחק או שחזר תמונה במכשיר זה באופן אוטומטי כאשר פעולה זו נעשית בדפדפן", + "advanced_settings_sync_remote_deletions_title": "סנכרן מחיקות שבוצעו במכשירים אחרים [נסיוני]", "advanced_settings_tile_subtitle": "הגדרות משתמש מתקדם", "advanced_settings_troubleshooting_subtitle": "אפשר תכונות נוספות לפתרון בעיות", "advanced_settings_troubleshooting_title": "פתרון בעיות", @@ -402,15 +406,15 @@ "album_thumbnail_card_item": "פריט 1", "album_thumbnail_card_items": "{} פריטים", "album_thumbnail_card_shared": " · משותף", - "album_thumbnail_shared_by": "משותף על ידי {}", + "album_thumbnail_shared_by": "שותף על ידי {}", "album_updated": "אלבום עודכן", - "album_updated_setting_description": "קבל הודעת דוא\"ל כאשר לאלבום משותף יש נכסים חדשים", + "album_updated_setting_description": "קבל הודעת דוא\"ל כאשר לאלבום משותף יש תמונות חדשות", "album_user_left": "עזב את {album}", "album_user_removed": "{user} הוסר", "album_viewer_appbar_delete_confirm": "האם את/ה בטוח/ה שברצונך למחוק את האלבום הזה מהחשבון שלך?", "album_viewer_appbar_share_err_delete": "מחיקת אלבום נכשלה", "album_viewer_appbar_share_err_leave": "עזיבת האלבום נכשלה", - "album_viewer_appbar_share_err_remove": "יש בעיות בהסרת הנכסים מהאלבום", + "album_viewer_appbar_share_err_remove": "יש בעיות בהסרת התמונות מהאלבום", "album_viewer_appbar_share_err_title": "נכשל בשינוי כותרת האלבום", "album_viewer_appbar_share_leave": "עזוב אלבום", "album_viewer_appbar_share_to": "שתף עם", @@ -439,57 +443,57 @@ "appears_in": "מופיע ב", "archive": "ארכיון", "archive_or_unarchive_photo": "העבר תמונה לארכיון או הוצא אותה משם", - "archive_page_no_archived_assets": "לא נמצאו נכסים בארכיון", - "archive_page_title": "ארכיון ({})", + "archive_page_no_archived_assets": "לא נמצאו תמונות בארכיון", + "archive_page_title": "בארכיון ({})", "archive_size": "גודל הארכיון", "archive_size_description": "הגדר את גודל הארכיון להורדות (ב-GiB)", "archived": "בארכיון", "archived_count": "{count, plural, other {# הועברו לארכיון}}", "are_these_the_same_person": "האם אלה אותו האדם?", "are_you_sure_to_do_this": "האם באמת ברצונך לעשות את זה?", - "asset_action_delete_err_read_only": "לא ניתן למחוק נכס(ים) לקריאה בלבד, מדלג", - "asset_action_share_err_offline": "לא ניתן להשיג נכס(ים) לא מקוונ(ים), מדלג ", + "asset_action_delete_err_read_only": "לא ניתן למחוק תמונות לקריאה בלבד, מדלג", + "asset_action_share_err_offline": "לא ניתן להשיג תמונות לא מקוונות, מדלג", "asset_added_to_album": "נוסף לאלבום", "asset_adding_to_album": "מוסיף לאלבום…", - "asset_description_updated": "תיאור הנכס עודכן", - "asset_filename_is_offline": "הנכס {filename} אינו מקוון", - "asset_has_unassigned_faces": "לנכס יש פנים שלא הוקצו", + "asset_description_updated": "תיאור התמונה עודכן", + "asset_filename_is_offline": "התמונה {filename} אינה מקוונת", + "asset_has_unassigned_faces": "לתמונה יש פנים שלא הוקצו", "asset_hashing": "מגבב…", "asset_list_group_by_sub_title": "קבץ לפי", "asset_list_layout_settings_dynamic_layout_title": "פריסה דינמית", "asset_list_layout_settings_group_automatically": "אוטומטי", - "asset_list_layout_settings_group_by": "קבץ נכסים לפי", + "asset_list_layout_settings_group_by": "קבץ תמונות לפי", "asset_list_layout_settings_group_by_month_day": "חודש + יום", "asset_list_layout_sub_title": "פריסה", "asset_list_settings_subtitle": "הגדרות תבנית רשת תמונות", "asset_list_settings_title": "רשת תמונות", - "asset_offline": "נכס לא מקוון", - "asset_offline_description": "הנכס החיצוני הזה כבר לא נמצא בדיסק. נא ליצור קשר עם מנהל Immich שלך לקבלת עזרה.", - "asset_restored_successfully": "נכס שוחזר בהצלחה", + "asset_offline": "תמונה לא מקוונת", + "asset_offline_description": "התמונה החיצונית הזאת כבר לא נמצאת בדיסק. נא ליצור קשר עם מנהל Immich שלך לקבלת עזרה.", + "asset_restored_successfully": "תמונה שוחזרה בהצלחה", "asset_skipped": "דילג", "asset_skipped_in_trash": "באשפה", "asset_uploaded": "הועלה", "asset_uploading": "מעלה…", "asset_viewer_settings_subtitle": "ניהול הגדרות מציג הגלריה שלך", - "asset_viewer_settings_title": "מציג הנכסים", - "assets": "נכסים", - "assets_added_count": "{count, plural, one {נוסף נכס #} other {נוספו # נכסים}}", - "assets_added_to_album_count": "{count, plural, one {נוסף נכס #} other {נוספו # נכסים}} לאלבום", - "assets_added_to_name_count": "{count, plural, one {נכס # נוסף} other {# נכסים נוספו}} אל {hasName, select, true {{name}} other {אלבום חדש}}", - "assets_count": "{count, plural, one {נכס #} other {# נכסים}}", - "assets_deleted_permanently": "{} נכס(ים) נמחקו לצמיתות", - "assets_deleted_permanently_from_server": "{} נכס(ים) נמחקו לצמיתות משרת ה-Immich", - "assets_moved_to_trash_count": "{count, plural, one {נכס # הועבר} other {# נכסים הועברו}} לאשפה", - "assets_permanently_deleted_count": "{count, plural, one {נכס # נמחק} other {# נכסים נמחקו}} לצמיתות", - "assets_removed_count": "{count, plural, one {נכס # הוסר} other {# נכסים הוסרו}}", - "assets_removed_permanently_from_device": "{} נכס(ים) נמחקו לצמיתות מהמכשיר שלך", - "assets_restore_confirmation": "האם באמת ברצונך לשחזר את כל הנכסים שבאשפה? אין באפשרותך לבטל את הפעולה הזו! יש לשים לב שלא ניתן לשחזר נכסים לא מקוונים בדרך זו.", - "assets_restored_count": "{count, plural, one {נכס # שוחזר} other {# נכסים שוחזרו}}", - "assets_restored_successfully": "{} נכס(ים) שוחזרו בהצלחה", - "assets_trashed": "{} נכס(ים) הועברו לאשפה", - "assets_trashed_count": "{count, plural, one {נכס # הושלך} other {# נכסים הושלכו}} לאשפה", - "assets_trashed_from_server": "{} נכס(ים) הועברו לאשפה משרת ה-Immich", - "assets_were_part_of_album_count": "{count, plural, one {נכס היה} other {נכסים היו}} כבר חלק מהאלבום", + "asset_viewer_settings_title": "מציג התמונות", + "assets": "תמונות", + "assets_added_count": "{count, plural, one {נוספה תומנה #} other {נוספו # תמונות}}", + "assets_added_to_album_count": "{count, plural, one {נוספה תמונה #} other {נוספו # תמונות}} לאלבום", + "assets_added_to_name_count": "{count, plural, one {תמונה # נוספה} other {# תמונות נוספו}} אל {hasName, select, true {{name}} other {אלבום חדש}}", + "assets_count": "{count, plural, one {תמונה #} other {# תמונות}}", + "assets_deleted_permanently": "{} תמונות נמחקו לצמיתות", + "assets_deleted_permanently_from_server": "{} תמונות נמחקו לצמיתות משרת ה-Immich", + "assets_moved_to_trash_count": "{count, plural, one {תמונה # הועברה} other {# תמונות הועברו}} לאשפה", + "assets_permanently_deleted_count": "{count, plural, one {תמונה # נמחקה} other {# תמונות נמחקו}} לצמיתות", + "assets_removed_count": "{count, plural, one {תמונה # הוסרה} other {# תמונות הוסרו}}", + "assets_removed_permanently_from_device": "{} תמונות נמחקו לצמיתות מהמכשיר שלך", + "assets_restore_confirmation": "האם באמת ברצונך לשחזר את כל התמונות שבאשפה? אין באפשרותך לבטל את הפעולה הזו! יש לשים לב שלא ניתן לשחזר תמונות לא מקוונות בדרך זו.", + "assets_restored_count": "{count, plural, one {תמונה # שוחזרה} other {# תמונות שוחזרו}}", + "assets_restored_successfully": "{} תמונות שוחזרו בהצלחה", + "assets_trashed": "{} תמונות הועברו לאשפה", + "assets_trashed_count": "{count, plural, one {תמונה # הושלכה} other {# תמונות הושלכו}} לאשפה", + "assets_trashed_from_server": "{} תמונות הועברו לאשפה מהשרת", + "assets_were_part_of_album_count": "{count, plural, one {תמונה הייתה} other {תמונות היו}} כבר חלק מהאלבום", "authorized_devices": "מכשירים מורשים", "automatic_endpoint_switching_subtitle": "התחבר מקומית דרך אינטרנט אלחוטי ייעודי כאשר זמין והשתמש בחיבורים חלופיים במקומות אחרים", "automatic_endpoint_switching_title": "החלפת כתובת אוטומטית", @@ -497,22 +501,22 @@ "back_close_deselect": "חזור, סגור, או בטל בחירה", "background_location_permission": "הרשאת מיקום ברקע", "background_location_permission_content": "כדי להחליף רשתות בעת ריצה ברקע, היישום צריך *תמיד* גישה למיקום מדויק על מנת לקרוא את השם של רשת האינטרנט האלחוטי", - "backup_album_selection_page_albums_device": "אלבומים במכשיר ({})", + "backup_album_selection_page_albums_device": "({}) אלבומים במכשיר", "backup_album_selection_page_albums_tap": "הקש כדי לכלול, הקש פעמיים כדי להחריג", - "backup_album_selection_page_assets_scatter": "נכסים יכולים להתפזר על פני אלבומים מרובים. לפיכך, ניתן לכלול או להחריג אלבומים במהלך תהליך הגיבוי", + "backup_album_selection_page_assets_scatter": "תמונות יכולות להתפזר על פני אלבומים מרובים. לפיכך, ניתן לכלול או להחריג אלבומים במהלך תהליך הגיבוי.", "backup_album_selection_page_select_albums": "בחירת אלבומים", "backup_album_selection_page_selection_info": "פרטי בחירה", - "backup_album_selection_page_total_assets": "סה״כ נכסים ייחודיים", + "backup_album_selection_page_total_assets": "סה״כ תמונות ייחודיות", "backup_all": "הכל", - "backup_background_service_backup_failed_message": "נכשל בגיבוי נכסים. מנסה שוב...", - "backup_background_service_connection_failed_message": "נכשל בהתחברות לשרת. מנסה שוב...", + "backup_background_service_backup_failed_message": "נכשל בגיבוי תמונות. מנסה שוב…", + "backup_background_service_connection_failed_message": "נכשל בהתחברות לשרת. מנסה שוב…", "backup_background_service_current_upload_notification": "מעלה {}", - "backup_background_service_default_notification": "מחפש נכסים חדשים...", + "backup_background_service_default_notification": "מחפש תמונות חדשות…", "backup_background_service_error_title": "שגיאת גיבוי", - "backup_background_service_in_progress_notification": "מגבה את הנכסים שלך...", - "backup_background_service_upload_failure_notification": "נכשל להעלות {}", + "backup_background_service_in_progress_notification": "מגבה את התמונות שלך…", + "backup_background_service_upload_failure_notification": "{} נכשל בהעלאה", "backup_controller_page_albums": "אלבומים לגיבוי", - "backup_controller_page_background_app_refresh_disabled_content": "אפשר רענון אפליקציה ברקע בהגדרות > כללי > רענון אפליקציה ברקע כדי להשתמש בגיבוי ברקע", + "backup_controller_page_background_app_refresh_disabled_content": "אפשר רענון אפליקציה ברקע בהגדרות > כללי > רענון אפליקציה ברקע כדי להשתמש בגיבוי ברקע.", "backup_controller_page_background_app_refresh_disabled_title": "רענון אפליקציה ברקע מושבת", "backup_controller_page_background_app_refresh_enable_button_text": "לך להגדרות", "backup_controller_page_background_battery_info_link": "הראה לי איך", @@ -521,8 +525,8 @@ "backup_controller_page_background_battery_info_title": "מיטובי סוללה", "backup_controller_page_background_charging": "רק בטעינה", "backup_controller_page_background_configure_error": "נכשל בהגדרת תצורת שירות הרקע", - "backup_controller_page_background_delay": "דחה גיבוי נכסים חדשים: {}", - "backup_controller_page_background_description": "הפעל את השירות רקע כדי לגבות באופן אוטומטי כל נכס חדש מבלי להצטרך לפתוח את היישום", + "backup_controller_page_background_delay": "השהה גיבוי של תמונות חדשות: {}", + "backup_controller_page_background_description": "הפעל את השירות רקע כדי לגבות באופן אוטומטי כל תמונה חדשה מבלי להצטרך לפתוח את היישום", "backup_controller_page_background_is_off": "גיבוי אוטומטי ברקע כבוי", "backup_controller_page_background_is_on": "גיבוי אוטומטי ברקע מופעל", "backup_controller_page_background_turn_off": "כבה שירות גיבוי ברקע", @@ -532,10 +536,10 @@ "backup_controller_page_backup_selected": "נבחרו: ", "backup_controller_page_backup_sub": "תמונות וסרטונים מגובים", "backup_controller_page_created": "נוצר ב: {}", - "backup_controller_page_desc_backup": "הפעל גיבוי חזית כדי להעלות באופן אוטומטי נכסים חדשים לשרת כשפותחים את היישום", + "backup_controller_page_desc_backup": "הפעל גיבוי חזית כדי להעלות באופן אוטומטי תמונות חדשות לשרת כשפותחים את היישום.", "backup_controller_page_excluded": "הוחרגו: ", - "backup_controller_page_failed": "נכשל ({})", - "backup_controller_page_filename": "שם קובץ: {} [{}]", + "backup_controller_page_failed": "({}) נכשלו", + "backup_controller_page_filename": "שם הקובץ: {} [{}]", "backup_controller_page_id": "מזהה: {}", "backup_controller_page_info": "פרטי גיבוי", "backup_controller_page_none_selected": "אין בחירה", @@ -545,14 +549,14 @@ "backup_controller_page_start_backup": "התחל גיבוי", "backup_controller_page_status_off": "גיבוי חזית אוטומטי כבוי", "backup_controller_page_status_on": "גיבוי חזית אוטומטי מופעל", - "backup_controller_page_storage_format": "{} מתוך {} נוצלו", + "backup_controller_page_storage_format": "{} מתוך {} בשימוש", "backup_controller_page_to_backup": "אלבומים לגבות", "backup_controller_page_total_sub": "כל התמונות והסרטונים הייחודיים מאלבומים שנבחרו", "backup_controller_page_turn_off": "כיבוי גיבוי חזית", "backup_controller_page_turn_on": "הפעל גיבוי חזית", "backup_controller_page_uploading_file_info": "מעלה מידע על הקובץ", "backup_err_only_album": "לא ניתן להסיר את האלבום היחיד", - "backup_info_card_assets": "נכסים", + "backup_info_card_assets": "תמונות", "backup_manual_cancelled": "בוטל", "backup_manual_in_progress": "העלאה כבר בתהליך. נסה אחרי זמן מה", "backup_manual_success": "הצלחה", @@ -566,25 +570,25 @@ "bugs_and_feature_requests": "באגים & בקשות לתכונות", "build": "גרסת בנייה", "build_image": "גרסת תוכנה", - "bulk_delete_duplicates_confirmation": "האם באמת ברצונך למחוק בכמות גדולה {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה ישמור על הנכס הכי גדול של כל קבוצה וימחק לצמיתות את כל שאר הכפילויות. אין באפשרותך לבטל את הפעולה הזו!", - "bulk_keep_duplicates_confirmation": "האם באמת ברצונך להשאיר {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה יפתור את כל הקבוצות הכפולות מבלי למחוק דבר.", - "bulk_trash_duplicates_confirmation": "האם באמת ברצונך להעביר לאשפה בכמות גדולה {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה ישמור על הנכס הגדול ביותר של כל קבוצה ויעביר לאשפה את כל שאר הכפילויות.", + "bulk_delete_duplicates_confirmation": "האם באמת ברצונך למחוק בכמות גדולה {count, plural, one {תמונה # כפולה} other {# תמונות כפולות}}? זה ישמור על התמונה הכי גדולה של כל קבוצה וימחק לצמיתות את כל שאר הכפילויות. אין באפשרותך לבטל את הפעולה הזו!", + "bulk_keep_duplicates_confirmation": "האם באמת ברצונך להשאיר {count, plural, one {תמונה # כפולה} other {# תמונות כפולות}}? זה יסגור את כל הקבוצות הכפולות מבלי למחוק דבר.", + "bulk_trash_duplicates_confirmation": "האם באמת ברצונך להעביר לאשפה בכמות גדולה {count, plural, one {תמונה # כפולה} other {# תמונות כפולות}}? זה ישמור על התמונה הגדולה ביותר של כל קבוצה ויעביר לאשפה את כל שאר הכפילויות.", "buy": "רכוש את Immich", - "cache_settings_album_thumbnails": "תמונות ממוזערות של דף ספרייה ({} נכסים)", + "cache_settings_album_thumbnails": "תמונות ממוזערות של דף ספרייה ({} תמונות)", "cache_settings_clear_cache_button": "ניקוי מטמון", - "cache_settings_clear_cache_button_title": "מנקה את המטמון של היישום. זה ישפיע באופן משמעותי על הביצועים של היישום עד שהמטמון נבנה מחדש", + "cache_settings_clear_cache_button_title": "מנקה את המטמון של היישום. זה ישפיע באופן משמעותי על הביצועים של היישום עד שהמטמון מתמלא מחדש.", "cache_settings_duplicated_assets_clear_button": "נקה", "cache_settings_duplicated_assets_subtitle": "תמונות וסרטונים שנמצאים ברשימה השחורה של היישום", - "cache_settings_duplicated_assets_title": "נכסים משוכפלים ({})", - "cache_settings_image_cache_size": "גודל מטמון תמונה ({} נכסים)", + "cache_settings_duplicated_assets_title": "({}) תמונות משוכפלות", + "cache_settings_image_cache_size": "גודל מטמון התמונה ({} תמונות)", "cache_settings_statistics_album": "תמונות ממוזערות של ספרייה", - "cache_settings_statistics_assets": "{} נכסים ({})", + "cache_settings_statistics_assets": "{} תמונות ({})", "cache_settings_statistics_full": "תמונות מלאות", "cache_settings_statistics_shared": "תמונות ממוזערות של אלבום משותף", "cache_settings_statistics_thumbnail": "תמונות ממוזערות", "cache_settings_statistics_title": "שימוש במטמון", - "cache_settings_subtitle": "שלוט בהתנהגות שמירת המטמון של היישום הנייד", - "cache_settings_thumbnail_size": "גודל מטמון תמונה ממוזערת ({} נכסים)", + "cache_settings_subtitle": "הגדר כיצד אפליקציית Immich שומרת נתונים באופן זמני", + "cache_settings_thumbnail_size": "גודל מטמון תמונה ממוזערת ({} תמונות)", "cache_settings_tile_subtitle": "שלוט בהתנהגות האחסון המקומי", "cache_settings_tile_title": "אחסון מקומי", "cache_settings_title": "הגדרות שמירת מטמון", @@ -613,9 +617,9 @@ "change_your_password": "החלף את הסיסמה שלך", "changed_visibility_successfully": "הנראות שונתה בהצלחה", "check_all": "לסמן הכל", - "check_corrupt_asset_backup": "בדוק גיבויים פגומים של נכסים", + "check_corrupt_asset_backup": "בדוק גיבויים פגומים של תמונות", "check_corrupt_asset_backup_button": "בצע בדיקה", - "check_corrupt_asset_backup_description": "הרץ בדיקה זו רק על Wi-Fi ולאחר שכל הנכסים גובו. ההליך עשוי לקחת כמה דקות.", + "check_corrupt_asset_backup_description": "הרץ בדיקה זו רק על Wi-Fi ולאחר שכל התמונות גובו. ההליך עשוי לקחת כמה דקות.", "check_logs": "בדוק יומני רישום", "choose_matching_people_to_merge": "בחר אנשים תואמים למיזוג", "city": "עיר", @@ -643,13 +647,13 @@ "comments_and_likes": "תגובות & לייקים", "comments_are_disabled": "תגובות מושבתות", "common_create_new_album": "צור אלבום חדש", - "common_server_error": "נא לבדוק את חיבור הרשת שלך, תוודא/י שהשרת נגיש ושגרסאות אפליקציה/שרת תואמות", + "common_server_error": "נא לבדוק את חיבור הרשת שלך, תוודא/י שהשרת נגיש ושגרסאות אפליקציה/שרת תואמות.", "completed": "הושלמו", "confirm": "אישור", "confirm_admin_password": "אישור סיסמת מנהל", - "confirm_delete_face": "האם באמת ברצונך למחוק את הפנים של {name} מהנכס?", + "confirm_delete_face": "האם באמת ברצונך למחוק את הפנים של {name} מהתמונה?", "confirm_delete_shared_link": "האם באמת ברצונך למחוק את הקישור המשותף הזה?", - "confirm_keep_this_delete_others": "כל שאר הנכסים בערימה יימחקו למעט נכס זה. האם באמת ברצונך להמשיך?", + "confirm_keep_this_delete_others": "כל שאר תמונות שבערימה יימחקו למעט תמונה זאת. האם באמת ברצונך להמשיך?", "confirm_password": "אשר סיסמה", "contain": "מכיל", "context": "הקשר", @@ -684,9 +688,9 @@ "create_link_to_share_description": "אפשר לכל אחד עם הקישור לראות את התמונות שנבחרו", "create_new": "צור חדש", "create_new_person": "צור אדם חדש", - "create_new_person_hint": "הקצה את הנכסים שנבחרו לאדם חדש", + "create_new_person_hint": "הקצה את התמונות שנבחרו לאדם חדש", "create_new_user": "צור משתמש חדש", - "create_shared_album_page_share_add_assets": "הוסף נכסים", + "create_shared_album_page_share_add_assets": "הוסף תמונות", "create_shared_album_page_share_select_photos": "בחירת תמונות", "create_tag": "צור תג", "create_tag_description": "צור תג חדש. עבור תגים מקוננים, נא להזין את הנתיב המלא של התג כולל קווים נטויים.", @@ -712,12 +716,12 @@ "deduplication_criteria_1": "גודל תמונה בבתים", "deduplication_criteria_2": "ספירת נתוני EXIF", "deduplication_info": "מידע על ביטול כפילויות", - "deduplication_info_description": "כדי לבחור מראש נכסים באופן אוטומטי ולהסיר כפילויות בכמות גדולה, אנו מסתכלים על:", + "deduplication_info_description": "כדי לבחור מראש תמונות באופן אוטומטי ולהסיר כפילויות בכמות גדולה, אנו מסתכלים על:", "default_locale": "שפת ברירת מחדל", "default_locale_description": "פורמט תאריכים ומספרים מבוסס שפת הדפדפן שלך", "delete": "מחק", "delete_album": "מחק אלבום", - "delete_api_key_prompt": "האם באמת ברצונך למחוק מפתח ה-API הזה?", + "delete_api_key_prompt": "האם אתה בטוח שברצונך למחוק מפתח ה-API הזה?", "delete_dialog_alert": "הפריטים האלה ימחקו לצמיתות מהשרת ומהמכשיר שלך", "delete_dialog_alert_local": "הפריטים האלה יוסרו לצמיתות מהמכשיר שלך אבל עדיין יהיו זמינים בשרת", "delete_dialog_alert_local_non_backed_up": "חלק מהפריטים לא מגובים לשרת ויוסרו לצמיתות מהמכשיר שלך", @@ -738,7 +742,7 @@ "delete_tag_confirmation_prompt": "האם באמת ברצונך למחוק תג {tagName}?", "delete_user": "מחק משתמש", "deleted_shared_link": "קישור משותף נמחק", - "deletes_missing_assets": "מוחק נכסים שחסרים בדיסק", + "deletes_missing_assets": "מוחק תמונות שחסרות בדיסק", "description": "תיאור", "description_input_hint_text": "הוסף תיאור...", "description_input_submit_error": "שגיאה בעדכון תיאור, בדוק את היומן לפרטים נוספים", @@ -753,7 +757,7 @@ "display_options": "הצגת אפשרויות", "display_order": "סדר תצוגה", "display_original_photos": "הצגת תמונות מקוריות", - "display_original_photos_setting_description": "העדף להציג את התמונה המקורית בעת צפיית נכס במקום תמונות ממוזערות כאשר הנכס המקורי תומך בתצוגה בדפדפן. זה עלול לגרום לתמונות להיות מוצגות באיטיות.", + "display_original_photos_setting_description": "הצג תמונה מקורית בעת צפייה בתמונה במקום תמונות ממוזערות כאשר התמונה המקורית תומכת בתצוגה בדפדפן. זה עלול לגרום לתמונות להיות מוצגות באיטיות.", "do_not_show_again": "אל תציג את ההודעה הזאת שוב", "documentation": "תיעוד", "done": "סיום", @@ -770,13 +774,13 @@ "download_notfound": "הורדה לא נמצא", "download_paused": "הורדה הופסקה", "download_settings": "הורדה", - "download_settings_description": "ניהול הגדרות הקשורות להורדת נכסים", + "download_settings_description": "ניהול הגדרות הקשורות להורדת תמונות", "download_started": "הורדה החלה", "download_sucess": "הצלחת הורדה", "download_sucess_android": "המדיה הורדה אל DCIM/Immich", "download_waiting_to_retry": "מחכה כדי לנסות שוב", "downloading": "מוריד", - "downloading_asset_filename": "מוריד נכס {filename}", + "downloading_asset_filename": "מוריד תמונה {filename}", "downloading_media": "מוריד מדיה", "drop_files_to_upload": "שחרר קבצים בכל מקום כדי להעלות", "duplicates": "כפילויות", @@ -809,7 +813,7 @@ "email": "דוא\"ל", "empty_folder": "תיקיה זו ריקה", "empty_trash": "רוקן אשפה", - "empty_trash_confirmation": "האם באמת ברצונך לרוקן את האשפה? זה יסיר לצמיתות את כל הנכסים באשפה מImmich.\nאין באפשרותך לבטל פעולה זו!", + "empty_trash_confirmation": "האם באמת ברצונך לרוקן את האשפה? זה יסיר לצמיתות את כל התמונות מהאשפה של השרת.\nאין באפשרותך לבטל פעולה זו!", "enable": "אפשר", "enabled": "מופעל", "end_date": "תאריך סיום", @@ -817,42 +821,42 @@ "enter_wifi_name": "הזן שם אינטרנט אלחוטי", "error": "שגיאה", "error_change_sort_album": "שינוי סדר מיון אלבום נכשל", - "error_delete_face": "שגיאה במחיקת פנים מנכס", + "error_delete_face": "שגיאה במחיקת פנים מתמונה", "error_loading_image": "שגיאה בטעינת התמונה", "error_saving_image": "שגיאה: {}", "error_title": "שגיאה - משהו השתבש", "errors": { - "cannot_navigate_next_asset": "לא ניתן לנווט לנכס הבא", - "cannot_navigate_previous_asset": "לא ניתן לנווט לנכס הקודם", + "cannot_navigate_next_asset": "לא ניתן לנווט לתמונה הבאה", + "cannot_navigate_previous_asset": "לא ניתן לנווט לתמונה הקודמת", "cant_apply_changes": "לא ניתן להחיל שינויים", "cant_change_activity": "לא ניתן {enabled, select, true {להשבית} other {לאפשר}} פעילות", - "cant_change_asset_favorite": "לא ניתן לשנות מצב מועדף עבור נכס", - "cant_change_metadata_assets_count": "לא ניתן לשנות את המטא-נתונים של {count, plural, one {נכס #} other {# נכסים}}", + "cant_change_asset_favorite": "לא ניתן לשנות סימון מועדף עבור התמונה", + "cant_change_metadata_assets_count": "לא ניתן לשנות את המטא-נתונים של {count, plural, one {תמונה #} other {# תמונות}}", "cant_get_faces": "לא ניתן לקבל פנים", "cant_get_number_of_comments": "לא ניתן לקבל את מספר התגובות", "cant_search_people": "לא ניתן לחפש אנשים", "cant_search_places": "לא ניתן לחפש מקומות", "cleared_jobs": "משימות נוקו עבור: {job}", - "error_adding_assets_to_album": "שגיאה בהוספת נכסים לאלבום", + "error_adding_assets_to_album": "שגיאה בהוספת תמונות לאלבום", "error_adding_users_to_album": "שגיאה בהוספת משתמשים לאלבום", "error_deleting_shared_user": "שגיאה במחיקת משתמש משותף", "error_downloading": "שגיאה בהורדת {filename}", "error_hiding_buy_button": "שגיאה בהסתרת לחצן 'קנה'", - "error_removing_assets_from_album": "שגיאה בהסרת נכסים מאלבום, בדוק את המסוף לפרטים נוספים", - "error_selecting_all_assets": "שגיאה בבחירת כל הנכסים", + "error_removing_assets_from_album": "שגיאה בהסרת תמונות מהאלבום, בדוק את היומנים לפרטים נוספים", + "error_selecting_all_assets": "שגיאה בבחירת כל התמונות", "exclusion_pattern_already_exists": "דפוס החרגה זה כבר קיים.", "failed_job_command": "הפקודה {command} נכשלה עבור המשימה: ‪‪{job}", "failed_to_create_album": "יצירת אלבום נכשלה", "failed_to_create_shared_link": "יצירת קישור משותף נכשלה", "failed_to_edit_shared_link": "עריכת קישור משותף נכשלה", "failed_to_get_people": "קבלת אנשים נכשלה", - "failed_to_keep_this_delete_others": "נכשל לשמור את הנכס הזה ולמחוק את הנכסים האחרים", - "failed_to_load_asset": "טעינת נכס נכשלה", - "failed_to_load_assets": "טעינת נכסים נכשלה", + "failed_to_keep_this_delete_others": "הפעולה נכשלה לא ניתן היה לשמור את התמונה הזו ולמחוק את שאר התמונות", + "failed_to_load_asset": "טעינת התמונה נכשלה", + "failed_to_load_assets": "טעינת התמונות נכשלה", "failed_to_load_people": "נכשל באחזור אנשים", "failed_to_remove_product_key": "הסרת מפתח מוצר נכשלה", - "failed_to_stack_assets": "יצירת ערימת נכסים נכשלה", - "failed_to_unstack_assets": "ביטול ערימת נכסים נכשל", + "failed_to_stack_assets": "יצירת ערימת תמונות נכשלה", + "failed_to_unstack_assets": "ביטול ערימת תמונות נכשלה", "import_path_already_exists": "נתיב הייבוא הזה כבר קיים.", "incorrect_email_or_password": "דוא\"ל או סיסמה שגויים", "paths_validation_failed": "{paths, plural, one {נתיב # נכשל} other {# נתיבים נכשלו}} אימות", @@ -860,17 +864,17 @@ "quota_higher_than_disk_size": "הגדרת מכסה גבוהה יותר מגודל הדיסק", "repair_unable_to_check_items": "לא ניתן לסמן {count, select, one {פריט} other {פריטים}}", "unable_to_add_album_users": "לא ניתן להוסיף משתמשים לאלבום", - "unable_to_add_assets_to_shared_link": "לא ניתן להוסיף נכסים לקישור משותף", + "unable_to_add_assets_to_shared_link": "לא ניתן להוסיף תמונות לקישור משותף", "unable_to_add_comment": "לא ניתן להוסיף תגובה", "unable_to_add_exclusion_pattern": "לא ניתן להוסיף דפוס החרגה", "unable_to_add_import_path": "לא ניתן להוסיף נתיב ייבוא", "unable_to_add_partners": "לא ניתן להוסיף שותפים", - "unable_to_add_remove_archive": "לא ניתן {archived, select, true {להסיר נכס מ} other {להוסיף נכס ל}}ארכיון", - "unable_to_add_remove_favorites": "לא ניתן {favorite, select, true {להוסיף נכס ל} other {להסיר נכס מ}}מועדפים", + "unable_to_add_remove_archive": "לא ניתן {archived, select, true {להסיר תמונה מ} other {להוסיף תמונה ל}}ארכיון", + "unable_to_add_remove_favorites": "לא ניתן {favorite, select, true {להוסיף תמונה ל} other {להסיר תמונה מ}}מועדפים", "unable_to_archive_unarchive": "לא ניתן {archived, select, true {להעביר לארכיון} other {להוציא מארכיון}}", "unable_to_change_album_user_role": "לא ניתן לשנות את התפקיד של משתמש האלבום", "unable_to_change_date": "לא ניתן לשנות תאריך", - "unable_to_change_favorite": "לא ניתן לשנות מצב מועדף עבור נכס", + "unable_to_change_favorite": "לא ניתן לשנות מצב מועדף עבור התמונה", "unable_to_change_location": "לא ניתן לשנות מיקום", "unable_to_change_password": "לא ניתן לשנות סיסמה", "unable_to_change_visibility": "לא ניתן לשנות את הנראות עבור {count, plural, one {אדם #} other {# אנשים}}", @@ -883,8 +887,8 @@ "unable_to_create_library": "לא ניתן ליצור ספרייה", "unable_to_create_user": "לא ניתן ליצור משתמש", "unable_to_delete_album": "לא ניתן למחוק אלבום", - "unable_to_delete_asset": "לא ניתן למחוק נכס", - "unable_to_delete_assets": "שגיאה במחיקת נכסים", + "unable_to_delete_asset": "לא ניתן למחוק את התמונה", + "unable_to_delete_assets": "שגיאה במחיקת התמונות", "unable_to_delete_exclusion_pattern": "לא ניתן למחוק דפוס החרגה", "unable_to_delete_import_path": "לא ניתן למחוק את נתיב הייבוא", "unable_to_delete_shared_link": "לא ניתן למחוק קישור משותף", @@ -901,19 +905,19 @@ "unable_to_link_motion_video": "לא ניתן לקשר סרטון תנועה", "unable_to_link_oauth_account": "לא ניתן לקשר חשבון OAuth", "unable_to_load_album": "לא ניתן לטעון אלבום", - "unable_to_load_asset_activity": "לא ניתן לטעון את פעילות הנכס", + "unable_to_load_asset_activity": "לא ניתן לטעון את הפעילות בתמונה", "unable_to_load_items": "לא ניתן לטעון פריטים", "unable_to_load_liked_status": "לא ניתן לטעון מצב 'אהבתי'", "unable_to_log_out_all_devices": "לא ניתן לנתק את כל המכשירים", "unable_to_log_out_device": "לא ניתן לנתק מכשיר", "unable_to_login_with_oauth": "לא ניתן להתחבר באמצעות OAuth", "unable_to_play_video": "לא ניתן לנגן סרטון", - "unable_to_reassign_assets_existing_person": "לא ניתן להקצות מחדש נכסים ל{name, select, null {אדם קיים} other {{name}}}", - "unable_to_reassign_assets_new_person": "לא ניתן להקצות מחדש נכסים לאדם חדש", + "unable_to_reassign_assets_existing_person": "לא ניתן להקצות מחדש תמונות ל{name, select, null {אדם קיים} other {{name}}}", + "unable_to_reassign_assets_new_person": "לא ניתן להקצות מחדש תמונות לאדם חדש", "unable_to_refresh_user": "לא ניתן לרענן את המשתמש", "unable_to_remove_album_users": "לא ניתן להסיר משתמשים מהאלבום", "unable_to_remove_api_key": "לא ניתן להסיר מפתח API", - "unable_to_remove_assets_from_shared_link": "לא ניתן להסיר נכסים מקישור משותף", + "unable_to_remove_assets_from_shared_link": "לא ניתן להסיר תמונות מקישור משותף", "unable_to_remove_deleted_assets": "לא ניתן להסיר קבצים לא מקוונים", "unable_to_remove_library": "לא ניתן להסיר ספרייה", "unable_to_remove_partner": "לא ניתן להסיר שותף", @@ -921,7 +925,7 @@ "unable_to_repair_items": "לא ניתן לתקן פריטים", "unable_to_reset_password": "לא ניתן לאפס סיסמה", "unable_to_resolve_duplicate": "לא ניתן לפתור כפילות", - "unable_to_restore_assets": "לא ניתן לשחזר נכסים", + "unable_to_restore_assets": "לא ניתן לשחזר תמונות", "unable_to_restore_trash": "לא ניתן לשחזר אשפה", "unable_to_restore_user": "לא ניתן לשחזר משתמש", "unable_to_save_album": "לא ניתן לשמור אלבום", @@ -935,7 +939,7 @@ "unable_to_set_feature_photo": "לא ניתן להגדיר תמונה מייצגת", "unable_to_set_profile_picture": "לא ניתן להגדיר תמונת פרופיל", "unable_to_submit_job": "לא ניתן לשלוח משימה", - "unable_to_trash_asset": "לא ניתן להעביר נכס לאשפה", + "unable_to_trash_asset": "לא ניתן להעביר תמונה לאשפה", "unable_to_unlink_account": "לא ניתן לבטל קישור חשבון", "unable_to_unlink_motion_video": "לא ניתן לבטל קישור סרטון תנועה", "unable_to_update_album_cover": "לא ניתן לעדכן עטיפת אלבום", @@ -955,7 +959,7 @@ "exif_bottom_sheet_person_add_person": "הוסף שם", "exif_bottom_sheet_person_age": "גיל {}", "exif_bottom_sheet_person_age_months": "גיל {} חודשים", - "exif_bottom_sheet_person_age_year_months": "גיל שנה, {} חודשים", + "exif_bottom_sheet_person_age_year_months": "גיל שנה ו-{} חודשים", "exif_bottom_sheet_person_age_years": "גיל {}", "exit_slideshow": "צא ממצגת שקופיות", "expand_all": "הרחב הכל", @@ -977,12 +981,12 @@ "external_network_sheet_info": "כאשר לא על רשת האינטרנט האלחוטי המועדפת, היישום יתחבר לשרת דרך הכתובת הראשונה שניתן להשיג מהכתובות שלהלן, החל מלמעלה למטה", "face_unassigned": "לא מוקצה", "failed": "נכשלו", - "failed_to_load_assets": "טעינת נכסים נכשלה", + "failed_to_load_assets": "טעינת תמונות נכשלה", "failed_to_load_folder": "טעינת תיקיה נכשלה", "favorite": "מועדף", "favorite_or_unfavorite_photo": "הוסף או הסר תמונה מהמועדפים", "favorites": "מועדפים", - "favorites_page_no_favorites": "לא נמצאו נכסים מועדפים", + "favorites_page_no_favorites": "לא נמצאו תמונות מועדפים", "feature_photo_updated": "תמונה מייצגת עודכנה", "features": "תכונות", "features_setting_description": "ניהול תכונות היישום", @@ -992,6 +996,7 @@ "filetype": "סוג קובץ", "filter": "סנן", "filter_people": "סנן אנשים", + "filter_places": "סינון מקומות", "find_them_fast": "מצא אותם מהר לפי שם עם חיפוש", "fix_incorrect_match": "תקן התאמה שגויה", "folder": "תיקיה", @@ -1029,24 +1034,24 @@ "hide_password": "הסתר סיסמה", "hide_person": "הסתר אדם", "hide_unnamed_people": "הסתר אנשים ללא שם", - "home_page_add_to_album_conflicts": "{added} נכסים נוספו לאלבום {album}. {failed} נכסים כבר נמצאים באלבום", - "home_page_add_to_album_err_local": "לא ניתן להוסיף נכסים מקומיים לאלבום עדיין, מדלג", - "home_page_add_to_album_success": "{added} נכסים נוספו לאלבום {album}", - "home_page_album_err_partner": "לא ניתן להוסיף נכסי שותף לאלבום עדיין, מדלג", - "home_page_archive_err_local": "לא ניתן להעביר לארכיון נכסים מקומיים עדיין, מדלג", - "home_page_archive_err_partner": "לא ניתן להעביר לארכיון נכסי שותף, מדלג", + "home_page_add_to_album_conflicts": "{added} תמונות נוספו לאלבום {album}. {failed} תמונות כבר נמצאות באלבום.", + "home_page_add_to_album_err_local": "לא ניתן להוסיף תמונות מקומיות לאלבום עדיין, מדלג", + "home_page_add_to_album_success": "{added} תמונות נוספו לאלבום {album}.", + "home_page_album_err_partner": "לא ניתן להוסיף תמונת שותף לאלבום עדיין, מדלג", + "home_page_archive_err_local": "לא ניתן להעביר לארכיון תמונות מקומיות עדיין, מדלג", + "home_page_archive_err_partner": "לא ניתן להעביר לארכיון תמונות של השותף, מדלג", "home_page_building_timeline": "בונה את ציר הזמן", - "home_page_delete_err_partner": "לא ניתן למחוק נכסי שותף, מדלג", - "home_page_delete_remote_err_local": "נכסים מקומיים נבחרו מרחוק למחיקה, מדלג", - "home_page_favorite_err_local": "לא ניתן להוסיף למועדפים נכסים מקומיים עדיין, מדלג", - "home_page_favorite_err_partner": "לא ניתן להוסיף למועדפים נכסי שותף עדיין, מדלג", + "home_page_delete_err_partner": "לא ניתן למחוק תמונות של השותף, מדלג", + "home_page_delete_remote_err_local": "תמונות מקומיות נבחרו מרחוק למחיקה, מדלג", + "home_page_favorite_err_local": "לא ניתן להוסיף למועדפים תמונות מקומיות עדיין, מדלג", + "home_page_favorite_err_partner": "לא ניתן להוסיף למועדפים תמונות של השותף עדיין, מדלג", "home_page_first_time_notice": "אם זאת הפעם הראשונה שאת/ה משתמש/ת ביישום, נא להקפיד לבחור אלבומ(ים) לגיבוי כך שציר הזמן יוכל לאכלס תמונות וסרטונים באלבומ(ים)", - "home_page_share_err_local": "לא ניתן לשתף נכסים מקומיים על ידי קישור, מדלג", - "home_page_upload_err_limit": "ניתן להעלות רק מקסימום של 30 נכסים בכל פעם, מדלג", + "home_page_share_err_local": "לא ניתן לשתף תמונות מקומיות על ידי קישור, מדלג", + "home_page_upload_err_limit": "ניתן להעלות רק מקסימום של 30 תמונות בכל פעם, מדלג", "host": "מארח", "hour": "שעה", "ignore_icloud_photos": "התעלם מתמונות iCloud", - "ignore_icloud_photos_description": "תמונות שמאוחסנות ב-iCloud לא יועלו לשרת ה-Immich", + "ignore_icloud_photos_description": "תמונות שמאוחסנות ב-iCloud לא יועלו לשרת", "image": "תמונה", "image_alt_text_date": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{date}", "image_alt_text_date_1_person": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} עם {person1} ב-{date}", @@ -1070,7 +1075,7 @@ "in_archive": "בארכיון", "include_archived": "כלול ארכיון", "include_shared_albums": "כלול אלבומים משותפים", - "include_shared_partner_assets": "כלול נכסי שותף משותפים", + "include_shared_partner_assets": "כלול תמונות ששותפו ע\"י השותף", "individual_share": "שיתוף יחיד", "individual_shares": "שיתופים בודדים", "info": "מידע", @@ -1089,7 +1094,7 @@ "keep": "שמור", "keep_all": "שמור הכל", "keep_this_delete_others": "שמור על זה, מחק אחרים", - "kept_this_deleted_others": "נכס זה נשמר ונמחקו {count, plural, one {נכס #} other {# נכסים}}", + "kept_this_deleted_others": "תמונה זו נשמרה ונמחקו {count, plural, one {תמונה #} other {# תמונות}}", "keyboard_shortcuts": "קיצורי מקלדת", "language": "שפה", "language_setting_description": "בחר את השפה המועדפת עליך", @@ -1104,7 +1109,7 @@ "library_options": "אפשרויות ספרייה", "library_page_device_albums": "אלבומים במכשיר", "library_page_new_album": "אלבום חדש", - "library_page_sort_asset_count": "מספר נכסים", + "library_page_sort_asset_count": "מספר תמונות", "library_page_sort_created": "תאריך יצירה", "library_page_sort_last_modified": "שונה לאחרונה", "library_page_sort_title": "כותרת אלבום", @@ -1132,7 +1137,7 @@ "logged_out_device": "מכשיר מנותק", "login": "כניסה", "login_disabled": "כניסה למערכת הושבתה", - "login_form_api_exception": "חריגת API. נא לבדוק את כתובת השרת ולנסות שוב", + "login_form_api_exception": "חריגת API. נא לבדוק את כתובת השרת ולנסות שוב.", "login_form_back_button_text": "חזרה", "login_form_email_hint": "yourmail@email.com", "login_form_endpoint_hint": "http://your-server-ip:port", @@ -1145,11 +1150,11 @@ "login_form_failed_get_oauth_server_config": "שגיאה בהתחברות באמצעות OAuth, בדוק את כתובת השרת", "login_form_failed_get_oauth_server_disable": "תכונת OAuth לא זמינה בשרת זה", "login_form_failed_login": "שגיאה בכניסה למערכת, בדוק את כתובת השרת, דוא\"ל וסיסמה", - "login_form_handshake_exception": "אירעה חריגת לחיצת יד עם השרת. אפשר תמיכה בתעודה בחתימה עצמית בהגדרות אם את/ה משתמש/ת בתעודה בחתימה עצמית", + "login_form_handshake_exception": "אירעה חריגה בעת ביצוע Handshake עם השרת. אפשר תמיכה בתעודה בחתימה עצמית בהגדרות אם את/ה משתמש/ת בתעודה בחתימה עצמית.", "login_form_password_hint": "סיסמה", "login_form_save_login": "הישאר/י מחובר/ת", - "login_form_server_empty": "הכנס כתובת שרת", - "login_form_server_error": "לא היה ניתן להתחבר לשרת", + "login_form_server_empty": "הכנס כתובת שרת.", + "login_form_server_error": "לא היה ניתן להתחבר לשרת.", "login_has_been_disabled": "הכניסה הושבתה.", "login_password_changed_error": "הייתה שגיאה בעדכון הסיסמה שלך", "login_password_changed_success": "סיסמה עודכנה בהצלחה", @@ -1170,24 +1175,24 @@ "manage_your_devices": "ניהול המכשירים המחוברים שלך", "manage_your_oauth_connection": "ניהול חיבור ה-OAuth שלך", "map": "מפה", - "map_assets_in_bound": "{} תמונה", + "map_assets_in_bound": "תמונה {}", "map_assets_in_bounds": "{} תמונות", "map_cannot_get_user_location": "לא ניתן לקבל את מיקום המשתמש", "map_location_dialog_yes": "כן", "map_location_picker_page_use_location": "השתמש במיקום הזה", - "map_location_service_disabled_content": "שירות מיקום צריך להיות מופעל כדי להציג נכסים מהמיקום הנוכחי שלך. האם ברצונך להפעיל אותו עכשיו?", + "map_location_service_disabled_content": "שירות המיקום צריך להיות מופעל כדי להציג תמונות מהמיקום הנוכחי שלך. האם ברצונך להפעיל אותו עכשיו?", "map_location_service_disabled_title": "שירות מיקום מבוטל", "map_marker_for_images": "סמן מפה לתמונות שצולמו ב{city}, {country}", "map_marker_with_image": "סמן מפה עם תמונה", "map_no_assets_in_bounds": "אין תמונות באזור זה", - "map_no_location_permission_content": "יש צורך בהרשאה למיקום כדי להציג נכסים מהמיקום הנוכחי שלך. האם ברצונך לאפשר זאת עכשיו?", + "map_no_location_permission_content": "יש צורך בהרשאה למיקום כדי להציג תמונות מהמיקום הנוכחי שלך. האם ברצונך לאפשר זאת עכשיו?", "map_no_location_permission_title": "הרשאה למיקום נדחתה", "map_settings": "הגדרות מפה", "map_settings_dark_mode": "מצב כהה", "map_settings_date_range_option_day": "24 שעות אחרונות", - "map_settings_date_range_option_days": "{} ימים אחרונים", + "map_settings_date_range_option_days": "ב-{} ימים אחרונים", "map_settings_date_range_option_year": "שנה אחרונה", - "map_settings_date_range_option_years": "{} שנים אחרונות", + "map_settings_date_range_option_years": "ב-{} שנים אחרונות", "map_settings_dialog_title": "הגדרות מפה", "map_settings_include_show_archived": "כלול ארכיון", "map_settings_include_show_partners": "כלול שותפים", @@ -1221,8 +1226,8 @@ "monthly_title_text_date_format": "MMMM y", "more": "עוד", "moved_to_trash": "הועבר לאשפה", - "multiselect_grid_edit_date_time_err_read_only": "לא ניתן לערוך תאריך של נכס(ים) לקריאה בלבד, מדלג", - "multiselect_grid_edit_gps_err_read_only": "לא ניתן לערוך מיקום של נכס(ים) לקריאה בלבד, מדלג", + "multiselect_grid_edit_date_time_err_read_only": "לא ניתן לערוך תאריך של תמונות לקריאה בלבד, מדלג", + "multiselect_grid_edit_gps_err_read_only": "לא ניתן לערוך מיקום של תמונות לקריאה בלבד, מדלג", "mute_memories": "השתקת זיכרונות", "my_albums": "האלבומים שלי", "name": "שם", @@ -1245,7 +1250,7 @@ "no_albums_yet": "זה נראה שאין לך עדיין אלבומים.", "no_archived_assets_message": "העבר תמונות וסרטונים לארכיון כדי להסתיר אותם מתצוגת התמונות שלך", "no_assets_message": "לחץ כדי להעלות את התמונה הראשונה שלך", - "no_assets_to_show": "אין נכסים להציג", + "no_assets_to_show": "אין תמונות להצגה", "no_duplicates_found": "לא נמצאו כפילויות.", "no_exif_info_available": "אין מידע זמין על מטא-נתונים (exif)", "no_explore_results_message": "העלה תמונות נוספות כדי לחקור את האוסף שלך.", @@ -1258,17 +1263,17 @@ "no_shared_albums_message": "צור אלבום כדי לשתף תמונות וסרטונים עם אנשים ברשת שלך", "not_in_any_album": "לא בשום אלבום", "not_selected": "לא נבחרו", - "note_apply_storage_label_to_previously_uploaded assets": "הערה: כדי להחיל את תווית האחסון על נכסים שהועלו בעבר, הפעל את", + "note_apply_storage_label_to_previously_uploaded assets": "הערה: כדי להחיל את תווית האחסון על תמונות שהועלו בעבר, הפעל את", "notes": "הערות", - "notification_permission_dialog_content": "כדי לאפשר התראות, לך להגדרות ובחר התר", - "notification_permission_list_tile_content": "הענק הרשאה כדי לאפשר התראות", + "notification_permission_dialog_content": "כדי לאפשר התראות, לך להגדרות המכשיר ובחר אפשר.", + "notification_permission_list_tile_content": "הענק הרשאה כדי לאפשר התראות.", "notification_permission_list_tile_enable_button": "אפשר התראות", "notification_permission_list_tile_title": "הרשאת התראה", "notification_toggle_setting_description": "אפשר התראות דוא\"ל", "notifications": "התראות", "notifications_setting_description": "ניהול התראות", "oauth": "OAuth", - "official_immich_resources": "משאבי Immich רשמיים", + "official_immich_resources": "מקורות רשמיים של Immich", "offline": "לא מקוון", "offline_paths": "נתיבים לא מקוונים", "offline_paths_description": "תוצאות אלו עשויות להיות עקב מחיקה ידנית של קבצים שאינם חלק מספרייה חיצונית.", @@ -1282,6 +1287,7 @@ "onboarding_welcome_user": "ברוך בואך, {user}", "online": "מקוון", "only_favorites": "רק מועדפים", + "open": "פתח", "open_in_map_view": "פתח בתצוגת מפה", "open_in_openstreetmap": "פתח ב-OpenStreetMap", "open_the_search_filters": "פתח את מסנני החיפוש", @@ -1300,12 +1306,12 @@ "partner_can_access_location": "המיקום שבו צולמו התמונות שלך", "partner_list_user_photos": "תמונות של {user}", "partner_list_view_all": "הצג הכל", - "partner_page_empty_message": "התמונות שלך עדיין לא משותפות עם אף שותף", + "partner_page_empty_message": "התמונות שלך עדיין לא משותפות עם אף שותף.", "partner_page_no_more_users": "אין עוד משתמשים להוסיף", "partner_page_partner_add_failed": "הוספת שותף נכשלה", "partner_page_select_partner": "בחירת שותף", "partner_page_shared_to_title": "משותף עם", - "partner_page_stop_sharing_content": "{} לא יוכל יותר לגשת לתמונות שלך", + "partner_page_stop_sharing_content": "{} לא יוכל יותר לגשת לתמונות שלך.", "partner_sharing": "שיתוף שותפים", "partners": "שותפים", "password": "סיסמה", @@ -1328,20 +1334,20 @@ "people_feature_description": "עיון בתמונות וסרטונים שקובצו על ידי אנשים", "people_sidebar_description": "הצג קישור אל אנשים בסרגל הצד", "permanent_deletion_warning": "אזהרת מחיקה לצמיתות", - "permanent_deletion_warning_setting_description": "הצג אזהרה בעת מחיקת נכסים לצמיתות", + "permanent_deletion_warning_setting_description": "הצג אזהרה בעת מחיקת תמונות לצמיתות", "permanently_delete": "מחק לצמיתות", - "permanently_delete_assets_count": "מחק לצמיתות {count, plural, one {נכס} other {נכסים}}", - "permanently_delete_assets_prompt": "האם באמת ברצונך למחוק לצמיתות {count, plural, one {נכס זה?} other {# נכסים אלה?}}זה גם יסיר {count, plural, one {אותו מאלבומו} other {אותם מאלבומם}}.", - "permanently_deleted_asset": "נכס נמחק לצמיתות", - "permanently_deleted_assets_count": "{count, plural, one {נכס # נמחק} other {# נכסים נמחקו}} לצמיתות", + "permanently_delete_assets_count": "מחק לצמיתות {count, plural, one {תמונה} other {תמונות}}", + "permanently_delete_assets_prompt": "האם באמת ברצונך למחוק לצמיתות {count, plural, one {תמונה זאת?} other {# תמונות אלו?}}זה גם יסיר {count, plural, one {אותו מאלבומו} other {אותם מאלבומים}}.", + "permanently_deleted_asset": "התמונה נמחקה לצמיתות", + "permanently_deleted_assets_count": "{count, plural, one {תמונה # נמחקה} other {# תמונות נמחקו}} לצמיתות", "permission_onboarding_back": "חזרה", "permission_onboarding_continue_anyway": "המשך בכל זאת", "permission_onboarding_get_started": "להתחיל", "permission_onboarding_go_to_settings": "לך להגדרות", - "permission_onboarding_permission_denied": "הרשאה נדחתה. כדי להשתמש ביישום, הענק הרשאה לתמונות וסרטונים בהגדרות", - "permission_onboarding_permission_granted": "ההרשאה ניתנה! את/ה מוכנ/ה", - "permission_onboarding_permission_limited": "הרשאה מוגבלת. כדי לתת ליישום לגבות ולנהל את כל אוסף הגלריה שלך, הענק הרשאה לתמונות וסרטונים בהגדרות", - "permission_onboarding_request": "היישום דורש הרשאה כדי לראות את התמונות והסרטונים שלך", + "permission_onboarding_permission_denied": "הרשאה נדחתה. כדי להשתמש ביישום, הענק הרשאה לתמונות וסרטונים בהגדרות.", + "permission_onboarding_permission_granted": "ההרשאה ניתנה! הכל מוכן.", + "permission_onboarding_permission_limited": "הרשאה מוגבלת. כדי לתת ליישום לגבות ולנהל את כל אוסף הגלריה שלך, הענק הרשאה לתמונות וסרטונים בהגדרות.", + "permission_onboarding_request": "היישום דורש הרשאה כדי לראות את התמונות והסרטונים שלך.", "person": "אדם", "person_birthdate": "נולד בתאריך {date}", "person_hidden": "{name}{hidden, select, true { (מוסתר)} other {}}", @@ -1369,12 +1375,12 @@ "primary": "ראשי", "privacy": "פרטיות", "profile_drawer_app_logs": "יומן", - "profile_drawer_client_out_of_date_major": "האפליקציה לנייד היא מיושנת. נא לעדכן לגרסה הראשית האחרונה", - "profile_drawer_client_out_of_date_minor": "האפליקציה לנייד היא מיושנת. נא לעדכן לגרסה המשנית האחרונה", + "profile_drawer_client_out_of_date_major": "גרסת היישום לנייד מיושנת. נא לעדכן לגרסה הראשית האחרונה.", + "profile_drawer_client_out_of_date_minor": "גרסת היישום לנייד מיושנת. נא לעדכן לגרסה המשנית האחרונה.", "profile_drawer_client_server_up_to_date": "הלקוח והשרת הם מעודכנים", "profile_drawer_github": "GitHub", - "profile_drawer_server_out_of_date_major": "השרת אינו מעודכן. נא לעדכן לגרסה הראשית האחרונה", - "profile_drawer_server_out_of_date_minor": "השרת אינו מעודכן. נא לעדכן לגרסה המשנית האחרונה", + "profile_drawer_server_out_of_date_major": "השרת אינו מעודכן. נא לעדכן לגרסה הראשית האחרונה.", + "profile_drawer_server_out_of_date_minor": "השרת אינו מעודכן. נא לעדכן לגרסה המשנית האחרונה.", "profile_image_of_user": "תמונת פרופיל של {user}", "profile_picture_set": "תמונת פרופיל נבחרה.", "public_album": "אלבום ציבורי", @@ -1418,9 +1424,9 @@ "reaction_options": "אפשרויות הגבה", "read_changelog": "קרא את יומן השינויים", "reassign": "הקצה מחדש", - "reassigned_assets_to_existing_person": "{count, plural, one {נכס # הוקצה} other {# נכסים הוקצו}} מחדש אל {name, select, null {אדם קיים} other {{name}}}", - "reassigned_assets_to_new_person": "{count, plural, one {נכס # הוקצה} other {# נכסים הוקצו}} מחדש לאדם חדש", - "reassing_hint": "הקצה נכסים שנבחרו לאדם קיים", + "reassigned_assets_to_existing_person": "{count, plural, one {תמונה # הוקצתה} other {# תמונות הוקצו}} מחדש אל {name, select, null {אדם קיים} other {{name}}}", + "reassigned_assets_to_new_person": "{count, plural, one {תמונה # הוקצתה} other {# תמונות הוקצו}} מחדש לאדם חדש", + "reassing_hint": "הקצה תמונות שנבחרו לאדם קיים", "recent": "חדש", "recent-albums": "אלבומים אחרונים", "recent_searches": "חיפושים אחרונים", @@ -1438,9 +1444,9 @@ "refreshing_metadata": "מרענן מטא-נתונים", "regenerating_thumbnails": "מחדש תמונות ממוזערות", "remove": "הסר", - "remove_assets_album_confirmation": "האם באמת ברצונך להסיר {count, plural, one {נכס #} other {# נכסים}} מהאלבום?", - "remove_assets_shared_link_confirmation": "האם באמת ברצונך להסיר {count, plural, one {נכס #} other {# נכסים}} מהקישור המשותף הזה?", - "remove_assets_title": "הסר נכסים?", + "remove_assets_album_confirmation": "האם באמת ברצונך להסיר {count, plural, one {תמונה #} other {# תמונות}} מהאלבום?", + "remove_assets_shared_link_confirmation": "האם אתה בטוח שברצונך להסיר {count, plural, one {תמונה #} other {# תמונות}} מהקישור המשותף הזה?", + "remove_assets_title": "להסיר תמונות?", "remove_custom_date_range": "הסר טווח תאריכים מותאם", "remove_deleted_assets": "הסר קבצים לא מקוונים", "remove_from_album": "הסר מאלבום", @@ -1456,7 +1462,7 @@ "removed_from_favorites_count": "{count, plural, other {הוסרו #}} מהמועדפים", "removed_memory": "זיכרון הוסר", "removed_photo_from_memory": "התמונה הוסרה מהזיכרון", - "removed_tagged_assets": "תג הוסר מ{count, plural, one {נכס #} other {# נכסים}}", + "removed_tagged_assets": "תג הוסר מ{count, plural, one {תמונה #} other {# תמונות}}", "rename": "שנה שם", "repair": "תיקון", "repair_no_results_message": "קבצים חסרי מעקב וחסרים יופיעו כאן", @@ -1474,7 +1480,7 @@ "restore": "שחזר", "restore_all": "שחזר הכל", "restore_user": "שחזר משתמש", - "restored_asset": "נכס משוחזר", + "restored_asset": "התמונה שוחזרה", "resume": "המשך", "retry_upload": "נסה שוב להעלות", "review_duplicates": "בדוק כפילויות", @@ -1540,7 +1546,7 @@ "search_result_page_new_search_hint": "חיפוש חדש", "search_settings": "הגדרות חיפוש", "search_state": "חיפוש מדינה...", - "search_suggestion_list_smart_search_hint_1": "חיפוש חכם מופעל כברירת מחדל, כדי לחפש מטא-נתונים השתמש בתחביר", + "search_suggestion_list_smart_search_hint_1": "חיפוש חכם מופעל כברירת מחדל, כדי לחפש מטא-נתונים השתמש בתחביר ", "search_suggestion_list_smart_search_hint_2": "תנאי-החיפוש-שלך:m", "search_tags": "חיפוש תגים...", "search_timezone": "חיפוש אזור זמן...", @@ -1581,10 +1587,10 @@ "set_date_of_birth": "הגדר תאריך לידה", "set_profile_picture": "הגדר תמונת פרופיל", "set_slideshow_to_fullscreen": "הגדר מצגת שקופיות למסך מלא", - "setting_image_viewer_help": "מציג הפרטים טוען את התמונה הממוזערת הקטנה קודם, לאחר מכן טוען את התצוגה המקדימה בגודל בינוני (אם מופעלת), לבסוף טוען את המקורית (אם מופעלת)", - "setting_image_viewer_original_subtitle": "אפשר לטעון את התמונה המקורית ברזלוציה מלאה (גדולה!). השבת כדי להקטין שימוש בנתונים (גם בשרת וגם בזיכרון המטמון שבמכשיר)", + "setting_image_viewer_help": "מציג הפרטים טוען את התמונה הממוזערת הקטנה קודם, לאחר מכן טוען את התצוגה המקדימה בגודל בינוני (אם מופעל), לבסוף טוען את המקורית (אם מופעל).", + "setting_image_viewer_original_subtitle": "אפשר לטעון את התמונה המקורית ברזלוציה מלאה (גדולה!). השבת כדי להקטין שימוש בנתונים (גם בשרת וגם בזיכרון המטמון שבמכשיר).", "setting_image_viewer_original_title": "טען תמונה מקורית", - "setting_image_viewer_preview_subtitle": "אפשר לטעון תמונה ברזלוציה בינונית. השבת כדי או לטעון את המקורית או רק להשתמש בתמונה הממוזערת", + "setting_image_viewer_preview_subtitle": "אפשר לטעון תמונה ברזלוציה בינונית. השבת כדי או לטעון את המקורית או רק להשתמש בתמונה הממוזערת.", "setting_image_viewer_preview_title": "טען תמונת תצוגה מקדימה", "setting_image_viewer_title": "תמונות", "setting_languages_apply": "החל", @@ -1596,10 +1602,10 @@ "setting_notifications_notify_minutes": "{} דקות", "setting_notifications_notify_never": "אף פעם", "setting_notifications_notify_seconds": "{} שניות", - "setting_notifications_single_progress_subtitle": "מידע מפורט על התקדמות העלאה לכל נכס", + "setting_notifications_single_progress_subtitle": "מידע מפורט על התקדמות העלאה לכל תמונה", "setting_notifications_single_progress_title": "הראה פרטי התקדמות גיבוי ברקע", "setting_notifications_subtitle": "התאם את העדפות ההתראה שלך", - "setting_notifications_total_progress_subtitle": "התקדמות העלאה כללית (בוצע/סה״כ נכסים)", + "setting_notifications_total_progress_subtitle": "התקדמות העלאה כללית (בוצע/סה״כ תמונות)", "setting_notifications_total_progress_title": "הראה סה״כ התקדמות גיבוי ברקע", "setting_video_viewer_looping_title": "הפעלה חוזרת", "setting_video_viewer_original_video_subtitle": "כאשר מזרימים סרטון מהשרת, נגן את המקורי אפילו כשהמרת קידוד זמינה. עלול להוביל לתקיעות. סרטונים זמינים מקומית מנוגנים באיכות מקורית ללא קשר להגדרה זו.", @@ -1623,7 +1629,7 @@ "shared_by_user": "משותף על ידי {user}", "shared_by_you": "משותף על ידך", "shared_from_partner": "תמונות מאת {partner}", - "shared_intent_upload_button_progress_text": "הועלו {} / {}", + "shared_intent_upload_button_progress_text": "{} / {} הועלו", "shared_link_app_bar_title": "קישורים משותפים", "shared_link_clipboard_copied_massage": "הועתק ללוח", "shared_link_clipboard_text": "קישור: {}\nסיסמה: {}", @@ -1640,14 +1646,14 @@ "shared_link_edit_password_hint": "הכנס את סיסמת השיתוף", "shared_link_edit_submit_button": "עדכן קישור", "shared_link_error_server_url_fetch": "לא ניתן להשיג את כתובת האינטרנט של השרת", - "shared_link_expires_day": "יפוג בעוד {} יום", + "shared_link_expires_day": "יפוג בעוד יום {}", "shared_link_expires_days": "יפוג בעוד {} ימים", - "shared_link_expires_hour": "יפוג בעוד {} שעה", + "shared_link_expires_hour": "יפוג בעוד שעה {}", "shared_link_expires_hours": "יפוג בעוד {} שעות", - "shared_link_expires_minute": "יפוג בעוד {} דקה", + "shared_link_expires_minute": "יפוג בעוד דקה {}", "shared_link_expires_minutes": "יפוג בעוד {} דקות", "shared_link_expires_never": "יפוג ∞", - "shared_link_expires_second": "יפוג בעוד {} שניה", + "shared_link_expires_second": "יפוג בעוד שנייה {}", "shared_link_expires_seconds": "יפוג בעוד {} שניות", "shared_link_individual_shared": "משותף ליחיד", "shared_link_info_chip_metadata": "EXIF", @@ -1661,12 +1667,12 @@ "sharing": "שיתוף", "sharing_enter_password": "נא להזין את הסיסמה כדי לצפות בדף זה.", "sharing_page_album": "אלבומים משותפים", - "sharing_page_description": "צור אלבומים משותפים כדי לשתף תמונות וסרטונים עם אנשים ברשת שלך", + "sharing_page_description": "צור אלבומים משותפים כדי לשתף תמונות וסרטונים עם אנשים ברשת שלך.", "sharing_page_empty_list": "רשימה ריקה", "sharing_sidebar_description": "הצג קישור אל שיתוף בסרגל הצד", "sharing_silver_appbar_create_shared_album": "אלבום משותף חדש", "sharing_silver_appbar_share_partner": "שיתוף עם שותף", - "shift_to_permanent_delete": "לחץ ⇧ כדי למחוק לצמיתות נכס", + "shift_to_permanent_delete": "לחץ ⇧ כדי למחוק תמונה לצמיתות", "show_album_options": "הצג אפשרויות אלבום", "show_albums": "הצג אלבומים", "show_all_people": "הצג את כל האנשים", @@ -1711,7 +1717,7 @@ "stack_duplicates": "צור ערימת כפילויות", "stack_select_one_photo": "בחר תמונה ראשית אחת עבור הערימה", "stack_selected_photos": "צור ערימת תמונות נבחרות", - "stacked_assets_count": "{count, plural, one {נכס # נערם} other {# נכסים נערמו}}", + "stacked_assets_count": "{count, plural, one {תמונה # נערמה} other {# תמונות נערמו}}", "stacktrace": "Stack trace", "start": "התחל", "start_date": "תאריך התחלה", @@ -1736,25 +1742,25 @@ "sync_albums_manual_subtitle": "סנכרן את כל הסרטונים והתמונות שהועלו לאלבומי הגיבוי שנבחרו", "sync_upload_album_setting_subtitle": "צור והעלה תמונות וסרטונים שלך לאלבומים שנבחרו ביישום", "tag": "תג", - "tag_assets": "תיוג נכסים", + "tag_assets": "תיוג תמונות", "tag_created": "נוצר תג: {tag}", "tag_feature_description": "עיון בתמונות וסרטונים שקובצו על ידי נושאי תג לוגיים", "tag_not_found_question": "לא מצליח למצוא תג? צור תג חדש", "tag_people": "תייג אנשים", "tag_updated": "תג מעודכן: {tag}", - "tagged_assets": "תויגו {count, plural, one {נכס #} other {# נכסים}}", + "tagged_assets": "תויגו {count, plural, one {תמונה #} other {# תמונות}}", "tags": "תגים", "template": "תבנית", "theme": "ערכת נושא", "theme_selection": "בחירת ערכת נושא", "theme_selection_description": "הגדר אוטומטית את ערכת הנושא לבהיר או כהה בהתבסס על העדפת המערכת של הדפדפן שלך", - "theme_setting_asset_list_storage_indicator_title": "הראה מחוון אחסון על אריחי נכסים", - "theme_setting_asset_list_tiles_per_row_title": "מספר נכסים בכל שורה ({})", - "theme_setting_colorful_interface_subtitle": "החל את הצבע העיקרי למשטחי רקע", + "theme_setting_asset_list_storage_indicator_title": "הצג סטטוס אחסון על גבי התמונות", + "theme_setting_asset_list_tiles_per_row_title": "מספר תמונות בכל שורה ({})", + "theme_setting_colorful_interface_subtitle": "החל את הצבע העיקרי למשטחי רקע.", "theme_setting_colorful_interface_title": "ממשק צבעוני", "theme_setting_image_viewer_quality_subtitle": "התאם את האיכות של מציג פרטי התמונות", "theme_setting_image_viewer_quality_title": "איכות מציג תמונות", - "theme_setting_primary_color_subtitle": "בחר צבע לפעולות עיקריות והדגשות", + "theme_setting_primary_color_subtitle": "בחר צבע לפעולות עיקריות והדגשות.", "theme_setting_primary_color_title": "צבע עיקרי", "theme_setting_system_primary_color_title": "השתמש בצבע המערכת", "theme_setting_system_theme_switch": "אוטומטי (עקוב אחרי הגדרת מערכת)", @@ -1779,15 +1785,15 @@ "trash": "אשפה", "trash_all": "העבר הכל לאשפה", "trash_count": "העבר לאשפה {count, number}", - "trash_delete_asset": "העבר לאשפה/מחק נכס", + "trash_delete_asset": "העבר לאשפה/מחק תמונה", "trash_emptied": "האשפה רוקנה", "trash_no_results_message": "תמונות וסרטונים שהועברו לאשפה יופיעו כאן.", "trash_page_delete_all": "מחק הכל", - "trash_page_empty_trash_dialog_content": "האם ברצונך לרוקן את הנכסים שבאשפה? הפריטים האלה ימחקו לצמיתות מהשרת", + "trash_page_empty_trash_dialog_content": "האם ברצונך לרוקן את התמונות שבאשפה? הפריטים האלה ימחקו לצמיתות מהשרת", "trash_page_info": "פריטים באשפה ימחקו לצמיתות לאחר {} ימים", - "trash_page_no_assets": "אין נכסים באשפה", + "trash_page_no_assets": "אין תמונות באשפה", "trash_page_restore_all": "שחזר הכל", - "trash_page_select_assets_btn": "בחר נכסים", + "trash_page_select_assets_btn": "בחר תמונות", "trash_page_title": "אשפה ({})", "trashed_items_will_be_permanently_deleted_after": "פריטים באשפה ימחקו לצמיתות לאחר {days, plural, one {יום #} other {# ימים}}.", "type": "סוג", @@ -1810,22 +1816,22 @@ "unselect_all": "בטל בחירה בהכל", "unselect_all_duplicates": "בטל בחירת כל הכפילויות", "unstack": "בטל ערימה", - "unstacked_assets_count": "{count, plural, one {נכס # הוסר} other {# נכסים הוסרו}} מערימה", + "unstacked_assets_count": "{count, plural, one {תמונה # הוסרה} other {# תמונות הוסרו}} מהערימה", "untracked_files": "קבצים ללא מעקב", "untracked_files_decription": "קבצים אלה אינם נמצאים במעקב של היישום. הם יכולים להיות תוצאות של העברות כושלות, העלאות שנקטעו, או שנותרו מאחור בגלל שיבוש בתוכנה", "up_next": "הבא בתור", "updated_password": "סיסמה עודכנה", "upload": "העלאה", "upload_concurrency": "בו-זמניות של העלאה", - "upload_dialog_info": "האם ברצונך לגבות את הנכס(ים) שנבחרו לשרת?", - "upload_dialog_title": "העלאת נכס", - "upload_errors": "העלאה הושלמה עם {count, plural, one {שגיאה #} other {# שגיאות}}, רענן את הדף כדי לראות נכסי העלאה חדשים.", + "upload_dialog_info": "האם ברצונך לגבות את התמונות שנבחרו לשרת?", + "upload_dialog_title": "העלאת תמונה", + "upload_errors": "העלאה הושלמה עם {count, plural, one {שגיאה #} other {# שגיאות}}, רענן את הדף כדי לראות תמונות שהועלו.", "upload_progress": "נותרו {remaining, number} - טופלו {processed, number}/{total, number}", - "upload_skipped_duplicates": "דילג על {count, plural, one {נכס כפול #} other {# נכסים כפולים}}", + "upload_skipped_duplicates": "דילג על {count, plural, one {תמונה כפולה #} other {# תמונות כפולות}}", "upload_status_duplicates": "כפילויות", "upload_status_errors": "שגיאות", "upload_status_uploaded": "הועלה", - "upload_success": "ההעלאה הצליחה, רענן את הדף כדי לראות נכסי העלאה חדשים.", + "upload_success": "ההעלאה בוצעה בהצלחה. רענן את הדף כדי לצפות בתמונות שהועלו.", "upload_to_immich": "העלה לשרת ({})", "uploading": "מעלה", "url": "URL", @@ -1834,7 +1840,7 @@ "use_custom_date_range": "השתמש בטווח תאריכים מותאם במקום", "user": "משתמש", "user_id": "מזהה משתמש", - "user_liked": "{user} אהב את {type, select, photo {התמונה הזאת} video {הסרטון הזה} asset {הנכס הזה} other {זה}}", + "user_liked": "{user} אהב את {type, select, photo {התמונה הזאת} video {הסרטון הזה} asset {התמונה הזאת} other {זה}}", "user_purchase_settings": "רכישה", "user_purchase_settings_description": "ניהול הרכישה שלך", "user_role_set": "הגדר את {user} בתור {role}", @@ -1853,7 +1859,7 @@ "version_announcement_overlay_release_notes": "הערות פרסום", "version_announcement_overlay_text_1": "הי חבר/ה, יש מהדורה חדשה של", "version_announcement_overlay_text_2": "אנא קח/י את הזמן שלך לבקר ב ", - "version_announcement_overlay_text_3": " ולוודא שמבנה ה docker-compose וה env. שלך עדכני כדי למנוע תצורות שגויות, במיוחד אם את/ה משתמש/ת ב WatchTower או בכל מנגנון שמטפל בעדכון יישום השרת שלך באופן אוטומטי", + "version_announcement_overlay_text_3": " ולוודא שמבנה ה docker-compose וה env. שלך עדכני כדי למנוע תצורות שגויות, במיוחד אם אתה משתמש ב WatchTower או במנגנון שמטפל בעדכון השרת באופן אוטומטי.", "version_announcement_overlay_title": "גרסת שרת חדשה זמינה 🎉", "version_history": "היסטוריית גרסאות", "version_history_item": "{version} הותקנה ב-{date}", @@ -1870,11 +1876,12 @@ "view_link": "הצג קישור", "view_links": "הצג קישורים", "view_name": "הצג", - "view_next_asset": "הצג את הנכס הבא", - "view_previous_asset": "הצג את הנכס הקודם", + "view_next_asset": "הצג את התמונה הבאה", + "view_previous_asset": "הצג את התמונה הקודמת", + "view_qr_code": "הצג ברקוד", "view_stack": "הצג ערימה", "viewer_remove_from_stack": "הסר מערימה", - "viewer_stack_use_as_main_asset": "השתמש כנכס ראשי", + "viewer_stack_use_as_main_asset": "השתמש כתמונה ראשית", "viewer_unstack": "ביטול ערימה", "visibility_changed": "הנראות השתנתה עבור {count, plural, one {אדם #} other {# אנשים}}", "waiting": "ממתין", @@ -1882,7 +1889,7 @@ "week": "שבוע", "welcome": "ברוכים הבאים", "welcome_to_immich": "ברוכים הבאים אל immich", - "wifi_name": "שם אינטרנט אלחוטי", + "wifi_name": "שם הרשת האלחוטית", "year": "שנה", "years_ago": "לפני {years, plural, one {שנה #} other {# שנים}}", "yes": "כן", diff --git a/i18n/hi.json b/i18n/hi.json index 7f50906791..50c17d9e5d 100644 --- a/i18n/hi.json +++ b/i18n/hi.json @@ -1142,7 +1142,7 @@ "partner_page_partner_add_failed": "Failed to add partner", "partner_page_select_partner": "Select partner", "partner_page_shared_to_title": "Shared to", - "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", + "partner_page_stop_sharing_content": "{} will no longer be able to access your photos।", "partner_sharing": "पार्टनर शेयरिंग", "partners": "भागीदारों", "password": "पासवर्ड", diff --git a/i18n/hr.json b/i18n/hr.json index 53a7cc2640..229ea91c03 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -4,6 +4,7 @@ "account_settings": "Postavke računa", "acknowledge": "Potvrdi", "action": "Akcija", + "action_common_update": "Ažuriranje", "actions": "Akcije", "active": "Aktivno", "activity": "Aktivnost", @@ -13,6 +14,7 @@ "add_a_location": "Dodaj lokaciju", "add_a_name": "Dodaj ime", "add_a_title": "Dodaj naslov", + "add_endpoint": "Dodaj krajnju točnu", "add_exclusion_pattern": "Dodaj uzorak izuzimanja", "add_import_path": "Dodaj import folder", "add_location": "Dodaj lokaciju", @@ -20,8 +22,10 @@ "add_partner": "Dodaj partnera", "add_path": "Dodaj putanju", "add_photos": "Dodaj slike", - "add_to": "Dodaj u...", + "add_to": "Dodaj u…", "add_to_album": "Dodaj u album", + "add_to_album_bottom_sheet_added": "Dodano u {album}", + "add_to_album_bottom_sheet_already_exists": "Već u {album}", "add_to_shared_album": "Dodaj u dijeljeni album", "add_url": "Dodaj URL", "added_to_archive": "Dodano u arhivu", @@ -41,6 +45,7 @@ "backup_settings": "Postavke sigurnosne kopije", "backup_settings_description": "Upravljanje postavkama sigurnosne kopije baze podataka", "check_all": "Provjeri sve", + "cleanup": "Čišćenje", "cleared_jobs": "Izbrisani poslovi za: {job}", "config_set_by_file": "Konfiguracija je trenutno postavljena konfiguracijskom datotekom", "confirm_delete_library": "Jeste li sigurni da želite izbrisati biblioteku {library}?", @@ -65,8 +70,13 @@ "forcing_refresh_library_files": "Prisilno osvježavanje svih datoteka knjižnice", "image_format": "Format", "image_format_description": "WebP proizvodi manje datoteke od JPEG-a, ali se sporije kodira.", + "image_fullsize_description": "Slika pune veličine bez meta podataka, koristi se prilikom zumiranja", + "image_fullsize_enabled": "Omogući generiranje slike pune veličine", + "image_fullsize_enabled_description": "Generiraj sliku pune veličine za formate koji nisu prilagođeni webu. Kada je opcija \"Preferiraj ugrađeni pregled\" omogućena, ugrađeni pregledi koriste se izravno bez konverzije. Ne utječe na formate prilagođene webu kao što je JPEG.", + "image_fullsize_quality_description": "Kvaliteta slike pune veličine od 1 do 100. Veća vrijednost znači bolja kvaliteta, ali stvara veće datoteke.", + "image_fullsize_title": "Postavke slike pune veličine", "image_prefer_embedded_preview": "Preferiraj ugrađeni pregled", - "image_prefer_embedded_preview_setting_description": "Koristite ugrađene preglede u RAW fotografije kao ulaz za obradu slike kada su dostupni. To može proizvesti preciznije boje za neke slike, ali kvaliteta pregleda ovisi o kameri i slika može imati više artefakata kompresije.", + "image_prefer_embedded_preview_setting_description": "Koristite ugrađene preglede u RAW fotografije kao ulaz za obradu slike kada su dostupni. To može proizvesti preciznije boje za neke slike, ali kvaliteta pregleda ovisi o kameri i slika može imati više artifakta kompresije.", "image_prefer_wide_gamut": "Preferirajte široku gamu", "image_prefer_wide_gamut_setting_description": "Koristite Display P3 za sličice. Ovo bolje čuva živost slika sa širokim prostorima boja, ali slike mogu izgledati drugačije na starim uređajima sa starom verzijom preglednika. sRGB slike čuvaju se kao sRGB kako bi se izbjegle promjene boja.", "image_preview_description": "Slika srednje veličine s ogoljenim metapodacima, koristi se prilikom pregledavanja jednog sredstva i za strojno učenje", @@ -96,7 +106,7 @@ "library_scanning_enable_description": "Omogući periodično skeniranje biblioteke", "library_settings": "Externa biblioteka", "library_settings_description": "Upravljajte postavkama vanjske biblioteke", - "library_tasks_description": "Obavljati bibliotekne zadatke", + "library_tasks_description": "Skeniraj eksterne biblioteke za nove i/ili promijenjene resurse", "library_watching_enable_description": "Pratite vanjske biblioteke za promjena datoteke", "library_watching_settings": "Gledanje biblioteke (EKSPERIMENTALNO)", "library_watching_settings_description": "Automatsko praćenje promijenjenih datoteke", @@ -131,7 +141,7 @@ "machine_learning_smart_search_description": "Pretražujte slike semantički koristeći CLIP ugradnje", "machine_learning_smart_search_enabled": "Omogući pametno pretraživanje", "machine_learning_smart_search_enabled_description": "Ako je onemogućeno, slike neće biti kodirane za pametno pretraživanje.", - "machine_learning_url_description": "URL poslužitelja strojnog učenja. Ako ste dodali više od jednog URLa, svaki server će biti kontaktiraj jedanput dok jedan ne odgovori uspješno, u redu od prvog do zadnjeg.", + "machine_learning_url_description": "URL poslužitelja strojnog učenja. Ako ste dodali više od jednog URLa, svaki server će biti kontaktiraj jedanput dok jedan ne odgovori uspješno, u redu od prvog do zadnjeg. Serveri koji ne odgovore će privremeno biti ignorirani dok ponovo ne postanu dostupni.", "manage_concurrency": "Upravljanje Istovremenošću", "manage_log_settings": "Upravljanje postavkama zapisivanje", "map_dark_style": "Tamni stil", @@ -147,6 +157,8 @@ "map_settings": "Karta", "map_settings_description": "Upravljanje postavkama karte", "map_style_description": "URL na style.json temu karte", + "memory_cleanup_job": "Čišćenje memorije", + "memory_generate_job": "Generiranje memorije", "metadata_extraction_job": "Izdvoj metapodatke", "metadata_extraction_job_description": "Izdvojite podatke o metapodacima iz svakog sredstva, kao što su GPS, lica i rezolucija", "metadata_faces_import_setting": "Omogući uvoz lica", @@ -239,7 +251,7 @@ "storage_template_hash_verification_enabled_description": "Omogućuje hash provjeru, nemojte je onemogućiti osim ako niste sigurni u implikacije", "storage_template_migration": "Migracija predloška za pohranu", "storage_template_migration_description": "Primijenite trenutni {template} na prethodno prenesena sredstva", - "storage_template_migration_info": "Promjene predloška primjenjivat će se samo na nova sredstva. Za retroaktivnu primjenu predloška na prethodno prenesena sredstva, pokrenite {job}.", + "storage_template_migration_info": "Predložak za pohranu će sve nastavke (ekstenzije) pretvoriti u mala slova. Promjene predloška primjenjivat će se samo na nova sredstva. Za retroaktivnu primjenu predloška na prethodno prenesena sredstva, pokrenite {job}.", "storage_template_migration_job": "Posao Migracije Predloška Pohrane", "storage_template_more_details": "Za više pojedinosti o ovoj značajci pogledajte Predložak pohrane i njegove implikacije", "storage_template_onboarding_description": "Kada je omogućena, ova će značajka automatski organizirati datoteke na temelju korisnički definiranog predloška. Zbog problema sa stabilnošću značajka je isključena prema zadanim postavkama. Za više informacija pogledajte dokumentaciju.", @@ -250,6 +262,15 @@ "system_settings": "Postavke Sustava", "tag_cleanup_job": "Čišćenje oznaka", "template_email_available_tags": "Možete koristiti sljedeće varijable u vašem predlošku:{tags}", + "template_email_if_empty": "Ukoliko je predložak prazan, koristit će se zadana e-mail adresa.", + "template_email_invite_album": "Predložak za pozivnicu u album", + "template_email_preview": "Pregled", + "template_email_settings": "E-mail Predlošci", + "template_email_settings_description": "Upravljanje prilagođenim predlošcima za obavijesti putem e-maila", + "template_email_update_album": "Ažuriraj Album Predložak", + "template_email_welcome": "Predložak e-maila dobrodošlice", + "template_settings": "Predložak Obavijesti", + "template_settings_description": "Upravljaj prilagođenim predlošcima za obavijesti.", "theme_custom_css_settings": "Prilagođeni CSS", "theme_custom_css_settings_description": "Kaskadni listovi stilova (CSS) omogućuju prilagođavanje dizajna Immicha.", "theme_settings": "Postavke tema", @@ -279,6 +300,8 @@ "transcoding_constant_rate_factor": "Faktor konstantne stope (-crf)", "transcoding_constant_rate_factor_description": "Razina kvalitete videa. Uobičajene vrijednosti su 23 za H.264, 28 za HEVC, 31 za VP9 i 35 za AV1. Niže je bolje, ali stvara veće datoteke.", "transcoding_disabled_description": "Nemojte transkodirati nijedan videozapis, može prekinuti reprodukciju na nekim klijentima", + "transcoding_encoding_options": "Opcije Kodiranja", + "transcoding_encoding_options_description": "Postavi kodeke, rezoluciju, kvalitetu i druge opcije za kodirane videje", "transcoding_hardware_acceleration": "Hardversko Ubrzanje", "transcoding_hardware_acceleration_description": "Eksperimentalno; puno brže, ali će imati nižu kvalitetu pri istoj bitrate postavci", "transcoding_hardware_decoding": "Hardversko dekodiranje", @@ -291,6 +314,8 @@ "transcoding_max_keyframe_interval": "Maksimalni interval ključnih sličica", "transcoding_max_keyframe_interval_description": "Postavlja maksimalnu udaljenost slika između ključnih kadrova. Niže vrijednosti pogoršavaju učinkovitost kompresije, ali poboljšavaju vrijeme traženja i mogu poboljšati kvalitetu u scenama s brzim kretanjem. 0 automatski postavlja ovu vrijednost.", "transcoding_optimal_description": "Videozapisi koji su veći od ciljne rezolucije ili nisu u prihvatljivom formatu", + "transcoding_policy": "Politika Transkodiranja", + "transcoding_policy_description": "Postavi kada će video biti transkodiran", "transcoding_preferred_hardware_device": "Preferirani hardverski uređaj", "transcoding_preferred_hardware_device_description": "Odnosi se samo na VAAPI i QSV. Postavlja dri node koji se koristi za hardversko transkodiranje.", "transcoding_preset_preset": "Preset (-preset)", @@ -299,7 +324,7 @@ "transcoding_reference_frames_description": "Broj slika za referencu prilikom komprimiranja određene slike. Više vrijednosti poboljšavaju učinkovitost kompresije, ali usporavaju kodiranje. 0 automatski postavlja ovu vrijednost.", "transcoding_required_description": "Samo videozapisi koji nisu u prihvaćenom formatu", "transcoding_settings": "Postavke Video Transkodiranja", - "transcoding_settings_description": "Upravljajte informacijama o razlučivosti i kodiranju video datoteka", + "transcoding_settings_description": "Upravljaj koji videozapisi će se transkodirati i kako ih obraditi", "transcoding_target_resolution": "Ciljana rezolucija", "transcoding_target_resolution_description": "Veće razlučivosti mogu sačuvati više detalja, ali trebaju dulje za kodiranje, imaju veće veličine datoteka i mogu smanjiti odziv aplikacije.", "transcoding_temporal_aq": "Vremenski AQ", @@ -312,7 +337,7 @@ "transcoding_transcode_policy_description": "Pravila o tome kada se video treba transkodirati. HDR videozapisi uvijek će biti transkodirani (osim ako je transkodiranje onemogućeno).", "transcoding_two_pass_encoding": "Kodiranje u dva prolaza", "transcoding_two_pass_encoding_setting_description": "Transkodiranje u dva prolaza za proizvodnju bolje kodiranih videozapisa. Kada je omogućena maksimalna brzina prijenosa (potrebna za rad s H.264 i HEVC), ovaj način rada koristi raspon brzine prijenosa na temelju maksimalne brzine prijenosa i zanemaruje CRF. Za VP9, CRF se može koristiti ako je maksimalna brzina prijenosa onemogućena.", - "transcoding_video_codec": "Video Kodek", + "transcoding_video_codec": "Video kodek", "transcoding_video_codec_description": "VP9 ima visoku učinkovitost i web-kompatibilnost, ali treba dulje za transkodiranje. HEVC ima sličnu izvedbu, ali ima slabiju web kompatibilnost. H.264 široko je kompatibilan i brzo se transkodira, ali proizvodi mnogo veće datoteke. AV1 je najučinkovitiji kodek, ali nema podršku na starijim uređajima.", "trash_enabled_description": "Omogućite značajke Smeća", "trash_number_of_days": "Broj dana", @@ -346,6 +371,20 @@ "admin_password": "Admin Lozinka", "administration": "Administracija", "advanced": "Napredno", + "advanced_settings_enable_alternate_media_filter_subtitle": "Koristite ovu opciju za filtriranje medija tijekom sinkronizacije na temelju alternativnih kriterija. Pokušajte ovo samo ako imate problema s aplikacijom koja ne prepoznaje sve albume.", + "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTALNO] Koristite alternativni filter za sinkronizaciju albuma na uređaju", + "advanced_settings_log_level_title": "Razina zapisivanja: {}", + "advanced_settings_prefer_remote_subtitle": "Neki uređaji sporo učitavaju sličice s resursa na uređaju. Aktivirajte ovu postavku kako biste umjesto toga učitali slike s udaljenih izvora.", + "advanced_settings_prefer_remote_title": "Preferiraj udaljene slike", + "advanced_settings_proxy_headers_subtitle": "Definirajte zaglavlja posrednika koja Immich treba slati sa svakim mrežnim zahtjevom.", + "advanced_settings_proxy_headers_title": "Zaglavlja Posrednika", + "advanced_settings_self_signed_ssl_subtitle": "Preskoči provjeru SSL certifikata za krajnju točku poslužitelja. Potrebno za samo-potpisane certifikate.", + "advanced_settings_self_signed_ssl_title": "Dopusti samo-potpisane SSL certifikate", + "advanced_settings_sync_remote_deletions_subtitle": "Automatski izbriši ili obnovi resurs na ovom uređaju kada se ta radnja izvrši na webu", + "advanced_settings_sync_remote_deletions_title": "Sinkroniziraj udaljena brisanja [EKSPERIMENTALNO]", + "advanced_settings_tile_subtitle": "Postavke za napredne korisnike", + "advanced_settings_troubleshooting_subtitle": "Omogući dodatne značajke za rješavanje problema", + "advanced_settings_troubleshooting_title": "Rješavanje problema", "age_months": "Dob {months, plural, one {# month} other {# months}}", "age_year_months": "Dob 1 godina, {months, plural, one {# month} other {# months}}", "age_years": "{years, plural, other {Age #}}", @@ -354,6 +393,8 @@ "album_cover_updated": "Naslovnica albuma ažurirana", "album_delete_confirmation": "Jeste li sigurni da želite izbrisati album {album}?", "album_delete_confirmation_description": "Ako se ovaj album dijeli, drugi korisnici mu više neće moći pristupiti.", + "album_info_card_backup_album_excluded": "IZUZETO", + "album_info_card_backup_album_included": "UKLJUČENO", "album_info_updated": "Podaci o albumu ažurirani", "album_leave": "Napustiti album?", "album_leave_confirmation": "Jeste li sigurni da želite napustiti {album}?", @@ -362,10 +403,22 @@ "album_remove_user": "Ukloni korisnika?", "album_remove_user_confirmation": "Jeste li sigurni da želite ukloniti {user}?", "album_share_no_users": "Čini se da ste podijelili ovaj album sa svim korisnicima ili nemate nijednog korisnika s kojim biste ga dijelili.", + "album_thumbnail_card_item": "1 stavka", + "album_thumbnail_card_items": "{} stavki", + "album_thumbnail_card_shared": " · Podijeljeno", + "album_thumbnail_shared_by": "Podijeljeno sa {}", "album_updated": "Album ažuriran", "album_updated_setting_description": "Primite obavijest e-poštom kada dijeljeni album ima nova sredstva", "album_user_left": "Napušten {album}", "album_user_removed": "Uklonjen {user}", + "album_viewer_appbar_delete_confirm": "Jeste li sigurni da želite izbrisati ovaj album s vašeg računa?", + "album_viewer_appbar_share_err_delete": "Neuspješno brisanje albuma", + "album_viewer_appbar_share_err_leave": "Neuspješno napuštanje albuma", + "album_viewer_appbar_share_err_remove": "Postoje problemi s uklanjanjem resursa iz albuma", + "album_viewer_appbar_share_err_title": "Neuspješno mijenjanje naslova albuma", + "album_viewer_appbar_share_leave": "Napusti album", + "album_viewer_appbar_share_to": "Podijeli s", + "album_viewer_page_share_add_users": "Dodaj korisnike", "album_with_link_access": "Dopusti svima s poveznicom pristup fotografijama i osobama u ovom albumu.", "albums": "Albumi", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albumi}}", @@ -377,47 +430,139 @@ "allow_edits": "Dozvoli izmjene", "allow_public_user_to_download": "Dopusti javnom korisniku preuzimanje", "allow_public_user_to_upload": "Dopusti javnom korisniku učitavanje", + "alt_text_qr_code": "Slika QR koda", "anti_clockwise": "Suprotno smjeru kazaljke na satu", "api_key": "API Ključ", "api_key_description": "Ova će vrijednost biti prikazana samo jednom. Obavezno ju kopirajte prije zatvaranja prozora.", "api_key_empty": "Naziv vašeg API ključa ne smije biti prazan", "api_keys": "API Ključevi", + "app_bar_signout_dialog_content": "Jeste li sigurni da se želite odjaviti?", + "app_bar_signout_dialog_ok": "Da", + "app_bar_signout_dialog_title": "Odjavi se", "app_settings": "Postavke Aplikacije", "appears_in": "Pojavljuje se u", "archive": "Arhiva", "archive_or_unarchive_photo": "Arhivirajte ili dearhivirajte fotografiju", + "archive_page_no_archived_assets": "Nema arhiviranih resursa", + "archive_page_title": "Arhiviraj {{}}", "archive_size": "Veličina arhive", "archive_size_description": "Konfigurirajte veličinu arhive za preuzimanja (u GiB)", + "archived": "Ahrivirano", "archived_count": "{count, plural, other {Archived #}}", "are_these_the_same_person": "Je li ovo ista osoba?", "are_you_sure_to_do_this": "Jeste li sigurni da to želite učiniti?", + "asset_action_delete_err_read_only": "Nije moguće izbrisati resurse samo za čitanje, preskačem", + "asset_action_share_err_offline": "Nije moguće dohvatiti izvanmrežne resurse, preskačem", "asset_added_to_album": "Dodano u album", - "asset_adding_to_album": "Dodavanje u album...", + "asset_adding_to_album": "Dodavanje u album…", "asset_description_updated": "Opis imovine je ažuriran", "asset_filename_is_offline": "Sredstvo {filename} je izvan mreže", "asset_has_unassigned_faces": "Materijal ima nedodijeljena lica", - "asset_hashing": "Hashiranje...", + "asset_hashing": "Sažimanje…", + "asset_list_group_by_sub_title": "Grupiraj po", + "asset_list_layout_settings_dynamic_layout_title": "Dinamički raspored", + "asset_list_layout_settings_group_automatically": "Automatski", + "asset_list_layout_settings_group_by": "Grupiraj resurse po", + "asset_list_layout_settings_group_by_month_day": "Mjesec + dan", + "asset_list_layout_sub_title": "Raspored", + "asset_list_settings_subtitle": "Postavke izgleda mreže fotografija", + "asset_list_settings_title": "Mreža Fotografija", "asset_offline": "Sredstvo izvan mreže", "asset_offline_description": "Ovaj materijal je izvan mreže. Immich ne može pristupiti lokaciji datoteke. Provjerite je li sredstvo dostupno, a zatim ponovno skenirajte biblioteku.", + "asset_restored_successfully": "Resurs uspješno obnovljen", "asset_skipped": "Preskočeno", "asset_skipped_in_trash": "U smeću", "asset_uploaded": "Učitano", - "asset_uploading": "Učitavanje...", + "asset_uploading": "Šaljem…", + "asset_viewer_settings_subtitle": "Upravljajte postavkama preglednika vaše galerije", + "asset_viewer_settings_title": "Preglednik Resursa", "assets": "Sredstva", "assets_added_count": "Dodano {count, plural, one {# asset} other {# assets}}", "assets_added_to_album_count": "Dodano {count, plural, one {# asset} other {# assets}} u album", "assets_added_to_name_count": "Dodano {count, plural, one {# asset} other {# assets}} u {hasName, select, true {{name}} other {new album}}", "assets_count": "{count, plural, one {# asset} other {# assets}}", + "assets_deleted_permanently": "{} resurs(i) uspješno uklonjeni", + "assets_deleted_permanently_from_server": "{} resurs(i) trajno obrisan(i) sa Immich poslužitelja", "assets_moved_to_trash_count": "{count, plural, one {# asset} other {# asset}} premješteno u smeće", "assets_permanently_deleted_count": "Trajno izbrisano {count, plural, one {# asset} other {# assets}}", "assets_removed_count": "Uklonjeno {count, plural, one {# asset} other {# assets}}", - "assets_restore_confirmation": "Jeste li sigurni da želite vratiti sve svoje resurse bačene u otpad? Ne možete poništiti ovu radnju!", + "assets_removed_permanently_from_device": "{} resurs(i) trajno uklonjen(i) s vašeg uređaja", + "assets_restore_confirmation": "Jeste li sigurni da želite obnoviti sve svoje resurse bačene u otpad? Ne možete poništiti ovu radnju! Imajte na umu da se bilo koji izvanmrežni resursi ne mogu obnoviti na ovaj način.", "assets_restored_count": "Vraćeno {count, plural, one {# asset} other {# assets}}", + "assets_restored_successfully": "{} resurs(i) uspješno obnovljen(i)", + "assets_trashed": "{} resurs(i) premješten(i) u smeće", "assets_trashed_count": "Bačeno u smeće {count, plural, one {# asset} other {# assets}}", + "assets_trashed_from_server": "{} resurs(i) premješten(i) u smeće s Immich poslužitelja", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} već dio albuma", "authorized_devices": "Ovlašteni Uređaji", + "automatic_endpoint_switching_subtitle": "Povežite se lokalno preko naznačene Wi-Fi mreže kada je dostupna i koristite alternativne veze na drugim lokacijama", + "automatic_endpoint_switching_title": "Automatsko prebacivanje URL-a", "back": "Nazad", "back_close_deselect": "Natrag, zatvorite ili poništite odabir", + "background_location_permission": "Dozvola za lokaciju u pozadini", + "background_location_permission_content": "Kako bi prebacivao mreže dok radi u pozadini, Immich mora *uvijek* imati pristup preciznoj lokaciji kako bi aplikacija mogla pročitati naziv Wi-Fi mreže", + "backup_album_selection_page_albums_device": "Albumi na uređaju {{}}", + "backup_album_selection_page_albums_tap": "Dodirnite za uključivanje, dvostruki dodir za isključivanje", + "backup_album_selection_page_assets_scatter": "Resursi mogu biti raspoređeni u više albuma. Stoga, albumi mogu biti uključeni ili isključeni tijekom procesa sigurnosnog kopiranja.", + "backup_album_selection_page_select_albums": "Odabrani albumi", + "backup_album_selection_page_selection_info": "Informacije o odabiru", + "backup_album_selection_page_total_assets": "Ukupan broj jedinstvenih resursa", + "backup_all": "Sve", + "backup_background_service_backup_failed_message": "Neuspješno sigurnosno kopiranje resursa. Pokušavam ponovo…", + "backup_background_service_connection_failed_message": "Neuspješno povezivanje s poslužiteljem. Pokušavam ponovo…", + "backup_background_service_current_upload_notification": "Šaljem {}", + "backup_background_service_default_notification": "Provjera novih resursa…", + "backup_background_service_error_title": "Pogreška pri sigurnosnom kopiranju", + "backup_background_service_in_progress_notification": "Sigurnosno kopiranje vaših resursa…", + "backup_background_service_upload_failure_notification": "Neuspješno slanje {}", + "backup_controller_page_albums": "Sigurnosno kopiranje albuma", + "backup_controller_page_background_app_refresh_disabled_content": "Omogućite osvježavanje aplikacije u pozadini u Postavke > Opće Postavke > Osvježavanje Aplikacija u Pozadini kako biste koristili sigurnosno kopiranje u pozadini.", + "backup_controller_page_background_app_refresh_disabled_title": "Osvježavanje aplikacija u pozadini je onemogućeno", + "backup_controller_page_background_app_refresh_enable_button_text": "Idite u postavke", + "backup_controller_page_background_battery_info_link": "Pokaži mi kako", + "backup_controller_page_background_battery_info_message": "Za najbolje iskustvo sigurnosnog kopiranja u pozadini, molimo onemogućite sve optimizacije baterije koje ograničavaju pozadinsku aktivnost Immicha.\n\nBudući da je ovo specifično za uređaj, molimo potražite potrebne informacije za proizvođača vašeg uređaja.", + "backup_controller_page_background_battery_info_ok": "U redu", + "backup_controller_page_background_battery_info_title": "Optimizacije baterije", + "backup_controller_page_background_charging": "Samo tijekom punjenja", + "backup_controller_page_background_configure_error": "Neuspješno konfiguriranje pozadinske usluge", + "backup_controller_page_background_delay": "Odgođeno sigurnosno kopiranje novih resursa: {}", + "backup_controller_page_background_description": "Uključite pozadinsku uslugu kako biste automatski sigurnosno kopirali nove resurse bez potrebe za otvaranjem aplikacije", + "backup_controller_page_background_is_off": "Automatsko sigurnosno kopiranje u pozadini je isključeno", + "backup_controller_page_background_is_on": "Automatsko sigurnosno kopiranje u pozadini je uključeno", + "backup_controller_page_background_turn_off": "Isključite pozadinsku uslugu", + "backup_controller_page_background_turn_on": "Uključite pozadinsku uslugu", + "backup_controller_page_background_wifi": "Samo na Wi-Fi mreži", + "backup_controller_page_backup": "Sigurnosna kopija", + "backup_controller_page_backup_selected": "Odabrani: ", + "backup_controller_page_backup_sub": "Sigurnosno kopirane fotografije i videozapisi", + "backup_controller_page_created": "Kreirano: {}", + "backup_controller_page_desc_backup": "Uključite sigurnosno kopiranje u prvom planu kako biste automatski prenijeli nove resurse na poslužitelj prilikom otvaranja aplikacije.", + "backup_controller_page_excluded": "Izuzeto: ", + "backup_controller_page_failed": "Neuspješno ({})", + "backup_controller_page_filename": "Naziv datoteke: {} [{}]", + "backup_controller_page_id": "ID: {}", + "backup_controller_page_info": "Informacije o sigurnosnom kopiranju", + "backup_controller_page_none_selected": "Nema odabranih", + "backup_controller_page_remainder": "Podsjetnik", + "backup_controller_page_remainder_sub": "Preostale fotografije i videozapisi za sigurnosno kopiranje iz odabira", + "backup_controller_page_server_storage": "Pohrana na poslužitelju", + "backup_controller_page_start_backup": "Pokreni Sigurnosno Kopiranje", + "backup_controller_page_status_off": "Automatsko sigurnosno kopiranje u prvom planu je isključeno", + "backup_controller_page_status_on": "Automatsko sigurnosno kopiranje u prvom planu je uključeno", + "backup_controller_page_storage_format": "{} od {} iskorišteno", + "backup_controller_page_to_backup": "Albumi za sigurnosno kopiranje", + "backup_controller_page_total_sub": "Sve jedinstvene fotografije i videozapisi iz odabranih albuma", + "backup_controller_page_turn_off": "Isključite sigurnosno kopiranje u prvom planu", + "backup_controller_page_turn_on": "Uključite sigurnosno kopiranje u prvom planu", + "backup_controller_page_uploading_file_info": "Slanje informacija o datoteci", + "backup_err_only_album": "Nije moguće ukloniti jedini album", + "backup_info_card_assets": "resursi", + "backup_manual_cancelled": "Otkazano", + "backup_manual_in_progress": "Slanje već u tijeku. Pokšuajte nakon nekog vremena", + "backup_manual_success": "Uspijeh", + "backup_manual_title": "Status slanja", + "backup_options_page_title": "Opcije sigurnosnog kopiranja", + "backup_setting_subtitle": "Upravljajte postavkama učitavanja u pozadini i prvom planu", "backward": "Unazad", "birthdate_saved": "Datum rođenja uspješno spremljen", "birthdate_set_description": "Datum rođenja se koristi za izračunavanje godina ove osobe u trenutku fotografije.", @@ -429,6 +574,16 @@ "bulk_keep_duplicates_confirmation": "Jeste li sigurni da želite zadržati {count, plural, one {# duplicate asset} other {# duplicate asset}}? Ovo će riješiti sve duplicirane grupe bez brisanja ičega.", "bulk_trash_duplicates_confirmation": "Jeste li sigurni da želite na veliko baciti u smeće {count, plural, one {# duplicate asset} other {# duplicate asset}}? Ovo će zadržati najveće sredstvo svake grupe i baciti sve ostale duplikate u smeće.", "buy": "Kupi Immich", + "cache_settings_album_thumbnails": "Sličice na stranici biblioteke ({} resursa)", + "cache_settings_clear_cache_button": "Očisti predmemoriju", + "cache_settings_clear_cache_button_title": "Briše predmemoriju aplikacije. Ovo će značajno utjecati na performanse aplikacije dok se predmemorija ponovno ne izgradi.", + "cache_settings_duplicated_assets_clear_button": "OČISTI", + "cache_settings_duplicated_assets_subtitle": "Fotografije i videozapisi koje je aplikacija stavila na crnu listu", + "cache_settings_duplicated_assets_title": "Duplicirani Resursi ({})", + "cache_settings_image_cache_size": "Veličina predmemorije slika ({} resursa)", + "cache_settings_statistics_album": "Sličice biblioteke", + "cache_settings_statistics_assets": "{} resursa ({})", + "cache_settings_statistics_full": "Pune slike", "camera": "Kamera", "camera_brand": "Marka kamere", "camera_model": "Model kamere", diff --git a/i18n/hu.json b/i18n/hu.json index 68dec7d036..88ce6033f8 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -529,11 +529,11 @@ "backup_controller_page_background_turn_on": "Háttérszolgáltatás bekapcsolása", "backup_controller_page_background_wifi": "Csak WiFi-n", "backup_controller_page_backup": "Mentés", - "backup_controller_page_backup_selected": "Kiválasztva:", + "backup_controller_page_backup_selected": "Kiválasztva: ", "backup_controller_page_backup_sub": "Mentett fotók és videók", "backup_controller_page_created": "Létrehozva: {}", "backup_controller_page_desc_backup": "Ha bekapcsolod az előtérben mentést, akkor az új elemek automatikusan feltöltődnek a szerverre, amikor megyitod az alkalmazást.", - "backup_controller_page_excluded": "Kivéve:", + "backup_controller_page_excluded": "Kivéve: ", "backup_controller_page_failed": "Sikertelen ({})", "backup_controller_page_filename": "Fájlnév: {}[{}]", "backup_controller_page_id": "Azonosító: {}", @@ -1540,7 +1540,7 @@ "search_result_page_new_search_hint": "Új Keresés", "search_settings": "Keresési beállítások", "search_state": "Megye/Állam keresése...", - "search_suggestion_list_smart_search_hint_1": "Az intelligens keresés alapértelmezetten be van kapcsolva, metaadatokat így kereshetsz:", + "search_suggestion_list_smart_search_hint_1": "Az intelligens keresés alapértelmezetten be van kapcsolva, metaadatokat így kereshetsz: ", "search_suggestion_list_smart_search_hint_2": "m:keresési-kifejezés", "search_tags": "Címkék keresése...", "search_timezone": "Időzóna keresése...", diff --git a/i18n/id.json b/i18n/id.json index e07e79faff..ceda55340d 100644 --- a/i18n/id.json +++ b/i18n/id.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "Anda yakin untuk menonaktifkan semua cara login? Login akan dinonaktikan secara menyeluruh.", "authentication_settings_reenable": "Untuk mengaktifkan ulang, gunakan Perintah Server.", "background_task_job": "Tugas Latar Belakang", - "backup_database": "Basis Data Cadangan", + "backup_database": "Buat Cadangan Basis Data", "backup_database_enable_description": "Aktifkan pencadangan basis data", "backup_keep_last_amount": "Jumlah cadangan untuk disimpan", - "backup_settings": "Pengaturan Pencadangan", - "backup_settings_description": "Kelola pengaturan pencadangan basis data", + "backup_settings": "Pengaturan Pencadangan Basis Data", + "backup_settings_description": "Kelola pengaturan pencadangan basis data. Catatan: Tugas ini tidak dipantau dan Anda tidak akan diberi tahu jika ada kesalahan.", "check_all": "Periksa Semua", "cleanup": "Pembersihan", "cleared_jobs": "Tugas terselesaikan untuk: {job}", @@ -70,8 +70,13 @@ "forcing_refresh_library_files": "Memaksakan penyegaran semua berkas pustaka", "image_format": "Format", "image_format_description": "WebP menghasilkan ukuran berkas yang lebih kecil daripada JPEG, tetapi lebih lambat untuk dienkode.", + "image_fullsize_description": "Gambar berukuran penuh tanpa metadata, digunakan ketika diperbesar", + "image_fullsize_enabled": "Aktifkan pembuatan gambar berukuran penuh", + "image_fullsize_enabled_description": "Buat gambar berukuran penuh untuk format yang tidak ramah web. Ketika \"Utamakan pratinjau tersemat\" diaktifkan, pratinjau tersema digunakan secara langsung tanpa konversi. Tidak memengaruhi format ramah web seperti JPEG.", + "image_fullsize_quality_description": "Kualitas gambar berukuran penuh dari 1-100. Lebih tinggi lebih baik, tetapi menghasilkan berkas lebih besar.", + "image_fullsize_title": "Pengaturan Gambar Berukuran Penuh", "image_prefer_embedded_preview": "Utamakan pratinjau tersemat", - "image_prefer_embedded_preview_setting_description": "Gunakan pratinjau tersemat dalam foto RAW sebagai masukan dalam pemrosesan gambar ketika tersedia. Ini dapat menghasilkan warna yang lebih akurat untuk beberapa gambar, tetapi kualitas pratinjau bergantung pada kamera dan gambarnya dapat memiliki lebih banyak artefak kompresi.", + "image_prefer_embedded_preview_setting_description": "Gunakan pratinjau tersemat dalam foto RAW sebagai masukan dalam pemrosesan gambar dan ketika tersedia. Ini dapat menghasilkan warna yang lebih akurat untuk beberapa gambar, tetapi kualitas pratinjau bergantung pada kamera dan gambarnya dapat memiliki lebih banyak artefak kompresi.", "image_prefer_wide_gamut": "Utamakan gamut luas", "image_prefer_wide_gamut_setting_description": "Gunakan Display P3 untuk gambar kecil. Ini menjaga kecerahan gambar dengan ruang warna yang luas, tetapi gambar dapat terlihat beda pada perangkat lawas dengan versi peramban yang lawas. Gambar sRGB tetap dalam sRGB untuk menghindari perubahan warna.", "image_preview_description": "Gambar berukuran sedang tanpa metadata, digunakan ketika melihat aset satuan dan untuk pembelajaran mesin", @@ -366,13 +371,17 @@ "admin_password": "Kata Sandi Admin", "administration": "Administrasi", "advanced": "Tingkat lanjut", - "advanced_settings_log_level_title": "Log level: {}", + "advanced_settings_enable_alternate_media_filter_subtitle": "Gunakan opsi ini untuk menyaring media saat sinkronisasi berdasarkan kriteria alternatif. Hanya coba ini dengan aplikasi mendeteksi semua album.", + "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTAL] Gunakan saringan sinkronisasi album perangkat alternatif", + "advanced_settings_log_level_title": "Tingkat log: {}", "advanced_settings_prefer_remote_subtitle": "Beberapa perangkat tidak dapat memuat thumbnail dengan cepat.\nMenyalakan ini akan memuat thumbnail dari server.", "advanced_settings_prefer_remote_title": "Prefer remote images", "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", "advanced_settings_proxy_headers_title": "Proxy Headers", "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", + "advanced_settings_sync_remote_deletions_subtitle": "Hapus atau pulihkan aset pada perangkat ini secara otomatis ketika tindakan dilakukan di web", + "advanced_settings_sync_remote_deletions_title": "Sinkronisasi penghapusan jarak jauh [EKSPERIMENTAL]", "advanced_settings_tile_subtitle": "Advanced user's settings", "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", "advanced_settings_troubleshooting_title": "Troubleshooting", @@ -472,7 +481,7 @@ "assets_added_to_album_count": "Ditambahkan {count, plural, one {# aset} other {# aset}} ke album", "assets_added_to_name_count": "Ditambahkan {count, plural, one {# aset} other {# aset}} ke {hasName, select, true {{name}} other {album baru}}", "assets_count": "{count, plural, one {# aset} other {# aset}}", - "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently": "{} asset dihapus secara permanen", "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", "assets_moved_to_trash_count": "Dipindahkan {count, plural, one {# aset} other {# aset}} ke sampah", "assets_permanently_deleted_count": "{count, plural, one {# aset} other {# aset}} dihapus secara permanen", @@ -505,7 +514,7 @@ "backup_background_service_default_notification": "Memeriksa aset baru...", "backup_background_service_error_title": "Backup error", "backup_background_service_in_progress_notification": "Mencadangkan asetmu...", - "backup_background_service_upload_failure_notification": "Gagal unggah {}", + "backup_background_service_upload_failure_notification": "Gagal mengunggah {}", "backup_controller_page_albums": "Cadangkan album", "backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.", "backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled", @@ -524,11 +533,11 @@ "backup_controller_page_background_turn_on": "Nyalakan layanan latar belakang", "backup_controller_page_background_wifi": "Hanya melalui WiFi", "backup_controller_page_backup": "Cadangkan", - "backup_controller_page_backup_selected": "Terpilih:", + "backup_controller_page_backup_selected": "Terpilih: ", "backup_controller_page_backup_sub": "Foto dan video yang dicadangkan", "backup_controller_page_created": "Dibuat pada: {}", "backup_controller_page_desc_backup": "Aktifkan pencadangan di latar depan untuk mengunggah otomatis aset baru ke server secara otomatis saat aplikasi terbuka.", - "backup_controller_page_excluded": "Dikecualikan:", + "backup_controller_page_excluded": "Dikecualikan: ", "backup_controller_page_failed": "Gagal ({})", "backup_controller_page_filename": "Nama file: {} [{}]", "backup_controller_page_id": "ID: {}", @@ -601,7 +610,7 @@ "change_password": "Ubah Kata Sandi", "change_password_description": "Ini merupakan pertama kali Anda masuk ke sistem atau ada permintaan untuk mengubah kata sandi Anda. Silakan masukkan kata sandi baru di bawah.", "change_password_form_confirm_password": "Konfirmasi Sandi", - "change_password_form_description": "Halo {},\n\nIni pertama kali anda masuk ke dalam sistem atau terdapat permintaan penggantian password.\nHarap masukkan password baru.", + "change_password_form_description": "Halo {name},\n\nIni pertama kali anda masuk ke dalam sistem atau terdapat permintaan penggantian password.\nHarap masukkan password baru.", "change_password_form_new_password": "Sandi Baru", "change_password_form_password_mismatch": "Sandi tidak cocok", "change_password_form_reenter_new_password": "Masukkan Ulang Sandi Baru", @@ -814,7 +823,7 @@ "error_change_sort_album": "Failed to change album sort order", "error_delete_face": "Terjadi kesalahan menghapus wajah dari aset", "error_loading_image": "Terjadi eror memuat gambar", - "error_saving_image": "Error: {}", + "error_saving_image": "Kesalahan: {}", "error_title": "Eror - Ada yang salah", "errors": { "cannot_navigate_next_asset": "Tidak dapat menuju ke aset berikutnya", @@ -948,10 +957,10 @@ "exif_bottom_sheet_location": "LOKASI", "exif_bottom_sheet_people": "ORANG", "exif_bottom_sheet_person_add_person": "Tambah nama", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", + "exif_bottom_sheet_person_age": "Umur {}", + "exif_bottom_sheet_person_age_months": "Umur {} months", + "exif_bottom_sheet_person_age_year_months": "Umur 1 tahun, {} bulan", + "exif_bottom_sheet_person_age_years": "Umur {}", "exit_slideshow": "Keluar dari Salindia", "expand_all": "Buka semua", "experimental_settings_new_asset_list_subtitle": "Memproses", @@ -969,7 +978,7 @@ "external": "Eksternal", "external_libraries": "Pustaka Eksternal", "external_network": "External network", - "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", + "external_network_sheet_info": "When not on the preferred Wi-Fi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "face_unassigned": "Tidak ada nama", "failed": "Failed", "failed_to_load_assets": "Gagal memuat aset", @@ -987,6 +996,7 @@ "filetype": "Jenis berkas", "filter": "Filter", "filter_people": "Saring orang", + "filter_places": "Saring tempat", "find_them_fast": "Temukan dengan cepat berdasarkan nama dengan pencarian", "fix_incorrect_match": "Perbaiki pencocokan salah", "folder": "Folder", @@ -1155,6 +1165,7 @@ "loop_videos": "Ulangi video", "loop_videos_description": "Aktifkan untuk mengulangi video secara otomatis dalam penampil detail.", "main_branch_warning": "Anda menggunakan versi pengembangan; kami sangat menyarankan menggunakan versi rilis!", + "main_menu": "Menu utama", "make": "Merek", "manage_shared_links": "Kelola tautan terbagi", "manage_sharing_with_partners": "Kelola pembagian dengan partner", @@ -1276,6 +1287,7 @@ "onboarding_welcome_user": "Selamat datang, {user}", "online": "Daring", "only_favorites": "Hanya favorit", + "open": "Buka", "open_in_map_view": "Buka dalam tampilan peta", "open_in_openstreetmap": "Buka di OpenStreetMap", "open_the_search_filters": "Buka saringan pencarian", @@ -1299,7 +1311,7 @@ "partner_page_partner_add_failed": "Gagal menambahkan partner", "partner_page_select_partner": "Pilih partner", "partner_page_shared_to_title": "Dibagikan dengan", - "partner_page_stop_sharing_content": "{} tidak akan bisa mengakses foto kamu lagi.", + "partner_page_stop_sharing_content": "{} tidak akan bisa mengakses foto Anda lagi.", "partner_sharing": "Pembagian Partner", "partners": "Partner", "password": "Kata sandi", @@ -1620,7 +1632,7 @@ "shared_intent_upload_button_progress_text": "{} / {} Uploaded", "shared_link_app_bar_title": "Link Berbagi", "shared_link_clipboard_copied_massage": "Tersalin ke papan klip", - "shared_link_clipboard_text": "Link: {}\nSandi: {}", + "shared_link_clipboard_text": "Tautan: {}\nSandi: {}", "shared_link_create_error": "Terjadi kesalahan saat membuat link berbagi", "shared_link_edit_description_hint": "Masukkan deskripsi link", "shared_link_edit_expire_after_option_day": "1 hari", @@ -1866,6 +1878,7 @@ "view_name": "Tampilkan", "view_next_asset": "Tampilkan aset berikutnya", "view_previous_asset": "Tampilkan aset sebelumnya", + "view_qr_code": "Tampilkan kode QR", "view_stack": "Tampilkan Tumpukan", "viewer_remove_from_stack": "Keluarkan dari Tumpukan", "viewer_stack_use_as_main_asset": "Gunakan sebagai aset utama", diff --git a/i18n/it.json b/i18n/it.json index 01aeb93721..987ad34d2d 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -371,13 +371,17 @@ "admin_password": "Password Amministratore", "administration": "Amministrazione", "advanced": "Avanzate", + "advanced_settings_enable_alternate_media_filter_subtitle": "Usa questa opzione per filtrare i contenuti multimediali durante la sincronizzazione in base a criteri alternativi. Prova questa opzione solo se riscontri problemi con il rilevamento di tutti gli album da parte dell'app.", + "advanced_settings_enable_alternate_media_filter_title": "[SPERIMENTALE] Usa un filtro alternativo per la sincronizzazione degli album del dispositivo", "advanced_settings_log_level_title": "Livello log: {}", "advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono molto lenti a caricare le anteprime delle immagini dal dispositivo. Attivare questa impostazione per caricare invece le immagini remote.", - "advanced_settings_prefer_remote_title": "Preferisci immagini remote.", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", + "advanced_settings_prefer_remote_title": "Preferisci immagini remote", + "advanced_settings_proxy_headers_subtitle": "Definisci gli header per i proxy che Immich dovrebbe inviare con ogni richiesta di rete", "advanced_settings_proxy_headers_title": "Proxy Headers", "advanced_settings_self_signed_ssl_subtitle": "Salta la verifica dei certificati SSL del server. Richiesto con l'uso di certificati self-signed.", "advanced_settings_self_signed_ssl_title": "Consenti certificati SSL self-signed", + "advanced_settings_sync_remote_deletions_subtitle": "Rimuovi o ripristina automaticamente un elemento su questo dispositivo se l'azione è stata fatta via web", + "advanced_settings_sync_remote_deletions_title": "Sincronizza le cancellazioni remote [SPERIMENTALE]", "advanced_settings_tile_subtitle": "Impostazioni aggiuntive utenti", "advanced_settings_troubleshooting_subtitle": "Attiva funzioni addizionali per la risoluzione dei problemi", "advanced_settings_troubleshooting_title": "Risoluzione problemi", @@ -399,19 +403,19 @@ "album_remove_user": "Rimuovi l'utente?", "album_remove_user_confirmation": "Sicuro di voler rimuovere l'utente {user}?", "album_share_no_users": "Sembra che tu abbia condiviso questo album con tutti gli utenti oppure non hai nessun utente con cui condividere.", - "album_thumbnail_card_item": "1 elemento ", + "album_thumbnail_card_item": "1 elemento", "album_thumbnail_card_items": "{} elementi", - "album_thumbnail_card_shared": "Condiviso", + "album_thumbnail_card_shared": " · Condiviso", "album_thumbnail_shared_by": "Condiviso da {}", "album_updated": "Album aggiornato", "album_updated_setting_description": "Ricevi una notifica email quando un album condiviso ha nuovi media", "album_user_left": "{album} abbandonato", "album_user_removed": "Utente {user} rimosso", "album_viewer_appbar_delete_confirm": "Sei sicuro di voler rimuovere questo album dal tuo account?", - "album_viewer_appbar_share_err_delete": "Impossibile eliminare l'album ", - "album_viewer_appbar_share_err_leave": "Impossibile lasciare l'album ", - "album_viewer_appbar_share_err_remove": "Ci sono problemi nel rimuovere oggetti dall'album ", - "album_viewer_appbar_share_err_title": "Impossibile cambiare il titolo dell'album ", + "album_viewer_appbar_share_err_delete": "Impossibile eliminare l'album", + "album_viewer_appbar_share_err_leave": "Impossibile lasciare l'album", + "album_viewer_appbar_share_err_remove": "Ci sono problemi nel rimuovere oggetti dall'album", + "album_viewer_appbar_share_err_title": "Impossibile cambiare il titolo dell'album", "album_viewer_appbar_share_leave": "Lascia album", "album_viewer_appbar_share_to": "Condividi a", "album_viewer_page_share_add_users": "Aggiungi utenti", @@ -440,7 +444,7 @@ "archive": "Archivio", "archive_or_unarchive_photo": "Archivia o ripristina foto", "archive_page_no_archived_assets": "Nessuna oggetto archiviato", - "archive_page_title": "Archivia ({})", + "archive_page_title": "Archivio ({})", "archive_size": "Dimensioni Archivio", "archive_size_description": "Imposta le dimensioni dell'archivio per i download (in GiB)", "archived": "Archiviati", @@ -477,18 +481,18 @@ "assets_added_to_album_count": "{count, plural, one {# asset aggiunto} other {# asset aggiunti}} all'album", "assets_added_to_name_count": "Aggiunti {count, plural, one {# asset} other {# assets}} a {hasName, select, true {{name}} other {new album}}", "assets_count": "{count, plural, other {# asset}}", - "assets_deleted_permanently": "{} asset(s) deleted permanently", - "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_deleted_permanently": "{} elementi rimossi definitivamente", + "assets_deleted_permanently_from_server": "{} elementi rimossi definitivamente dal server Immich", "assets_moved_to_trash_count": "{count, plural, one {# asset spostato} other {# asset spostati}} nel cestino", "assets_permanently_deleted_count": "{count, plural, one {# asset cancellato} other {# asset cancellati}} definitivamente", "assets_removed_count": "{count, plural, one {# asset rimosso} other {# asset rimossi}}", - "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_removed_permanently_from_device": "{} elementi rimossi definitivamente dal tuo dispositivo", "assets_restore_confirmation": "Sei sicuro di voler ripristinare tutti gli asset cancellati? Non puoi annullare questa azione! Tieni presente che eventuali risorse offline NON possono essere ripristinate in questo modo.", "assets_restored_count": "{count, plural, one {# asset ripristinato} other {# asset ripristinati}}", - "assets_restored_successfully": "{} asset(s) restored successfully", - "assets_trashed": "{} asset(s) trashed", + "assets_restored_successfully": "{} elementi ripristinati", + "assets_trashed": "{} elementi cestinati", "assets_trashed_count": "{count, plural, one {Spostato # asset} other {Spostati # assets}} nel cestino", - "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "assets_trashed_from_server": "{} elementi cestinati dal server Immich", "assets_were_part_of_album_count": "{count, plural, one {L'asset era} other {Gli asset erano}} già parte dell'album", "authorized_devices": "Dispositivi autorizzati", "automatic_endpoint_switching_subtitle": "Connetti localmente quando la rete Wi-Fi specificata è disponibile e usa le connessioni alternative negli altri casi", @@ -498,7 +502,7 @@ "background_location_permission": "Permesso di localizzazione in background", "background_location_permission_content": "Per fare in modo che sia possibile cambiare rete quando è in esecuzione in background, Immich deve *sempre* avere accesso alla tua posizione precisa in modo da poter leggere il nome della rete Wi-Fi", "backup_album_selection_page_albums_device": "Album sul dispositivo ({})", - "backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere.", + "backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere", "backup_album_selection_page_assets_scatter": "Visto che le risorse possono trovarsi in più album, questi possono essere inclusi o esclusi dal backup.", "backup_album_selection_page_select_albums": "Seleziona gli album", "backup_album_selection_page_selection_info": "Informazioni sulla selezione", @@ -529,11 +533,11 @@ "backup_controller_page_background_turn_on": "Abilita servizi in background", "backup_controller_page_background_wifi": "Solo su WiFi", "backup_controller_page_backup": "Backup", - "backup_controller_page_backup_selected": "Selezionati:", + "backup_controller_page_backup_selected": "Selezionati: ", "backup_controller_page_backup_sub": "Foto e video caricati", "backup_controller_page_created": "Creato il: {}", "backup_controller_page_desc_backup": "Attiva il backup per eseguire il caricamento automatico sul server all'apertura dell'applicazione.", - "backup_controller_page_excluded": "Esclusi:", + "backup_controller_page_excluded": "Esclusi: ", "backup_controller_page_failed": "Falliti: ({})", "backup_controller_page_filename": "Nome file: {} [{}]", "backup_controller_page_id": "ID: {}", @@ -543,18 +547,18 @@ "backup_controller_page_remainder_sub": "Foto e video che devono essere ancora caricati", "backup_controller_page_server_storage": "Spazio sul server", "backup_controller_page_start_backup": "Avvia backup", - "backup_controller_page_status_off": "Backup è disattivato ", + "backup_controller_page_status_off": "Backup è disattivato", "backup_controller_page_status_on": "Backup è attivato", "backup_controller_page_storage_format": "{} di {} usati", "backup_controller_page_to_backup": "Album da caricare", - "backup_controller_page_total_sub": "Tutte le foto e i video unici caricati dagli album selezionati ", + "backup_controller_page_total_sub": "Tutte le foto e i video unici caricati dagli album selezionati", "backup_controller_page_turn_off": "Disattiva backup", - "backup_controller_page_turn_on": "Attiva backup ", + "backup_controller_page_turn_on": "Attiva backup", "backup_controller_page_uploading_file_info": "Caricamento informazioni file", "backup_err_only_album": "Non è possibile rimuovere l'unico album", "backup_info_card_assets": "risorse", "backup_manual_cancelled": "Annullato", - "backup_manual_in_progress": "Caricamento già in corso. Riprova più tardi.", + "backup_manual_in_progress": "Caricamento già in corso. Riprova più tardi", "backup_manual_success": "Successo", "backup_manual_title": "Stato del caricamento", "backup_options_page_title": "Opzioni di Backup", @@ -570,21 +574,21 @@ "bulk_keep_duplicates_confirmation": "Sei sicuro di voler tenere {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione risolverà tutti i gruppi duplicati senza cancellare nulla.", "bulk_trash_duplicates_confirmation": "Sei davvero sicuro di voler cancellare {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione manterrà l'asset più pesante di ogni gruppo e cancellerà permanentemente tutti gli altri duplicati.", "buy": "Acquista Immich", - "cache_settings_album_thumbnails": "Anteprime pagine librerie ({} risorse)", + "cache_settings_album_thumbnails": "Anteprime pagine librerie ({} elementi)", "cache_settings_clear_cache_button": "Pulisci cache", "cache_settings_clear_cache_button_title": "Pulisce la cache dell'app. Questo impatterà significativamente le prestazioni dell''app fino a quando la cache non sarà rigenerata.", "cache_settings_duplicated_assets_clear_button": "PULISCI", "cache_settings_duplicated_assets_subtitle": "Foto e video che sono nella black list dell'applicazione", "cache_settings_duplicated_assets_title": "Elementi duplicati ({})", - "cache_settings_image_cache_size": "Dimensione cache delle immagini ({} risorse)", + "cache_settings_image_cache_size": "Dimensione cache delle immagini ({} elementi)", "cache_settings_statistics_album": "Anteprime librerie", - "cache_settings_statistics_assets": "{} risorse ({})", + "cache_settings_statistics_assets": "{} elementi ({})", "cache_settings_statistics_full": "Immagini complete", "cache_settings_statistics_shared": "Anteprime album condivisi", "cache_settings_statistics_thumbnail": "Anteprime", "cache_settings_statistics_title": "Uso della cache", "cache_settings_subtitle": "Controlla il comportamento della cache dell'applicazione mobile immich", - "cache_settings_thumbnail_size": "Dimensione cache dei thumbnail ({} assets)", + "cache_settings_thumbnail_size": "Dimensione cache anteprime ({} elementi)", "cache_settings_tile_subtitle": "Controlla il comportamento dello storage locale", "cache_settings_tile_title": "Archiviazione locale", "cache_settings_title": "Impostazioni della Cache", @@ -593,7 +597,7 @@ "camera_model": "Modello fotocamera", "cancel": "Annulla", "cancel_search": "Annulla ricerca", - "canceled": "Canceled", + "canceled": "Annullato", "cannot_merge_people": "Impossibile unire le persone", "cannot_undo_this_action": "Non puoi annullare questa azione!", "cannot_update_the_description": "Impossibile aggiornare la descrizione", @@ -606,14 +610,14 @@ "change_password": "Modifica Password", "change_password_description": "È stato richiesto di cambiare la password (oppure è la prima volta che accedi). Inserisci la tua nuova password qui sotto.", "change_password_form_confirm_password": "Conferma Password", - "change_password_form_description": "Ciao {name},\n\nQuesto è la prima volta che accedi al sistema oppure è stato fatto una richiesta di cambiare la password. Per favore inserisca la nuova password qui sotto", + "change_password_form_description": "Ciao {name},\n\nQuesto è la prima volta che accedi al sistema oppure è stato fatto una richiesta di cambiare la password. Per favore inserisca la nuova password qui sotto.", "change_password_form_new_password": "Nuova Password", "change_password_form_password_mismatch": "Le password non coincidono", - "change_password_form_reenter_new_password": "Inserisci ancora la nuova password ", + "change_password_form_reenter_new_password": "Inserisci ancora la nuova password", "change_your_password": "Modifica la tua password", "changed_visibility_successfully": "Visibilità modificata con successo", "check_all": "Controlla Tutti", - "check_corrupt_asset_backup": "Verifica la presenza di backup di asset corrotti ", + "check_corrupt_asset_backup": "Verifica la presenza di backup di asset corrotti", "check_corrupt_asset_backup_button": "Effettua controllo", "check_corrupt_asset_backup_description": "Effettua questo controllo solo sotto rete Wi-Fi e quando tutti gli asset sono stati sottoposti a backup. La procedura potrebbe impiegare qualche minuto.", "check_logs": "Controlla i log", @@ -627,11 +631,11 @@ "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove_msg": "Client certificate is removed", - "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", + "client_cert_import_success_msg": "Certificato client importato", + "client_cert_invalid_msg": "File certificato invalido o password errata", + "client_cert_remove_msg": "Certificato client rimosso", + "client_cert_subtitle": "Supporta solo il formato PKCS12 (.p12, .pfx). L'importazione/rimozione del certificato è disponibile solo prima del login", + "client_cert_title": "Certificato Client SSL", "clockwise": "Senso orario", "close": "Chiudi", "collapse": "Restringi", @@ -643,8 +647,8 @@ "comments_and_likes": "Commenti & mi piace", "comments_are_disabled": "I commenti sono disabilitati", "common_create_new_album": "Crea nuovo Album", - "common_server_error": "Si prega di controllare la connessione network, che il server sia raggiungibile e che le versione del server e app sono gli stessi", - "completed": "Completed", + "common_server_error": "Si prega di controllare la connessione network, che il server sia raggiungibile e che le versione del server e app sono gli stessi.", + "completed": "Completato", "confirm": "Conferma", "confirm_admin_password": "Conferma password dell'amministratore", "confirm_delete_face": "Sei sicuro di voler cancellare il volto di {name} dall'asset?", @@ -660,7 +664,7 @@ "control_bottom_app_bar_delete_from_local": "Elimina dal dispositivo", "control_bottom_app_bar_edit_location": "Modifica posizione", "control_bottom_app_bar_edit_time": "Modifica data e ora", - "control_bottom_app_bar_share_link": "Share Link", + "control_bottom_app_bar_share_link": "Condividi Link", "control_bottom_app_bar_share_to": "Condividi a", "control_bottom_app_bar_trash_from_immich": "Sposta nel cestino", "copied_image_to_clipboard": "Immagine copiata negli appunti.", @@ -772,12 +776,12 @@ "download_settings": "Scarica", "download_settings_description": "Gestisci le impostazioni relative al download delle risorse", "download_started": "Download avviato", - "download_sucess": "Download success", - "download_sucess_android": "The media has been downloaded to DCIM/Immich", + "download_sucess": "Download completato", + "download_sucess_android": "I contenuti multimediali sono stati scaricati in DCIM/Immich", "download_waiting_to_retry": "In attesa di riprovare", "downloading": "Scaricando", "downloading_asset_filename": "Scaricando la risorsa {filename}", - "downloading_media": "Downloading media", + "downloading_media": "Scaricamento file multimediali", "drop_files_to_upload": "Rilascia i file ovunque per caricarli", "duplicates": "Duplicati", "duplicates_description": "Risolvi ciascun gruppo indicando quali sono, se esistono, i duplicati", @@ -807,19 +811,19 @@ "editor_crop_tool_h2_aspect_ratios": "Proporzioni", "editor_crop_tool_h2_rotation": "Rotazione", "email": "Email", - "empty_folder": "This folder is empty", + "empty_folder": "La cartella è vuota", "empty_trash": "Svuota cestino", "empty_trash_confirmation": "Sei sicuro di volere svuotare il cestino? Questo rimuoverà tutte le risorse nel cestino in modo permanente da Immich.\nNon puoi annullare questa azione!", "enable": "Abilita", "enabled": "Abilitato", "end_date": "Data Fine", - "enqueued": "Enqueued", + "enqueued": "Accodato", "enter_wifi_name": "Inserisci il nome della rete Wi-Fi", "error": "Errore", "error_change_sort_album": "Errore nel cambiare l'ordine di degli album", "error_delete_face": "Errore nel cancellare la faccia dalla foto", "error_loading_image": "Errore nel caricamento dell'immagine", - "error_saving_image": "Error: {}", + "error_saving_image": "Errore: {}", "error_title": "Errore - Qualcosa è andato storto", "errors": { "cannot_navigate_next_asset": "Impossibile passare alla risorsa successiva", @@ -953,10 +957,10 @@ "exif_bottom_sheet_location": "POSIZIONE", "exif_bottom_sheet_people": "PERSONE", "exif_bottom_sheet_person_add_person": "Aggiungi nome", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", + "exif_bottom_sheet_person_age": "Età {}", + "exif_bottom_sheet_person_age_months": "Età {} mesi", + "exif_bottom_sheet_person_age_year_months": "Età 1 anno e {} mesi", + "exif_bottom_sheet_person_age_years": "Età {}", "exit_slideshow": "Esci dalla presentazione", "expand_all": "Espandi tutto", "experimental_settings_new_asset_list_subtitle": "Lavori in corso", @@ -976,9 +980,9 @@ "external_network": "Rete esterna", "external_network_sheet_info": "Quando non si è connessi alla rete Wi-Fi preferita, l'app si collegherà al server tramite il primo degli indirizzi della lista che riuscirà a raggiungere, dall'alto verso il basso", "face_unassigned": "Non assegnata", - "failed": "Failed", + "failed": "Fallito", "failed_to_load_assets": "Impossibile caricare gli asset", - "failed_to_load_folder": "Failed to load folder", + "failed_to_load_folder": "Impossibile caricare la cartella", "favorite": "Preferito", "favorite_or_unfavorite_photo": "Aggiungi o rimuovi foto da preferiti", "favorites": "Preferiti", @@ -992,10 +996,11 @@ "filetype": "Tipo file", "filter": "Filtro", "filter_people": "Filtra persone", + "filter_places": "Filtra luoghi", "find_them_fast": "Trovale velocemente con la ricerca", "fix_incorrect_match": "Correggi corrispondenza errata", - "folder": "Folder", - "folder_not_found": "Folder not found", + "folder": "Cartella", + "folder_not_found": "Cartella non trovata", "folders": "Cartelle", "folders_feature_description": "Navigare la visualizzazione a cartelle per le foto e i video sul file system", "forward": "Avanti", @@ -1016,12 +1021,12 @@ "haptic_feedback_switch": "Abilita feedback aptico", "haptic_feedback_title": "Feedback aptico", "has_quota": "Ha limite", - "header_settings_add_header_tip": "Add Header", - "header_settings_field_validator_msg": "Value cannot be empty", - "header_settings_header_name_input": "Header name", - "header_settings_header_value_input": "Header value", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", + "header_settings_add_header_tip": "Aggiungi Header", + "header_settings_field_validator_msg": "Il valore non può essere vuoto", + "header_settings_header_name_input": "Nome header", + "header_settings_header_value_input": "Valore header", + "headers_settings_tile_subtitle": "Definisci gli header per i proxy che l'app deve inviare con ogni richiesta di rete", + "headers_settings_tile_title": "Header proxy personalizzati", "hi_user": "Ciao {name} ({email})", "hide_all_people": "Nascondi tutte le persone", "hide_gallery": "Nascondi galleria", @@ -1031,7 +1036,7 @@ "hide_unnamed_people": "Nascondi persone senza nome", "home_page_add_to_album_conflicts": "Aggiunti {added} elementi all'album {album}. {failed} elementi erano già presenti nell'album.", "home_page_add_to_album_err_local": "Non puoi aggiungere in album risorse non ancora caricate, azione ignorata", - "home_page_add_to_album_success": "Aggiunti {added} elementi all'album {album}", + "home_page_add_to_album_success": "Aggiunti {added} elementi all'album {album}.", "home_page_album_err_partner": "Non puoi aggiungere risorse del partner a un album, azione ignorata", "home_page_archive_err_local": "Non puoi archiviare immagini non ancora caricate, azione ignorata", "home_page_archive_err_partner": "Non puoi archiviare risorse del partner, azione ignorata", @@ -1040,7 +1045,7 @@ "home_page_delete_remote_err_local": "Risorse locali presenti nella selezione della eliminazione remota, azione ignorata", "home_page_favorite_err_local": "Non puoi aggiungere tra i preferiti delle risorse non ancora caricate, azione ignorata", "home_page_favorite_err_partner": "Non puoi mettere le risorse del partner nei preferiti, azione ignorata", - "home_page_first_time_notice": "Se è la prima volta che utilizzi l'app, assicurati di scegliere uno o più album di backup, in modo che la timeline possa popolare le foto e i video presenti negli album.", + "home_page_first_time_notice": "Se è la prima volta che utilizzi l'app, assicurati di scegliere uno o più album di backup, in modo che la timeline possa popolare le foto e i video presenti negli album", "home_page_share_err_local": "Non puoi condividere una risorsa locale tramite link, azione ignorata", "home_page_upload_err_limit": "Puoi caricare al massimo 30 file per volta, ignora quelli in eccesso", "host": "Host", @@ -1059,7 +1064,7 @@ "image_alt_text_date_place_3_people": "{isVideo, select, true {Video girato} other {Foto scattata}} a {city}, {country} con {person1}, {person2}, e {person3} il giorno {date}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video girato} other {Foto scattata}} a {city}, {country} con {person1}, {person2} e {additionalCount, number} altre persone il {date}", "image_saved_successfully": "Immagine salvata", - "image_viewer_page_state_provider_download_started": "Download Started", + "image_viewer_page_state_provider_download_started": "Download iniziato", "image_viewer_page_state_provider_download_success": "Download con successo", "image_viewer_page_state_provider_share_error": "Errore di condivisione", "immich_logo": "Logo Immich", @@ -1080,8 +1085,8 @@ "night_at_midnight": "Ogni notte a mezzanotte", "night_at_twoam": "Ogni notte alle 2" }, - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", + "invalid_date": "Data invalida", + "invalid_date_format": "Formato data invalido", "invite_people": "Invita Persone", "invite_to_album": "Invita nell'album", "items_count": "{count, plural, one {# elemento} other {# elementi}}", @@ -1132,24 +1137,24 @@ "logged_out_device": "Disconnesso dal dispositivo", "login": "Login", "login_disabled": "L'accesso è stato disattivato", - "login_form_api_exception": "API error, per favore ricontrolli URL del server e riprovi", + "login_form_api_exception": "API error, per favore ricontrolli URL del server e riprovi.", "login_form_back_button_text": "Indietro", "login_form_email_hint": "tuaemail@email.com", "login_form_endpoint_hint": "http://ip-del-tuo-server:port", - "login_form_endpoint_url": "Server Endpoint URL", + "login_form_endpoint_url": "URL dell'Endpoint del Server", "login_form_err_http": "Per favore specificare http:// o https://", "login_form_err_invalid_email": "Email non valida", "login_form_err_invalid_url": "URL invalido", - "login_form_err_leading_whitespace": "Whitespace all'inizio ", + "login_form_err_leading_whitespace": "Whitespace all'inizio", "login_form_err_trailing_whitespace": "Whitespace alla fine", "login_form_failed_get_oauth_server_config": "Errore di login usando OAuth, controlla l'URL del server", "login_form_failed_get_oauth_server_disable": "OAuth non è disponibile su questo server", "login_form_failed_login": "Errore nel login, controlla URL del server e le credenziali (email e password)", "login_form_handshake_exception": "Si è verificata un'eccezione di handshake con il server. Abilita il supporto del certificato self-signed nelle impostazioni se si utilizza questo tipo di certificato.", - "login_form_password_hint": "password ", - "login_form_save_login": "Rimani connesso ", - "login_form_server_empty": "Inserisci URL del server", - "login_form_server_error": "Non è possibile connettersi al server", + "login_form_password_hint": "password", + "login_form_save_login": "Rimani connesso", + "login_form_server_empty": "Inserisci URL del server.", + "login_form_server_error": "Non è possibile connettersi al server.", "login_has_been_disabled": "Il login è stato disabilitato.", "login_password_changed_error": "C'è stato un errore durante l'aggiornamento della password", "login_password_changed_success": "Password aggiornata con successo", @@ -1202,8 +1207,8 @@ "memories_setting_description": "Gestisci cosa vedi nei tuoi ricordi", "memories_start_over": "Ricomincia", "memories_swipe_to_close": "Scorri sopra per chiudere", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", + "memories_year_ago": "Una anno fa", + "memories_years_ago": "{} anni fa", "memory": "Memoria", "memory_lane_title": "Sentiero dei Ricordi {title}", "menu": "Menu", @@ -1257,11 +1262,11 @@ "no_results_description": "Prova ad usare un sinonimo oppure una parola chiave più generica", "no_shared_albums_message": "Crea un album per condividere foto e video con le persone nella tua rete", "not_in_any_album": "In nessun album", - "not_selected": "Not selected", + "not_selected": "Non selezionato", "note_apply_storage_label_to_previously_uploaded assets": "Nota: Per aggiungere l'etichetta dell'archiviazione agli asset caricati in precedenza, esegui", "notes": "Note", - "notification_permission_dialog_content": "Per attivare le notifiche, vai alle Impostazioni e seleziona concedi", - "notification_permission_list_tile_content": "Concedi i permessi per attivare le notifiche", + "notification_permission_dialog_content": "Per attivare le notifiche, vai alle Impostazioni e seleziona concedi.", + "notification_permission_list_tile_content": "Concedi i permessi per attivare le notifiche.", "notification_permission_list_tile_enable_button": "Attiva notifiche", "notification_permission_list_tile_title": "Permessi delle Notifiche", "notification_toggle_setting_description": "Attiva le notifiche via email", @@ -1282,6 +1287,7 @@ "onboarding_welcome_user": "Benvenuto, {user}", "online": "Online", "only_favorites": "Solo preferiti", + "open": "Apri", "open_in_map_view": "Apri nella visualizzazione mappa", "open_in_openstreetmap": "Apri su OpenStreetMap", "open_the_search_filters": "Apri filtri di ricerca", @@ -1301,9 +1307,9 @@ "partner_list_user_photos": "Foto di {user}", "partner_list_view_all": "Mostra tutto", "partner_page_empty_message": "Le tue foto non sono ancora condivise con alcun partner.", - "partner_page_no_more_users": "Nessun altro utente da aggiungere.", - "partner_page_partner_add_failed": "Aggiunta del partner non riuscita.", - "partner_page_select_partner": "Seleziona partner.", + "partner_page_no_more_users": "Nessun altro utente da aggiungere", + "partner_page_partner_add_failed": "Aggiunta del partner non riuscita", + "partner_page_select_partner": "Seleziona partner", "partner_page_shared_to_title": "Condividi con", "partner_page_stop_sharing_content": "{} non sarà più in grado di accedere alle tue foto.", "partner_sharing": "Condivisione Compagno", @@ -1338,10 +1344,10 @@ "permission_onboarding_continue_anyway": "Continua lo stesso", "permission_onboarding_get_started": "Inizia", "permission_onboarding_go_to_settings": "Vai a Impostazioni", - "permission_onboarding_permission_denied": "Permessi negati. Per usare Immich concedi i permessi ai video e foto dalle impostazioni", - "permission_onboarding_permission_granted": "Concessi i permessi! Ora sei tutto apposto", + "permission_onboarding_permission_denied": "Permessi negati. Per usare Immich concedi i permessi ai video e foto dalle impostazioni.", + "permission_onboarding_permission_granted": "Concessi i permessi! Ora sei tutto apposto.", "permission_onboarding_permission_limited": "Permessi limitati. Per consentire a Immich di gestire e fare i backup di tutta la galleria, concedi i permessi Foto e Video dalle Impostazioni.", - "permission_onboarding_request": "Immich richiede i permessi per vedere le tue foto e video", + "permission_onboarding_request": "Immich richiede i permessi per vedere le tue foto e video.", "person": "Persona", "person_birthdate": "Nato il {date}", "person_hidden": "{name}{hidden, select, true { (nascosto)} other {}}", @@ -1368,7 +1374,7 @@ "previous_or_next_photo": "Precedente o prossima foto", "primary": "Primario", "privacy": "Privacy", - "profile_drawer_app_logs": "Logs", + "profile_drawer_app_logs": "Registri", "profile_drawer_client_out_of_date_major": "L'applicazione non è aggiornata. Per favore aggiorna all'ultima versione principale.", "profile_drawer_client_out_of_date_minor": "L'applicazione non è aggiornata. Per favore aggiorna all'ultima versione minore.", "profile_drawer_client_server_up_to_date": "Client e server sono aggiornati", @@ -1487,7 +1493,7 @@ "saved_profile": "Profilo salvato", "saved_settings": "Impostazioni salvate", "say_something": "Dici qualcosa", - "scaffold_body_error_occurred": "Si è verificato un errore.", + "scaffold_body_error_occurred": "Si è verificato un errore", "scan_all_libraries": "Analizza tutte le librerie", "scan_library": "Scansione", "scan_settings": "Impostazioni Analisi", @@ -1504,24 +1510,24 @@ "search_city": "Cerca città...", "search_country": "Cerca paese...", "search_filter_apply": "Applica filtro", - "search_filter_camera_title": "Select camera type", + "search_filter_camera_title": "Seleziona il tipo di camera", "search_filter_date": "Date", "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", + "search_filter_date_title": "Scegli un range di date", "search_filter_display_option_not_in_album": "Non nell'album", - "search_filter_display_options": "Display Options", - "search_filter_filename": "Search by file name", - "search_filter_location": "Location", - "search_filter_location_title": "Select location", + "search_filter_display_options": "Opzioni di Visualizzazione", + "search_filter_filename": "Cerca per nome file", + "search_filter_location": "Posizione", + "search_filter_location_title": "Seleziona posizione", "search_filter_media_type": "Media Type", "search_filter_media_type_title": "Seleziona il tipo di media", "search_filter_people_title": "Seleziona persone", "search_for": "Cerca per", "search_for_existing_person": "Cerca per persona esistente", - "search_no_more_result": "No more results", + "search_no_more_result": "Non ci sono altri risultati", "search_no_people": "Nessuna persona", "search_no_people_named": "Nessuna persona chiamate \"{name}\"", - "search_no_result": "No results found, try a different search term or combination", + "search_no_result": "Nessun risultato trovato, prova con un termine o combinazione diversi", "search_options": "Opzioni Ricerca", "search_page_categories": "Categoria", "search_page_motion_photos": "Foto in movimento", @@ -1532,16 +1538,16 @@ "search_page_selfies": "Selfie", "search_page_things": "Oggetti", "search_page_view_all_button": "Guarda tutto", - "search_page_your_activity": "Tua attività ", + "search_page_your_activity": "Le tua attività", "search_page_your_map": "La tua mappa", "search_people": "Cerca persone", "search_places": "Cerca luoghi", "search_rating": "Cerca per valutazione...", - "search_result_page_new_search_hint": "Nuova ricerca ", + "search_result_page_new_search_hint": "Nuova ricerca", "search_settings": "Cerca Impostazioni", "search_state": "Cerca stato...", - "search_suggestion_list_smart_search_hint_1": "\nRicerca Smart è attiva di default, per usare la ricerca con i metadata usare la seguente sintassi", - "search_suggestion_list_smart_search_hint_2": "m:your-search-term", + "search_suggestion_list_smart_search_hint_1": "Ricerca Smart è attiva di default, per usare la ricerca con i metadata usare la seguente sintassi ", + "search_suggestion_list_smart_search_hint_2": "m:termine-di-ricerca", "search_tags": "Cerca tag...", "search_timezone": "Cerca fuso orario...", "search_type": "Cerca tipo", @@ -1562,7 +1568,7 @@ "select_new_face": "Seleziona nuovo volto", "select_photos": "Seleziona foto", "select_trash_all": "Seleziona cestina tutto", - "select_user_for_sharing_page_err_album": "Impossibile nel creare l'album ", + "select_user_for_sharing_page_err_album": "Impossibile nel creare l'album", "selected": "Selezionato", "selected_count": "{count, plural, one {# selezionato} other {# selezionati}}", "send_message": "Manda messaggio", @@ -1584,9 +1590,9 @@ "setting_image_viewer_help": "Il visualizzatore dettagliato carica una piccola thumbnail per prima, per poi caricare un immagine di media grandezza (se abilitato). Ed infine carica l'originale (se abilitato).", "setting_image_viewer_original_subtitle": "Abilita per caricare l'immagine originale a risoluzione massima (grande!). Disabilita per ridurre l'utilizzo di banda (sia sul network che nella cache del dispositivo).", "setting_image_viewer_original_title": "Carica l'immagine originale", - "setting_image_viewer_preview_subtitle": "Abilita per caricare un'immagine a risoluzione media.\nDisabilita per caricare direttamente l'immagine originale o usare la thumbnail.", + "setting_image_viewer_preview_subtitle": "Abilita per caricare un'immagine a risoluzione media. Disabilita per caricare direttamente l'immagine originale o usare la thumbnail.", "setting_image_viewer_preview_title": "Carica immagine di anteprima", - "setting_image_viewer_title": "Images", + "setting_image_viewer_title": "Immagini", "setting_languages_apply": "Applica", "setting_languages_subtitle": "Cambia la lingua dell'app", "setting_languages_title": "Lingue", @@ -1609,7 +1615,7 @@ "settings_saved": "Impostazioni salvate", "share": "Condivisione", "share_add_photos": "Aggiungi foto", - "share_assets_selected": "{} selected", + "share_assets_selected": "{} selezionati", "share_dialog_preparing": "Preparo…", "shared": "Condivisi", "shared_album_activities_input_disable": "I commenti sono disabilitati", @@ -1623,7 +1629,7 @@ "shared_by_user": "Condiviso da {user}", "shared_by_you": "Condiviso da te", "shared_from_partner": "Foto da {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", + "shared_intent_upload_button_progress_text": "{} / {} Inviati", "shared_link_app_bar_title": "Link condivisi", "shared_link_clipboard_copied_massage": "Copiato negli appunti", "shared_link_clipboard_text": "Link: {}\nPassword: {}", @@ -1635,8 +1641,8 @@ "shared_link_edit_expire_after_option_hours": "{} ore", "shared_link_edit_expire_after_option_minute": "1 minuto", "shared_link_edit_expire_after_option_minutes": "{} minuti", - "shared_link_edit_expire_after_option_months": "{} months", - "shared_link_edit_expire_after_option_year": "{} year", + "shared_link_edit_expire_after_option_months": "{} mesi", + "shared_link_edit_expire_after_option_year": "{} anno", "shared_link_edit_password_hint": "Inserire la password di condivisione", "shared_link_edit_submit_button": "Aggiorna link", "shared_link_error_server_url_fetch": "Non è possibile trovare l'indirizzo del server", @@ -1649,7 +1655,7 @@ "shared_link_expires_never": "Scadenza ∞", "shared_link_expires_second": "Scade tra {} secondo", "shared_link_expires_seconds": "Scade tra {} secondi", - "shared_link_individual_shared": "Individual shared", + "shared_link_individual_shared": "Condiviso individualmente", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Gestisci link condivisi", "shared_link_options": "Opzioni link condiviso", @@ -1732,9 +1738,9 @@ "support_third_party_description": "La tua installazione di Immich è stata costruita da terze parti. I problemi che riscontri potrebbero essere causati da altri pacchetti, quindi ti preghiamo di sollevare il problema in prima istanza utilizzando i link sottostanti.", "swap_merge_direction": "Scambia direzione di unione", "sync": "Sincronizza", - "sync_albums": "Sync albums", - "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", + "sync_albums": "Sincronizza album", + "sync_albums_manual_subtitle": "Sincronizza tutti i video e le foto caricate sull'album di backup selezionato", + "sync_upload_album_setting_subtitle": "Crea e carica le tue foto e video sull'album selezionato in Immich", "tag": "Tag", "tag_assets": "Tagga risorse", "tag_created": "Tag creata: {tag}", @@ -1749,12 +1755,12 @@ "theme_selection": "Selezione tema", "theme_selection_description": "Imposta automaticamente il tema chiaro o scuro in base all'impostazione del tuo browser", "theme_setting_asset_list_storage_indicator_title": "Mostra indicatore dello storage nei titoli dei contenuti", - "theme_setting_asset_list_tiles_per_row_title": "Numero di contenuti per riga ({})", - "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", - "theme_setting_colorful_interface_title": "Colorful interface", + "theme_setting_asset_list_tiles_per_row_title": "Numero di elementi per riga ({})", + "theme_setting_colorful_interface_subtitle": "Applica il colore primario alle superfici di sfondo.", + "theme_setting_colorful_interface_title": "Interfaccia colorata", "theme_setting_image_viewer_quality_subtitle": "Cambia la qualità del dettaglio dell'immagine", "theme_setting_image_viewer_quality_title": "Qualità immagine", - "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_subtitle": "Scegli un colore per le azioni primarie e accentate.", "theme_setting_primary_color_title": "Colore primario", "theme_setting_system_primary_color_title": "Usa colori di sistema", "theme_setting_system_theme_switch": "Automatico (Segue le impostazioni di sistema)", @@ -1780,7 +1786,7 @@ "trash_all": "Cestina Tutto", "trash_count": "Cancella {count, number}", "trash_delete_asset": "Cestina/Cancella Asset", - "trash_emptied": "Emptied trash", + "trash_emptied": "Cestino svuotato", "trash_no_results_message": "Le foto cestinate saranno mostrate qui.", "trash_page_delete_all": "Elimina tutti", "trash_page_empty_trash_dialog_content": "Vuoi eliminare gli elementi nel cestino? Questi elementi saranno eliminati definitivamente da Immich", @@ -1826,8 +1832,8 @@ "upload_status_errors": "Errori", "upload_status_uploaded": "Caricato", "upload_success": "Caricamento completato con successo, aggiorna la pagina per vedere i nuovi asset caricati.", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", + "upload_to_immich": "Invio ad Immich ({})", + "uploading": "Caricamento", "url": "URL", "usage": "Utilizzo", "use_current_connection": "usa la connessione attuale", @@ -1853,7 +1859,7 @@ "version_announcement_overlay_release_notes": "note di rilascio", "version_announcement_overlay_text_1": "Ciao, c'è una nuova versione di", "version_announcement_overlay_text_2": "per favore prenditi il tuo tempo per visitare le ", - "version_announcement_overlay_text_3": " e verifica che il tuo docker-compose e il file .env siano aggiornati per impedire qualsiasi errore di configurazione, specialmente se utilizzate WatchTower o altri strumenti per l'aggiornamento automatico dell'applicativo", + "version_announcement_overlay_text_3": " e verifica che il tuo docker-compose e il file .env siano aggiornati per impedire qualsiasi errore di configurazione, specialmente se utilizzate WatchTower o altri strumenti per l'aggiornamento automatico dell'applicativo.", "version_announcement_overlay_title": "Nuova versione del server disponibile 🎉", "version_history": "Storico delle Versioni", "version_history_item": "Versione installata {version} il {date}", diff --git a/i18n/ja.json b/i18n/ja.json index 39496cc7f5..eb6b355615 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -496,7 +496,7 @@ "back_close_deselect": "戻る、閉じる、選択解除", "background_location_permission": "バックグラウンド位置情報アクセス", "background_location_permission_content": "正常にWi-Fiの名前(SSID)を獲得するにはアプリが常に詳細な位置情報にアクセスできる必要があります", - "backup_album_selection_page_albums_device": "端末上のアルバム数: {} ", + "backup_album_selection_page_albums_device": "端末上のアルバム数: {}", "backup_album_selection_page_albums_tap": "タップで選択、ダブルタップで除外", "backup_album_selection_page_assets_scatter": "アルバムを選択・除外してバックアップする写真を選ぶ (同じ写真が複数のアルバムに登録されていることがあるため)", "backup_album_selection_page_select_albums": "アルバムを選択", @@ -505,7 +505,7 @@ "backup_all": "すべて", "backup_background_service_backup_failed_message": "アップロードに失敗しました。リトライ中", "backup_background_service_connection_failed_message": "サーバーに接続できません。リトライ中", - "backup_background_service_current_upload_notification": " {}をアップロード中", + "backup_background_service_current_upload_notification": "{}をアップロード中", "backup_background_service_default_notification": "新しい写真を確認中", "backup_background_service_error_title": "バックアップエラー", "backup_background_service_in_progress_notification": "バックアップ中", @@ -534,7 +534,7 @@ "backup_controller_page_desc_backup": "アプリを開いているときに写真と動画をバックアップします", "backup_controller_page_excluded": "除外中のアルバム:", "backup_controller_page_failed": "失敗: ({})", - "backup_controller_page_filename": "ファイル名: {} [{}] ", + "backup_controller_page_filename": "ファイル名: {} [{}]", "backup_controller_page_id": "ID: {}", "backup_controller_page_info": "バックアップ情報", "backup_controller_page_none_selected": "なし", @@ -575,7 +575,7 @@ "cache_settings_duplicated_assets_clear_button": "クリア", "cache_settings_duplicated_assets_subtitle": "サーバーにアップロード済みと認識された写真や動画の数", "cache_settings_duplicated_assets_title": "{}項目の重複", - "cache_settings_image_cache_size": "キャッシュのサイズ ({}枚) ", + "cache_settings_image_cache_size": "キャッシュのサイズ ({}枚)", "cache_settings_statistics_album": "ライブラリのサムネイル", "cache_settings_statistics_assets": "{}枚 ({}枚中)", "cache_settings_statistics_full": "フル画像", diff --git a/i18n/ka.json b/i18n/ka.json index a521539d11..b07dcfe6fa 100644 --- a/i18n/ka.json +++ b/i18n/ka.json @@ -30,11 +30,11 @@ "authentication_settings_disable_all": "ნამდვილად გინდა ავტორიზაციის ყველა მეთოდის გამორთვა? ავტორიზაციას ვეღარანაირად შეძლებ.", "authentication_settings_reenable": "რეაქტივაციისთვის, გამოიყენე სერვერის ბრძანება.", "background_task_job": "ფონური დავალებები", - "backup_database": "შექმენი სარეზერვო ასლი", - "backup_database_enable_description": "ჩართე სარეზერვო ასლების ფუნქცია", - "backup_keep_last_amount": "შესანახი სარეზერვო ასლების რაოდენობა", - "backup_settings": "სარეზერვო ასლების პარამეტრები", - "backup_settings_description": "მონაცემთა ბაზის სარეზერვო ასლების პარამეტრების მართვა", + "backup_database": "ბაზის დამპის შექმნა", + "backup_database_enable_description": "ბაზის დამპების ჩართვა", + "backup_keep_last_amount": "წინა დამპების შესანარჩუნებელი რაოდენობა", + "backup_settings": "მონაცემთა ბაზის დამპის მორგება", + "backup_settings_description": "მონაცემთა ბაზის პარამეტრების ამრთვა. შენიშვნა: ამ დავალებების მონიტორინგი არ ხდება და თქვენ არ მოგივათ შეტყობინება, თუ ის ჩავარდება.", "check_all": "შეამოწმე ყველა", "cleanup": "გასუფთავება", "confirm_delete_library": "ნამდვილად გინდა {library} ბიბლიოთეკის წაშლა?", diff --git a/i18n/kk.json b/i18n/kk.json index 0967ef424b..e3a1f51c3e 100644 --- a/i18n/kk.json +++ b/i18n/kk.json @@ -1 +1,16 @@ -{} +{ + "add_photos": "суреттерді қосу", + "add_to": "қосу…", + "add_to_album": "альбомға қосу", + "add_to_album_bottom_sheet_added": "{album}'ға қосылған", + "add_to_album_bottom_sheet_already_exists": "Онсыз да {album} болған", + "add_to_shared_album": "бөліскен альбомға қосу", + "add_url": "URL таңдау", + "added_to_archive": "Архивке жіберілген", + "added_to_favorites": "таңдаулыларға қосылған", + "admin": { + "check_all": "Бәрін тексеріп алу", + "create_job": "Жұмысты бастау" + }, + "zoom_image": "суретті үлкейту" +} diff --git a/i18n/ko.json b/i18n/ko.json index def2ad2a5e..1a129eb72a 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -14,7 +14,7 @@ "add_a_location": "위치 추가", "add_a_name": "이름 추가", "add_a_title": "제목 추가", - "add_endpoint": "Add endpoint", + "add_endpoint": "엔드포인트 추가", "add_exclusion_pattern": "제외 규칙 추가", "add_import_path": "가져올 경로 추가", "add_location": "위치 추가", @@ -25,29 +25,29 @@ "add_to": "앨범에 추가…", "add_to_album": "앨범에 추가", "add_to_album_bottom_sheet_added": "{album}에 추가되었습니다.", - "add_to_album_bottom_sheet_already_exists": "{album}에 이미 존재하는 항목입니다.", + "add_to_album_bottom_sheet_already_exists": "{album}에 이미 존재합니다.", "add_to_shared_album": "공유 앨범에 추가", "add_url": "URL 추가", "added_to_archive": "보관함에 추가되었습니다.", "added_to_favorites": "즐겨찾기에 추가되었습니다.", - "added_to_favorites_count": "즐겨찾기에 항목 {count, number}개 추가됨", + "added_to_favorites_count": "즐겨찾기에 {count, number}개 추가됨", "admin": { "add_exclusion_pattern_description": "규칙에 *, ** 및 ? 를 사용할 수 있습니다. 이름이 \"Raw\"인 디렉터리의 모든 파일을 제외하려면 \"**/Raw/**\"를, \".tif\"로 끝나는 모든 파일을 제외하려면 \"**/*.tif\"를 사용하고, 절대 경로의 경우 \"/path/to/ignore/**\"와 같은 방식으로 사용합니다.", - "asset_offline_description": "외부 라이브러리에 포함된 이 항목을 디스크에서 더이상 찾을 수 없어 휴지통으로 이동되었습니다. 파일이 라이브러리 내에서 이동된 경우 타임라인에서 새로 연결된 항목을 확인하세요. 이 항목을 복원하려면 아래 파일 경로에 Immich가 접근할 수 있는지 확인하고 라이브러리 스캔을 진행하세요.", + "asset_offline_description": "외부 라이브러리에 포함된 이 항목을 디스크에서 더이상 찾을 수 없어 휴지통으로 이동되었습니다. 파일이 라이브러리 내에서 이동된 경우 타임라인에서 새로 연결된 항목을 확인하세요. 항목을 복원하려면 아래의 파일 경로에 Immich가 접근할 수 있는지 확인하고 라이브러리 스캔을 진행하세요.", "authentication_settings": "인증 설정", "authentication_settings_description": "비밀번호, OAuth 및 기타 인증 설정 관리", - "authentication_settings_disable_all": "로그인 기능을 모두 비활성화하시겠습니까? 로그인하지 않아도 서버에 접근할 수 있습니다.", - "authentication_settings_reenable": "다시 활성화하려면 서버 커맨드를 사용하세요.", + "authentication_settings_disable_all": "로그인 수단을 모두 비활성화하시겠습니까? 로그인이 완전히 비활성화됩니다.", + "authentication_settings_reenable": "다시 활성화하려면 서버 명령어를 사용하세요.", "background_task_job": "백그라운드 작업", - "backup_database": "데이터베이스 백업", - "backup_database_enable_description": "데이터베이스 백업 활성화", - "backup_keep_last_amount": "보관할 백업의 개수", - "backup_settings": "백업 설정", - "backup_settings_description": "데이터베이스 백업 설정 관리", + "backup_database": "데이터베이스 덤프 생성", + "backup_database_enable_description": "데이터베이스 덤프 활성화", + "backup_keep_last_amount": "보관할 이전 덤프의 수", + "backup_settings": "데이터베이스 덤프 설정", + "backup_settings_description": "데이터베이스 덤프 설정을 관리합니다. 참고: 이 작업들은 모니터링되지 않으며, 실패 시 알림을 받지 않습니다.", "check_all": "모두 확인", "cleanup": "정리", "cleared_jobs": "작업 중단: {job}", - "config_set_by_file": "현재 설정은 구성 파일에 의해 관리됩니다.", + "config_set_by_file": "현재 구성은 설정 파일을 통해 지정되어 있습니다.", "confirm_delete_library": "{library} 라이브러리를 삭제하시겠습니까?", "confirm_delete_library_assets": "이 라이브러리를 삭제하시겠습니까? Immich에서 항목 {count, plural, one {#개} other {#개}}가 삭제되며 되돌릴 수 없습니다. 원본 파일은 삭제되지 않습니다.", "confirm_email_below": "계속 진행하려면 아래에 \"{email}\" 입력", @@ -59,40 +59,40 @@ "cron_expression_presets": "Cron 표현식 사전 설정", "disable_login": "로그인 비활성화", "duplicate_detection_job_description": "기계 학습을 통해 유사한 이미지를 감지합니다. 스마트 검색이 활성화되어 있어야 합니다.", - "exclusion_pattern_description": "제외 규칙을 사용하여 라이브러리 스캔 시 특정 파일과 폴더를 제외할 수 있습니다. 폴더에 원하지 않는 파일(RAW 파일 등)이 존재하는 경우 유용합니다.", + "exclusion_pattern_description": "제외 규칙을 사용하여 특정 파일과 폴더를 라이브러리 스캔에서 제외할 수 있습니다. 가져오기 원하지 않는 파일(RAW 파일 등)이 폴더에 존재하는 경우 유용합니다.", "external_library_created_at": "외부 라이브러리 ({date}에 생성됨)", "external_library_management": "외부 라이브러리 관리", "face_detection": "얼굴 감지", - "face_detection_description": "기계 학습을 통해 항목에 존재하는 얼굴을 감지합니다. 동영상의 경우 섬네일만 사용합니다. \"새로고침\"은 이미 처리된 항목을 포함한 모든 항목을 다시 처리합니다. \"초기화\"는 모든 얼굴 데이터를 삭제합니다. \"누락\"은 처리되지 않은 항목을 대기열에 추가합니다. 얼굴 감지 작업이 완료되면 얼굴 인식 작업이 진행되어 감지된 얼굴을 기존 인물이나 새 인물로 그룹화합니다.", - "facial_recognition_job_description": "감지된 얼굴을 인물로 그룹화합니다. 이 작업은 얼굴 감지 작업이 완료된 후 진행됩니다. \"초기화\"는 모든 얼굴의 그룹화를 다시 진행합니다. \"누락\"은 그룹화되지 않은 얼굴을 대기열에 추가합니다.", + "face_detection_description": "기계 학습을 통해 항목에서 얼굴을 감지합니다. 동영상의 경우 섬네일만 분석에 사용됩니다. \"새로고침\"은 모든 항목을 (재)처리하며, \"초기화\"는 현재 모든 얼굴 데이터를 추가로 삭제합니다. \"누락됨\"은 아직 처리되지 않은 항목을 대기열에 추가합니다. 얼굴 감지가 완료되면 감지된 얼굴들은 얼굴 인식 단계로 넘어가며, 기존 인물이나 새로운 인물로 그룹화됩니다.", + "facial_recognition_job_description": "감지된 얼굴을 인물별로 그룹화합니다. 이 작업은 얼굴 감지 작업이 완료된 후 진행됩니다. \"초기화\"는 모든 얼굴의 그룹화를 다시 진행합니다. \"누락\"은 그룹화되지 않은 얼굴을 대기열에 추가합니다.", "failed_job_command": "{job} 작업에서 {command} 실패", - "force_delete_user_warning": "경고: 사용자 및 사용자가 업로드한 모든 항목이 즉시 삭제됩니다. 이 작업은 되돌릴 수 없으며 파일을 복구할 수 없습니다.", - "forcing_refresh_library_files": "라이브러리의 모든 파일을 다시 스캔하는 중...", + "force_delete_user_warning": "경고: 이 작업은 해당 사용자와 그 사용자가 소유한 모든 항목을 즉시 삭제합니다. 이 작업은 되돌릴 수 없으며, 삭제된 파일은 복구할 수 없습니다.", + "forcing_refresh_library_files": "모든 라이브러리 파일 강제 새로고침 중...", "image_format": "형식", - "image_format_description": "WebP는 JPEG보다 파일 크기가 작지만 변환에 더 많은 시간이 소요됩니다.", - "image_fullsize_description": "메타데이터가 제거된 풀사이즈 이미지 (확대 시 사용)", - "image_fullsize_enabled": "풀사이즈 이미지 생성 활성화", - "image_fullsize_enabled_description": "웹 친화적이지 않은 형식의 경우 풀사이즈 이미지를 생성합니다. '임베드된 미리보기 선호'를 활성화하면 변환 없이 임베드된 미리보기가 바로 사용됩니다. JPEG와 같은 웹 친화적인 형식에는 영향을 미치지 않습니다.", - "image_fullsize_quality_description": "풀사이즈 이미지 품질은 1~100입니다. 높을수록 좋지만 파일이 커집니다.", - "image_fullsize_title": "풀사이즈 이미지 설정", - "image_prefer_embedded_preview": "포함된 미리 보기 선호", - "image_prefer_embedded_preview_setting_description": "가능한 경우 이미지 처리 시 RAW 사진에 포함된 미리 보기를 사용합니다. 포함된 미리 보기는 카메라에서 생성된 것으로 카메라마다 품질이 다릅니다. 일부 이미지의 경우 더 정확한 색상이 표현될 수 있지만 반대로 더 많은 아티팩트가 있을 수도 있습니다.", - "image_prefer_wide_gamut": "넓은 색 영역 선호", - "image_prefer_wide_gamut_setting_description": "섬네일 이미지에 Display P3를 사용합니다. 많은 색상을 표현할 수 있어 더 정확한 표현이 가능하지만, 오래된 브라우저를 사용하는 경우 이미지가 다르게 보일 수 있습니다. 색상 왜곡을 방지하기 위해 sRGB 이미지는 이 설정이 적용되지 않습니다.", - "image_preview_description": "메타데이터를 제거한 중간 크기의 이미지, 단일 항목을 보는 경우 및 기계 학습에 사용됨", - "image_preview_quality_description": "1부터 100 사이의 미리보기 품질. 값이 높을수록 좋지만 파일 크기가 커져 앱의 반응성이 떨어질 수 있으며, 값이 낮으면 기계 학습의 품질이 떨어질 수 있습니다.", + "image_format_description": "WebP는 JPEG보다 파일 크기가 작지만, 인코딩에 더 많은 시간이 소요됩니다.", + "image_fullsize_description": "확대 보기 시 사용되는, 메타데이터가 없는 전체 크기 이미지", + "image_fullsize_enabled": "전체 크기 이미지 생성 활성화", + "image_fullsize_enabled_description": "웹에 적합하지 않은 형식인 경우 전체 크기 이미지를 생성합니다. \"내장 미리보기 우선 사용\"이 활성화되어 있으면, 변환 없이 내장된 미리보기를 그대로 사용합니다. JPEG과 같은 웹 친화적인 형식에는 영향을 주지 않습니다.", + "image_fullsize_quality_description": "전체 크기 이미지의 품질 (1~100). 숫자가 높을수록 품질이 좋지만 파일 크기도 커집니다.", + "image_fullsize_title": "전체 크기 이미지 설정", + "image_prefer_embedded_preview": "내장 미리보기 우선 사용", + "image_prefer_embedded_preview_setting_description": "RAW 사진에 포함된 내장 미리보기를 이미지 처리 시 입력으로 사용합니다(사용 가능한 경우에 한함). 이 방식은 일부 이미지에서 더 정확한 색상을 얻을 수 있지만, 미리보기의 품질은 카메라에 따라 다르며 압축으로 인한 품질 저하가 나타날 수 있습니다.", + "image_prefer_wide_gamut": "광색역 우선 사용", + "image_prefer_wide_gamut_setting_description": "섬네일에 Display P3 색역을 사용합니다. 광색역 이미지를 보다 생생하게 표현할 수 있지만, 구형 브라우저나 장치에서는 다르게 보일 수 있습니다. sRGB 이미지의 경우 색상 왜곡을 방지하기 위해 그대로 유지됩니다.", + "image_preview_description": "단일 항목을 보거나 기계 학습에 사용되는, 메타데이터가 제거된 중간 크기 이미지", + "image_preview_quality_description": "미리보기 품질 (1~100). 숫자가 높을수록 품질이 좋아지지만, 파일 크기가 커지고 앱 반응 속도가 느려질 수 있습니다. 너무 낮게 설정하면 기계 학습 품질에 영향을 줄 수 있습니다.", "image_preview_title": "미리보기 설정", "image_quality": "품질", "image_resolution": "해상도", - "image_resolution_description": "해상도가 높을 수록 디테일이 보존되지만 파일이 크고 인코딩이 오래 걸리며 앱 응답성이 떨어질 수 있습니다.", + "image_resolution_description": "해상도가 높을수록 세부 정보가 잘 보존되지만, 인코딩이 느려지고 파일이 커지며 앱 반응 속도가 떨어질 수 있습니다.", "image_settings": "이미지 설정", "image_settings_description": "생성된 이미지의 품질 및 해상도 관리", - "image_thumbnail_description": "메타데이터가 제거된 작은 섬네일 이미지, 타임라인 등 사진을 그룹화하여 보는 경우에 사용됨", - "image_thumbnail_quality_description": "섬네일 품질(1~100). 높을수록 좋지만 파일크기가 커져 앱의 반응성이 떨어질 수 있습니다.", + "image_thumbnail_description": "메타데이터가 제거된 작은 섬네일로, 메인 타임라인 등에서 여러 항목을 볼 때 사용됩니다.", + "image_thumbnail_quality_description": "섬네일 품질 (1~100). 숫자가 높을수록 픔질이 좋지만, 파일 크기가 커지고 앱 반응 속도가 느려질 수 있습니다.", "image_thumbnail_title": "섬네일 설정", "job_concurrency": "{job} 동시성", "job_created": "작업이 생성되었습니다.", - "job_not_concurrency_safe": "이 작업은 동시 실행이 제한됩니다.", + "job_not_concurrency_safe": "이 작업은 동시 실행에 안전하지 않습니다.", "job_settings": "작업 설정", "job_settings_description": "작업 동시성 관리", "job_status": "작업 상태", @@ -100,23 +100,23 @@ "jobs_failed": "{jobCount, plural, other {#개}} 실패", "library_created": "{library} 라이브러리를 생성했습니다.", "library_deleted": "라이브러리가 삭제되었습니다.", - "library_import_path_description": "가져올 폴더를 선택하세요. 선택한 폴더 및 하위 폴더에서 사진과 동영상을 스캔합니다.", + "library_import_path_description": "가져올 폴더를 지정하세요. 해당 폴더와 모든 하위 폴더에서 이미지와 동영상을 스캔합니다.", "library_scanning": "주기적 스캔", - "library_scanning_description": "주기적인 라이브러리 스캔 구성", - "library_scanning_enable_description": "주기적인 라이브러리 스캔 활성화", + "library_scanning_description": "라이브러리 주기적 스캔 구성", + "library_scanning_enable_description": "라이브러리 주기적 스캔 활성화", "library_settings": "외부 라이브러리", "library_settings_description": "외부 라이브러리 설정 관리", - "library_tasks_description": "외부 라이브러리에서 새 자산 및/또는 변경된 자산을 검색합니다", - "library_watching_enable_description": "외부 라이브러리의 파일 변경 감시", - "library_watching_settings": "라이브러리 감시 (실험 기능)", - "library_watching_settings_description": "파일 변겅을 자동으로 감지", - "logging_enable_description": "로그 기록 활성화", - "logging_level_description": "활성화된 경우 사용할 로그 레벨을 선택합니다.", - "logging_settings": "로그 설정", + "library_tasks_description": "외부 라이브러리에서 새 항목 또는 변경된 항목을 스캔", + "library_watching_enable_description": "외부 라이브러리 파일 변경 감시", + "library_watching_settings": "라이브러리 감시 (실험적)", + "library_watching_settings_description": "파일 변경 자동 감지", + "logging_enable_description": "로깅 활성화", + "logging_level_description": "활성화 시 사용할 로그 레벨을 선택합니다.", + "logging_settings": "로깅", "machine_learning_clip_model": "CLIP 모델", - "machine_learning_clip_model_description": "CLIP 모델의 종류는 이곳을 참조하세요. 한국어 등 다국어 검색을 사용하려면 Multilingual CLIP 모델을 선택하세요. 모델을 변경한 후 모든 항목에 대한 스마트 검색 작업을 다시 진행해야 합니다.", - "machine_learning_duplicate_detection": "비슷한 항목 감지", - "machine_learning_duplicate_detection_enabled": "비슷한 항목 감지 활성화", + "machine_learning_clip_model_description": "CLIP 모델의 종류는 이곳을 참조하세요. 한국어를 포함한 다국어 검색을 사용하려면 Multilingual CLIP 모델을 선택하세요. 모델을 변경한 경우, 모든 항목에 대해 '스마트 검색' 작업을 다시 실행해야 합니다.", + "machine_learning_duplicate_detection": "중복 감지", + "machine_learning_duplicate_detection_enabled": "중복 감지 활성화", "machine_learning_duplicate_detection_enabled_description": "비활성화된 경우에도 완전히 동일한 항목은 중복 제거됩니다.", "machine_learning_duplicate_detection_setting_description": "CLIP 임베딩을 사용하여 비슷한 항목 찾기", "machine_learning_enabled": "기계 학습 활성화", @@ -128,7 +128,7 @@ "machine_learning_facial_recognition_setting": "얼굴 인식 활성화", "machine_learning_facial_recognition_setting_description": "비활성화된 경우 이미지에서 얼굴 인식을 진행하지 않으며, 탐색 페이지에 인물 목록이 표시되지 않습니다.", "machine_learning_max_detection_distance": "최대 감지 거리", - "machine_learning_max_detection_distance_description": "두 이미지를 유사한 이미지로 간주하는 거리의 최댓값을 0.001에서 0.1 사이로 설정합니다. 값이 높으면 민감도가 낮아져 유사한 이미지로 감지하는 비율이 높아지나, 잘못된 결과를 보일 수 있습니다.", + "machine_learning_max_detection_distance_description": "두 이미지를 중복으로 간주하기 위한 최대 거릿값 (0.001~0.1). 값이 클수록 더 많은 중복을 감지할 수 있지만, 잘못된 감지가 발생할 수 있습니다.", "machine_learning_max_recognition_distance": "최대 인식 거리", "machine_learning_max_recognition_distance_description": "두 얼굴을 동일인으로 인식하는 거리의 최댓값을 0에서 2 사이로 설정합니다. 이 값을 낮추면 다른 인물을 동일인으로 인식하는 것을 방지할 수 있고, 값을 높이면 동일인을 다른 인물로 인식하는 것을 방지할 수 있습니다. 두 인물을 병합하는 것이 한 인물을 두 명으로 분리하는 것보다 쉬우므로, 가능한 낮은 임계값을 사용하세요.", "machine_learning_min_detection_score": "최소 신뢰도 점수", @@ -157,8 +157,8 @@ "map_settings": "지도", "map_settings_description": "지도 설정 관리", "map_style_description": "지도 테마 style.json URL", - "memory_cleanup_job": "메모리 정리", - "memory_generate_job": "메모리 생성", + "memory_cleanup_job": "추억 정리", + "memory_generate_job": "추억 생성", "metadata_extraction_job": "메타데이터 추출", "metadata_extraction_job_description": "각 항목에서 GPS, 인물 및 해상도 등의 메타데이터 정보 추출", "metadata_faces_import_setting": "얼굴 가져오기 활성화", @@ -224,8 +224,8 @@ "registration": "관리자 계정 생성", "registration_description": "첫 번째로 생성되는 사용자는 관리자 권한을 부여받으며, 관리 및 사용자 생성이 가능합니다.", "repair_all": "모두 수리", - "repair_matched_items": "동일 항목 {count, plural, one {#개} other {#개}}를 확인했습니다.", - "repaired_items": "항목 {count, plural, one {#개} other {#개}}를 수리했습니다.", + "repair_matched_items": "항목 {count, plural, one {#개} other {#개}}가 일치합니다.", + "repaired_items": "항목 {count, plural, one {#개} other {#개}}를 복구했습니다.", "require_password_change_on_login": "첫 로그인 시 비밀번호 변경 요구", "reset_settings_to_default": "설정을 기본값으로 복원", "reset_settings_to_recent_saved": "마지막으로 저장된 설정으로 복원", @@ -251,7 +251,7 @@ "storage_template_hash_verification_enabled_description": "해시 검증을 활성화합니다. 이 설정의 결과를 확실히 이해하지 않는 한 비활성화하지 마세요.", "storage_template_migration": "스토리지 템플릿 마이그레이션", "storage_template_migration_description": "이전에 업로드된 항목에 현재 {template} 적용", - "storage_template_migration_info": "저장소 템플릿은 모든 확장자를 소문자로 변환합니다. 템플릿 변경 사항은 새 자산에만 적용됩니다. 이전에 업로드한 자산에 템플릿을 적용하려면 {job}를 실행하세요.", + "storage_template_migration_info": "스토리지 템플릿은 모든 확장자를 소문자로 변환합니다. 템플릿 변경 사항은 새로 업로드한 항목에만 적용됩니다. 기존에 업로드된 항목에 적용하려면 {job}을 실행하세요.", "storage_template_migration_job": "스토리지 템플릿 마이그레이션 작업", "storage_template_more_details": "이 기능에 대한 자세한 내용은 스토리지 템플릿설명을 참조하세요.", "storage_template_onboarding_description": "이 기능을 활성화하면 사용자 정의 템플릿을 사용하여 파일을 자동으로 정리할 수 있습니다. 안정성 문제로 인해 해당 기능은 기본적으로 비활성화되어 있습니다. 자세한 내용은 문서를 참조하세요.", @@ -263,12 +263,12 @@ "tag_cleanup_job": "태그 정리", "template_email_available_tags": "템플릿에서 다음 변수를 사용할 수 있습니다: {tags}", "template_email_if_empty": "비어 있는 경우 기본 템플릿이 사용됩니다.", - "template_email_invite_album": "앨범 템플릿 초대", + "template_email_invite_album": "앨범 초대 템플릿", "template_email_preview": "미리보기", "template_email_settings": "이메일 템플릿", "template_email_settings_description": "사용자 정의 이메일 템플릿 관리", "template_email_update_album": "앨범 템플릿 업데이트", - "template_email_welcome": "이메일 템플릿에 오신것을 환영합니다", + "template_email_welcome": "웰컴 메일 템플릿", "template_settings": "알림 템플릿", "template_settings_description": "알림을 위한 사용자 지정 템플릿을 관리합니다.", "theme_custom_css_settings": "사용자 정의 CSS", @@ -295,13 +295,13 @@ "transcoding_audio_codec_description": "Opus는 가장 좋은 품질의 옵션이지만 기기 및 소프트웨어가 오래된 경우 호환되지 않을 수 있습니다.", "transcoding_bitrate_description": "최대 비트레이트를 초과하는 동영상 또는 허용되지 않는 형식의 동영상", "transcoding_codecs_learn_more": "여기에서 사용되는 용어에 대한 자세한 내용은 FFmpeg 문서의 H.264 코덱, HEVC 코덱VP9 코덱 항목을 참조하세요.", - "transcoding_constant_quality_mode": "고정 품질 모드", + "transcoding_constant_quality_mode": "Constant quality mode", "transcoding_constant_quality_mode_description": "ICQ는 CQP보다 나은 성능을 보이나 일부 기기의 하드웨어 가속에서 지원되지 않을 수 있습니다. 이 옵션을 설정하면 품질 기반 인코딩 시 지정된 모드를 우선적으로 사용합니다. NVENC에서는 ICQ를 지원하지 않아 이 설정이 적용되지 않습니다.", - "transcoding_constant_rate_factor": "상수 비율 계수(-CRF)", + "transcoding_constant_rate_factor": "Constant rate factor (-crf)", "transcoding_constant_rate_factor_description": "일반적으로 H.264는 23, HEVC는 28, VP9는 31, AV1는 35를 사용합니다. 값이 낮으면 품질이 향상되지만 파일 크기가 증가합니다.", "transcoding_disabled_description": "동영상을 트랜스코딩하지 않음. 일부 기기에서 재생이 불가능할 수 있습니다.", "transcoding_encoding_options": "인코딩 옵션", - "transcoding_encoding_options_description": "인코딩된 동영상의 코덱, 해상도, 품질 및 기타 옵션을 설정합니다", + "transcoding_encoding_options_description": "인코딩된 동영상의 코덱, 해상도, 품질 및 기타 옵션 설정", "transcoding_hardware_acceleration": "하드웨어 가속", "transcoding_hardware_acceleration_description": "실험적인 기능입니다. 속도가 향상되지만 동일 비트레이트에서 품질이 상대적으로 낮을 수 있습니다.", "transcoding_hardware_decoding": "하드웨어 디코딩", @@ -315,7 +315,7 @@ "transcoding_max_keyframe_interval_description": "키프레임 사이 최대 프레임 거리를 설정합니다. 값이 낮으면 압축 효율이 저하되지만 검색 시간이 개선되고 빠른 움직임이 있는 장면에서 품질이 향상됩니다. 0을 입력한 경우 자동으로 설정합니다.", "transcoding_optimal_description": "목표 해상도보다 높은 동영상 또는 허용되지 않는 형식의 동영상", "transcoding_policy": "트랜스코드 정책", - "transcoding_policy_description": "동영상 트랜스코딩 시기 설정하기", + "transcoding_policy_description": "트랜스코딩 대상 동영상 설정", "transcoding_preferred_hardware_device": "선호하는 하드웨어 기기", "transcoding_preferred_hardware_device_description": "하드웨어 트랜스코딩에 사용할 dri 노드를 설정합니다. (VAAPI와 QSV만 해당)", "transcoding_preset_preset": "프리셋 (-preset)", @@ -324,10 +324,10 @@ "transcoding_reference_frames_description": "특정 프레임을 압축할 때 참조하는 프레임 수를 설정합니다. 값이 높으면 압축 효율이 향상되나 인코딩 속도가 저하됩니다. 0을 입력한 경우 자동으로 설정합니다.", "transcoding_required_description": "허용된 형식이 아닌 동영상만", "transcoding_settings": "동영상 트랜스코딩 설정", - "transcoding_settings_description": "트랜스코딩할 동영상과 처리 방법 관리하기", + "transcoding_settings_description": "트랜스코딩할 동영상 및 처리 방법 관리", "transcoding_target_resolution": "목표 해상도", "transcoding_target_resolution_description": "높은 해상도를 선택한 경우 세부 묘사의 손실을 최소화할 수 있지만, 인코딩 시간과 파일 크기가 증가하여 앱의 반응 속도가 느려질 수 있습니다.", - "transcoding_temporal_aq": "일시적 AQ", + "transcoding_temporal_aq": "Temporal AQ", "transcoding_temporal_aq_description": "세부 묘사가 많고 움직임이 적은 장면의 품질이 향상됩니다. 오래된 기기와 호환되지 않을 수 있습니다. (NVENC만 해당)", "transcoding_threads": "스레드", "transcoding_threads_description": "값이 높으면 인코딩 속도가 향상되지만 리소스 사용량이 증가합니다. 값은 CPU 코어 수보다 작아야 하며, 설정하지 않으려면 0을 입력합니다.", @@ -371,13 +371,17 @@ "admin_password": "관리자 비밀번호", "administration": "관리", "advanced": "고급", + "advanced_settings_enable_alternate_media_filter_subtitle": "이 옵션을 사용하면 동기화 중 미디어를 대체 기준으로 필터링할 수 있습니다. 앱이 모든 앨범을 제대로 감지하지 못할 때만 사용하세요.", + "advanced_settings_enable_alternate_media_filter_title": "대체 기기 앨범 동기화 필터 사용 (실험적)", "advanced_settings_log_level_title": "로그 레벨: {}", "advanced_settings_prefer_remote_subtitle": "일부 기기의 경우 기기 내의 섬네일을 로드하는 속도가 매우 느립니다. 서버 이미지를 대신 로드하려면 이 설정을 활성화하세요.", "advanced_settings_prefer_remote_title": "서버 이미지 선호", - "advanced_settings_proxy_headers_subtitle": "각 네트워크 요청을 보낼 때 Immich가 사용할 프록시 헤더를 정의합니다.", + "advanced_settings_proxy_headers_subtitle": "네트워크 요청을 보낼 때 Immich가 사용할 프록시 헤더를 정의합니다.", "advanced_settings_proxy_headers_title": "프록시 헤더", "advanced_settings_self_signed_ssl_subtitle": "서버 엔드포인트에 대한 SSL 인증서 확인을 건너뜁니다. 자체 서명된 인증서를 사용하는 경우 활성화하세요.", "advanced_settings_self_signed_ssl_title": "자체 서명된 SSL 인증서 허용", + "advanced_settings_sync_remote_deletions_subtitle": "웹에서 삭제하거나 복원한 항목을 이 기기에서도 자동으로 처리하도록 설정", + "advanced_settings_sync_remote_deletions_title": "원격 삭제 동기화 (실험적)", "advanced_settings_tile_subtitle": "고급 사용자 설정", "advanced_settings_troubleshooting_subtitle": "문제 해결을 위한 추가 기능 사용", "advanced_settings_troubleshooting_title": "문제 해결", @@ -408,10 +412,10 @@ "album_user_left": "{album} 앨범에서 나옴", "album_user_removed": "{user}님을 앨범에서 제거함", "album_viewer_appbar_delete_confirm": "이 앨범을 삭제하시겠습니까?", - "album_viewer_appbar_share_err_delete": "앨범 삭제에 실패했습니다.", - "album_viewer_appbar_share_err_leave": "앨범 나가기에 실패했습니다.", - "album_viewer_appbar_share_err_remove": "앨범에서 항목을 제거하지 못했습니다.", - "album_viewer_appbar_share_err_title": "앨범명 변경에 실패했습니다.", + "album_viewer_appbar_share_err_delete": "앨범 삭제 실패", + "album_viewer_appbar_share_err_leave": "앨범에서 나가지 못했습니다.", + "album_viewer_appbar_share_err_remove": "앨범에서 항목을 제거하는 중 문제가 발생했습니다.", + "album_viewer_appbar_share_err_title": "앨범 제목을 변경하지 못했습니다.", "album_viewer_appbar_share_leave": "앨범 나가기", "album_viewer_appbar_share_to": "공유 대상", "album_viewer_page_share_add_users": "사용자 추가", @@ -426,7 +430,7 @@ "allow_edits": "편집자로 설정", "allow_public_user_to_download": "모든 사용자의 다운로드 허용", "allow_public_user_to_upload": "모든 사용자의 업로드 허용", - "alt_text_qr_code": "QR코드 이미지", + "alt_text_qr_code": "QR 코드 이미지", "anti_clockwise": "반시계 방향", "api_key": "API 키", "api_key_description": "이 값은 한 번만 표시됩니다. 창을 닫기 전 반드시 복사해주세요.", @@ -444,11 +448,11 @@ "archive_size": "압축 파일 크기", "archive_size_description": "다운로드할 압축 파일의 크기 구성 (GiB 단위)", "archived": "보관함", - "archived_count": "보관함으로 항목 {count, plural, other {#개}} 이동됨", + "archived_count": "보관함으로 {count, plural, other {#개}} 항목 이동됨", "are_these_the_same_person": "동일한 인물인가요?", "are_you_sure_to_do_this": "계속 진행하시겠습니까?", - "asset_action_delete_err_read_only": "읽기 전용 항목은 삭제할 수 없습니다. 건너뜁니다.", - "asset_action_share_err_offline": "누락된 항목을 불러올 수 없습니다. 건너뜁니다.", + "asset_action_delete_err_read_only": "읽기 전용 항목은 삭제할 수 없으므로 건너뜁니다.", + "asset_action_share_err_offline": "오프라인 항목을 가져올 수 없으므로 건너뜁니다.", "asset_added_to_album": "앨범에 추가되었습니다.", "asset_adding_to_album": "앨범에 추가 중…", "asset_description_updated": "항목의 설명이 업데이트되었습니다.", @@ -470,15 +474,15 @@ "asset_skipped_in_trash": "휴지통의 항목", "asset_uploaded": "업로드 완료", "asset_uploading": "업로드 중…", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", + "asset_viewer_settings_subtitle": "갤러리 뷰어 설정 관리", "asset_viewer_settings_title": "보기 옵션", "assets": "항목", - "assets_added_count": "항목 {count, plural, one {#개} other {#개}}가 추가되었습니다.", + "assets_added_count": "{count, plural, one {#개} other {#개}} 항목 추가됨", "assets_added_to_album_count": "앨범에 항목 {count, plural, one {#개} other {#개}} 추가됨", "assets_added_to_name_count": "{hasName, select, true {{name}} other {새 앨범}}에 항목 {count, plural, one {#개} other {#개}} 추가됨", "assets_count": "{count, plural, one {#개} other {#개}} 항목", "assets_deleted_permanently": "{}개 항목이 영구적으로 삭제됨", - "assets_deleted_permanently_from_server": "Immich에서 항목 {}개가 영구적으로 삭제됨", + "assets_deleted_permanently_from_server": "서버에서 항목 {}개가 영구적으로 삭제됨", "assets_moved_to_trash_count": "휴지통으로 항목 {count, plural, one {#개} other {#개}} 이동됨", "assets_permanently_deleted_count": "항목 {count, plural, one {#개} other {#개}}가 영구적으로 삭제됨", "assets_removed_count": "항목 {count, plural, one {#개} other {#개}}를 제거했습니다.", @@ -486,30 +490,30 @@ "assets_restore_confirmation": "휴지통으로 이동된 항목을 모두 복원하시겠습니까? 이 작업은 되돌릴 수 없습니다! 누락된 항목의 경우 복원되지 않습니다.", "assets_restored_count": "항목 {count, plural, one {#개} other {#개}}를 복원했습니다.", "assets_restored_successfully": "항목 {}개를 복원했습니다.", - "assets_trashed": "휴지통으로 항목 {}개가 이동되었습니다.", + "assets_trashed": "휴지통으로 항목 {}개 이동됨", "assets_trashed_count": "휴지통으로 항목 {count, plural, one {#개} other {#개}} 이동됨", - "assets_trashed_from_server": "휴지통으로 Immich 항목 {}개가 이동되었습니다.", + "assets_trashed_from_server": "휴지통으로 서버에 있는 항목 {}개가 이동되었습니다.", "assets_were_part_of_album_count": "앨범에 이미 존재하는 {count, plural, one {항목} other {항목}}입니다.", "authorized_devices": "인증된 기기", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", + "automatic_endpoint_switching_subtitle": "지정된 Wi-Fi가 사용 가능한 경우 내부망을 통해 연결하고, 그렇지 않으면 다른 연결 방식을 사용합니다.", + "automatic_endpoint_switching_title": "자동 URL 전환", "back": "뒤로", "back_close_deselect": "뒤로, 닫기, 선택 취소", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", + "background_location_permission": "백그라운드 위치 권한", + "background_location_permission_content": "백그라운드에서 네트워크를 전환하려면, Immich가 Wi-Fi 네트워크 이름을 확인할 수 있도록 '정확한 위치' 권한을 항상 허용해야 합니다.", "backup_album_selection_page_albums_device": "기기의 앨범 ({})", - "backup_album_selection_page_albums_tap": "한 번 눌러 선택, 두 번 눌러 제외하세요.", + "backup_album_selection_page_albums_tap": "한 번 탭하면 포함되고, 두 번 탭하면 제외됩니다.", "backup_album_selection_page_assets_scatter": "각 항목은 여러 앨범에 포함될 수 있으며, 백업 진행 중에도 대상 앨범을 포함하거나 제외할 수 있습니다.", "backup_album_selection_page_select_albums": "앨범 선택", "backup_album_selection_page_selection_info": "선택한 앨범", "backup_album_selection_page_total_assets": "전체 항목", "backup_all": "모두", - "backup_background_service_backup_failed_message": "항목을 백업하지 못했습니다. 다시 시도하는 중...", - "backup_background_service_connection_failed_message": "서버에 연결하지 못했습니다. 다시 시도하는 중...", + "backup_background_service_backup_failed_message": "항목을 백업하지 못했습니다. 다시 시도하는 중…", + "backup_background_service_connection_failed_message": "서버에 연결하지 못했습니다. 다시 시도하는 중…", "backup_background_service_current_upload_notification": "{} 업로드 중", - "backup_background_service_default_notification": "백업할 항목을 확인하는 중...", + "backup_background_service_default_notification": "새로운 항목을 확인하는 중…", "backup_background_service_error_title": "백업 오류", - "backup_background_service_in_progress_notification": "선택한 항목을 백업하는 중...", + "backup_background_service_in_progress_notification": "항목을 백업하는 중…", "backup_background_service_upload_failure_notification": "{} 업로드 실패", "backup_controller_page_albums": "백업할 앨범", "backup_controller_page_background_app_refresh_disabled_content": "백그라운드 백업을 사용하려면 설정 > 일반 > 백그라운드 앱 새로 고침에서 백그라운드 앱 새로 고침을 활성화하세요.", @@ -521,30 +525,30 @@ "backup_controller_page_background_battery_info_title": "배터리 최적화", "backup_controller_page_background_charging": "충전 중에만", "backup_controller_page_background_configure_error": "백그라운드 서비스 구성 실패", - "backup_controller_page_background_delay": "새 콘텐츠 백업 간격: {}", - "backup_controller_page_background_description": "백그라운드 서비스를 활성화하여 앱을 실행하지 않고 새 항목을 자동으로 백업하세요.", - "backup_controller_page_background_is_off": "백그라운드 백업이 비활성화되었습니다.", - "backup_controller_page_background_is_on": "백그라운드 백업이 활성화되었습니다.", + "backup_controller_page_background_delay": "새 미디어 백업 간격: {}", + "backup_controller_page_background_description": "앱을 열지 않아도 새로 추가된 항목이 자동으로 백업되도록 하려면 백그라운드 서비스를 활성화하세요.", + "backup_controller_page_background_is_off": "백그라운드 자동 백업이 비활성화되었습니다.", + "backup_controller_page_background_is_on": "백그라운드 자동 백업이 활성화되었습니다.", "backup_controller_page_background_turn_off": "백그라운드 서비스 비활성화", "backup_controller_page_background_turn_on": "백그라운드 서비스 활성화", "backup_controller_page_background_wifi": "Wi-Fi에서만", "backup_controller_page_backup": "백업", - "backup_controller_page_backup_selected": "선택됨:", + "backup_controller_page_backup_selected": "선택됨: ", "backup_controller_page_backup_sub": "백업된 사진 및 동영상", "backup_controller_page_created": "생성일: {}", "backup_controller_page_desc_backup": "포그라운드 백업을 활성화하여 앱을 시작할 때 새 항목을 서버에 자동으로 업로드하세요.", - "backup_controller_page_excluded": "제외됨:", + "backup_controller_page_excluded": "제외됨: ", "backup_controller_page_failed": "실패 ({})", "backup_controller_page_filename": "파일명: {} [{}]", "backup_controller_page_id": "ID: {}", "backup_controller_page_info": "백업 정보", - "backup_controller_page_none_selected": "선택한 항목이 없습니다.", + "backup_controller_page_none_selected": "선택된 항목 없음", "backup_controller_page_remainder": "남은 항목", "backup_controller_page_remainder_sub": "백업 대기 중인 사진 및 동영상", "backup_controller_page_server_storage": "저장 공간", "backup_controller_page_start_backup": "백업 시작", - "backup_controller_page_status_off": "포그라운드 백업이 비활성화되었습니다.", - "backup_controller_page_status_on": "포그라운드 백업이 활성화되었습니다.", + "backup_controller_page_status_off": "포그라운드 자동 백업이 비활성화되었습니다.", + "backup_controller_page_status_on": "포그라운드 자동 백업이 활성화되었습니다.", "backup_controller_page_storage_format": "{} 사용 중, 전체 {}", "backup_controller_page_to_backup": "백업할 앨범 목록", "backup_controller_page_total_sub": "선택한 앨범의 고유한 사진 및 동영상", @@ -558,7 +562,7 @@ "backup_manual_success": "성공", "backup_manual_title": "업로드 상태", "backup_options_page_title": "백업 옵션", - "backup_setting_subtitle": "Manage background and foreground upload settings", + "backup_setting_subtitle": "백그라운드 및 포그라운드 업로드 설정 관리", "backward": "뒤로", "birthdate_saved": "생년월일이 성공적으로 저장되었습니다.", "birthdate_set_description": "생년월일은 사진 촬영 당시 인물의 나이를 계산하는 데 사용됩니다.", @@ -593,12 +597,12 @@ "camera_model": "카메라 모델", "cancel": "닫기", "cancel_search": "검색 닫기", - "canceled": "Canceled", + "canceled": "중단됨", "cannot_merge_people": "인물을 병합할 수 없습니다.", "cannot_undo_this_action": "이 작업은 되돌릴 수 없습니다!", "cannot_update_the_description": "설명을 변경할 수 없습니다.", "change_date": "날짜 변경", - "change_display_order": "Change display order", + "change_display_order": "표시 순서 변경", "change_expiration_time": "만료일 변경", "change_location": "위치 변경", "change_name": "이름 변경", @@ -613,9 +617,9 @@ "change_your_password": "비밀번호 변경", "changed_visibility_successfully": "표시 여부가 성공적으로 변경되었습니다.", "check_all": "모두 확인", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", + "check_corrupt_asset_backup": "백업된 항목의 손상 여부 확인", + "check_corrupt_asset_backup_button": "확인 수행", + "check_corrupt_asset_backup_description": "이 검사는 모든 항목이 백업된 후 Wi-Fi가 연결된 상태에서만 실행하세요. 이 작업은 몇 분 정도 소요될 수 있습니다.", "check_logs": "로그 확인", "choose_matching_people_to_merge": "병합할 인물 선택", "city": "도시", @@ -627,10 +631,10 @@ "client_cert_dialog_msg_confirm": "확인", "client_cert_enter_password": "비밀번호 입력", "client_cert_import": "가져오기", - "client_cert_import_success_msg": "클라이언트 인증서를 가져왔습니다.", - "client_cert_invalid_msg": "유효하지 않은 인증서 또는 패스프레이즈가 일치하지 않습니다.", - "client_cert_remove_msg": "클라이언트 인증서가 제거되었습니다.", - "client_cert_subtitle": "인증서 가져오기/제거는 로그인 전에만 가능합니다. PKCS12 (.p12, .pfx) 형식을 지원합니다.", + "client_cert_import_success_msg": "클라이언트 인증서 가져오기 완료", + "client_cert_invalid_msg": "인증서가 유효하지 않거나 비밀번호가 올바르지 않음", + "client_cert_remove_msg": "클라이언트 인증서 제거됨", + "client_cert_subtitle": "PKCS12 (.p12, .pfx) 형식을 지원합니다. 인증서 가져오기 및 제거는 로그인 전에만 가능합니다.", "client_cert_title": "SSL 클라이언트 인증서", "clockwise": "시계 방향", "close": "닫기", @@ -644,12 +648,12 @@ "comments_are_disabled": "댓글이 비활성화되었습니다.", "common_create_new_album": "앨범 생성", "common_server_error": "네트워크 연결 상태를 확인하고, 서버에 접속할 수 있는지, 앱/서버 버전이 호환되는지 확인해주세요.", - "completed": "Completed", + "completed": "완료됨", "confirm": "확인", "confirm_admin_password": "관리자 비밀번호 확인", - "confirm_delete_face": "에셋에서 {name} 얼굴을 삭제하시겠습니까?", + "confirm_delete_face": "항목에서 {name}의 얼굴을 삭제하시겠습니까?", "confirm_delete_shared_link": "이 공유 링크를 삭제하시겠습니까?", - "confirm_keep_this_delete_others": "이 에셋을 제외한 스택의 다른 모든 에셋이 삭제됩니다. 계속하시겠습니까?", + "confirm_keep_this_delete_others": "이 항목을 제외한 스택의 모든 항목이 삭제됩니다. 계속하시겠습니까?", "confirm_password": "비밀번호 확인", "contain": "맞춤", "context": "내용", @@ -659,8 +663,8 @@ "control_bottom_app_bar_delete_from_immich": "Immich에서 삭제", "control_bottom_app_bar_delete_from_local": "기기에서 삭제", "control_bottom_app_bar_edit_location": "위치 편집", - "control_bottom_app_bar_edit_time": "날짜 및 시간 변경", - "control_bottom_app_bar_share_link": "Share Link", + "control_bottom_app_bar_edit_time": "날짜 변경", + "control_bottom_app_bar_share_link": "공유 링크", "control_bottom_app_bar_share_to": "공유 대상", "control_bottom_app_bar_trash_from_immich": "휴지통", "copied_image_to_clipboard": "이미지가 클립보드에 복사되었습니다.", @@ -695,7 +699,7 @@ "crop": "자르기", "curated_object_page_title": "사물", "current_device": "현재 기기", - "current_server_address": "Current server address", + "current_server_address": "현재 서버 주소", "custom_locale": "사용자 지정 로케일", "custom_locale_description": "언어 및 지역에 따른 날짜 및 숫자 형식 지정", "daily_title_text_date": "M월 d일 EEEE", @@ -709,19 +713,19 @@ "date_range": "날짜 범위", "day": "일", "deduplicate_all": "모두 삭제", - "deduplication_criteria_1": "이미지 크기(바이트)", - "deduplication_criteria_2": "EXIF 데이터 개수", + "deduplication_criteria_1": "이미지 크기 (바이트)", + "deduplication_criteria_2": "EXIF 정보 항목 수", "deduplication_info": "중복 제거 정보", - "deduplication_info_description": "자산을 자동으로 미리 선택하고 일괄적으로 중복을 제거하려면 다음을 살펴보세요:", + "deduplication_info_description": "항목을 자동으로 미리 선택하고 중복 항목을 일괄 제거하려면 다음을 확인하세요:", "default_locale": "기본 로케일", "default_locale_description": "브라우저 로케일에 따른 날짜 및 숫자 형식 지정", "delete": "삭제", "delete_album": "앨범 삭제", "delete_api_key_prompt": "API 키를 삭제하시겠습니까?", - "delete_dialog_alert": "이 항목이 Immich 및 기기에서 영구적으로 삭제됩니다.", - "delete_dialog_alert_local": "이 항목이 기기에서 영구적으로 삭제됩니다. Immich에서는 삭제되지 않습니다.", - "delete_dialog_alert_local_non_backed_up": "일부 항목이 백업되지 않았습니다. 백업되지 않은 항목이 기기에서 영구적으로 삭제됩니다.", - "delete_dialog_alert_remote": "이 항목이 Immich에서 영구적으로 삭제됩니다.", + "delete_dialog_alert": "이 항목들이 Immich와 기기에서 영구적으로 삭제됩니다.", + "delete_dialog_alert_local": "이 항목들이 기기에서 영구적으로 삭제됩니다. Immich 서버에서는 삭제되지 않습니다.", + "delete_dialog_alert_local_non_backed_up": "일부 항목이 Immich에 백업되지 않았으며, 기기에서 영구적으로 삭제됩니다.", + "delete_dialog_alert_remote": "이 항목들이 Immich 서버에서 영구적으로 삭제됩니다.", "delete_dialog_ok_force": "무시하고 삭제", "delete_dialog_title": "영구적으로 삭제", "delete_duplicates_confirmation": "비슷한 항목들을 영구적으로 삭제하시겠습니까?", @@ -731,7 +735,7 @@ "delete_link": "링크 삭제", "delete_local_dialog_ok_backed_up_only": "백업된 항목만 삭제", "delete_local_dialog_ok_force": "무시하고 삭제", - "delete_others": "다른 사람 삭제", + "delete_others": "다른 인물 삭제", "delete_shared_link": "공유 링크 삭제", "delete_shared_link_dialog_title": "공유 링크 삭제", "delete_tag": "태그 삭제", @@ -741,12 +745,12 @@ "deletes_missing_assets": "디스크에 존재하지 않는 항목 제거", "description": "설명", "description_input_hint_text": "설명 추가...", - "description_input_submit_error": "설명을 변경하는 중 문제가 발생했습니다. 자세한 내용은 로그를 참조하세요.", + "description_input_submit_error": "설명 업데이트 중 오류가 발생했습니다. 자세한 내용은 로그를 확인하세요.", "details": "상세 정보", "direction": "방향", "disabled": "비활성화됨", "disallow_edits": "뷰어로 설정", - "discord": "디스코드", + "discord": "Discord", "discover": "탐색", "dismiss_all_errors": "모든 오류 무시", "dismiss_error": "오류 무시", @@ -761,8 +765,8 @@ "download_canceled": "다운로드가 취소되었습니다.", "download_complete": "다은로드가 완료되었습니다.", "download_enqueue": "대기열에 다운로드", - "download_error": "다운로드 중 문제가 발생했습니다.", - "download_failed": "다운로드에 실패하였습니다.", + "download_error": "다운로드 오류", + "download_failed": "다운로드 실패", "download_filename": "파일: {}", "download_finished": "다운로드가 완료되었습니다.", "download_include_embedded_motion_videos": "내장된 동영상", @@ -773,7 +777,7 @@ "download_settings_description": "다운로드 설정 관리", "download_started": "다운로드가 시작되었습니다.", "download_sucess": "다운로드가 완료되었습니다.", - "download_sucess_android": "미디어가 DCIM/Immich에 저장되었습니다.", + "download_sucess_android": "미디어가 DCIM/Immich 폴더에 저장되었습니다.", "download_waiting_to_retry": "재시도 대기 중", "downloading": "다운로드", "downloading_asset_filename": "{filename} 다운로드 중...", @@ -807,17 +811,17 @@ "editor_crop_tool_h2_aspect_ratios": "종횡비", "editor_crop_tool_h2_rotation": "회전", "email": "이메일", - "empty_folder": "This folder is empty", + "empty_folder": "폴더가 비어 있음", "empty_trash": "휴지통 비우기", "empty_trash_confirmation": "휴지통을 비우시겠습니까? 휴지통에 있는 모든 항목이 Immich에서 영구적으로 삭제됩니다.\n이 작업은 되돌릴 수 없습니다!", "enable": "활성화", "enabled": "활성화됨", "end_date": "종료일", - "enqueued": "Enqueued", + "enqueued": "대기열에 추가됨", "enter_wifi_name": "Enter WiFi name", "error": "오류", - "error_change_sort_album": "Failed to change album sort order", - "error_delete_face": "에셋에서 얼굴 삭제 오류", + "error_change_sort_album": "앨범 표시 순서 변경 실패", + "error_delete_face": "얼굴 삭제 중 오류가 발생했습니다.", "error_loading_image": "이미지 로드 오류", "error_saving_image": "오류: {}", "error_title": "오류 - 문제가 발생했습니다", @@ -846,7 +850,7 @@ "failed_to_create_shared_link": "공유 링크를 생성하지 못했습니다.", "failed_to_edit_shared_link": "공유 링크를 수정하지 못했습니다.", "failed_to_get_people": "인물 로드 실패", - "failed_to_keep_this_delete_others": "이 자산을 유지하고 다른 자산을 삭제하지 못했습니다", + "failed_to_keep_this_delete_others": "이 항목을 유지하고 다른 항목을 삭제하지 못했습니다.", "failed_to_load_asset": "항목 로드 실패", "failed_to_load_assets": "항목 로드 실패", "failed_to_load_people": "인물 로드 실패", @@ -953,10 +957,10 @@ "exif_bottom_sheet_location": "위치", "exif_bottom_sheet_people": "인물", "exif_bottom_sheet_person_add_person": "이름 추가", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", + "exif_bottom_sheet_person_age": "{}세", + "exif_bottom_sheet_person_age_months": "생후 {}개월", + "exif_bottom_sheet_person_age_year_months": "생후 1년 {}개월", + "exif_bottom_sheet_person_age_years": "{}세", "exit_slideshow": "슬라이드 쇼 종료", "expand_all": "모두 확장", "experimental_settings_new_asset_list_subtitle": "진행 중", @@ -973,12 +977,12 @@ "extension": "확장자", "external": "외부", "external_libraries": "외부 라이브러리", - "external_network": "External network", + "external_network": "외부 네트워크", "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", "face_unassigned": "알 수 없음", - "failed": "Failed", - "failed_to_load_assets": "에셋 로드에 실패했습니다", - "failed_to_load_folder": "Failed to load folder", + "failed": "실패함", + "failed_to_load_assets": "항목 로드 실패", + "failed_to_load_folder": "폴더 로드 실패", "favorite": "즐겨찾기", "favorite_or_unfavorite_photo": "즐겨찾기 추가/제거", "favorites": "즐겨찾기", @@ -992,26 +996,27 @@ "filetype": "파일 형식", "filter": "필터", "filter_people": "인물 필터", + "filter_places": "장소 필터링", "find_them_fast": "이름으로 검색하여 빠르게 찾기", "fix_incorrect_match": "잘못된 분류 수정", - "folder": "Folder", - "folder_not_found": "Folder not found", + "folder": "폴더", + "folder_not_found": "폴더를 찾을 수 없음", "folders": "폴더", "folders_feature_description": "파일 시스템의 사진 및 동영상을 폴더 뷰로 탐색", "forward": "앞으로", "general": "일반", "get_help": "도움 요청", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "get_wifiname_error": "Wi-Fi 이름을 가져올 수 없습니다. 필요한 권한이 허용되어 있고 Wi-Fi 네트워크에 연결되어 있는지 확인하세요.", "getting_started": "시작하기", "go_back": "뒤로", "go_to_folder": "폴더로 이동", "go_to_search": "검색으로 이동", - "grant_permission": "Grant permission", + "grant_permission": "권한 부여", "group_albums_by": "다음으로 앨범 그룹화...", - "group_country": "국가별 그룹화", + "group_country": "국가별로 그룹화", "group_no": "그룹화 없음", "group_owner": "소유자로 그룹화", - "group_places_by": "장소 그룹화 기준...", + "group_places_by": "다음으로 장소 그룹화…", "group_year": "연도로 그룹화", "haptic_feedback_switch": "햅틱 피드백 활성화", "haptic_feedback_title": "햅틱 피드백", @@ -1020,7 +1025,7 @@ "header_settings_field_validator_msg": "값은 비워둘 수 없습니다.", "header_settings_header_name_input": "헤더 이름", "header_settings_header_value_input": "헤더 값", - "headers_settings_tile_subtitle": "각 네트워크 요청을 보낼 때 사용할 프록시 헤더를 정의합니다.", + "headers_settings_tile_subtitle": "앱이 각 네트워크 요청에 함께 전송할 프록시 헤더를 정의합니다.", "headers_settings_tile_title": "사용자 정의 프록시 헤더", "hi_user": "안녕하세요 {name}님, ({email})", "hide_all_people": "모든 인물 숨기기", @@ -1040,13 +1045,13 @@ "home_page_delete_remote_err_local": "서버에서 삭제된 항목입니다. 건너뜁니다.", "home_page_favorite_err_local": "기기의 항목은 즐겨찾기에 추가할 수 없습니다. 건너뜁니다.", "home_page_favorite_err_partner": "파트너의 항목은 즐겨찾기에 추가할 수 없습니다. 건너뜁니다.", - "home_page_first_time_notice": "앱을 처음 사용하는 경우 타임라인에 앨범의 사진과 동영상을 채울 수 있도록 백업할 앨범을 선택하세요.", - "home_page_share_err_local": "기기의 항목은 링크로 공유할 수 없습니다. 건너뜁니다.", + "home_page_first_time_notice": "앱을 처음 사용하는 경우, 타임라인에 사진과 동영상이 표시될 수 있도록 백업 앨범을 선택해주세요.", + "home_page_share_err_local": "기기에만 저장된 항목은 링크로 공유할 수 없어 건너뜁니다.", "home_page_upload_err_limit": "한 번에 최대 30개의 항목만 업로드할 수 있습니다.", "host": "호스트", "hour": "시간", "ignore_icloud_photos": "iCloud 사진 제외", - "ignore_icloud_photos_description": "iCloud에 저장된 사진은 Immich 서버에 업로드되지 않습니다.", + "ignore_icloud_photos_description": "iCloud에 저장된 사진은 Immich 서버로 업로드되지 않습니다.", "image": "이미지", "image_alt_text_date": "{date} 촬영한 {isVideo, select, true {동영상} other {사진}}", "image_alt_text_date_1_person": "{date} {person1}님과 함께한 {isVideo, select, true {동영상} other {사진}}", @@ -1080,16 +1085,16 @@ "night_at_midnight": "매일 밤 자정", "night_at_twoam": "매일 새벽 2시" }, - "invalid_date": "잘못된 날짜입니다.", - "invalid_date_format": "잘못된 날짜 형식입니다.", + "invalid_date": "유효하지 않은 날짜", + "invalid_date_format": "유효하지 않은 날짜 형식", "invite_people": "사용자 초대", "invite_to_album": "앨범으로 초대", "items_count": "{count, plural, one {#개} other {#개}} 항목", "jobs": "작업", "keep": "유지", "keep_all": "모두 유지", - "keep_this_delete_others": "이 항목은 보관하고 다른 항목은 삭제", - "kept_this_deleted_others": "이 자산을 유지하고 {count, plural, one {# asset} other {# assets}}을 삭제했습니다", + "keep_this_delete_others": "이 항목은 유지하고 나머지는 삭제", + "kept_this_deleted_others": "이 항목을 유지하고 {count, plural, one {#개의 항목} other {#개의 항목}}을 삭제했습니다.", "keyboard_shortcuts": "키보드 단축키", "language": "언어", "language_setting_description": "선호하는 언어 선택", @@ -1118,9 +1123,9 @@ "loading": "로드 중", "loading_search_results_failed": "검색 결과 로드 실패", "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", - "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", + "local_network_sheet_info": "지정한 Wi-Fi에 연결된 경우 앱은 해당 URL을 통해 서버에 연결합니다.", + "location_permission": "위치 권한", + "location_permission_content": "자동 전환 기능을 사용하려면 Immich가 현재 Wi-Fi 네트워크 이름을 확인하기 위한 '정확한 위치' 권한이 필요합니다.", "location_picker_choose_on_map": "지도에서 선택", "location_picker_latitude_error": "유효한 위도를 입력하세요.", "location_picker_latitude_hint": "이곳에 위도 입력", @@ -1140,11 +1145,11 @@ "login_form_err_http": "http:// 또는 https://로 시작해야 합니다.", "login_form_err_invalid_email": "유효하지 않은 이메일", "login_form_err_invalid_url": "잘못된 URL입니다.", - "login_form_err_leading_whitespace": "문자 시작에 공백이 있습니다.", - "login_form_err_trailing_whitespace": "문자 끝에 공백이 있습니다.", - "login_form_failed_get_oauth_server_config": "OAuth 로그인 중 문제 발생, 서버 URL을 확인하세요.", + "login_form_err_leading_whitespace": "선행 공백을 확인하세요.", + "login_form_err_trailing_whitespace": "후행 공백을 확인하세요.", + "login_form_failed_get_oauth_server_config": "OAuth 로그인 중 오류가 발생했습니다. 서버 URL을 확인하세요.", "login_form_failed_get_oauth_server_disable": "이 서버는 OAuth 기능을 지원하지 않습니다.", - "login_form_failed_login": "로그인 오류. 서버 URL, 이메일 및 비밀번호를 확인하세요.", + "login_form_failed_login": "로그인 중 오류가 발생했습니다. 서버 URL, 이메일, 비밀번호를 확인하세요.", "login_form_handshake_exception": "서버와 통신 중 인증서 예외가 발생했습니다. 자체 서명된 인증서를 사용 중이라면, 설정에서 자체 서명된 인증서 허용을 활성화하세요.", "login_form_password_hint": "비밀번호", "login_form_save_login": "로그인 유지", @@ -1152,7 +1157,7 @@ "login_form_server_error": "서버에 연결할 수 없습니다.", "login_has_been_disabled": "로그인이 비활성화되었습니다.", "login_password_changed_error": "비밀번호를 변경하던 중 문제가 발생했습니다.", - "login_password_changed_success": "비밀번호가 변경되었습니다.", + "login_password_changed_success": "비밀번호가 성공적으로 변경되었습니다.", "logout_all_device_confirmation": "모든 기기에서 로그아웃하시겠습니까?", "logout_this_device_confirmation": "이 기기에서 로그아웃하시겠습니까?", "longitude": "경도", @@ -1172,7 +1177,7 @@ "map": "지도", "map_assets_in_bound": "사진 {}개", "map_assets_in_bounds": "사진 {}개", - "map_cannot_get_user_location": "사용자의 위치를 불러올 수 없습니다.", + "map_cannot_get_user_location": "사용자의 위치를 가져올 수 없습니다.", "map_location_dialog_yes": "예", "map_location_picker_page_use_location": "이 위치 사용", "map_location_service_disabled_content": "현재 위치의 항목을 표시하려면 위치 서비스를 활성화해야 합니다. 지금 활성화하시겠습니까?", @@ -1227,8 +1232,8 @@ "my_albums": "내 앨범", "name": "이름", "name_or_nickname": "이름 또는 닉네임", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", + "networking_settings": "네트워킹", + "networking_subtitle": "서버 엔드포인트 설정 관리", "never": "없음", "new_album": "새 앨범", "new_api_key": "API 키 생성", @@ -1257,7 +1262,7 @@ "no_results_description": "동의어 또는 더 일반적인 단어를 사용해 보세요.", "no_shared_albums_message": "공유 앨범을 만들어 주변 사람들과 사진 및 동영상 공유", "not_in_any_album": "앨범에 없음", - "not_selected": "Not selected", + "not_selected": "선택되지 않음", "note_apply_storage_label_to_previously_uploaded assets": "참고: 이전에 업로드한 항목에도 스토리지 레이블을 적용하려면 다음을 실행합니다,", "notes": "참고", "notification_permission_dialog_content": "알림을 활성화하려면 설정에서 알림 권한을 허용하세요.", @@ -1282,6 +1287,7 @@ "onboarding_welcome_user": "{user}님, 환영합니다", "online": "온라인", "only_favorites": "즐겨찾기만", + "open": "열기", "open_in_map_view": "지도 보기에서 열기", "open_in_openstreetmap": "OpenStreetMap에서 열기", "open_the_search_filters": "검색 필터 열기", @@ -1371,7 +1377,7 @@ "profile_drawer_app_logs": "로그", "profile_drawer_client_out_of_date_major": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", "profile_drawer_client_out_of_date_minor": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", - "profile_drawer_client_server_up_to_date": "클라이언트와 서버가 최신입니다.", + "profile_drawer_client_server_up_to_date": "클라이언트와 서버가 최신 상태입니다.", "profile_drawer_github": "Github", "profile_drawer_server_out_of_date_major": "서버 버전이 최신이 아닙니다. 최신 버전으로 업데이트하세요.", "profile_drawer_server_out_of_date_minor": "서버 버전이 최신이 아닙니다. 최신 버전으로 업데이트하세요.", @@ -1447,15 +1453,15 @@ "remove_from_favorites": "즐겨찾기에서 제거", "remove_from_shared_link": "공유 링크에서 제거", "remove_memory": "추억 제거", - "remove_photo_from_memory": "이 추억에서 사진 제거", + "remove_photo_from_memory": "추억에서 사진 제거", "remove_url": "URL 제거", "remove_user": "사용자 삭제", "removed_api_key": "API 키 삭제: {name}", "removed_from_archive": "보관함에서 제거되었습니다.", "removed_from_favorites": "즐겨찾기에서 제거되었습니다.", "removed_from_favorites_count": "즐겨찾기에서 항목 {count, plural, other {#개}} 제거됨", - "removed_memory": "추억 제거", - "removed_photo_from_memory": "이 추억에서 사진 제거", + "removed_memory": "추억이 제거되었습니다.", + "removed_photo_from_memory": "추억에서 사진을 제거했습니다.", "removed_tagged_assets": "항목 {count, plural, one {#개} other {#개}}에서 태그를 제거함", "rename": "이름 바꾸기", "repair": "수리", @@ -1496,7 +1502,7 @@ "search_albums": "앨범 검색", "search_by_context": "내용 검색", "search_by_description": "설명으로 검색", - "search_by_description_example": "사파에서 즐기는 하이킹", + "search_by_description_example": "설악산에서 즐기는 하이킹", "search_by_filename": "파일명 또는 확장자로 검색", "search_by_filename_example": "예시: IMG_1234.JPG or PNG", "search_camera_make": "카메라 제조사 검색...", @@ -1510,7 +1516,7 @@ "search_filter_date_title": "날짜 범위 선택", "search_filter_display_option_not_in_album": "앨범에 없음", "search_filter_display_options": "표시 옵션", - "search_filter_filename": "Search by file name", + "search_filter_filename": "파일명으로 검색", "search_filter_location": "위치", "search_filter_location_title": "위치 선택", "search_filter_media_type": "미디어 종류", @@ -1518,7 +1524,7 @@ "search_filter_people_title": "인물 선택", "search_for": "검색", "search_for_existing_person": "존재하는 인물 검색", - "search_no_more_result": "No more results", + "search_no_more_result": "더이상 결과 없음", "search_no_people": "인물이 없습니다.", "search_no_people_named": "\"{name}\" 인물을 찾을 수 없음", "search_no_result": "No results found, try a different search term or combination", @@ -1528,7 +1534,7 @@ "search_page_no_objects": "사용 가능한 사물 정보 없음", "search_page_no_places": "사용 가능한 위치 정보 없음", "search_page_screenshots": "스크린샷", - "search_page_search_photos_videos": "Search for your photos and videos", + "search_page_search_photos_videos": "사진 및 동영상을 검색하세요", "search_page_selfies": "셀피", "search_page_things": "사물", "search_page_view_all_button": "모두 보기", @@ -1540,7 +1546,7 @@ "search_result_page_new_search_hint": "새 검색", "search_settings": "설정 검색", "search_state": "지역 검색...", - "search_suggestion_list_smart_search_hint_1": "스마트 검색이 기본적으로 활성화되어 있습니다. 메타데이터로 검색하려면 다음 구문을 사용하세요.", + "search_suggestion_list_smart_search_hint_1": "스마트 검색이 기본적으로 활성화되어 있습니다. 메타데이터로 검색하려면 다음을 사용하세요. ", "search_suggestion_list_smart_search_hint_2": "m:your-search-term", "search_tags": "태그로 검색...", "search_timezone": "시간대 검색...", @@ -1564,10 +1570,10 @@ "select_trash_all": "모두 삭제", "select_user_for_sharing_page_err_album": "앨범을 생성하지 못했습니다.", "selected": "선택됨", - "selected_count": "{count, plural, other {#개}} 항목 선택됨", + "selected_count": "{count, plural, other {#개}} 선택됨", "send_message": "메시지 전송", "send_welcome_email": "환영 이메일 전송", - "server_endpoint": "Server Endpoint", + "server_endpoint": "서버 엔드포인트", "server_info_box_app_version": "앱 버전", "server_info_box_server_url": "서버 URL", "server_offline": "오프라인", @@ -1576,7 +1582,7 @@ "server_version": "서버 버전", "set": "설정", "set_as_album_cover": "앨범 커버로 설정", - "set_as_featured_photo": "추천 사진으로 설정", + "set_as_featured_photo": "대표 사진으로 설정", "set_as_profile_picture": "프로필 사진으로 설정", "set_date_of_birth": "생년월일 설정", "set_profile_picture": "프로필 사진으로 설정", @@ -1588,7 +1594,7 @@ "setting_image_viewer_preview_title": "미리 보기 이미지 불러오기", "setting_image_viewer_title": "이미지", "setting_languages_apply": "적용", - "setting_languages_subtitle": "Change the app's language", + "setting_languages_subtitle": "앱 언어 변경", "setting_languages_title": "언어", "setting_notifications_notify_failures_grace_period": "백그라운드 백업 실패 알림: {}", "setting_notifications_notify_hours": "{}시간 후", @@ -1603,13 +1609,13 @@ "setting_notifications_total_progress_title": "백그라운드 백업 전체 진행률 표시", "setting_video_viewer_looping_title": "반복", "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", + "setting_video_viewer_original_video_title": "원본 동영상 강제 사용", "settings": "설정", "settings_require_restart": "설정을 적용하려면 Immich를 다시 시작하세요.", "settings_saved": "설정이 저장되었습니다.", "share": "공유", "share_add_photos": "사진 추가", - "share_assets_selected": "{}개 항목 선택됨", + "share_assets_selected": "{}개 선택됨", "share_dialog_preparing": "준비 중...", "shared": "공유됨", "shared_album_activities_input_disable": "댓글이 비활성화되었습니다", @@ -1623,7 +1629,7 @@ "shared_by_user": "{user}님이 공유함", "shared_by_you": "내가 공유함", "shared_from_partner": "{partner}님의 사진", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", + "shared_intent_upload_button_progress_text": "{} / {} 업로드됨", "shared_link_app_bar_title": "공유 링크", "shared_link_clipboard_copied_massage": "클립보드에 복사되었습니다.", "shared_link_clipboard_text": "링크: {}\n비밀번호: {}", @@ -1703,7 +1709,7 @@ "sort_items": "항목 수", "sort_modified": "수정된 날짜", "sort_oldest": "오래된 사진", - "sort_people_by_similarity": "유사성을 기준으로 사람 정렬", + "sort_people_by_similarity": "유사성을 기준으로 인물 정렬", "sort_recent": "최근 사진", "sort_title": "제목", "source": "소스", @@ -1740,7 +1746,7 @@ "tag_created": "태그 생성됨: {tag}", "tag_feature_description": "사진 및 동영상을 주제별 그룹화된 태그로 탐색", "tag_not_found_question": "태그를 찾을 수 없나요? 새 태그를 생성하세요.", - "tag_people": "사람 태그", + "tag_people": "인물 태그", "tag_updated": "태그 업데이트됨: {tag}", "tagged_assets": "항목 {count, plural, one {#개} other {#개}}에 태그를 적용함", "tags": "태그", @@ -1754,12 +1760,12 @@ "theme_setting_colorful_interface_title": "미려한 인터페이스", "theme_setting_image_viewer_quality_subtitle": "상세 보기 이미지 품질 조정", "theme_setting_image_viewer_quality_title": "이미지 보기 품질", - "theme_setting_primary_color_subtitle": "주 기능 및 강조에 사용되는 색상 선택", + "theme_setting_primary_color_subtitle": "주요 기능과 강조 색상에 적용할 테마 색상을 선택하세요.", "theme_setting_primary_color_title": "대표 색상", "theme_setting_system_primary_color_title": "시스템 색상 사용", "theme_setting_system_theme_switch": "자동 (시스템 설정)", "theme_setting_theme_subtitle": "앱 테마 선택", - "theme_setting_three_stage_loading_subtitle": "이 기능은 앱의 로드 성능을 향상시킬 수 있지만 더 많은 데이터를 사용합니다.", + "theme_setting_three_stage_loading_subtitle": "3단계 로딩은 로드 성능을 향상시킬 수 있으나, 네트워크 부하가 크게 증가할 수 있습니다.", "theme_setting_three_stage_loading_title": "3단계 로드 활성화", "they_will_be_merged_together": "선택한 인물들이 병합됩니다.", "third_party_resources": "서드 파티 리소스", @@ -1802,7 +1808,7 @@ "unlink_motion_video": "모션 비디오 링크 해제", "unlink_oauth": "OAuth 연결 해제", "unlinked_oauth_account": "OAuth 계정 연결이 해제되었습니다.", - "unmute_memories": "추억 음소거 해제", + "unmute_memories": "음소거 해제", "unnamed_album": "이름 없는 앨범", "unnamed_album_delete_confirmation": "선텍한 앨범을 삭제하시겠습니까?", "unnamed_share": "이름 없는 공유", @@ -1826,11 +1832,11 @@ "upload_status_errors": "오류", "upload_status_uploaded": "완료", "upload_success": "업로드가 완료되었습니다. 업로드된 항목을 보려면 페이지를 새로고침하세요.", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", + "upload_to_immich": "Immich에 업로드 ({})", + "uploading": "업로드 중", "url": "URL", "usage": "사용량", - "use_current_connection": "use current connection", + "use_current_connection": "현재 네트워크 사용", "use_custom_date_range": "대신 맞춤 기간 사용", "user": "사용자", "user_id": "사용자 ID", @@ -1845,15 +1851,15 @@ "users": "사용자", "utilities": "도구", "validate": "검증", - "validate_endpoint_error": "Please enter a valid URL", + "validate_endpoint_error": "유효한 URL을 입력하세요.", "variables": "변수", "version": "버전", "version_announcement_closing": "당신의 친구, Alex가", "version_announcement_message": "안녕하세요! 새 버전의 Immich를 사용할 수 있습니다. 잘못된 구성을 방지하고 Immich를 최신 상태로 유지하기 위해 잠시 시간을 내어 릴리스 노트를 읽어보는 것을 권장합니다. 특히 WatchTower 등의 자동 업데이트 기능을 사용하는 경우 의도하지 않은 동작을 방지하기 위해 더더욱 권장됩니다.", "version_announcement_overlay_release_notes": "릴리스 노트", "version_announcement_overlay_text_1": "안녕하세요,", - "version_announcement_overlay_text_2": "새 버전의 Immich를 사용할 수 있습니다.", - "version_announcement_overlay_text_3": "WatchTower 등의 자동 업데이트 기능을 사용하는 경우 의도하지 않은 동작을 방지하기 위해 docker-compose.yml 및 .env 구성이 최신인지 확인하세요.", + "version_announcement_overlay_text_2": "새 버전의 Immich를 사용할 수 있습니다. ", + "version_announcement_overlay_text_3": " WatchTower 등의 자동 업데이트 기능을 사용하는 경우 의도하지 않은 동작을 방지하기 위해 docker-compose.yml 및 .env 구성이 최신인지 확인하세요.", "version_announcement_overlay_title": "새 서버 버전 사용 가능 🎉", "version_history": "버전 기록", "version_history_item": "{date} 버전 {version} 설치", @@ -1888,6 +1894,6 @@ "years_ago": "{years, plural, one {#년} other {#년}} 전", "yes": "네", "you_dont_have_any_shared_links": "생성한 공유 링크가 없습니다.", - "your_wifi_name": "Your WiFi name", + "your_wifi_name": "Wi-Fi 네트워크 이름", "zoom_image": "이미지 확대" } diff --git a/i18n/lv.json b/i18n/lv.json index fbefc9a521..c9002b559c 100644 --- a/i18n/lv.json +++ b/i18n/lv.json @@ -401,11 +401,11 @@ "backup_controller_page_background_turn_on": "Ieslēgt fona pakalpojumu", "backup_controller_page_background_wifi": "Tikai WiFi tīklā", "backup_controller_page_backup": "Dublēšana", - "backup_controller_page_backup_selected": "Atlasīts:", + "backup_controller_page_backup_selected": "Atlasīts: ", "backup_controller_page_backup_sub": "Dublētie Fotoattēli un videoklipi", "backup_controller_page_created": "Izveidots: {}", "backup_controller_page_desc_backup": "Ieslēdziet priekšplāna dublēšanu, lai, atverot programmu, serverī automātiski augšupielādētu jaunus aktīvus.", - "backup_controller_page_excluded": "Izņemot:", + "backup_controller_page_excluded": "Izņemot: ", "backup_controller_page_failed": "Neizdevās ({})", "backup_controller_page_filename": "Faila nosaukums: {} [{}]", "backup_controller_page_id": "ID: {}", diff --git a/i18n/nb_NO.json b/i18n/nb_NO.json index df2fba7517..3a70867431 100644 --- a/i18n/nb_NO.json +++ b/i18n/nb_NO.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "Er du sikker på at du ønsker å deaktivere alle innloggingsmetoder? Innlogging vil bli fullstendig deaktivert.", "authentication_settings_reenable": "For å aktivere på nytt, bruk en Server Command.", "background_task_job": "Bakgrunnsjobber", - "backup_database": "Backupdatabase", - "backup_database_enable_description": "Aktiver databasebackup", - "backup_keep_last_amount": "Antall backuper å beholde", - "backup_settings": "Backupinnstillinger", - "backup_settings_description": "Håndter innstillinger for databasebackup", + "backup_database": "Opprett database-dump", + "backup_database_enable_description": "Aktiver database-dump", + "backup_keep_last_amount": "Antall database-dumps å beholde", + "backup_settings": "Database-dump instillinger", + "backup_settings_description": "Håndter innstillinger for database-dump. Merk: Disse jobbene overvåkes ikke, og du vil ikke bli varslet ved feil.", "check_all": "Merk Alle", "cleanup": "Opprydding", "cleared_jobs": "Ryddet opp jobber for: {job}", @@ -529,11 +529,11 @@ "backup_controller_page_background_turn_on": "Skru på bakgrunnstjenesten", "backup_controller_page_background_wifi": "Kun på WiFi", "backup_controller_page_backup": "Sikkerhetskopier", - "backup_controller_page_backup_selected": "Valgte:", + "backup_controller_page_backup_selected": "Valgte: ", "backup_controller_page_backup_sub": "Opplastede bilder og videoer", "backup_controller_page_created": "Opprettet: {}", "backup_controller_page_desc_backup": "Slå på sikkerhetskopiering i forgrunnen for automatisk å laste opp nye objekter til serveren når du åpner appen.", - "backup_controller_page_excluded": "Ekskludert:", + "backup_controller_page_excluded": "Ekskludert: ", "backup_controller_page_failed": "Feilet ({})", "backup_controller_page_filename": "Filnavn: {} [{}]", "backup_controller_page_id": "ID: {}", diff --git a/i18n/nl.json b/i18n/nl.json index 609d2add5c..99fb554d5a 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -371,6 +371,8 @@ "admin_password": "Beheerder wachtwoord", "administration": "Beheer", "advanced": "Geavanceerd", + "advanced_settings_enable_alternate_media_filter_subtitle": "Gebruik deze optie om media te filteren tijdens de synchronisatie op basis van alternatieve criteria. Gebruik dit enkel als de app problemen heeft met het detecteren van albums.", + "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTEEL] Gebruik een alternatieve album synchronisatie filter", "advanced_settings_log_level_title": "Log niveau: {}", "advanced_settings_prefer_remote_subtitle": "Sommige apparaten zijn traag met het laden van afbeeldingen die lokaal zijn opgeslagen op het apparaat. Activeer deze instelling om in plaats daarvan externe afbeeldingen te laden.", "advanced_settings_prefer_remote_title": "Externe afbeeldingen laden", @@ -378,6 +380,7 @@ "advanced_settings_proxy_headers_title": "Proxy headers", "advanced_settings_self_signed_ssl_subtitle": "Slaat SSL-certificaatverificatie voor de connectie met de server over. Deze optie is vereist voor zelfondertekende certificaten", "advanced_settings_self_signed_ssl_title": "Zelfondertekende SSL-certificaten toestaan", + "advanced_settings_sync_remote_deletions_title": "Synchroniseer verwijderingen op afstand [EXPERIMENTEEL]", "advanced_settings_tile_subtitle": "Geavanceerde gebruikersinstellingen", "advanced_settings_troubleshooting_subtitle": "Schakel extra functies voor probleemoplossing in ", "advanced_settings_troubleshooting_title": "Probleemoplossing", diff --git a/i18n/nn.json b/i18n/nn.json index 6de0f3401b..5fd9caa232 100644 --- a/i18n/nn.json +++ b/i18n/nn.json @@ -2,8 +2,9 @@ "about": "Om", "account": "Konto", "account_settings": "Kontoinnstillingar", - "acknowledge": "Bekreft", + "acknowledge": "Merk som lese", "action": "Handling", + "action_common_update": "Oppdater", "actions": "Handlingar", "active": "Aktive", "activity": "Aktivitet", @@ -121,7 +122,7 @@ "machine_learning_max_detection_distance_description": "Den største skilnaden mellom to bilete for å rekne dei som duplikat, frå 0.001-0.1. Større verdiar finn fleire duplikat, men kan gje falske treff.", "machine_learning_max_recognition_distance": "Maksimal attkjenningsverdi", "machine_learning_min_detection_score": "Minimum deteksjonsresultat", - "machine_learning_min_detection_score_description": "Minimum tillitspoeng for at eit ansikt skal bli oppdaga, på ein skala frå 0-1. Lågare verdiar vil oppdaga fleire ansikt, men kan føre til falske positive", + "machine_learning_min_detection_score_description": "Minimum tillitspoeng for at eit ansikt skal bli oppdaga, på ein skala frå 0 til 1. Lågare verdiar vil oppdage fleire ansikt, men kan føre til feilaktige treff.", "machine_learning_min_recognized_faces": "Minimum gjenkjende ansikt", "machine_learning_settings": "Innstillingar for maskinlæring", "machine_learning_settings_description": "Administrer maskinlæringsfunksjonar og innstillingar", @@ -200,14 +201,45 @@ "backward": "Bakover", "camera": "Kamera", "cancel": "Avbryt", + "change_password_form_confirm_password": "Stadfest passord", "city": "By", - "clear": "Fjern", + "clear": "Tøm", + "clear_all": "Tøm alt", + "clear_all_recent_searches": "Tøm alle nylige søk", + "clear_message": "Tøm melding", + "clear_value": "Tøm verdi", + "client_cert_dialog_msg_confirm": "OK", + "client_cert_enter_password": "Oppgi passord", + "client_cert_import": "Importer", + "client_cert_import_success_msg": "Klientsertifikat vart importert", + "client_cert_invalid_msg": "Ugyldig sertifikatfil eller feil passord", + "client_cert_remove_msg": "Klientsertifikat er fjerna", + "client_cert_subtitle": "Støttar berre PKCS12-formatet (.p12, .pfx). Import og fjerning av sertifikat er berre tilgjengeleg før innlogging", + "client_cert_title": "SSL-klientsertifikat", "clockwise": "Med klokka", "close": "Lukk", + "collapse": "Gøym", + "collapse_all": "Gøym alle", "color": "Farge", - "confirm": "Bekreft", - "contain": "Inneheld", + "color_theme": "Fargetema", + "comment_deleted": "Kommentar vart sletta", + "comment_options": "Kommentarval", + "comments_and_likes": "Kommentarar og likerklikk", + "comments_are_disabled": "Kommentering er slått av", + "common_create_new_album": "Lag nytt album", + "common_server_error": "Kontroller nettverkstilkoplinga di, sørg for at tenaren er tilgjengeleg, og at app- og tenarversjonane er kompatible.", + "completed": "Fullført", + "confirm": "Stadfest", + "confirm_admin_password": "Stadfest administratorpassord", + "confirm_delete_face": "Er du sikker på at du vil slette {name} sitt ansikt frå ressursen?", + "confirm_delete_shared_link": "Er du sikker på at du vil slette denne delte lenka?", + "confirm_keep_this_delete_others": "Alle andre ressursar i bunken vil bli sletta, bortsett frå denne. Er du sikker på at du vil halde fram?", + "confirm_password": "Stadfest passord", + "contain": "Tilpass til vindauget", + "context": "Samanheng", "continue": "Hald fram", + "control_bottom_app_bar_album_info_shared": "{} element · Delt", + "control_bottom_app_bar_create_new_album": "Lag nytt album", "country": "Land", "cover": "Dekk", "covers": "Dekker", @@ -313,21 +345,154 @@ "purchase_server_title": "Server", "reassign": "Vel på nytt", "recent": "Nyleg", - "refresh": "Oppdater", + "refresh": "Last inn på nytt", + "refresh_encoded_videos": "Oppfrisk ferdigbehandla videoa", + "refresh_faces": "Oppfrisk ansikt", + "refresh_metadata": "Oppfrisk metadata", + "refresh_thumbnails": "Oppfrisk miniatyrbilete", "refreshed": "Oppdatert", + "refreshes_every_file": "Les alle eksisterande og nye filer på nytt", + "refreshing_encoded_video": "Lastar inn ferdigbehandla video på nytt", + "refreshing_faces": "Oppfriskar ansiktsdata", + "refreshing_metadata": "Oppfriskar metadata", + "regenerating_thumbnails": "Regenererer miniatyrbilete", "remove": "Fjern", - "rename": "Endre namn", - "repair": "Reparasjon", + "remove_assets_album_confirmation": "Er du sikker på at du vil fjerne {count, plural, one {# asset} other {# assets}} fra albumet?", + "remove_assets_shared_link_confirmation": "Er du sikker på at du vil fjerne {count, plural, one {# asset} other {# assets}} frå denne delte lenka?", + "remove_assets_title": "Fjern ressursar?", + "remove_custom_date_range": "Fjern egendefinert datoperiode", + "remove_deleted_assets": "Fjern sletta ressursar", + "remove_from_album": "Fjern frå album", + "remove_from_favorites": "Fjern frå favorittar", + "remove_from_shared_link": "Fjern frå delt lenke", + "remove_memory": "Fjern minne", + "remove_photo_from_memory": "Fjern bilete frå dette minne", + "remove_url": "Fjern URL", + "remove_user": "Fjern brukar", + "removed_api_key": "Fjerna API-nøkkel: {name}", + "removed_from_archive": "Fjerna frå arkiv", + "removed_from_favorites": "Fjerna frå favorittar", + "removed_from_favorites_count": "{count, plural, other {Fjerna #}} frå favorittar", + "removed_memory": "Fjerna minne", + "removed_photo_from_memory": "Fjerna bilete frå minne", + "removed_tagged_assets": "Fjerna tagg frå {count, plural, one {# ressurs} other {# ressursar}}", + "rename": "Gi nytt namn", + "repair": "Reparer", + "repair_no_results_message": "Uspora og manglande filer vil visast her", + "replace_with_upload": "Erstatt med opplasting", + "repository": "Lager", + "require_password": "Krev passord", + "require_user_to_change_password_on_first_login": "Krev at brukaren endrar passord ved første innlogging", + "rescan": "Skann på nytt", "reset": "Tilbakestill", - "restore": "Tilbakestill", - "resume": "Fortsett", + "reset_password": "Tilbakestill passord", + "reset_people_visibility": "Tilbakestill synlegheit for personar", + "reset_to_default": "Tilbakestill til standard", + "resolve_duplicates": "Handter duplikat", + "resolved_all_duplicates": "Alle duplikat er handterte", + "restore": "Gjenopprett", + "restore_all": "Gjenopprett alle", + "restore_user": "Gjenopprett brukar", + "restored_asset": "Ressurs gjenoppretta", + "resume": "Gjenoppta", + "retry_upload": "Prøv opplasting på nytt", + "review_duplicates": "Gå gjennom duplikat", "role": "Rolle", + "role_editor": "Redaktør", + "role_viewer": "Observatør", "save": "Lagre", + "save_to_gallery": "Lagre til galleri", + "saved_api_key": "API-nøkkel lagra", + "saved_profile": "Profil lagra", + "saved_settings": "Innstillingar lagra", + "say_something": "Skriv ein kommentar", + "scaffold_body_error_occurred": "Det oppstod ein feil", + "scan_all_libraries": "Skann gjennom alle bibliotek", "scan_library": "Skann", + "scan_settings": "Skann innstillingar", + "scanning_for_album": "Skanning for album...", "search": "Søk", + "search_albums": "Søk album", + "search_by_context": "Søk etter samanheng", + "search_by_description": "Søk etter beskrivelse", + "search_by_description_example": "Søndagstur med kvikklunsj", + "search_by_filename": "Søk etter filnamn eller filformat", + "search_by_filename_example": "t.d. IMG_1234.JPG eller PNG", + "search_camera_make": "Søk etter kamera produsent...", + "search_camera_model": "Søk etter kamera modell...", + "search_city": "Søk etter by...", + "search_country": "Søk etter land...", + "search_filter_apply": "Bruk filter", + "search_filter_camera_title": "Vel kameratype", + "search_filter_date": "Dato", + "search_filter_date_interval": "{start} til {end}", + "search_filter_date_title": "Vel eit datointervall", + "search_filter_display_option_not_in_album": "Ikkje i album", + "search_filter_display_options": "Visingsval", + "search_filter_filename": "Søk etter filnamn", + "search_filter_location": "Lokasjon", + "search_filter_location_title": "Vel lokasjon", + "search_filter_media_type": "Mediatype", + "search_filter_media_type_title": "Vel mediatype", + "search_filter_people_title": "Vel personar", + "search_for": "Søk etter", + "search_for_existing_person": "Søk etter ein eksisterande person", + "search_no_more_result": "Ingen fleire resultat", + "search_no_people": "Ingen personar", + "search_no_people_named": "Ingen personar ved namn \"{name}\"", + "search_no_result": "Fann ingen resultat – prøv eit anna søkjeord eller ei anna kombinasjon", + "search_options": "Søkjeval", + "search_page_categories": "Kategoriar", + "search_page_motion_photos": "Levande bilete", + "search_page_no_objects": "Ingen objektinformasjon tilgjengeleg", + "search_page_no_places": "Ingen stadinformasjon tilgjengeleg", + "search_page_screenshots": "Skjermbilete", + "search_page_search_photos_videos": "Søk etter bileta og videoane dine", + "search_page_selfies": "Sjølvbilete", + "search_page_things": "Ting", + "search_page_view_all_button": "Vis alle", + "search_page_your_activity": "Din aktivitet", + "search_page_your_map": "Ditt kart", + "search_people": "Søk etter personar", + "search_places": "Søk etter stad", + "search_rating": "Søk etter vurdering …", + "search_result_page_new_search_hint": "Nytt søk", + "search_settings": "Søkjeinnstillingar", + "search_state": "Søk etter fylke …", + "search_suggestion_list_smart_search_hint_1": "Smart søk er aktivert som standard. For å søkje etter metadata, bruk denne syntaksen: ", + "search_suggestion_list_smart_search_hint_2": "m:søkjeord", + "search_tags": "Søk etter taggar …", + "search_timezone": "Søk etter tidssone …", + "search_type": "Søketype", "search_your_photos": "Søk i dine bilete", + "searching_locales": "Søkjer etter språkinnstillingar…", "second": "Sekund", + "see_all_people": "Sjå alle personar", + "select": "Vel", + "select_album_cover": "Vel forsidebilete", + "select_all": "Vel alle", + "select_all_duplicates": "Vel alle duplikatar", + "select_avatar_color": "Vel avatarfarge", + "select_face": "Vel ansikt", + "select_featured_photo": "Vel framheva bilete", + "select_from_computer": "Vel frå datamaskin", + "select_keep_all": "Vel å behald alle", + "select_library_owner": "Vel bibliotekeigar", + "select_new_face": "Vel nytt ansikt", + "select_photos": "Vel bilete", + "select_trash_all": "Vel fjern alle", + "select_user_for_sharing_page_err_album": "Feil ved oppretting av album", "selected": "Valgt", + "selected_count": "{count, plural, other {# valgt}}", + "send_message": "Send melding", + "send_welcome_email": "Send velkomst-e-post", + "server_endpoint": "Tenar-endepunkt", + "server_info_box_app_version": "App Versjon", + "server_info_box_server_url": "Tenar URL", + "server_offline": "Tenar Frakopla", + "server_online": "Tenar i drift", + "server_stats": "Tenarstatistikk", + "server_version": "Tenarversjon", "set": "Sett", "settings": "Innstillingar", "share": "Del", diff --git a/i18n/pl.json b/i18n/pl.json index 1fc130ff56..eb48a69e51 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -478,14 +478,14 @@ "assets_added_to_name_count": "Dodano {count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}} do {hasName, select, true {{name}} other {new album}}", "assets_count": "{count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}}", "assets_deleted_permanently": "{} zasoby trwale usunięto", - "assets_deleted_permanently_from_server": " {} zasoby zostały trwale usunięte z serwera Immich", + "assets_deleted_permanently_from_server": "{} zasoby zostały trwale usunięte z serwera Immich", "assets_moved_to_trash_count": "Przeniesiono {count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}} do kosza", "assets_permanently_deleted_count": "Trwale usunięto {count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}}", "assets_removed_count": "Usunięto {count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}}", - "assets_removed_permanently_from_device": " {} zasoby zostały trwale usunięte z Twojego urządzenia", + "assets_removed_permanently_from_device": "{} zasoby zostały trwale usunięte z Twojego urządzenia", "assets_restore_confirmation": "Na pewno chcesz przywrócić wszystkie zasoby z kosza? Nie da się tego cofnąć! Należy pamiętać, że w ten sposób nie można przywrócić zasobów offline.", "assets_restored_count": "Przywrócono {count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}}", - "assets_restored_successfully": " {} zasoby pomyślnie przywrócono", + "assets_restored_successfully": "{} zasoby pomyślnie przywrócono", "assets_trashed": "{} zasoby zostały usunięte", "assets_trashed_count": "Wrzucono do kosza {count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}}", "assets_trashed_from_server": "{} zasoby usunięte z serwera Immich", @@ -1609,7 +1609,7 @@ "settings_saved": "Ustawienia zapisane", "share": "Udostępnij", "share_add_photos": "Dodaj zdjęcia", - "share_assets_selected": "{} wybrano ", + "share_assets_selected": "{} wybrano", "share_dialog_preparing": "Przygotowywanie...", "shared": "Udostępnione", "shared_album_activities_input_disable": "Komentarz jest wyłączony", diff --git a/i18n/pt.json b/i18n/pt.json index 3e1bd463ef..05b2ebfdd9 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "Tem a certeza que deseja desativar todos os métodos de início de sessão? O início de sessão será completamente desativado.", "authentication_settings_reenable": "Para reativar, use um Comando de servidor.", "background_task_job": "Tarefas em segundo plano", - "backup_database": "Cópia de Segurança da Base de Dados", - "backup_database_enable_description": "Ativar cópias de segurança da base de dados", - "backup_keep_last_amount": "Quantidade de cópias de segurança anteriores a manter", - "backup_settings": "Definições de Cópia de Segurança", - "backup_settings_description": "Gerir definições de cópia de segurança da base de dados", + "backup_database": "Criar Cópia da Base de Dados", + "backup_database_enable_description": "Ativar cópias da base de dados", + "backup_keep_last_amount": "Quantidade de cópias anteriores a manter", + "backup_settings": "Definições de Cópia da Base de Dados", + "backup_settings_description": "Gerir definições de cópia da base de dados. Aviso: Estas tarefas não são monitorizadas, pelo que não será notificado(a) em caso de erro.", "check_all": "Selecionar Tudo", "cleanup": "Limpeza", "cleared_jobs": "Eliminadas as tarefas de: {job}", @@ -371,13 +371,17 @@ "admin_password": "Palavra-passe do administrador", "administration": "Administração", "advanced": "Avançado", - "advanced_settings_log_level_title": "Nível de log: {}", + "advanced_settings_enable_alternate_media_filter_subtitle": "Utilize esta definição para filtrar ficheiros durante a sincronização baseada em critérios alternativos. Utilize apenas se a aplicação estiver com problemas a detetar todos os álbuns.", + "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Utilizar um filtro alternativo de sincronização de álbuns em dispositivos", + "advanced_settings_log_level_title": "Nível de registo: {}", "advanced_settings_prefer_remote_subtitle": "Alguns dispositivos são extremamente lentos para carregar miniaturas da memória. Ative esta opção para preferir imagens do servidor.", "advanced_settings_prefer_remote_title": "Preferir imagens do servidor", "advanced_settings_proxy_headers_subtitle": "Defina os cabeçalhos do proxy que o Immich deve enviar em todas comunicações com a rede", "advanced_settings_proxy_headers_title": "Cabeçalhos do Proxy", "advanced_settings_self_signed_ssl_subtitle": "Não validar o certificado SSL com o endereço do servidor. Isto é necessário para certificados auto-assinados.", "advanced_settings_self_signed_ssl_title": "Permitir certificados SSL auto-assinados", + "advanced_settings_sync_remote_deletions_subtitle": "Automaticamente eliminar ou restaurar um ficheiro neste dispositivo quando essa mesma ação for efetuada na web", + "advanced_settings_sync_remote_deletions_title": "Sincronizar ficheiros eliminados remotamente [EXPERIMENTAL]", "advanced_settings_tile_subtitle": "Configurações avançadas do usuário", "advanced_settings_troubleshooting_subtitle": "Ativar funcionalidades adicionais para a resolução de problemas", "advanced_settings_troubleshooting_title": "Resolução de problemas", @@ -400,9 +404,9 @@ "album_remove_user_confirmation": "Tem a certeza de que quer remover {user}?", "album_share_no_users": "Parece que tem este álbum partilhado com todos os utilizadores ou que não existem utilizadores com quem o partilhar.", "album_thumbnail_card_item": "1 arquivo", - "album_thumbnail_card_items": "{} arquivos", + "album_thumbnail_card_items": "{} ficheiros", "album_thumbnail_card_shared": " · Compartilhado", - "album_thumbnail_shared_by": "Compartilhado por {}", + "album_thumbnail_shared_by": "Partilhado por {}", "album_updated": "Álbum atualizado", "album_updated_setting_description": "Receber uma notificação por e-mail quando um álbum partilhado tiver novos ficheiros", "album_user_left": "Saíu do {album}", @@ -440,7 +444,7 @@ "archive": "Arquivo", "archive_or_unarchive_photo": "Arquivar ou desarquivar foto", "archive_page_no_archived_assets": "Nenhum arquivo encontrado", - "archive_page_title": "Arquivado ({})", + "archive_page_title": "Arquivo ({})", "archive_size": "Tamanho do arquivo", "archive_size_description": "Configure o tamanho do arquivo para transferências (em GiB)", "archived": "Arquivado", @@ -477,18 +481,18 @@ "assets_added_to_album_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}} ao álbum", "assets_added_to_name_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}} a {hasName, select, true {{name}} other {novo álbum}}", "assets_count": "{count, plural, one {# ficheiro} other {# ficheiros}}", - "assets_deleted_permanently": "{} arquivo(s) excluído permanentemente", - "assets_deleted_permanently_from_server": "{} arquivo(s) excluídos permanentemente do servidor", + "assets_deleted_permanently": "{} ficheiro(s) eliminado(s) permanentemente", + "assets_deleted_permanently_from_server": "{} ficheiro(s) eliminado(s) permanentemente do servidor Immich", "assets_moved_to_trash_count": "{count, plural, one {# ficheiro movido} other {# ficheiros movidos}} para a reciclagem", "assets_permanently_deleted_count": "{count, plural, one {# ficheiro} other {# ficheiros}} eliminados permanentemente", "assets_removed_count": "{count, plural, one {# ficheiro eliminado} other {# ficheiros eliminados}}", - "assets_removed_permanently_from_device": "{} arquivo(s) removidos permanentemente do seu dispositivo", + "assets_removed_permanently_from_device": "{} ficheiro(s) removido(s) permanentemente do seu dispositivo", "assets_restore_confirmation": "Tem a certeza de que quer recuperar todos os ficheiros apagados? Não é possível anular esta ação! Tenha em conta de que quaisquer ficheiros indisponíveis não podem ser restaurados desta forma.", "assets_restored_count": "{count, plural, one {# ficheiro restaurado} other {# ficheiros restaurados}}", - "assets_restored_successfully": "{} arquivo(s) restaurados com sucesso", - "assets_trashed": "{} arquivo(s) enviados para a lixeira", + "assets_restored_successfully": "{} ficheiro(s) restaurados com sucesso", + "assets_trashed": "{} ficheiro(s) enviado(s) para a reciclagem", "assets_trashed_count": "{count, plural, one {# ficheiro enviado} other {# ficheiros enviados}} para a reciclagem", - "assets_trashed_from_server": "{} arquivo(s) do servidor foram enviados para a lixeira", + "assets_trashed_from_server": "{} ficheiro(s) do servidor Immich foi/foram enviados para a reciclagem", "assets_were_part_of_album_count": "{count, plural, one {O ficheiro já fazia} other {Os ficheiros já faziam}} parte do álbum", "authorized_devices": "Dispositivos Autorizados", "automatic_endpoint_switching_subtitle": "Conecte-se localmente quando estiver em uma rede uma Wi-Fi específica e use conexões alternativas em outras redes", @@ -506,11 +510,11 @@ "backup_all": "Tudo", "backup_background_service_backup_failed_message": "Falha ao fazer backup dos arquivos. Tentando novamente…", "backup_background_service_connection_failed_message": "Falha na conexão com o servidor. Tentando novamente...", - "backup_background_service_current_upload_notification": "Enviando {}", + "backup_background_service_current_upload_notification": "A enviar {}", "backup_background_service_default_notification": "Verificando novos arquivos…", "backup_background_service_error_title": "Erro de backup", "backup_background_service_in_progress_notification": "Fazendo backup dos arquivos…", - "backup_background_service_upload_failure_notification": "Falha ao carregar {}", + "backup_background_service_upload_failure_notification": "Ocorreu um erro ao enviar {}", "backup_controller_page_albums": "Backup Álbuns", "backup_controller_page_background_app_refresh_disabled_content": "Para utilizar o backup em segundo plano, ative a atualização da aplicação em segundo plano em Configurações > Geral > Atualização do app em segundo plano ", "backup_controller_page_background_app_refresh_disabled_title": "Atualização do app em segundo plano desativada", @@ -521,7 +525,7 @@ "backup_controller_page_background_battery_info_title": "Otimizações de bateria", "backup_controller_page_background_charging": "Apenas enquanto carrega a bateria", "backup_controller_page_background_configure_error": "Falha ao configurar o serviço em segundo plano", - "backup_controller_page_background_delay": "Atrasar o backup de novos arquivos: {}", + "backup_controller_page_background_delay": "Atrasar a cópia de segurança de novos ficheiros: {}", "backup_controller_page_background_description": "Ative o serviço em segundo plano para fazer backup automático de novos arquivos sem precisar abrir o aplicativo", "backup_controller_page_background_is_off": "O backup automático em segundo plano está desativado", "backup_controller_page_background_is_on": "O backup automático em segundo plano está ativado", @@ -529,14 +533,14 @@ "backup_controller_page_background_turn_on": "Ativar o serviço em segundo plano", "backup_controller_page_background_wifi": "Apenas no WiFi", "backup_controller_page_backup": "Backup", - "backup_controller_page_backup_selected": "Selecionado:", + "backup_controller_page_backup_selected": "Selecionado: ", "backup_controller_page_backup_sub": "Fotos e vídeos salvos em backup", "backup_controller_page_created": "Criado em: {}", "backup_controller_page_desc_backup": "Ative o backup para enviar automáticamente novos arquivos para o servidor.", - "backup_controller_page_excluded": "Excluídos:", + "backup_controller_page_excluded": "Eliminado: ", "backup_controller_page_failed": "Falhou ({})", - "backup_controller_page_filename": "Nome do arquivo: {} [{}]", - "backup_controller_page_id": "ID:{}", + "backup_controller_page_filename": "Nome do ficheiro: {} [{}]", + "backup_controller_page_id": "ID: {}", "backup_controller_page_info": "Informações do backup", "backup_controller_page_none_selected": "Nenhum selecionado", "backup_controller_page_remainder": "Restante", @@ -545,7 +549,7 @@ "backup_controller_page_start_backup": "Iniciar Backup", "backup_controller_page_status_off": "Backup automático desativado", "backup_controller_page_status_on": "Backup automático ativado", - "backup_controller_page_storage_format": "{} de {} usados", + "backup_controller_page_storage_format": "{} de {} utilizado", "backup_controller_page_to_backup": "Álbuns para fazer backup", "backup_controller_page_total_sub": "Todas as fotos e vídeos dos álbuns selecionados", "backup_controller_page_turn_off": "Desativar backup", @@ -570,21 +574,21 @@ "bulk_keep_duplicates_confirmation": "Tem a certeza de que deseja manter {count, plural, one {# ficheiro duplicado} other {# ficheiros duplicados}}? Isto resolverá todos os grupos duplicados sem eliminar nada.", "bulk_trash_duplicates_confirmation": "Tem a certeza de que deseja mover para a reciclagem {count, plural, one {# ficheiro duplicado} other {# ficheiros duplicados}}? Isto manterá o maior ficheiro de cada grupo e irá mover para a reciclagem todos os outros duplicados.", "buy": "Comprar Immich", - "cache_settings_album_thumbnails": "Miniaturas da página da biblioteca ({} arquivos)", + "cache_settings_album_thumbnails": "Miniaturas da página da biblioteca ({} ficheiros)", "cache_settings_clear_cache_button": "Limpar cache", "cache_settings_clear_cache_button_title": "Limpa o cache do aplicativo. Isso afetará significativamente o desempenho do aplicativo até que o cache seja reconstruído.", "cache_settings_duplicated_assets_clear_button": "LIMPAR", "cache_settings_duplicated_assets_subtitle": "Fotos e vídeos que estão na lista negra da aplicação", - "cache_settings_duplicated_assets_title": "Arquivos duplicados ({})", - "cache_settings_image_cache_size": "Tamanho do cache de imagem ({} arquivos)", + "cache_settings_duplicated_assets_title": "Ficheiros duplicados ({})", + "cache_settings_image_cache_size": "Tamanho da cache de imagem ({} ficheiros)", "cache_settings_statistics_album": "Miniaturas da biblioteca", - "cache_settings_statistics_assets": "{} arquivos ({})", + "cache_settings_statistics_assets": "{} ficheiros ({})", "cache_settings_statistics_full": "Imagens completas", "cache_settings_statistics_shared": "Miniaturas de álbuns compartilhados", "cache_settings_statistics_thumbnail": "Miniaturas", "cache_settings_statistics_title": "Uso de cache", "cache_settings_subtitle": "Controle o comportamento de cache do aplicativo Immich", - "cache_settings_thumbnail_size": "Tamanho do cache de miniaturas ({} arquivos)", + "cache_settings_thumbnail_size": "Tamanho da cache das miniaturas ({} ficheiros)", "cache_settings_tile_subtitle": "Controlar o comportamento do armazenamento local", "cache_settings_tile_title": "Armazenamento local", "cache_settings_title": "Configurações de cache", @@ -606,7 +610,7 @@ "change_password": "Alterar a palavra-passe", "change_password_description": "Esta é a primeira vez que está a entrar no sistema ou um pedido foi feito para alterar a sua palavra-passe. Insira a nova palavra-passe abaixo.", "change_password_form_confirm_password": "Confirme a senha", - "change_password_form_description": "Esta é a primeira vez que você está acessando o sistema ou foi feita uma solicitação para alterar sua senha. Por favor, insira a nova senha abaixo.", + "change_password_form_description": "Olá, {name}\n\nEsta é a primeira vez que está a aceder ao sistema, ou então foi feito um pedido para alterar a palavra-passe. Por favor insira uma nova palavra-passe abaixo.", "change_password_form_new_password": "Nova senha", "change_password_form_password_mismatch": "As senhas não estão iguais", "change_password_form_reenter_new_password": "Confirme a nova senha", @@ -654,7 +658,7 @@ "contain": "Ajustar", "context": "Contexto", "continue": "Continuar", - "control_bottom_app_bar_album_info_shared": "{} arquivos · Compartilhado", + "control_bottom_app_bar_album_info_shared": "{} ficheiros · Partilhado", "control_bottom_app_bar_create_new_album": "Criar novo álbum", "control_bottom_app_bar_delete_from_immich": "Excluir do Immich", "control_bottom_app_bar_delete_from_local": "Excluir do dispositivo", @@ -763,7 +767,7 @@ "download_enqueue": "Na fila", "download_error": "Erro ao baixar", "download_failed": "Falha", - "download_filename": "arquivo: {}", + "download_filename": "ficheiro: {}", "download_finished": "Concluído", "download_include_embedded_motion_videos": "Vídeos incorporados", "download_include_embedded_motion_videos_description": "Incluir vídeos incorporados em fotos em movimento como um ficheiro separado", @@ -953,10 +957,10 @@ "exif_bottom_sheet_location": "LOCALIZAÇÃO", "exif_bottom_sheet_people": "PESSOAS", "exif_bottom_sheet_person_add_person": "Adicionar nome", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", + "exif_bottom_sheet_person_age": "Idade {}", + "exif_bottom_sheet_person_age_months": "Idade {} meses", + "exif_bottom_sheet_person_age_year_months": "Idade 1 ano, {} meses", + "exif_bottom_sheet_person_age_years": "Idade {}", "exit_slideshow": "Sair da apresentação", "expand_all": "Expandir tudo", "experimental_settings_new_asset_list_subtitle": "Trabalho em andamento", @@ -974,7 +978,7 @@ "external": "Externo", "external_libraries": "Bibliotecas externas", "external_network": "Rede externa", - "external_network_sheet_info": "Quando não estiver na rede Wi-Fi especificada, o aplicativo irá se conectar usando a primeira URL abaixo que obtiver sucesso, começando do topo da lista para baixo.", + "external_network_sheet_info": "Quando não estiver ligado à rede Wi-Fi especificada, a aplicação irá ligar-se utilizando o primeiro URL abaixo que conseguir aceder, a começar do topo da lista para baixo.", "face_unassigned": "Sem atribuição", "failed": "Falhou", "failed_to_load_assets": "Falha ao carregar ficheiros", @@ -992,6 +996,7 @@ "filetype": "Tipo de ficheiro", "filter": "Filtro", "filter_people": "Filtrar pessoas", + "filter_places": "Filtrar lugares", "find_them_fast": "Encontre-as mais rapidamente pelo nome numa pesquisa", "fix_incorrect_match": "Corrigir correspondência incorreta", "folder": "Folder", @@ -1203,7 +1208,7 @@ "memories_start_over": "Ver de novo", "memories_swipe_to_close": "Deslize para cima para fechar", "memories_year_ago": "Um ano atrás", - "memories_years_ago": "{} anos atrás", + "memories_years_ago": "Há {} anos atrás", "memory": "Memória", "memory_lane_title": "Memórias {title}", "menu": "Menu", @@ -1282,6 +1287,7 @@ "onboarding_welcome_user": "Bem-vindo(a), {user}", "online": "Online", "only_favorites": "Apenas favoritos", + "open": "Abrir", "open_in_map_view": "Abrir na visualização de mapa", "open_in_openstreetmap": "Abrir no OpenStreetMap", "open_the_search_filters": "Abrir os filtros de pesquisa", @@ -1305,7 +1311,7 @@ "partner_page_partner_add_failed": "Falha ao adicionar parceiro", "partner_page_select_partner": "Selecionar parceiro", "partner_page_shared_to_title": "Compartilhar com", - "partner_page_stop_sharing_content": "{} não poderá mais acessar as suas fotos.", + "partner_page_stop_sharing_content": "{} irá deixar de ter acesso às suas fotos.", "partner_sharing": "Partilha com Parceiro", "partners": "Parceiros", "password": "Palavra-passe", @@ -1590,7 +1596,7 @@ "setting_languages_apply": "Aplicar", "setting_languages_subtitle": "Alterar o idioma do aplicativo", "setting_languages_title": "Idioma", - "setting_notifications_notify_failures_grace_period": "Notifique falhas de backup em segundo plano: {}", + "setting_notifications_notify_failures_grace_period": "Notificar erros da cópia de segurança em segundo plano: {}", "setting_notifications_notify_hours": "{} horas", "setting_notifications_notify_immediately": "imediatamente", "setting_notifications_notify_minutes": "{} minutos", @@ -1626,7 +1632,7 @@ "shared_intent_upload_button_progress_text": "Enviados {} de {}", "shared_link_app_bar_title": "Links compartilhados", "shared_link_clipboard_copied_massage": "Copiado para a área de transferência", - "shared_link_clipboard_text": "Link: {}\nPassword: {}", + "shared_link_clipboard_text": "Link: {}\nPalavra-passe: {}", "shared_link_create_error": "Erro ao criar o link compartilhado", "shared_link_edit_description_hint": "Digite a descrição do compartilhamento", "shared_link_edit_expire_after_option_day": "1 dia", @@ -1635,7 +1641,7 @@ "shared_link_edit_expire_after_option_hours": "{} horas", "shared_link_edit_expire_after_option_minute": "1 minuto", "shared_link_edit_expire_after_option_minutes": "{} minutos", - "shared_link_edit_expire_after_option_months": "{} Mêses", + "shared_link_edit_expire_after_option_months": "{} meses", "shared_link_edit_expire_after_option_year": "{} ano", "shared_link_edit_password_hint": "Digite uma senha para proteger este link", "shared_link_edit_submit_button": "Atualizar link", @@ -1749,7 +1755,7 @@ "theme_selection": "Selecionar tema", "theme_selection_description": "Definir automaticamente o tema como claro ou escuro com base na preferência do sistema do seu navegador", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de armazenamento na grade de fotos", - "theme_setting_asset_list_tiles_per_row_title": "Quantidade de arquivos por linha ({})", + "theme_setting_asset_list_tiles_per_row_title": "Quantidade de ficheiros por linha ({})", "theme_setting_colorful_interface_subtitle": "Aplica a cor primária ao fundo", "theme_setting_colorful_interface_title": "Interface colorida", "theme_setting_image_viewer_quality_subtitle": "Ajuste a qualidade do visualizador de imagens detalhadas", @@ -1784,11 +1790,11 @@ "trash_no_results_message": "Fotos e vídeos enviados para a reciclagem aparecem aqui.", "trash_page_delete_all": "Excluir tudo", "trash_page_empty_trash_dialog_content": "Deseja esvaziar a lixera? Estes arquivos serão apagados de forma permanente do Immich", - "trash_page_info": "Arquivos na lixeira são excluídos de forma permanente após {} dias", + "trash_page_info": "Ficheiros na reciclagem irão ser eliminados permanentemente após {} dias", "trash_page_no_assets": "Lixeira vazia", "trash_page_restore_all": "Restaurar tudo", "trash_page_select_assets_btn": "Selecionar arquivos", - "trash_page_title": "Lixeira ({})", + "trash_page_title": "Reciclagem ({})", "trashed_items_will_be_permanently_deleted_after": "Os itens da reciclagem são eliminados permanentemente após {days, plural, one {# dia} other {# dias}}.", "type": "Tipo", "unarchive": "Desarquivar", diff --git a/i18n/pt_BR.json b/i18n/pt_BR.json index 982455a697..3db825d567 100644 --- a/i18n/pt_BR.json +++ b/i18n/pt_BR.json @@ -4,6 +4,7 @@ "account_settings": "Configurações da Conta", "acknowledge": "Entendi", "action": "Ação", + "action_common_update": "Atualizar", "actions": "Ações", "active": "Em execução", "activity": "Atividade", @@ -832,6 +833,7 @@ "filename": "Nome do arquivo", "filetype": "Tipo de arquivo", "filter_people": "Filtrar pessoas", + "filter_places": "Filtrar lugares", "find_them_fast": "Encontre pelo nome em uma pesquisa", "fix_incorrect_match": "Corrigir correspondência incorreta", "folders": "Pastas", diff --git a/i18n/ro.json b/i18n/ro.json index 7b4913e348..82bdc6d1dd 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -371,6 +371,8 @@ "admin_password": "Parolă Administrator", "administration": "Administrare", "advanced": "Avansat", + "advanced_settings_enable_alternate_media_filter_subtitle": "Utilizați această opțiune pentru a filtra conținutul media în timpul sincronizării pe baza unor criterii alternative. Încercați numai dacă întâmpinați probleme cu aplicația la detectarea tuturor albumelor.", + "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Utilizați filtrul alternativ de sincronizare a albumelor de pe dispozitiv", "advanced_settings_log_level_title": "Nivel log: {}", "advanced_settings_prefer_remote_subtitle": "Unele dispozitive întâmpină dificultăți în încărcarea miniaturilor pentru resursele de pe dispozitiv. Activează această setare pentru a încărca imaginile de la distanță în schimb.", "advanced_settings_prefer_remote_title": "Preferă fotografii la distanță", @@ -378,6 +380,8 @@ "advanced_settings_proxy_headers_title": "Proxy Headers", "advanced_settings_self_signed_ssl_subtitle": "Omite verificare certificate SSL pentru distinația server-ului, necesar pentru certificate auto-semnate.", "advanced_settings_self_signed_ssl_title": "Permite certificate SSL auto-semnate", + "advanced_settings_sync_remote_deletions_subtitle": "Ștergeți sau restaurați automat un element de pe acest dispozitiv atunci când acțiunea este efectuată pe web", + "advanced_settings_sync_remote_deletions_title": "Sincronizează stergerile efectuate la distanță [EXPERIMENTAL]", "advanced_settings_tile_subtitle": "Setări avansate pentru utilizator", "advanced_settings_troubleshooting_subtitle": "Activează funcționalități suplimentare pentru depanare", "advanced_settings_troubleshooting_title": "Depanare", @@ -401,7 +405,7 @@ "album_share_no_users": "Se pare că ai partajat acest album cu toți utilizatorii sau nu ai niciun utilizator cu care să-l partajezi.", "album_thumbnail_card_item": "1 element", "album_thumbnail_card_items": "{} elemente", - "album_thumbnail_card_shared": "Distribuit", + "album_thumbnail_card_shared": " · Distribuit", "album_thumbnail_shared_by": "Distribuit de {}", "album_updated": "Album actualizat", "album_updated_setting_description": "Primiți o notificare prin e-mail când un album partajat are elemente noi", @@ -504,12 +508,12 @@ "backup_album_selection_page_selection_info": "Informații selecție", "backup_album_selection_page_total_assets": "Total resurse unice", "backup_all": "Toate", - "backup_background_service_backup_failed_message": "Eșuare backup resurse. Re-încercare...", - "backup_background_service_connection_failed_message": "Conectare la server eșuată. Reîncercare...", + "backup_background_service_backup_failed_message": "Eșuare backup resurse. Reîncercare…", + "backup_background_service_connection_failed_message": "Conectare la server eșuată. Reîncercare…", "backup_background_service_current_upload_notification": "Încărcare {}", - "backup_background_service_default_notification": "Verificare resurse noi...", + "backup_background_service_default_notification": "Verificare resurse noi…", "backup_background_service_error_title": "Eroare backup", - "backup_background_service_in_progress_notification": "Se face backup al resurselor tale...", + "backup_background_service_in_progress_notification": "Se face backup al resurselor tale…", "backup_background_service_upload_failure_notification": "Încărcare eșuată {}", "backup_controller_page_albums": "Backup albume", "backup_controller_page_background_app_refresh_disabled_content": "Activează reîmprospătarea aplicației în fundal în Setări > General > Reîmprospătare aplicații în fundal pentru a folosi copia de siguranță în fundal.", @@ -529,11 +533,11 @@ "backup_controller_page_background_turn_on": "Activează serviciul în fundal", "backup_controller_page_background_wifi": "Doar conectat la WiFi", "backup_controller_page_backup": "Backup", - "backup_controller_page_backup_selected": "Selectat(e):", + "backup_controller_page_backup_selected": "Selectat(e): ", "backup_controller_page_backup_sub": "S-a făcut backup pentru fotografii și videoclipuri", "backup_controller_page_created": "Creat la: {}", - "backup_controller_page_desc_backup": "Activează backup-ul în prim-plan pentru a încărca resursele pe server când deschizi aplicația ", - "backup_controller_page_excluded": "Exclus(e):", + "backup_controller_page_desc_backup": "Activează backup-ul în prim-plan pentru a încărca resursele pe server când deschizi aplicația.", + "backup_controller_page_excluded": "Exclus(e): ", "backup_controller_page_failed": "Eșuate ({})", "backup_controller_page_filename": "Nume fișier: {} [{}]", "backup_controller_page_id": "ID: {}", @@ -721,7 +725,7 @@ "delete_dialog_alert": "Aceste elemente vor fi șterse permanent de pe server-ul Immich și din dispozitivul tău", "delete_dialog_alert_local": "Aceste fișiere vor fi șterse permanent din dispozitiv, dar vor fi disponibile pe server-ul Immich", "delete_dialog_alert_local_non_backed_up": "Pentru unele fișere nu s-a făcut backup în Immich și vor fi șterse permanent din dispozitiv", - "delete_dialog_alert_remote": "Aceste fișiere vor fi șterse permanent de pe server-ul Immich.", + "delete_dialog_alert_remote": "Aceste fișiere vor fi șterse permanent de pe server-ul Immich", "delete_dialog_ok_force": "Șterge oricum", "delete_dialog_title": "Șterge permanent", "delete_duplicates_confirmation": "Sunteți sigur că doriți să ștergeți permanent aceste duplicate?", @@ -960,7 +964,7 @@ "exit_slideshow": "Ieșire din Prezentare", "expand_all": "Extindeți-le pe toate", "experimental_settings_new_asset_list_subtitle": "Acțiune în desfășurare", - "experimental_settings_new_asset_list_title": "Activează grila experimentală de fotografii.", + "experimental_settings_new_asset_list_title": "Activează grila experimentală de fotografii", "experimental_settings_subtitle": "Folosește pe propria răspundere!", "experimental_settings_title": "Experimental", "expire_after": "Expiră după", @@ -992,6 +996,7 @@ "filetype": "Tipul fișierului", "filter": "Filter", "filter_people": "Filtrați persoanele", + "filter_places": "Filtrează locurile", "find_them_fast": "Găsiți-le rapid prin căutare după nume", "fix_incorrect_match": "Remediați potrivirea incorectă", "folder": "Folder", @@ -1040,7 +1045,7 @@ "home_page_delete_remote_err_local": "Resursele locale sunt în selecția pentru ștergere la distanță, omitere", "home_page_favorite_err_local": "Resursele locale nu pot fi adăugate la favorite încă, omitere", "home_page_favorite_err_partner": "Momentan nu se pot adăuga fișierele partenerului la favorite, omitere", - "home_page_first_time_notice": "Dacă este prima dată când utilizezi aplicația, te rugăm să te asiguri că alegi unul sau mai multe albume de backup, astfel încât cronologia să poată fi populată cu fotografiile și videoclipurile din aceste albume.", + "home_page_first_time_notice": "Dacă este prima dată când utilizezi aplicația, te rugăm să te asiguri că alegi unul sau mai multe albume de backup, astfel încât cronologia să poată fi populată cu fotografiile și videoclipurile din aceste albume", "home_page_share_err_local": "Nu se pot distribui fișiere locale prin link, omitere", "home_page_upload_err_limit": "Se pot încărca maxim 30 de resurse odată, omitere", "host": "Gazdă", @@ -1282,6 +1287,7 @@ "onboarding_welcome_user": "Bun venit, {user}", "online": "Online", "only_favorites": "Doar favorite", + "open": "Deschide", "open_in_map_view": "Deschideți în vizualizarea hărții", "open_in_openstreetmap": "Deschideți în OpenStreetMap", "open_the_search_filters": "Deschideți filtrele de căutare", @@ -1339,7 +1345,7 @@ "permission_onboarding_get_started": "Începe", "permission_onboarding_go_to_settings": "Mergi la setări", "permission_onboarding_permission_denied": "Permisiune refuzată. Pentru a utiliza Immich, acordă permisiuni pentru fotografii și videoclipuri în Setări.", - "permission_onboarding_permission_granted": "Permisiune acordată!", + "permission_onboarding_permission_granted": "Permisiune acordată! Sunteți gata.", "permission_onboarding_permission_limited": "Permisiune limitată. Pentru a permite Immich să facă copii de siguranță și să gestioneze întreaga colecție de galerii, acordă permisiuni pentru fotografii și videoclipuri în Setări.", "permission_onboarding_request": "Immich necesită permisiunea de a vizualiza fotografiile și videoclipurile tale.", "person": "Persoanǎ", @@ -1526,7 +1532,7 @@ "search_page_categories": "Categorii", "search_page_motion_photos": "Fotografii în mișcare", "search_page_no_objects": "Nu sunt informații disponibile despre obiecte", - "search_page_no_places": "Nici o informație disponibilă despre locuri ", + "search_page_no_places": "Nici o informație disponibilă despre locuri", "search_page_screenshots": "Capturi de ecran", "search_page_search_photos_videos": "Search for your photos and videos", "search_page_selfies": "Selfie-uri", @@ -1540,7 +1546,7 @@ "search_result_page_new_search_hint": "Căutare nouă", "search_settings": "Setări de căutare", "search_state": "Starea căutării...", - "search_suggestion_list_smart_search_hint_1": "Căutarea inteligentă este activată în mod implicit, pentru a căuta metadata, utilizează sintaxa\n", + "search_suggestion_list_smart_search_hint_1": "Căutarea inteligentă este activată în mod implicit, pentru a căuta metadata, utilizează sintaxa ", "search_suggestion_list_smart_search_hint_2": "m:termen-de-căutare", "search_tags": "Căutați etichete...", "search_timezone": "Căutați fusul orar...", @@ -1615,7 +1621,7 @@ "shared_album_activities_input_disable": "Cometariile sunt dezactivate", "shared_album_activity_remove_content": "Dorești să ștergi această activitate?", "shared_album_activity_remove_title": "Șterge activitate", - "shared_album_section_people_action_error": "Eroare la părăsirea/ștergerea din album.", + "shared_album_section_people_action_error": "Eroare la părăsirea/ștergerea din album", "shared_album_section_people_action_leave": "Șterge utilizator din album", "shared_album_section_people_action_remove_user": "Șterge utilizator din album", "shared_album_section_people_title": "PERSOANE", @@ -1852,9 +1858,9 @@ "version_announcement_message": "Bună! Este disponibilă o nouă versiune de Immich. Vă rugăm să vă faceți timp să citiți notele de lansare pentru a vă asigura că configurația dvs. este actualizată pentru a preveni orice configurare greșită, mai ales dacă utilizați WatchTower sau orice mecanism care se ocupă de actualizarea automată a instanței dvs. Immich.", "version_announcement_overlay_release_notes": "informații update", "version_announcement_overlay_text_1": "Salut, există un update nou pentru", - "version_announcement_overlay_text_2": "te rugăm verifică", - "version_announcement_overlay_text_3": "și asigură-te că fișierul .env și configurația ta docker-compose sunt actualizate pentru a preveni orice erori de configurație, în special dacă folosești WatchTower sau orice mecanism care gestionează actualizarea automată a aplicației server-ului tău.", - "version_announcement_overlay_title": "O nouă versiune pentru server este disponibilă 🎉", + "version_announcement_overlay_text_2": "te rugăm verifică ", + "version_announcement_overlay_text_3": " și asigură-te că fișierul .env și configurația ta docker-compose sunt actualizate pentru a preveni orice erori de configurație, în special dacă folosești WatchTower sau orice mecanism care gestionează actualizarea automată a aplicației server-ului tău.", + "version_announcement_overlay_title": "O nouă versiune pentru server este disponibilă 🎉", "version_history": "Istoric Versiuni", "version_history_item": "Instalat {version} pe data de {date}", "video": "Videoclip", diff --git a/i18n/ru.json b/i18n/ru.json index ab883f9aec..a78a9d6701 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "Вы уверены, что хотите отключить все методы входа? Вход будет полностью отключен.", "authentication_settings_reenable": "Чтобы снова включить, используйте Команду сервера.", "background_task_job": "Фоновые задачи", - "backup_database": "Резервное копирование базы данных", - "backup_database_enable_description": "Включить резервное копирование базы данных", - "backup_keep_last_amount": "Количество хранимых резервных копий", - "backup_settings": "Настройки резервного копирования", - "backup_settings_description": "Управление настройками резервного копирования базы данных", + "backup_database": "Создать резервную копию базы данных", + "backup_database_enable_description": "Включить дампы базы данных", + "backup_keep_last_amount": "Количество хранимых резервных копий базы данных", + "backup_settings": "Настройки резервного копирования базы данных", + "backup_settings_description": "Настройки автоматического создания резервных копий базы данных. Примечание: выполнение не контролируется, вы не получите уведомление в случае сбоя.", "check_all": "Проверить все", "cleanup": "Очистка", "cleared_jobs": "Очищены задачи для: {job}", @@ -54,9 +54,9 @@ "confirm_reprocess_all_faces": "Вы уверены, что хотите повторно определить все лица? Будут также удалены имена со всех лиц.", "confirm_user_password_reset": "Вы уверены, что хотите сбросить пароль пользователя {user}?", "create_job": "Создать задание", - "cron_expression": "Выражение cron", - "cron_expression_description": "Задайте интервал сканирований в формате cron. Для получения дополнительной информации, ознакомьтесь с Crontab Guru", - "cron_expression_presets": "Предустановки выражений cron", + "cron_expression": "Расписание (выражение планировщика cron)", + "cron_expression_description": "Частота и время выполнения задания в формате планировщика cron. Воспользуйтесь онлайн генератором Crontab Guru при необходимости.", + "cron_expression_presets": "Расписание (предустановленные варианты)", "disable_login": "Отключить вход", "duplicate_detection_job_description": "Запускает определение похожих изображений при помощи машинного зрения (зависит от умного поиска)", "exclusion_pattern_description": "Шаблоны исключения позволяют игнорировать файлы и папки при сканировании вашей библиотеки. Это полезно, если у вас есть папки, содержащие файлы, которые вы не хотите импортировать, например, RAW-файлы.", @@ -371,13 +371,17 @@ "admin_password": "Пароль администратора", "administration": "Управление сервером", "advanced": "Расширенные", - "advanced_settings_log_level_title": "Уровень логирования:", + "advanced_settings_enable_alternate_media_filter_subtitle": "Используйте этот параметр для фильтрации медиафайлов во время синхронизации на основе альтернативных критериев. Пробуйте только в том случае, если у вас есть проблемы с обнаружением приложением всех альбомов.", + "advanced_settings_enable_alternate_media_filter_title": "[ЭКСПЕРИМЕНТАЛЬНО] Использование фильтра синхронизации альбомов альтернативных устройств", + "advanced_settings_log_level_title": "Уровень логирования: {}", "advanced_settings_prefer_remote_subtitle": "Некоторые устройства очень медленно загружают локальные изображения. Активируйте эту настройку, чтобы изображения всегда загружались с сервера.", "advanced_settings_prefer_remote_title": "Предпочитать фото на сервере", - "advanced_settings_proxy_headers_subtitle": "Определите заголовки прокси-сервера, которые Immich должен отправлять с каждым сетевым запросом.", + "advanced_settings_proxy_headers_subtitle": "Определите заголовки прокси-сервера, которые Immich должен отправлять с каждым сетевым запросом", "advanced_settings_proxy_headers_title": "Заголовки прокси", "advanced_settings_self_signed_ssl_subtitle": "Пропускать проверку SSL-сертификата сервера. Требуется для самоподписанных сертификатов.", "advanced_settings_self_signed_ssl_title": "Разрешить самоподписанные SSL-сертификаты", + "advanced_settings_sync_remote_deletions_subtitle": "Автоматически удалять или восстанавливать объект на этом устройстве, когда это действие выполняется через веб-интерфейс", + "advanced_settings_sync_remote_deletions_title": "Синхронизация удаленных удалений [ЭКСПЕРИМЕНТАЛЬНО]", "advanced_settings_tile_subtitle": "Расширенные настройки", "advanced_settings_troubleshooting_subtitle": "Включить расширенные возможности для решения проблем", "advanced_settings_troubleshooting_title": "Решение проблем", @@ -401,7 +405,7 @@ "album_share_no_users": "Похоже, вы поделились этим альбомом со всеми пользователями или у вас нет пользователей, с которыми можно поделиться.", "album_thumbnail_card_item": "1 элемент", "album_thumbnail_card_items": "{} элементов", - "album_thumbnail_card_shared": "· Общий", + "album_thumbnail_card_shared": " · Общий", "album_thumbnail_shared_by": "Поделился {}", "album_updated": "Альбом обновлён", "album_updated_setting_description": "Получать уведомление по электронной почте при добавлении новых ресурсов в общий альбом", @@ -477,15 +481,15 @@ "assets_added_to_album_count": "В альбом добавлено {count, plural, one {# объект} few {# объекта} other {# объектов}}", "assets_added_to_name_count": "Добавлено {count, plural, one {# объект} few {# объекта} other {# объектов}} в {hasName, select, true {{name}} other {новый альбом}}", "assets_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}}", - "assets_deleted_permanently": "{} объект(ы) удален(ы) навсегда", - "assets_deleted_permanently_from_server": "{} объект(ы) удален(ы) навсегда с сервера Immich", + "assets_deleted_permanently": "{} объект(ы) удален(ы) навсегда", + "assets_deleted_permanently_from_server": "{} объект(ы) удален(ы) навсегда с сервера Immich", "assets_moved_to_trash_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}} перемещено в корзину", "assets_permanently_deleted_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}} удалено навсегда", "assets_removed_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}} удалено", - "assets_removed_permanently_from_device": "{} объект(ы) удален(ы) навсегда с вашего устройства", + "assets_removed_permanently_from_device": "{} объект(ы) удален(ы) навсегда с вашего устройства", "assets_restore_confirmation": "Вы уверены, что хотите восстановить все объекты из корзины? Это действие нельзя отменить! Обратите внимание, что любые оффлайн-объекты не могут быть восстановлены таким способом.", "assets_restored_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}} восстановлено", - "assets_restored_successfully": "{} объект(ы) успешно восстановлен(ы)", + "assets_restored_successfully": "{} объект(ы) успешно восстановлен(ы)", "assets_trashed": "{} объект(ы) помещен(ы) в корзину", "assets_trashed_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}} перемещено в корзину", "assets_trashed_from_server": "{} объект(ы) помещен(ы) в корзину на сервере Immich", @@ -498,14 +502,14 @@ "background_location_permission": "Доступ к местоположению в фоне", "background_location_permission_content": "Чтобы считывать имя Wi-Fi сети в фоне, приложению *всегда* необходим доступ к точному местоположению устройства", "backup_album_selection_page_albums_device": "Альбомы на устройстве ({})", - "backup_album_selection_page_albums_tap": "Нажмите, чтобы включить,\nнажмите дважды, чтобы исключить", + "backup_album_selection_page_albums_tap": "Нажмите, чтобы включить, дважды, чтобы исключить", "backup_album_selection_page_assets_scatter": "Ваши изображения и видео могут находиться в разных альбомах. Вы можете выбрать, какие альбомы включить, а какие исключить из резервного копирования.", "backup_album_selection_page_select_albums": "Выбор альбомов", "backup_album_selection_page_selection_info": "Информация о выборе", "backup_album_selection_page_total_assets": "Всего уникальных объектов", "backup_all": "Все", "backup_background_service_backup_failed_message": "Не удалось выполнить резервное копирование. Повторная попытка…", - "backup_background_service_connection_failed_message": "Не удалось подключиться к серверу. Повторная попытка...", + "backup_background_service_connection_failed_message": "Не удалось подключиться к серверу. Повторная попытка…", "backup_background_service_current_upload_notification": "Загружается {}", "backup_background_service_default_notification": "Поиск новых объектов…", "backup_background_service_error_title": "Ошибка резервного копирования", @@ -533,7 +537,7 @@ "backup_controller_page_backup_sub": "Загруженные фото и видео", "backup_controller_page_created": "Создано: {}", "backup_controller_page_desc_backup": "Включите резервное копирование в активном режиме, чтобы автоматически загружать новые объекты при открытии приложения.", - "backup_controller_page_excluded": "Исключены:", + "backup_controller_page_excluded": "Исключены: ", "backup_controller_page_failed": "Неудачных ({})", "backup_controller_page_filename": "Имя файла: {} [{}]", "backup_controller_page_id": "ID: {}", @@ -615,7 +619,7 @@ "check_all": "Выбрать всё", "check_corrupt_asset_backup": "Проверка поврежденных резервных копий", "check_corrupt_asset_backup_button": "Проверить", - "check_corrupt_asset_backup_description": "Проводите проверку только через Wi-Fi и только после резервного копирования всех объектов. Эта операция может занять несколько минут", + "check_corrupt_asset_backup_description": "Запускайте проверку только через Wi-Fi и после создания резервной копии всех объектов. Операция может занять несколько минут.", "check_logs": "Проверить журналы", "choose_matching_people_to_merge": "Выберите подходящих людей для слияния", "city": "Город", @@ -631,7 +635,7 @@ "client_cert_invalid_msg": "Неверный файл сертификата или неверный пароль", "client_cert_remove_msg": "Клиентский сертификат удален", "client_cert_subtitle": "Поддерживается только формат PKCS12 (.p12, .pfx). Импорт/удаление сертификата доступно только перед входом в систему", - "client_cert_title": "Клиентский SSL-сертификат ", + "client_cert_title": "Клиентский SSL-сертификат", "clockwise": "По часовой", "close": "Закрыть", "collapse": "Свернуть", @@ -656,11 +660,11 @@ "continue": "Продолжить", "control_bottom_app_bar_album_info_shared": "{} элементов · Общий", "control_bottom_app_bar_create_new_album": "Создать альбом", - "control_bottom_app_bar_delete_from_immich": "Удалить из Immich\n", + "control_bottom_app_bar_delete_from_immich": "Удалить из Immich", "control_bottom_app_bar_delete_from_local": "Удалить с устройства", "control_bottom_app_bar_edit_location": "Изменить место", "control_bottom_app_bar_edit_time": "Изменить дату", - "control_bottom_app_bar_share_link": "Share Link", + "control_bottom_app_bar_share_link": "Поделиться ссылкой", "control_bottom_app_bar_share_to": "Поделиться", "control_bottom_app_bar_trash_from_immich": "В корзину", "copied_image_to_clipboard": "Изображение скопировано в буфер обмена.", @@ -807,7 +811,7 @@ "editor_crop_tool_h2_aspect_ratios": "Соотношения сторон", "editor_crop_tool_h2_rotation": "Вращение", "email": "Электронная почта", - "empty_folder": "This folder is empty", + "empty_folder": "Пустая папка", "empty_trash": "Очистить корзину", "empty_trash_confirmation": "Вы уверены, что хотите очистить корзину? Все объекты в корзине будут навсегда удалены из Immich.\nВы не сможете отменить это действие!", "enable": "Включить", @@ -953,10 +957,10 @@ "exif_bottom_sheet_location": "МЕСТО", "exif_bottom_sheet_people": "ЛЮДИ", "exif_bottom_sheet_person_add_person": "Добавить имя", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", + "exif_bottom_sheet_person_age": "Возраст {}", + "exif_bottom_sheet_person_age_months": "Возраст {} месяцев", + "exif_bottom_sheet_person_age_year_months": "Возраст 1 год, {} месяцев", + "exif_bottom_sheet_person_age_years": "Возраст {}", "exit_slideshow": "Выйти из слайд-шоу", "expand_all": "Развернуть всё", "experimental_settings_new_asset_list_subtitle": "В разработке", @@ -978,7 +982,7 @@ "face_unassigned": "Не назначено", "failed": "Ошибка", "failed_to_load_assets": "Не удалось загрузить объекты", - "failed_to_load_folder": "Failed to load folder", + "failed_to_load_folder": "Ошибка при загрузке папки", "favorite": "Избранное", "favorite_or_unfavorite_photo": "Добавить или удалить фотографию из избранного", "favorites": "Избранное", @@ -992,10 +996,11 @@ "filetype": "Тип файла", "filter": "Фильтр", "filter_people": "Фильтр по людям", + "filter_places": "Фильтр по местам", "find_them_fast": "Быстро найдите их по имени с помощью поиска", "fix_incorrect_match": "Исправить неправильное соответствие", - "folder": "Folder", - "folder_not_found": "Folder not found", + "folder": "Папка", + "folder_not_found": "Папка не найдена", "folders": "Папки", "folders_feature_description": "Просмотр папок с фотографиями и видео в файловой системе", "forward": "Вперёд", @@ -1040,7 +1045,7 @@ "home_page_delete_remote_err_local": "Невозможно удалить локальные файлы с сервера, пропуск", "home_page_favorite_err_local": "Пока нельзя добавить в избранное локальные файлы, пропуск", "home_page_favorite_err_partner": "Пока нельзя добавить в избранное медиа партнера, пропуск", - "home_page_first_time_notice": "Если вы используете приложение впервые, выберите альбомы для резервного копирования или загрузите их вручную, чтобы заполнить ими временную шкалу.", + "home_page_first_time_notice": "Перед началом использования приложения выберите альбом с объектами для резервного копирования, чтобы они отобразились на временной шкале", "home_page_share_err_local": "Нельзя поделиться локальными файлами по ссылке, пропуск", "home_page_upload_err_limit": "Вы можете загрузить максимум 30 файлов за раз, пропуск", "host": "Хост", @@ -1145,7 +1150,7 @@ "login_form_failed_get_oauth_server_config": "Ошибка авторизации с использованием OAuth, проверьте URL-адрес сервера", "login_form_failed_get_oauth_server_disable": "Авторизация через OAuth недоступна на этом сервере", "login_form_failed_login": "Ошибка при входе, проверьте URL-адрес сервера, адрес электронной почты и пароль", - "login_form_handshake_exception": "Ошибка проверки сертификата. Если вы используете самоподписанный сертификат, включите поддержку самоподписанных сертификатов в настройках.", + "login_form_handshake_exception": "Ошибка проверки сертификата. Если вы используете самоподписанный сертификат, включите поддержку самоподписанных сертификатов в настройках.", "login_form_password_hint": "пароль", "login_form_save_login": "Оставаться в системе", "login_form_server_empty": "Введите URL-адрес сервера.", @@ -1185,7 +1190,7 @@ "map_settings": "Настройки карты", "map_settings_dark_mode": "Темный режим", "map_settings_date_range_option_day": "24 часа", - "map_settings_date_range_option_days": "{} дней", + "map_settings_date_range_option_days": "Последние {} дней", "map_settings_date_range_option_year": "Год", "map_settings_date_range_option_years": "{} года", "map_settings_dialog_title": "Настройки карты", @@ -1261,7 +1266,7 @@ "note_apply_storage_label_to_previously_uploaded assets": "Примечание: Чтобы применить тег хранилища к ранее загруженным ресурсам, запустите", "notes": "Примечание", "notification_permission_dialog_content": "Чтобы включить уведомления, перейдите в «Настройки» и выберите «Разрешить».", - "notification_permission_list_tile_content": "Предоставьте разрешение на включение уведомлений", + "notification_permission_list_tile_content": "Предоставьте разрешение на показ уведомлений.", "notification_permission_list_tile_enable_button": "Включить уведомления", "notification_permission_list_tile_title": "Разрешение на уведомление", "notification_toggle_setting_description": "Включить уведомления по электронной почте", @@ -1282,6 +1287,7 @@ "onboarding_welcome_user": "Добро пожаловать, {user}", "online": "Доступен", "only_favorites": "Только избранное", + "open": "Открыть", "open_in_map_view": "Открыть в режиме просмотра карты", "open_in_openstreetmap": "Открыть в OpenStreetMap", "open_the_search_filters": "Открыть фильтры поиска", @@ -1298,14 +1304,14 @@ "partner_can_access": "{partner} имеет доступ", "partner_can_access_assets": "Все ваши фотографии и видеозаписи, кроме тех, которые находятся в Архиве и Корзине", "partner_can_access_location": "Местоположение, где были сделаны ваши фотографии", - "partner_list_user_photos": "Фотографии {user}", + "partner_list_user_photos": "Фотографии пользователя {user}", "partner_list_view_all": "Посмотреть все", - "partner_page_empty_message": "У вашего партнёра еще нет доступа к вашим фото", + "partner_page_empty_message": "У вашего партнёра еще нет доступа к вашим фото.", "partner_page_no_more_users": "Выбраны все доступные пользователи", "partner_page_partner_add_failed": "Не удалось добавить партнёра", "partner_page_select_partner": "Выбрать партнёра", "partner_page_shared_to_title": "Поделиться с...", - "partner_page_stop_sharing_content": "{} больше не сможет получить доступ к вашим фотографиям", + "partner_page_stop_sharing_content": "{} больше не сможет получить доступ к вашим фотографиям.", "partner_sharing": "Совместное использование", "partners": "Партнёры", "password": "Пароль", @@ -1341,7 +1347,7 @@ "permission_onboarding_permission_denied": "Не удалось получить доступ. Чтобы использовать приложение, разрешите доступ к \"Фото и видео\" в настройках.", "permission_onboarding_permission_granted": "Доступ получен! Всё готово.", "permission_onboarding_permission_limited": "Доступ к файлам ограничен. Чтобы Immich мог создавать резервные копии и управлять вашей галереей, пожалуйста, предоставьте приложению разрешение на доступ к \"Фото и видео\" в настройках.", - "permission_onboarding_request": "Приложению необходимо разрешение на доступ к вашим фото и видео", + "permission_onboarding_request": "Приложению необходимо разрешение на доступ к вашим фото и видео.", "person": "Человек", "person_birthdate": "Дата рождения: {date}", "person_hidden": "{name}{hidden, select, true { (скрыт)} other {}}", @@ -1510,7 +1516,7 @@ "search_filter_date_title": "Выберите промежуток", "search_filter_display_option_not_in_album": "Не в альбоме", "search_filter_display_options": "Настройки отображения", - "search_filter_filename": "Search by file name", + "search_filter_filename": "Поиск по имени файла", "search_filter_location": "Место", "search_filter_location_title": "Выберите место", "search_filter_media_type": "Тип файла", @@ -1518,10 +1524,10 @@ "search_filter_people_title": "Выберите людей", "search_for": "Поиск по", "search_for_existing_person": "Поиск существующего человека", - "search_no_more_result": "No more results", + "search_no_more_result": "Больше результатов нет", "search_no_people": "Нет людей", "search_no_people_named": "Нет людей с именем \"{name}\"", - "search_no_result": "No results found, try a different search term or combination", + "search_no_result": "Ничего не найдено, попробуйте изменить поисковый запрос", "search_options": "Параметры поиска", "search_page_categories": "Категории", "search_page_motion_photos": "Динамические фото", @@ -1538,9 +1544,9 @@ "search_places": "Поиск мест", "search_rating": "Поиск по рейтингу...", "search_result_page_new_search_hint": "Новый поиск", - "search_settings": "Настройки поиска", + "search_settings": "Поиск настроек", "search_state": "Поиск региона...", - "search_suggestion_list_smart_search_hint_1": "Интеллектуальный поиск включен по умолчанию, для поиска метаданных используйте специальный синтаксис", + "search_suggestion_list_smart_search_hint_1": "Интеллектуальный поиск включен по умолчанию, для поиска метаданных используйте специальный синтаксис ", "search_suggestion_list_smart_search_hint_2": "m:ваш-поисковый-запрос", "search_tags": "Поиск по тегам...", "search_timezone": "Поиск часового пояса...", @@ -1581,10 +1587,10 @@ "set_date_of_birth": "Установить дату рождения", "set_profile_picture": "Установить изображение профиля", "set_slideshow_to_fullscreen": "Переведите слайд-шоу в полноэкранный режим", - "setting_image_viewer_help": "При просмотре изображения сперва загружается миниатюра, затем \nуменьшенное изображение среднего качества (если включено), а затем оригинал (если включено).", - "setting_image_viewer_original_subtitle": "Включите для загрузки исходного изображения в полном разрешении (большое!).\nОтключите, чтобы уменьшить объем данных (как сети, так и кэша устройства).", + "setting_image_viewer_help": "При просмотре изображения сперва загружается миниатюра, затем уменьшенное изображение среднего качества (если включено), а затем оригинал (если включено).", + "setting_image_viewer_original_subtitle": "Включите для загрузки исходного изображения в полном разрешении (большое!). Отключите для уменьшения объёма данных (как сети, так и кэша устройства).", "setting_image_viewer_original_title": "Загружать исходное изображение", - "setting_image_viewer_preview_subtitle": "Включите для загрузки изображения среднего разрешения.\nОтключите, чтобы загружать только оригинал или миниатюру.", + "setting_image_viewer_preview_subtitle": "Включите для загрузки изображения среднего разрешения. Отключите, чтобы загружать только оригинал или миниатюру.", "setting_image_viewer_preview_title": "Загружать уменьшенное изображение", "setting_image_viewer_title": "Изображения", "setting_languages_apply": "Применить", @@ -1602,7 +1608,7 @@ "setting_notifications_total_progress_subtitle": "Общий прогресс загрузки (выполнено/всего объектов)", "setting_notifications_total_progress_title": "Показать общий прогресс фонового резервного копирования", "setting_video_viewer_looping_title": "Циклическое воспроизведение", - "setting_video_viewer_original_video_subtitle": "При воспроизведении видео с сервера загружать оригинал, даже если доступна транскодированная версия. Может привести к буферизации. Не влияет на локальные видео", + "setting_video_viewer_original_video_subtitle": "При воспроизведении видео с сервера загружать оригинал, даже если доступна транскодированная версия. Может привести к буферизации. Видео, доступные локально, всегда воспроизводятся в исходном качестве.", "setting_video_viewer_original_video_title": "Только оригинальное видео", "settings": "Настройки", "settings_require_restart": "Пожалуйста, перезапустите приложение, чтобы изменения вступили в силу", @@ -1750,7 +1756,7 @@ "theme_selection_description": "Автоматически устанавливать тему в зависимости от системных настроек вашего браузера", "theme_setting_asset_list_storage_indicator_title": "Показать индикатор хранилища на плитках объектов", "theme_setting_asset_list_tiles_per_row_title": "Количество объектов в строке ({})", - "theme_setting_colorful_interface_subtitle": "Добавить оттенок к фону", + "theme_setting_colorful_interface_subtitle": "Добавить оттенок к фону.", "theme_setting_colorful_interface_title": "Цвет фона", "theme_setting_image_viewer_quality_subtitle": "Настройка качества просмотра изображения", "theme_setting_image_viewer_quality_title": "Качество просмотра изображений", @@ -1783,8 +1789,8 @@ "trash_emptied": "Корзина очищена", "trash_no_results_message": "Здесь будут отображаться удалённые фотографии и видео.", "trash_page_delete_all": "Удалить все", - "trash_page_empty_trash_dialog_content": "Очистить корзину? Эти файлы будут навсегда удалены из Immich.", - "trash_page_info": "Элементы в корзине будут окончательно удалены через {} дней", + "trash_page_empty_trash_dialog_content": "Очистить корзину? Объекты в ней будут навсегда удалены из Immich.", + "trash_page_info": "Объекты в корзине будут окончательно удалены через {} дней", "trash_page_no_assets": "Корзина пуста", "trash_page_restore_all": "Восстановить все", "trash_page_select_assets_btn": "Выбранные объекты", @@ -1817,7 +1823,7 @@ "updated_password": "Пароль обновлён", "upload": "Загрузить", "upload_concurrency": "Параллельность загрузки", - "upload_dialog_info": "Хотите создать резервную копию выбранных объектов на сервере?", + "upload_dialog_info": "Хотите загрузить выбранные объекты на сервер?", "upload_dialog_title": "Загрузить объект", "upload_errors": "Загрузка завершена с {count, plural, one {# ошибкой} other {# ошибками}}, обновите страницу, чтобы увидеть новые загруженные ресурсы.", "upload_progress": "Осталось {remaining, number} - Обработано {processed, number}/{total, number}", @@ -1849,11 +1855,11 @@ "variables": "Переменные", "version": "Версия", "version_announcement_closing": "Твой друг Алекс", - "version_announcement_message": "Здравствуйте! Доступна новая версия приложения. Пожалуйста, прочтите заметки к выпуску и убедитесь, что ваши параметры docker-compose.yml и .env актуальны, чтобы избежать ошибок в конфигурации, особенно если вы используете WatchTower или другой механизм автоматического обновления приложения.", + "version_announcement_message": "Здравствуйте! Доступна новая версия приложения. Пожалуйста, прочтите заметки к выпуску и убедитесь, что параметры конфигурации актуальны, дабы избежать ошибок, особенно если используется WatchTower или другой механизм автоматического обновления приложения.", "version_announcement_overlay_release_notes": "примечания к выпуску", "version_announcement_overlay_text_1": "Привет, друг! Вышла новая версия", - "version_announcement_overlay_text_2": "пожалуйста, посетите", - "version_announcement_overlay_text_3": " и убедитесь, что ваши настройки docker-compose и .env обновлены, особенно если вы используете WatchTower или любой другой механизм, который автоматически обновляет сервер.", + "version_announcement_overlay_text_2": "пожалуйста, посетите ", + "version_announcement_overlay_text_3": " и убедитесь, что параметры в docker-compose и .env файлах актуальны, особенно если используется WatchTower или любой другой механизм для автоматического обновления приложений.", "version_announcement_overlay_title": "Доступна новая версия сервера 🎉", "version_history": "История версий", "version_history_item": "Версия {version} установлена {date}", diff --git a/i18n/sl.json b/i18n/sl.json index 1839212037..c6b3e153d5 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -25,7 +25,7 @@ "add_to": "Dodaj v…", "add_to_album": "Dodaj v album", "add_to_album_bottom_sheet_added": "Dodano v {album}", - "add_to_album_bottom_sheet_already_exists": "Že v {albumu}", + "add_to_album_bottom_sheet_already_exists": "Že v {album}", "add_to_shared_album": "Dodaj k deljenemu albumu", "add_url": "Dodaj URL", "added_to_archive": "Dodano v arhiv", @@ -39,17 +39,17 @@ "authentication_settings_disable_all": "Ali zares želite onemogočiti vse prijavne metode? Prijava bo popolnoma onemogočena.", "authentication_settings_reenable": "Ponovno omogoči z uporabo strežniškega ukaza.", "background_task_job": "Opravila v ozadju", - "backup_database": "Varnostna kopija baze", - "backup_database_enable_description": "Omogoči varnostno kopiranje baze", - "backup_keep_last_amount": "Število prejšnjih obdržanih varnostnih kopij", - "backup_settings": "Nastavitve varnostnega kopiranja", - "backup_settings_description": "Upravljanje nastavitev varnostnih kopij", + "backup_database": "Ustvari izpis baze podatkov", + "backup_database_enable_description": "Omogoči izpise baze podatkov", + "backup_keep_last_amount": "Število prejšnjih odlagališč, ki jih je treba obdržati", + "backup_settings": "Nastavitve izpisa baze podatkov", + "backup_settings_description": "Upravljanje nastavitev izpisa baze podatkov. Opomba: Ta opravila se ne spremljajo in o neuspehu ne boste obveščeni.", "check_all": "Označi vse", "cleanup": "Čiščenje", "cleared_jobs": "Razčiščeno opravilo za: {job}", "config_set_by_file": "Konfiguracija je trenutno nastavljena s konfiguracijsko datoteko", "confirm_delete_library": "Ali ste prepričani, da želite izbrisati knjižnico {library}?", - "confirm_delete_library_assets": "Ali ste prepričani, da želite izbrisati to knjižnico? To bo iz Immicha izbrisalo {count, plural, one {# contained asset} other {all # vsebovanih virov}} in tega ni možno razveljaviti. Datoteke bodo ostale na disku.", + "confirm_delete_library_assets": "Ali ste prepričani, da želite izbrisati to knjižnico? To bo iz Immicha izbrisalo {count, plural, one {# vsebovani vir} two {# vsebovana vira} few {# vsebovane vire} other {vseh # vsebovanih virov}} in tega ni možno razveljaviti. Datoteke bodo ostale na disku.", "confirm_email_below": "Za potrditev vnesite \"{email}\" spodaj", "confirm_reprocess_all_faces": "Ali ste prepričani, da želite znova obdelati vse obraze? S tem boste počistili tudi že imenovane osebe.", "confirm_user_password_reset": "Ali ste prepričani, da želite ponastaviti geslo uporabnika {user}?", @@ -347,7 +347,7 @@ "untracked_files": "Nesledene datoteke", "untracked_files_description": "Tem datotekam aplikacija ne sledi. Lahko so posledica neuspelih premikov, prekinjenih nalaganj ali zaostalih zaradi hrošča", "user_cleanup_job": "Čiščenje uporabnika", - "user_delete_delay": "Račun in sredstva {user} bodo načrtovani za trajno brisanje čez {delay, plural, one {# day} other {# days}}.", + "user_delete_delay": "Račun in sredstva {user} bodo načrtovani za trajno brisanje čez {delay, plural, one {# dan} other {# dni}}.", "user_delete_delay_settings": "Zamakni izbris", "user_delete_delay_settings_description": "Število dni po odstranitvi za trajno brisanje uporabnikovega računa in sredstev. Opravilo za brisanje uporabnikov se izvaja ob polnoči, da se preveri, ali so uporabniki pripravljeni na izbris. Spremembe te nastavitve bodo ovrednotene pri naslednji izvedbi.", "user_delete_immediately": "Račun in sredstva uporabnika {user} bodo v čakalni vrsti za trajno brisanje takoj.", @@ -371,6 +371,8 @@ "admin_password": "Skrbniško geslo", "administration": "Administracija", "advanced": "Napredno", + "advanced_settings_enable_alternate_media_filter_subtitle": "Uporabite to možnost za filtriranje medijev med sinhronizacijo na podlagi alternativnih meril. To poskusite le, če imate težave z aplikacijo, ki zaznava vse albume.", + "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTALNO] Uporabite alternativni filter za sinhronizacijo albuma v napravi", "advanced_settings_log_level_title": "Nivo dnevnika: {}", "advanced_settings_prefer_remote_subtitle": "Nekatere naprave zelo počasi nalagajo sličice iz sredstev v napravi. Aktivirajte to nastavitev, če želite namesto tega naložiti oddaljene slike.", "advanced_settings_prefer_remote_title": "Uporabi raje oddaljene slike", @@ -378,12 +380,14 @@ "advanced_settings_proxy_headers_title": "Proxy glave", "advanced_settings_self_signed_ssl_subtitle": "Preskoči preverjanje potrdila SSL za končno točko strežnika. Zahtevano za samopodpisana potrdila.", "advanced_settings_self_signed_ssl_title": "Dovoli samopodpisana SSL potrdila", + "advanced_settings_sync_remote_deletions_subtitle": "Samodejno izbriši ali obnovi sredstvo v tej napravi, ko je to dejanje izvedeno v spletu", + "advanced_settings_sync_remote_deletions_title": "Sinhroniziraj oddaljene izbrise [EKSPERIMENTALNO]", "advanced_settings_tile_subtitle": "Napredne uporabniške nastavitve", "advanced_settings_troubleshooting_subtitle": "Omogočite dodatne funkcije za odpravljanje težav", "advanced_settings_troubleshooting_title": "Odpravljanje težav", - "age_months": "Starost {months, plural, one {# month} other {# months}}", - "age_year_months": "Starost 1 leto, {months, plural, one {# month} other {# months}}", - "age_years": "{years, plural, other {starost #}}", + "age_months": "Starost {months, plural, one {# mesec} two {# meseca} few {# mesece} other {# mesecev}}", + "age_year_months": "Starost 1 leto, {months, plural, one {# mesec} two {# meseca} few {# mesece} other {# mesecev}}", + "age_years": "{years, plural, one {# leto} two {# leti} few {# leta} other {# let}}", "album_added": "Album dodan", "album_added_notification_setting_description": "Prejmite e-poštno obvestilo, ko ste dodani v album v skupni rabi", "album_cover_updated": "Naslovnica albuma posodobljena", @@ -400,9 +404,9 @@ "album_remove_user_confirmation": "Ali ste prepričani, da želite odstraniti {user}?", "album_share_no_users": "Videti je, da ste ta album dali v skupno rabo z vsemi uporabniki ali pa nimate nobenega uporabnika, s katerim bi ga lahko delili.", "album_thumbnail_card_item": "1 element", - "album_thumbnail_card_items": "{} elementov", - "album_thumbnail_card_shared": "· V skupni rabi", - "album_thumbnail_shared_by": "Delil {}", + "album_thumbnail_card_items": "{count, plural, one {# element} two {# elementa} few {# elementi} other {# elementov}}", + "album_thumbnail_card_shared": " · V skupni rabi", + "album_thumbnail_shared_by": "Delil {user}", "album_updated": "Album posodobljen", "album_updated_setting_description": "Prejmite e-poštno obvestilo, ko ima album v skupni rabi nova sredstva", "album_user_left": "Zapustil {album}", @@ -413,11 +417,11 @@ "album_viewer_appbar_share_err_remove": "Pri odstranjevanju sredstev iz albuma so težave", "album_viewer_appbar_share_err_title": "Naslova albuma ni bilo mogoče spremeniti", "album_viewer_appbar_share_leave": "Zapusti album", - "album_viewer_appbar_share_to": "Deli z", + "album_viewer_appbar_share_to": "Deli s/z", "album_viewer_page_share_add_users": "Dodaj uporabnike", "album_with_link_access": "Omogočite vsem s povezavo ogled fotografij in ljudi v tem albumu.", "albums": "Albumi", - "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albumi}}", + "albums_count": "{count, plural, one {{count, number} album} two {{count, number} albuma} few {{count, number} albumi} other {{count, number} albumov}}", "all": "Vse", "all_albums": "Vsi albumi", "all_people": "Vsi ljudje", @@ -427,7 +431,7 @@ "allow_public_user_to_download": "Dovoli javnemu uporabniku prenos", "allow_public_user_to_upload": "Dovolite javnemu uporabniku nalaganje", "alt_text_qr_code": "Slika QR kode", - "anti_clockwise": "V nasprotni smeri urnega kazalca", + "anti_clockwise": "V nasprotni smeri urinega kazalca", "api_key": "API ključ", "api_key_description": "Ta vrednost bo prikazana samo enkrat. Ne pozabite jo kopirati, preden zaprete okno.", "api_key_empty": "Ime ključa API ne sme biti prazno", @@ -440,14 +444,14 @@ "archive": "Arhiv", "archive_or_unarchive_photo": "Arhivirajte ali odstranite fotografijo iz arhiva", "archive_page_no_archived_assets": "Ni arhiviranih sredstev", - "archive_page_title": "Arhiv ({})\n", + "archive_page_title": "Arhiv ({number})", "archive_size": "Velikost arhiva", "archive_size_description": "Konfigurirajte velikost arhiva za prenose (v GiB)", "archived": "Arhivirano", - "archived_count": "{count, plural, other {arhivirano #}}", + "archived_count": "{count, plural, one {# arhiviran} two {# arhivirana} few {# arhivirani} other {# arhiviranih}}", "are_these_the_same_person": "Ali je to ista oseba?", "are_you_sure_to_do_this": "Ste prepričani, da želite to narediti?", - "asset_action_delete_err_read_only": "Sredstev samo za branje ni mogoče izbrisati, preskočim\n", + "asset_action_delete_err_read_only": "Sredstev samo za branje ni mogoče izbrisati, preskočim", "asset_action_share_err_offline": "Ni mogoče pridobiti sredstev brez povezave, preskočim", "asset_added_to_album": "Dodano v album", "asset_adding_to_album": "Dodajanje v album…", @@ -473,23 +477,23 @@ "asset_viewer_settings_subtitle": "Upravljaj nastavitve pregledovalnika galerije", "asset_viewer_settings_title": "Pregledovalnik sredstev", "assets": "Sredstva", - "assets_added_count": "Dodano{count, plural, one {# sredstvo} other {# sredstev}}", - "assets_added_to_album_count": "Dodano{count, plural, one {# sredstvo} other {# sredstev}} v album", - "assets_added_to_name_count": "Dodano {count, plural, one {# sredstvo} other {# sredstev}} v {hasName, select, true {{name}} other {new album}}", - "assets_count": "{count, plural, one {# sredstvo} other {# sredstev}}", - "assets_deleted_permanently": "Št. za vedno izbrisanih sredstev: {}", - "assets_deleted_permanently_from_server": "Št. za vedno izbrisanih sredstev iz srežnika Immich: {}", - "assets_moved_to_trash_count": "Premaknjeno {count, plural, one {# sredstev} other {# sredstev}} v smetnjak", - "assets_permanently_deleted_count": "Trajno izbrisano {count, plural, one {# sredstvo} other {# sredstev}}", - "assets_removed_count": "Odstranjeno {count, plural, one {# sredstvo} other {# sredstev}}", - "assets_removed_permanently_from_device": "Št. za vedno izbrisanih sredstev iz vaše naprave: {}", + "assets_added_count": "Dodano{count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", + "assets_added_to_album_count": "Dodano {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v album", + "assets_added_to_name_count": "Dodano {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v {hasName, select, true {{name}} other {new album}}", + "assets_count": "{count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", + "assets_deleted_permanently": "{count, plural, one {# sredstvo trajno izbrisano} two {# sredstvi trajno izbrisani} few {# sredstva trajno izbrisana} other {# sredstev trajno izbrisanih}}", + "assets_deleted_permanently_from_server": "{count, plural, one {# sredstvo trajno izbrisano iz strežnika Immich} two {# sredstvi trajno izbrisani iz strežnika Immich} few {# sredstva trajno izbrisana iz strežnika Immich} other {# sredstev trajno izbrisanih iz strežnika Immich}}", + "assets_moved_to_trash_count": "Premaknjeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v smetnjak", + "assets_permanently_deleted_count": "Trajno izbrisano {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", + "assets_removed_count": "Odstranjeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", + "assets_removed_permanently_from_device": "{count, plural, one {# sredstvo trajno izbrisano iz naprave} two {# sredstvi strajno izbrisani iz naprave} few {# sredstva trajno izbrisana iz naprave} other {# sredstve trajno izbrisanih iz naprave}}", "assets_restore_confirmation": "Ali ste prepričani, da želite obnoviti vsa sredstva, ki ste jih odstranili? Tega dejanja ne morete razveljaviti! Upoštevajte, da sredstev brez povezave ni mogoče obnoviti na ta način.", - "assets_restored_count": "Obnovljeno {count, plural, one {# sredstvo} other {# sredstev}}", - "assets_restored_successfully": "Št. uspešno obnovljenih sredstev: {}", - "assets_trashed": "Št. sredstev premaknjenih v smetnjak: {}", - "assets_trashed_count": "V smetnjak {count, plural, one {# sredstvo} other {# sredstev}}", - "assets_trashed_from_server": "Št sredstev izbrisanih iz strežnika Immich: {}", - "assets_were_part_of_album_count": "{count, plural, one {sredstvo je} other {sredstev je}} že del albuma", + "assets_restored_count": "Obnovljeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", + "assets_restored_successfully": "{count, plural, one {# sredstvo uspešno obnovljeno} two {# sredstvi uspešno obnovljeni} few {# sredstva uspešno obnovljena} other {# sredstev uspešno obnovljenih}}", + "assets_trashed": "{count, plural, one {# sredstvo premaknjeno v smetnjak} two {# sredstvi premaknjeni v smetnjak} few {# sredstva premaknjena v smetnjak} other {# sredstev premaknjenih v smetnjak}}", + "assets_trashed_count": "V smetnjak {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", + "assets_trashed_from_server": "{count, plural, one {# sredstvo iz Immich strežnika v smetnjak} two {# sredstvi iz Immich strežnika v smetnjak} few {# sredstva iz Immich strežnika v smetnjak} other {# sredstev iz Immich strežnika v smetnjak}}", + "assets_were_part_of_album_count": "{count, plural, one {sredstvo je} two {sredstvi sta} few {sredstva so} other {sredstev je}} že del albuma", "authorized_devices": "Pooblaščene naprave", "automatic_endpoint_switching_subtitle": "Povežite se lokalno prek določenega omrežja Wi-Fi, ko je na voljo, in uporabite druge povezave drugje", "automatic_endpoint_switching_title": "Samodejno preklapljanje URL-jev", @@ -497,7 +501,7 @@ "back_close_deselect": "Nazaj, zaprite ali prekličite izbiro", "background_location_permission": "Dovoljenje za iskanje lokacije v ozadju", "background_location_permission_content": "Ko deluje v ozadju mora imeti Immich za zamenjavo omrežij, *vedno* dostop do natančne lokacije, da lahko aplikacija prebere ime omrežja Wi-Fi", - "backup_album_selection_page_albums_device": "Albumi v napravi ({})", + "backup_album_selection_page_albums_device": "Albumi v napravi ({number})", "backup_album_selection_page_albums_tap": "Tapnite za vključitev, dvakrat tapnite za izključitev", "backup_album_selection_page_assets_scatter": "Sredstva so lahko razpršena po več albumih. Tako je mogoče med postopkom varnostnega kopiranja albume vključiti ali izključiti.", "backup_album_selection_page_select_albums": "Izberi albume", @@ -509,7 +513,7 @@ "backup_background_service_current_upload_notification": "Nalagam {}", "backup_background_service_default_notification": "Preverjam za novimi sredstvi…", "backup_background_service_error_title": "Napaka varnostnega kopiranja", - "backup_background_service_in_progress_notification": "Varnostno kopiranje vaših sredstev ...", + "backup_background_service_in_progress_notification": "Varnostno kopiranje vaših sredstev…", "backup_background_service_upload_failure_notification": "Nalaganje {} ni uspelo", "backup_controller_page_albums": "Varnostno kopiranje albumov", "backup_controller_page_background_app_refresh_disabled_content": "Omogočite osveževanje aplikacij v ozadju v Nastavitve > Splošno > Osvežitev aplikacij v ozadju, če želite uporabiti varnostno kopiranje v ozadju.", @@ -529,11 +533,11 @@ "backup_controller_page_background_turn_on": "Vklopi storitev v ozadju", "backup_controller_page_background_wifi": "Samo na WiFi", "backup_controller_page_backup": "Varnostna kopija", - "backup_controller_page_backup_selected": "Izbrano", + "backup_controller_page_backup_selected": "Izbrano: ", "backup_controller_page_backup_sub": "Varnostno kopirane fotografije in videoposnetki", - "backup_controller_page_created": "Ustvarjeno: {}", + "backup_controller_page_created": "Ustvarjeno: {date}", "backup_controller_page_desc_backup": "Vklopite varnostno kopiranje v ospredju za samodejno nalaganje novih sredstev na strežnik, ko odprete aplikacijo.", - "backup_controller_page_excluded": "Izključeno:", + "backup_controller_page_excluded": "Izključeno: ", "backup_controller_page_failed": "Neuspešno ({})", "backup_controller_page_filename": "Ime datoteke: {} [{}]", "backup_controller_page_id": "ID: {}", @@ -545,7 +549,7 @@ "backup_controller_page_start_backup": "Zaženi varnostno kopiranje", "backup_controller_page_status_off": "Samodejno varnostno kopiranje v ospredju je izklopljeno", "backup_controller_page_status_on": "Samodejno varnostno kopiranje v ospredju je vklopljeno", - "backup_controller_page_storage_format": "Uporabljeno {} od {}", + "backup_controller_page_storage_format": "{used} od {available} uporabljeno", "backup_controller_page_to_backup": "Albumi, ki bodo varnostno kopirani", "backup_controller_page_total_sub": "Vse edinstvene fotografije in videi iz izbranih albumov", "backup_controller_page_turn_off": "Izklopite varnostno kopiranje v ospredju", @@ -566,25 +570,25 @@ "bugs_and_feature_requests": "Napake in zahteve po funkcijah", "build": "Različica", "build_image": "Različica slike", - "bulk_delete_duplicates_confirmation": "Ali ste prepričani, da želite množično izbrisati {count, plural, one {# dvojnik} other {# dvojnikov}}? S tem boste ohranili največje sredstvo vsake skupine in trajno izbrisali vse druge dvojnike. Tega dejanja ne morete razveljaviti!", - "bulk_keep_duplicates_confirmation": "Ali ste prepričani, da želite obdržati {count, plural, one {# dvojnik} other {# dvojnikov}}? S tem boste razrešili vse podvojene skupine, ne da bi karkoli izbrisali.", - "bulk_trash_duplicates_confirmation": "Ali ste prepričani, da želite množično vreči v smetnjak {count, plural, one {# dvojnik} other {# dvojnikov}}? S tem boste obdržali največje sredstvo vsake skupine in odstranili vse druge dvojnike.", + "bulk_delete_duplicates_confirmation": "Ali ste prepričani, da želite množično izbrisati {count, plural, one {# dvojnik} two {# dvojnika} few {# dvojnike} other {# dvojnikov}}? S tem boste ohranili največje sredstvo vsake skupine in trajno izbrisali vse druge dvojnike. Tega dejanja ne morete razveljaviti!", + "bulk_keep_duplicates_confirmation": "Ali ste prepričani, da želite obdržati {count, plural, one {# dvojnik} two {# dvojnika} few {# dvojnike} other {# dvojnikov}}? S tem boste razrešili vse podvojene skupine, ne da bi karkoli izbrisali.", + "bulk_trash_duplicates_confirmation": "Ali ste prepričani, da želite množično vreči v smetnjak {count, plural, one {# dvojnik} two {# dvojnika} few {# dvojnike} other {# dvojnikov}}? S tem boste obdržali največje sredstvo vsake skupine in odstranili vse druge dvojnike.", "buy": "Kupi Immich", - "cache_settings_album_thumbnails": "Sličice strani knjižnice ({} sredstev)", + "cache_settings_album_thumbnails": "Sličice strani knjižnice ({count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}})", "cache_settings_clear_cache_button": "Počisti predpomnilnik", "cache_settings_clear_cache_button_title": "Počisti predpomnilnik aplikacije. To bo znatno vplivalo na delovanje aplikacije, dokler se predpomnilnik ne obnovi.", "cache_settings_duplicated_assets_clear_button": "POČISTI", "cache_settings_duplicated_assets_subtitle": "Fotografije in videoposnetki, ki jih je aplikacija uvrstila na črni seznam", - "cache_settings_duplicated_assets_title": "Podvojena sredstva ({})", - "cache_settings_image_cache_size": "Velikost predpomnilnika slik ({} sredstev)", + "cache_settings_duplicated_assets_title": "{count, plural, one {# podvojeno sredstvo} two {# podvojeni sredstvi} few {# podvojena sredstva} other {# podvojenih sredstev}}", + "cache_settings_image_cache_size": "Velikost predpomnilnika slik ({count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}})", "cache_settings_statistics_album": "Sličice knjižnice", "cache_settings_statistics_assets": "{} sredstva ({})", - "cache_settings_statistics_full": "Polne slike", + "cache_settings_statistics_full": "Izvirne slike", "cache_settings_statistics_shared": "Sličice albuma v skupni rabi", "cache_settings_statistics_thumbnail": "Sličice", "cache_settings_statistics_title": "Uporaba predpomnilnika", "cache_settings_subtitle": "Nadzirajte delovanje predpomnjenja mobilne aplikacije Immich", - "cache_settings_thumbnail_size": "Velikost predpomnilnika sličic ({} sredstev)", + "cache_settings_thumbnail_size": "Velikost predpomnilnika sličic ({count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}})", "cache_settings_tile_subtitle": "Nadzoruj vedenje lokalnega shranjevanja", "cache_settings_tile_title": "Lokalna shramba", "cache_settings_title": "Nastavitve predpomnjenja", @@ -654,15 +658,15 @@ "contain": "Vsebuje", "context": "Kontekst", "continue": "Nadaljuj", - "control_bottom_app_bar_album_info_shared": "{} elementov · V skupni rabi", + "control_bottom_app_bar_album_info_shared": "{count, plural, one {# element v skupni rabi} two {# elementa v skupni rabi} few {# elementi v skupni rabi} other {# elementov v skupni rabi}}", "control_bottom_app_bar_create_new_album": "Ustvari nov album", "control_bottom_app_bar_delete_from_immich": "Izbriši iz Immicha", "control_bottom_app_bar_delete_from_local": "Izbriši iz naprave", "control_bottom_app_bar_edit_location": "Uredi lokacijo", "control_bottom_app_bar_edit_time": "Uredi datum in uro", "control_bottom_app_bar_share_link": "Deli povezavo", - "control_bottom_app_bar_share_to": "Deli z", - "control_bottom_app_bar_trash_from_immich": "Prestavi v smeti", + "control_bottom_app_bar_share_to": "Deli s/z", + "control_bottom_app_bar_trash_from_immich": "Prestavi v smetnjak", "copied_image_to_clipboard": "Slika kopirana v odložišče.", "copied_to_clipboard": "Kopirano v odložišče!", "copy_error": "Napaka pri kopiranju", @@ -686,7 +690,7 @@ "create_new_person": "Ustvari novo osebo", "create_new_person_hint": "Dodeli izbrana sredstva novi osebi", "create_new_user": "Ustvari novega uporabnika", - "create_shared_album_page_share_add_assets": "DODAJ SREDSTVO", + "create_shared_album_page_share_add_assets": "DODAJ SREDSTVA", "create_shared_album_page_share_select_photos": "Izberi fotografije", "create_tag": "Ustvari oznako", "create_tag_description": "Ustvarite novo oznako. Za ugnezdene oznake vnesite celotno pot oznake, vključno s poševnicami.", @@ -711,7 +715,7 @@ "deduplicate_all": "Odstrani vse podvojene", "deduplication_criteria_1": "Velikost slike v bajtih", "deduplication_criteria_2": "Število podatkov EXIF", - "deduplication_info": "Informacije o deduplikaciji", + "deduplication_info": "Informacije o zaznavanju dvojnikov", "deduplication_info_description": "Za samodejno vnaprejšnjo izbiro sredstev in množično odstranjevanje dvojnikov si ogledamo:", "default_locale": "Privzeti jezik", "default_locale_description": "Oblikujte datume in številke glede na lokalne nastavitve brskalnika", @@ -742,7 +746,7 @@ "description": "Opis", "description_input_hint_text": "Dodaj opis ...", "description_input_submit_error": "Napaka pri posodabljanju opisa, preverite dnevnik za več podrobnosti", - "details": "PODROBNOSTI", + "details": "Podrobnosti", "direction": "Usmeritev", "disabled": "Onemogočeno", "disallow_edits": "Onemogoči urejanje", @@ -808,7 +812,7 @@ "editor_crop_tool_h2_rotation": "Vrtenje", "email": "E-pošta", "empty_folder": "Ta mapa je prazna", - "empty_trash": "Izprazni smeti", + "empty_trash": "Izprazni smetnjak", "empty_trash_confirmation": "Ste prepričani, da želite izprazniti smetnjak? S tem boste iz Immicha trajno odstranili vsa sredstva v smetnjaku.\nTega dejanja ne morete razveljaviti!", "enable": "Omogoči", "enabled": "Omogočeno", @@ -855,7 +859,7 @@ "failed_to_unstack_assets": "Sredstev ni bilo mogoče razložiti", "import_path_already_exists": "Ta uvozna pot že obstaja.", "incorrect_email_or_password": "Napačen e-poštni naslov ali geslo", - "paths_validation_failed": "{paths, plural, one {# pot} other {# poti}} ni bilo uspešno preverjeno", + "paths_validation_failed": "{paths, plural, one {# pot ni bila uspešno preverjena} two {# poti nista bili uspešno preverjeni} few {# poti niso bile uspešno preverjene} other {# poti ni bilo uspešno preverjenih}}", "profile_picture_transparent_pixels": "Profilne slike ne smejo imeti prosojnih slikovnih pik. Povečajte in/ali premaknite sliko.", "quota_higher_than_disk_size": "Nastavili ste kvoto, ki je višja od velikosti diska", "repair_unable_to_check_items": "Ni mogoče preveriti {count, select, one {predmeta} other {predmetov}}", @@ -873,7 +877,7 @@ "unable_to_change_favorite": "Ni mogoče spremeniti priljubljenega za sredstvo", "unable_to_change_location": "Lokacije ni mogoče spremeniti", "unable_to_change_password": "Gesla ni mogoče spremeniti", - "unable_to_change_visibility": "Ni mogoče spremeniti vidnosti za {count, plural, one {# osebo} other {# oseb}}", + "unable_to_change_visibility": "Ni mogoče spremeniti vidnosti za {count, plural, one {# osebo} two {# osebi} few {# osebe} other {# oseb}}", "unable_to_complete_oauth_login": "Prijave OAuth ni mogoče dokončati", "unable_to_connect": "Ni mogoče vzpostaviti povezave", "unable_to_connect_to_server": "Ni mogoče vzpostaviti povezave s strežnikom", @@ -953,10 +957,10 @@ "exif_bottom_sheet_location": "LOKACIJA", "exif_bottom_sheet_people": "OSEBE", "exif_bottom_sheet_person_add_person": "Dodaj ime", - "exif_bottom_sheet_person_age": "Starost {}", - "exif_bottom_sheet_person_age_months": "Starost {} mesecev", - "exif_bottom_sheet_person_age_year_months": "Starost 1 leto, {} mesecev", - "exif_bottom_sheet_person_age_years": "Starost {}", + "exif_bottom_sheet_person_age": "Starost {count, plural, one {# leto} two {# leti} few {# leta} other {# let}}", + "exif_bottom_sheet_person_age_months": "Starost {months, plural, one {# mesec} two {# meseca} few {# mesece} other {# mesecev}}", + "exif_bottom_sheet_person_age_year_months": "Starost 1 leto, {months, plural, one {# mesec} two {# meseca} few {# mesece} other {# mesecev}}", + "exif_bottom_sheet_person_age_years": "Starost {years, plural, one {# leto} two {# leti} few {# leta} other {# let}}", "exit_slideshow": "Zapustite diaprojekcijo", "expand_all": "Razširi vse", "experimental_settings_new_asset_list_subtitle": "Delo v teku", @@ -992,6 +996,7 @@ "filetype": "Vrsta datoteke", "filter": "Filter", "filter_people": "Filtriraj ljudi", + "filter_places": "Filtriraj kraje", "find_them_fast": "Z iskanjem jih hitro poiščite po imenu", "fix_incorrect_match": "Popravi napačno ujemanje", "folder": "Mapa", @@ -1040,7 +1045,7 @@ "home_page_delete_remote_err_local": "Lokalna sredstva pri brisanju oddaljenega izbora, preskakujem", "home_page_favorite_err_local": "Lokalnih sredstev še ni mogoče dodati med priljubljene, preskakujem", "home_page_favorite_err_partner": "Sredstev partnerja še ni mogoče dodati med priljubljene, preskakujem", - "home_page_first_time_notice": "Če aplikacijo uporabljate prvič, se prepričajte, da ste izbrali rezervne albume, tako da lahko časovna premica zapolni fotografije in videoposnetke v albumih.", + "home_page_first_time_notice": "Če aplikacijo uporabljate prvič, se prepričajte, da ste izbrali rezervne albume, tako da lahko časovna premica zapolni fotografije in videoposnetke v albumih", "home_page_share_err_local": "Lokalnih sredstev ni mogoče deliti prek povezave, preskakujem", "home_page_upload_err_limit": "Hkrati lahko naložite največ 30 sredstev, preskakujem", "host": "Gostitelj", @@ -1066,7 +1071,7 @@ "immich_web_interface": "Immich spletni vmesnik", "import_from_json": "Uvoz iz JSON", "import_path": "Pot uvoza", - "in_albums": "V {count, plural, one {# album} other {# albumov}}", + "in_albums": "V {count, plural, one {# album} two {# albuma} few {# albume} other {# albumov}}", "in_archive": "V arhiv", "include_archived": "Vključi arhivirane", "include_shared_albums": "Vključite skupne albume", @@ -1076,7 +1081,7 @@ "info": "Info", "interval": { "day_at_onepm": "Vsak dan ob 13h", - "hours": "Vsakih {hours, plural, one {uro} other {{hours, number} ur/e}}", + "hours": "Vsakih {hours, plural, one {uro} two {uri} few {ure} other {{hours, number} ur}}", "night_at_midnight": "Vsak večer ob polnoči", "night_at_twoam": "Vsako noč ob 2h" }, @@ -1084,12 +1089,12 @@ "invalid_date_format": "Neveljavna oblika datuma", "invite_people": "Povabi ljudi", "invite_to_album": "Povabi v album", - "items_count": "{count, plural, one {# predmet} other {# predmetov}}", + "items_count": "{count, plural, one {# predmet} two {# predmeta} few {# predmeti} other {# predmetov}}", "jobs": "Opravila", "keep": "Obdrži", "keep_all": "Obdrži vse", "keep_this_delete_others": "Obdrži to, izbriši ostalo", - "kept_this_deleted_others": "Obdrži to sredstvo in izbriši {count, plural, one {# sredstvo} other {# sredstev}}", + "kept_this_deleted_others": "Obdrži to sredstvo in izbriši {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", "keyboard_shortcuts": "Bližnjice na tipkovnici", "language": "Jezik", "language_setting_description": "Izberite želeni jezik", @@ -1170,8 +1175,8 @@ "manage_your_devices": "Upravljajte svoje prijavljene naprave", "manage_your_oauth_connection": "Upravljajte svojo OAuth povezavo", "map": "Zemljevid", - "map_assets_in_bound": "{} slika", - "map_assets_in_bounds": "{} slik", + "map_assets_in_bound": "{count, plural, one {# slika}}", + "map_assets_in_bounds": "{count, plural, two {# sliki} few {# slike} other {# slik}}", "map_cannot_get_user_location": "Lokacije uporabnika ni mogoče dobiti", "map_location_dialog_yes": "Da", "map_location_picker_page_use_location": "Uporabi to lokacijo", @@ -1185,9 +1190,9 @@ "map_settings": "Nastavitve zemljevida", "map_settings_dark_mode": "Temni način", "map_settings_date_range_option_day": "Zadnjih 24 ur", - "map_settings_date_range_option_days": "Zadnjih {} dni", + "map_settings_date_range_option_days": "{count, plural, one {Zadnji # dan} two {Zadnja # dneva} few {Zadnje # dni} other {Zadnjih # dni}}", "map_settings_date_range_option_year": "Zadnje leto", - "map_settings_date_range_option_years": "Zadnjih {} let", + "map_settings_date_range_option_years": "{count, plural, two {Zadnje # leti} few {# Zadnja # leta} other {Zadnjih # let}}", "map_settings_dialog_title": "Nastavitve zemljevida", "map_settings_include_show_archived": "Vključi arhivirane", "map_settings_include_show_partners": "Vključi partnerjeve", @@ -1203,7 +1208,7 @@ "memories_start_over": "Začni od začetka", "memories_swipe_to_close": "Podrsaj gor za zapiranje", "memories_year_ago": "Leto dni nazaj", - "memories_years_ago": "{} let nazaj", + "memories_years_ago": "{years, plural, two {# leti} few {# leta} other {# let}} nazaj", "memory": "Spomin", "memory_lane_title": "Spominski trak {title}", "menu": "Meni", @@ -1282,6 +1287,7 @@ "onboarding_welcome_user": "Pozdravljen/a, {user}", "online": "Povezano", "only_favorites": "Samo priljubljene", + "open": "Odpri", "open_in_map_view": "Odpri v pogledu zemljevida", "open_in_openstreetmap": "Odpri v OpenStreetMap", "open_the_search_filters": "Odpri iskalne filtre", @@ -1298,14 +1304,14 @@ "partner_can_access": "{partner} ima dostop", "partner_can_access_assets": "Vse vaše fotografije in videoposnetki, razen tistih v arhivu in izbrisanih", "partner_can_access_location": "Lokacija, kjer so bile vaše fotografije posnete", - "partner_list_user_photos": "{user}ovih fotografij", + "partner_list_user_photos": "fotografije od {user}", "partner_list_view_all": "Poglej vse", "partner_page_empty_message": "Vaše fotografije še niso v skupni rabi z nobenim partnerjem.", "partner_page_no_more_users": "Ni več uporabnikov za dodajanje", "partner_page_partner_add_failed": "Partnerja ni bilo mogoče dodati", "partner_page_select_partner": "Izberi partnerja", "partner_page_shared_to_title": "V skupni rabi z", - "partner_page_stop_sharing_content": "{} ne bo imel več dostopa do vaših fotografij.", + "partner_page_stop_sharing_content": "{user} ne bo imel več dostopa do vaših fotografij.", "partner_sharing": "Skupna raba s partnerjem", "partners": "Partnerji", "password": "Geslo", @@ -1313,9 +1319,9 @@ "password_required": "Zahtevano je geslo", "password_reset_success": "Ponastavitev gesla je uspela", "past_durations": { - "days": "Pretek-el/-lih {days, plural, one {dan} other {# dni}}", - "hours": "Pretek-lo/-lih {hours, plural, one {uro} other {# ur}}", - "years": "Pretek-lo/-lih {years, plural, one {leto} other {# let}}" + "days": "{days, plural, one {Pretekel dan} two {Pretekla # dni} few {Pretekle # dni} other {Preteklih # dni}}", + "hours": "{hours, plural, one {Preteklo uro} two {Pretekli # uri} few {Pretekle # ure} other {Preteklih # ur}}", + "years": "{years, plural, one {Preteklo leto} two {Pretekli # leti} few {Pretekla # leta} other {Preteklih # let}}" }, "path": "Pot", "pattern": "Vzorec", @@ -1324,13 +1330,13 @@ "paused": "Zaustavljeno", "pending": "V teku", "people": "Osebe", - "people_edits_count": "Urejen-a/-ih {count, plural, one {# oseba} other {# oseb}}", + "people_edits_count": "{count, plural, one {Urejena # oseba} two {Urejeni # osebi} few {Urejene # osebe} other {Urejenih # oseb}}", "people_feature_description": "Brskanje po fotografijah in videoposnetkih, razvrščenih po osebah", "people_sidebar_description": "Prikažite povezavo do Ljudje v stranski vrstici", "permanent_deletion_warning": "Opozorilo o trajnem izbrisu", "permanent_deletion_warning_setting_description": "Pokaži opozorilo pri trajnem brisanju sredstev", "permanently_delete": "Trajno izbriši", - "permanently_delete_assets_count": "Trajno izbriši {count, plural, one {sredstvo} other {sredstev}}", + "permanently_delete_assets_count": "Trajno izbriši {count, plural, one {sredstvo} two {sredstvi} few {sredstva} other {sredstev}}", "permanently_delete_assets_prompt": "Ali ste prepričani, da želite trajno izbrisati {count, plural, one {to sredstvo?} other {ta # sredstva?}} S tem boste odstranili tudi {count, plural, one {tega od teh} other {telih iz telih}} album- /-ov.", "permanently_deleted_asset": "Trajno izbrisano sredstvo", "permanently_deleted_assets_count": "Trajno izbrisano {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", @@ -1348,12 +1354,12 @@ "photo_shared_all_users": "Videti je, da ste svoje fotografije delili z vsemi uporabniki ali pa nimate nobenega uporabnika, s katerim bi jih delili.", "photos": "Slike", "photos_and_videos": "Fotografije & videi", - "photos_count": "{count, plural, one {{count, number} slika} other {{count, number} slik}}", + "photos_count": "{count, plural, one {{count, number} slika} two {{count, number} sliki} few {{count, number} slike} other {{count, number} slik}}", "photos_from_previous_years": "Fotografije iz prejšnjih let", "pick_a_location": "Izberi lokacijo", "place": "Lokacija", "places": "Lokacije", - "places_count": "{count, plural, one {{count, number} kraj} other {{count, number} krajev}}", + "places_count": "{count, plural, one {{count, number} kraj} two {{count, number} kraja} few {{count, number} kraji} other {{count, number} krajev}}", "play": "Predvajaj", "play_memories": "Predvajaj spomine", "play_motion_photo": "Predvajaj premikajočo fotografijo", @@ -1418,7 +1424,7 @@ "reaction_options": "Možnosti reakcije", "read_changelog": "Preberi dnevnik sprememb", "reassign": "Prerazporedi", - "reassigned_assets_to_existing_person": "Ponovno dodeljeno {count, plural, one {# sredstvo} other {# sredstev}} za {name, select, null {an existing person} other {{name}}}", + "reassigned_assets_to_existing_person": "Ponovno dodeljeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} za {name, select, null {an existing person} other {{name}}}", "reassigned_assets_to_new_person": "Ponovno dodeljeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} za novo osebo", "reassing_hint": "Dodeli izbrana sredstva obstoječi osebi", "recent": "Nedavno", @@ -1540,7 +1546,7 @@ "search_result_page_new_search_hint": "Novo iskanje", "search_settings": "Nastavitve iskanja", "search_state": "Iskanje dežele...", - "search_suggestion_list_smart_search_hint_1": "Pametno iskanje je privzeto omogočeno, za iskanje metapodatkov uporabite sintakso", + "search_suggestion_list_smart_search_hint_1": "Pametno iskanje je privzeto omogočeno, za iskanje metapodatkov uporabite sintakso ", "search_suggestion_list_smart_search_hint_2": "m:vaš-iskani-pojem", "search_tags": "Iskanje oznak...", "search_timezone": "Iskanje časovnega pasu...", @@ -1590,12 +1596,12 @@ "setting_languages_apply": "Uporabi", "setting_languages_subtitle": "Spremeni jezik aplikacije", "setting_languages_title": "Jeziki", - "setting_notifications_notify_failures_grace_period": "Obvesti o napakah varnostnega kopiranja v ozadju: {}", - "setting_notifications_notify_hours": "{} ur", + "setting_notifications_notify_failures_grace_period": "Obvesti o napakah varnostnega kopiranja v ozadju: {user}", + "setting_notifications_notify_hours": "{count, plural, one {# ura} two {# uri} few {# ure} other {# ur}}", "setting_notifications_notify_immediately": "takoj", - "setting_notifications_notify_minutes": "{} minut", + "setting_notifications_notify_minutes": "{count, plural, one {# minuta} two {# minuti} few {# minute} other {# minut}}", "setting_notifications_notify_never": "nikoli", - "setting_notifications_notify_seconds": "{} sekund", + "setting_notifications_notify_seconds": "{count, plural, one {# sekunda} two {# sekundi} few {# sekunde} other {# sekund}}", "setting_notifications_single_progress_subtitle": "Podrobne informacije o napredku nalaganja po sredstvih", "setting_notifications_single_progress_title": "Pokaži napredek varnostnega kopiranja v ozadju", "setting_notifications_subtitle": "Prilagodite svoje nastavitve obvestil", @@ -1609,7 +1615,7 @@ "settings_saved": "Nastavitve shranjene", "share": "Deli", "share_add_photos": "Dodaj fotografije", - "share_assets_selected": "{} izbrano", + "share_assets_selected": "{count, plural, one {# izbrana} two {# izbrani} few {# izbrane} other {# izbranih}}", "share_dialog_preparing": "Priprava...", "shared": "V skupni rabi", "shared_album_activities_input_disable": "Komentiranje je onemogočeno", @@ -1618,7 +1624,7 @@ "shared_album_section_people_action_error": "Napaka pri zapuščanju/odstranjevanju iz albuma", "shared_album_section_people_action_leave": "Odstrani uporabnika iz albuma", "shared_album_section_people_action_remove_user": "Odstrani uporabnika iz albuma", - "shared_album_section_people_title": "LJUDJE", + "shared_album_section_people_title": "OSEBE", "shared_by": "Skupna raba s/z", "shared_by_user": "Skupna raba s/z {user}", "shared_by_you": "Deliš", @@ -1630,25 +1636,25 @@ "shared_link_create_error": "Napaka pri ustvarjanju povezave skupne rabe", "shared_link_edit_description_hint": "Vnesi opis skupne rabe", "shared_link_edit_expire_after_option_day": "1 dan", - "shared_link_edit_expire_after_option_days": "{} dni", + "shared_link_edit_expire_after_option_days": "{count, plural, other {# dni}}", "shared_link_edit_expire_after_option_hour": "1 ura", - "shared_link_edit_expire_after_option_hours": "{} ur", + "shared_link_edit_expire_after_option_hours": "{count, plural, two {# uri} few {# ure} other {# ur}}", "shared_link_edit_expire_after_option_minute": "1 minuta", - "shared_link_edit_expire_after_option_minutes": "{} minut", - "shared_link_edit_expire_after_option_months": "{} mesecev", - "shared_link_edit_expire_after_option_year": "{} let", + "shared_link_edit_expire_after_option_minutes": "{count, plural, two {# minuti} few {# minute} other {# minut}}", + "shared_link_edit_expire_after_option_months": "{count, plural, one {# mesec} two {# meseca} few {# mesece} other {# mesecev}}", + "shared_link_edit_expire_after_option_year": "{count, plural, one {# leto} two {# leti} few {# leta} other {# let}}", "shared_link_edit_password_hint": "Vnesi geslo za skupno rabo", "shared_link_edit_submit_button": "Posodobi povezavo", "shared_link_error_server_url_fetch": "URL-ja strežnika ni mogoče pridobiti", - "shared_link_expires_day": "Poteče čez {} dan", - "shared_link_expires_days": "Poteče čez {} dni", - "shared_link_expires_hour": "Poteče čez {} uro", - "shared_link_expires_hours": "Poteče čez {} ur", - "shared_link_expires_minute": "Poteče čez {} minuto\n", - "shared_link_expires_minutes": "Poteče čez {} minut", + "shared_link_expires_day": "Poteče čez {count, plural, one {# dan}}", + "shared_link_expires_days": "Poteče čez {count, plural, other {# dni}}", + "shared_link_expires_hour": "Poteče čez {count, plural, one {# uro}}", + "shared_link_expires_hours": "Poteče čez {count, plural, two {# uri} few {# ure} other {# ur}}", + "shared_link_expires_minute": "Poteče čez {count, plural, one {# minuto}}", + "shared_link_expires_minutes": "Poteče čez {count, plural, two {# minuti} few {# minute} other {# minut}}", "shared_link_expires_never": "Poteče ∞", - "shared_link_expires_second": "Poteče čez {} sekundo", - "shared_link_expires_seconds": "Poteče čez {} sekund", + "shared_link_expires_second": "Poteče čez {count,plural, one {# sekundo}}", + "shared_link_expires_seconds": "Poteče čez {count, plural, two {# sekundi} few {# sekunde} other {# sekund}}", "shared_link_individual_shared": "Individualno deljeno", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Upravljanje povezav v skupni rabi", @@ -1784,11 +1790,11 @@ "trash_no_results_message": "Fotografije in videoposnetki, ki so v smetnjaku, bodo prikazani tukaj.", "trash_page_delete_all": "Izbriši vse", "trash_page_empty_trash_dialog_content": "Ali želite izprazniti svoja sredstva v smeti? Ti elementi bodo trajno odstranjeni iz Immicha", - "trash_page_info": "Elementi v smeteh bodo trajno izbrisani po {} dneh", + "trash_page_info": "Elementi v smeteh bodo trajno izbrisani po {number} dneh", "trash_page_no_assets": "Ni sredstev v smeteh", "trash_page_restore_all": "Obnovi vse", "trash_page_select_assets_btn": "Izberite sredstva", - "trash_page_title": "Smetnjak ({})", + "trash_page_title": "Smetnjak ({count, number})", "trashed_items_will_be_permanently_deleted_after": "Elementi v smetnjaku bodo trajno izbrisani po {days, plural, one {# dnevu} two {# dnevih} few {# dnevih} other {# dneh}}.", "type": "Vrsta", "unarchive": "Odstrani iz arhiva", @@ -1852,8 +1858,8 @@ "version_announcement_message": "Pozdravljeni! Na voljo je nova različica Immich. Vzemite si nekaj časa in preberite opombe ob izdaji, da zagotovite, da so vaše nastavitve posodobljene, da preprečite morebitne napačne konfiguracije, zlasti če uporabljate WatchTower ali kateri koli mehanizem, ki samodejno posodablja vaš primerek Immich.", "version_announcement_overlay_release_notes": "opombe ob izdaji", "version_announcement_overlay_text_1": "Živjo prijatelj, na voljo je nova izdaja", - "version_announcement_overlay_text_2": "vzemi si čas in obišči", - "version_announcement_overlay_text_3": "in zagotovite, da sta vaša nastavitev docker-compose in .env posodobljena, da preprečite morebitne napačne konfiguracije, zlasti če uporabljate WatchTower ali kateri koli mehanizem, ki samodejno posodablja vašo strežniško aplikacijo.", + "version_announcement_overlay_text_2": "vzemi si čas in obišči ", + "version_announcement_overlay_text_3": " in zagotovite, da sta vaša nastavitev docker-compose in .env posodobljena, da preprečite morebitne napačne konfiguracije, zlasti če uporabljate WatchTower ali kateri koli mehanizem, ki samodejno posodablja vašo strežniško aplikacijo.", "version_announcement_overlay_title": "Na voljo je nova različica strežnika 🎉", "version_history": "Zgodovina različic", "version_history_item": "{version} nameščena {date}", diff --git a/i18n/sr_Cyrl.json b/i18n/sr_Cyrl.json index c8aa6a9f27..97fe461afd 100644 --- a/i18n/sr_Cyrl.json +++ b/i18n/sr_Cyrl.json @@ -1,5 +1,5 @@ { - "about": "О Апликацији", + "about": "О апликацији", "account": "Профил", "account_settings": "Подешавања за Профил", "acknowledge": "Потврди", @@ -14,8 +14,8 @@ "add_a_location": "Додај Локацију", "add_a_name": "Додај име", "add_a_title": "Додај наслов", - "add_endpoint": "Add endpoint", - "add_exclusion_pattern": "Додај образац изузимања", + "add_endpoint": "Додајте крајњу тачку", + "add_exclusion_pattern": "Додајте образац изузимања", "add_import_path": "Додај путању за преузимање", "add_location": "Додај локацију", "add_more_users": "Додај кориснике", @@ -24,8 +24,8 @@ "add_photos": "Додај фотографије", "add_to": "Додај у…", "add_to_album": "Додај у албум", - "add_to_album_bottom_sheet_added": "Added to {album}", - "add_to_album_bottom_sheet_already_exists": "Already in {album}", + "add_to_album_bottom_sheet_added": "Додато у {album}", + "add_to_album_bottom_sheet_already_exists": "Већ у {album}", "add_to_shared_album": "Додај у дељен албум", "add_url": "Додај URL", "added_to_archive": "Додато у архиву", @@ -106,7 +106,7 @@ "library_scanning_enable_description": "Омогућите периодично скенирање библиотеке", "library_settings": "Спољна библиотека", "library_settings_description": "Управљајте подешавањима спољне библиотеке", - "library_tasks_description": "Скенирајте спољне библиотеке у потрази за новим и/или промењеним средствима", + "library_tasks_description": "Обављај задатке библиотеке", "library_watching_enable_description": "Пратите спољне библиотеке за промене датотека", "library_watching_settings": "Надгледање библиотеке (ЕКСПЕРИМЕНТАЛНО)", "library_watching_settings_description": "Аутоматски пратите промењене датотеке", @@ -141,7 +141,7 @@ "machine_learning_smart_search_description": "Потражите слике семантички користећи уграђени ЦЛИП", "machine_learning_smart_search_enabled": "Омогућите паметну претрагу", "machine_learning_smart_search_enabled_description": "Ако је oneмогућено, слике неће бити кодиране за паметну претрагу.", - "machine_learning_url_description": "URL сервера за машинско учење. Ако је наведено више од једне URL адресе, сваки сервер ће се покушавати један по један док један не одговори успешно, редом од првог до последњег. Сервери који не реагују биће привремено занемарени док се не врате на мрежу.", + "machine_learning_url_description": "УРЛ сервера за машинско учење. Ако је наведено више од једне УРЛ адресе, сваки сервер ће се покушавати један по један док један не одговори успешно, редом од првог до последњег. Сервери који не реагују биће привремено занемарени док се не врате на мрежу.", "manage_concurrency": "Управљање паралелношћу", "manage_log_settings": "Управљајте подешавањима евиденције", "map_dark_style": "Тамни стил", @@ -251,7 +251,7 @@ "storage_template_hash_verification_enabled_description": "Омогућава хеш верификацију, не oneмогућавајте ово осим ако нисте сигурни у последице", "storage_template_migration": "Миграција шаблона за складиштење", "storage_template_migration_description": "Примените тренутни {template} на претходно отпремљене елементе", - "storage_template_migration_info": "Шаблон за складиштење ће претворити све екстензије у мала слова. Промене шаблона ће се применити само на нове датотеке. Да бисте ретроактивно применили шаблон на претходно отпремљене датотеке, покрените {job}.", + "storage_template_migration_info": "Промене шаблона ће се применити само на нове датотеке. Да бисте ретроактивно применили шаблон на претходно отпремљене датотеке, покрените {job}.", "storage_template_migration_job": "Посао миграције складишта", "storage_template_more_details": "За више детаља о овој функцији погледајте Шаблон за складиште и његове импликације", "storage_template_onboarding_description": "Када је омогућена, ова функција ће аутоматски организовати датотеке на основу шаблона који дефинише корисник. Због проблема са стабилношћу ова функција је подразумевано искључена. За више информација погледајте документацију.", @@ -310,7 +310,7 @@ "transcoding_max_b_frames": "Максимални Б-кадри", "transcoding_max_b_frames_description": "Више вредности побољшавају ефикасност компресије, али успоравају кодирање. Можда није компатибилно са хардверским убрзањем на старијим уређајима. 0 oneмогућава Б-кадре, док -1 аутоматски поставља ову вредност.", "transcoding_max_bitrate": "Максимални битрате", - "transcoding_max_bitrate_description": "Подешавање максималног битрате-а може учинити величине датотека предвидљивијим уз мању цену квалитета. При 720п, типичне вредности су 2600kbit/s за ВП9 или ХЕВЦ, или 4500kbit/s за Х.264. oneмогућено ако је постављено на 0.", + "transcoding_max_bitrate_description": "Подешавање максималног битрате-а може учинити величине датотека предвидљивијим уз мању цену квалитета. При 720п, типичне вредности су 2600к за ВП9 или ХЕВЦ, или 4500к за Х.264. oneмогућено ако је постављено на 0.", "transcoding_max_keyframe_interval": "Максимални интервал keyframe-a", "transcoding_max_keyframe_interval_description": "Поставља максималну удаљеност кадрова између кључних кадрова. Ниже вредности погоршавају ефикасност компресије, али побољшавају време тражења и могу побољшати квалитет сцена са брзим кретањем. 0 аутоматски поставља ову вредност.", "transcoding_optimal_description": "Видео снимци већи од циљне резолуције или нису у прихваћеном формату", @@ -324,7 +324,7 @@ "transcoding_reference_frames_description": "Број оквира (фрамес) за референцу приликом компресије датог оквира. Више вредности побољшавају ефикасност компресије, али успоравају кодирање. 0 аутоматски поставља ову вредност.", "transcoding_required_description": "Само видео снимци који нису у прихваћеном формату", "transcoding_settings": "Подешавања видео транскодирања", - "transcoding_settings_description": "Управљајте које видео снимке желите да транскодујете и како их обрадити", + "transcoding_settings_description": "Управљајте резолуцијом и информацијама о кодирању видео датотека", "transcoding_target_resolution": "Циљана резолуција", "transcoding_target_resolution_description": "Веће резолуције могу да сачувају више детаља, али им је потребно више времена за кодирање, имају веће величине датотека и могу да смање брзину апликације.", "transcoding_temporal_aq": "Временски (Темпорал) AQ", @@ -371,13 +371,17 @@ "admin_password": "Администраторска Лозинка", "administration": "Администрација", "advanced": "Напредно", - "advanced_settings_log_level_title": "Log level: {}", - "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", - "advanced_settings_prefer_remote_title": "Prefer remote images", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", + "advanced_settings_enable_alternate_media_filter_subtitle": "Користите ову опцију за филтрирање медија током синхронизације на основу алтернативних критеријума. Покушајте ово само ако имате проблема са апликацијом да открије све албуме.", + "advanced_settings_enable_alternate_media_filter_title": "[ЕКСПЕРИМЕНТАЛНО] Користите филтер за синхронизацију албума на алтернативном уређају", + "advanced_settings_log_level_title": "Ниво евиденције(log): {}", + "advanced_settings_prefer_remote_subtitle": "Неки уређаји веома споро учитавају сличице са средстава на уређају. Активирајте ово подешавање да бисте уместо тога учитали удаљене слике.", + "advanced_settings_prefer_remote_title": "Преферирајте удаљене слике", + "advanced_settings_proxy_headers_subtitle": "Дефинишите прокси заглавља које Имич треба да пошаље са сваким мрежним захтевом", + "advanced_settings_proxy_headers_title": "Прокси Хеадери (headers)", + "advanced_settings_self_signed_ssl_subtitle": "Прескаче верификацију SSL сертификата за крајњу тачку сервера. Обавезно за самопотписане сертификате.", + "advanced_settings_self_signed_ssl_title": "Дозволите самопотписане SSL сертификате", + "advanced_settings_sync_remote_deletions_subtitle": "Аутоматски избришите или вратите средство на овом уређају када се та радња предузме на вебу", + "advanced_settings_sync_remote_deletions_title": "Синхронизујте удаљена брисања [ЕКСПЕРИМЕНТАЛНО]", "advanced_settings_tile_subtitle": "Advanced user's settings", "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", "advanced_settings_troubleshooting_title": "Troubleshooting", @@ -400,9 +404,9 @@ "album_remove_user_confirmation": "Да ли сте сигурни да желите да уклоните {user}?", "album_share_no_users": "Изгледа да сте поделили овај албум са свим корисницима или да немате ниједног корисника са којим бисте делили.", "album_thumbnail_card_item": "1 item", - "album_thumbnail_card_items": "{} items", + "album_thumbnail_card_items": "{} ставке", "album_thumbnail_card_shared": " · Shared", - "album_thumbnail_shared_by": "Shared by {}", + "album_thumbnail_shared_by": "Дели {}", "album_updated": "Албум ажуриран", "album_updated_setting_description": "Примите обавештење е-поштом када дељени албум има нова својства", "album_user_left": "Напустио/ла {album}", @@ -443,7 +447,7 @@ "archive_page_title": "Archive ({})", "archive_size": "Величина архиве", "archive_size_description": "Подеси величину архиве за преузимање (у ГиБ)", - "archived": "Archived", + "archived": "Arhivirano", "archived_count": "{count, plural, other {Архивирано #}}", "are_these_the_same_person": "Да ли су ово иста особа?", "are_you_sure_to_do_this": "Јесте ли сигурни да желите ово да урадите?", @@ -992,6 +996,7 @@ "filetype": "Врста документа", "filter": "Filter", "filter_people": "Филтрирање особа", + "filter_places": "Филтрирајте места", "find_them_fast": "Брзо их пронађите по имену помоћу претраге", "fix_incorrect_match": "Исправите нетачно подударање", "folder": "Folder", @@ -1282,6 +1287,7 @@ "onboarding_welcome_user": "Добродошли, {user}", "online": "Доступан (Онлине)", "only_favorites": "Само фаворити", + "open": "Отвори", "open_in_map_view": "Отвори у приказу мапе", "open_in_openstreetmap": "Отворите у ОпенСтреетМап-у", "open_the_search_filters": "Отворите филтере за претрагу", @@ -1849,7 +1855,7 @@ "variables": "Променљиве (вариаблес)", "version": "Верзија", "version_announcement_closing": "Твој пријатељ, Алекс", - "version_announcement_message": "Здраво пријатељу, постоји нова верзија апликације, молимо вас да одвојите време да посетите напомене о издању и уверите се да је сервер ажуриран како би се спречиле било какве погрешне конфигурације, посебно ако користите WatchTower или било који механизам који аутоматски управља ажурирањем ваше апликације.", + "version_announcement_message": "Здраво пријатељу, постоји нова верзија апликације, молимо вас да одвојите време да посетите напомене о издању и уверите се у своје docker-compose.yml, и .env подешавање је ажурирано како би се спречиле било какве погрешне конфигурације, посебно ако користите WatchTower или било који механизам који аутоматски управља ажурирањем ваше апликације.", "version_announcement_overlay_release_notes": "release notes", "version_announcement_overlay_text_1": "Hi friend, there is a new release of", "version_announcement_overlay_text_2": "please take your time to visit the ", diff --git a/i18n/sr_Latn.json b/i18n/sr_Latn.json index c3280ee4fb..093bf06df7 100644 --- a/i18n/sr_Latn.json +++ b/i18n/sr_Latn.json @@ -14,7 +14,7 @@ "add_a_location": "Dodaj Lokaciju", "add_a_name": "Dodaj ime", "add_a_title": "Dodaj naslov", - "add_endpoint": "Add endpoint", + "add_endpoint": "Dodajte krajnju tačku", "add_exclusion_pattern": "Dodaj obrazac izuzimanja", "add_import_path": "Dodaj putanju za preuzimanje", "add_location": "Dodaj lokaciju", @@ -371,13 +371,17 @@ "admin_password": "Administratorska Lozinka", "administration": "Administracija", "advanced": "Napredno", - "advanced_settings_log_level_title": "Log level: {}", - "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", - "advanced_settings_prefer_remote_title": "Prefer remote images", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", + "advanced_settings_enable_alternate_media_filter_subtitle": "Koristite ovu opciju za filtriranje medija tokom sinhronizacije na osnovu alternativnih kriterijuma. Pokušajte ovo samo ako imate problema sa aplikacijom da otkrije sve albume.", + "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTALNO] Koristite filter za sinhronizaciju albuma na alternativnom uređaju", + "advanced_settings_log_level_title": "Nivo evidencije (log): {}", + "advanced_settings_prefer_remote_subtitle": "Neki uređaji veoma sporo učitavaju sličice sa sredstava na uređaju. Aktivirajte ovo podešavanje da biste umesto toga učitali udaljene slike.", + "advanced_settings_prefer_remote_title": "Preferirajte udaljene slike", + "advanced_settings_proxy_headers_subtitle": "Definišite proksi zaglavlja koje Immich treba da pošalje sa svakim mrežnim zahtevom", + "advanced_settings_proxy_headers_title": "Proksi Headeri (headers)", + "advanced_settings_self_signed_ssl_subtitle": "Preskače verifikaciju SSL sertifikata za krajnju tačku servera. Obavezno za samopotpisane sertifikate.", "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", + "advanced_settings_sync_remote_deletions_subtitle": "Automatski izbrišite ili vratite sredstvo na ovom uređaju kada se ta radnja preduzme na vebu", + "advanced_settings_sync_remote_deletions_title": "Sinhronizujte udaljena brisanja [EKSPERIMENTALNO]", "advanced_settings_tile_subtitle": "Advanced user's settings", "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", "advanced_settings_troubleshooting_title": "Troubleshooting", @@ -402,7 +406,7 @@ "album_thumbnail_card_item": "1 stavka", "album_thumbnail_card_items": "{} stavki", "album_thumbnail_card_shared": "Deljeno", - "album_thumbnail_shared_by": "Shared by {}", + "album_thumbnail_shared_by": "Deli {}", "album_updated": "Album ažuriran", "album_updated_setting_description": "Primite obaveštenje e-poštom kada deljeni album ima nova svojstva", "album_user_left": "Napustio/la {album}", @@ -529,11 +533,11 @@ "backup_controller_page_background_turn_on": "Uključi pozadinski servis", "backup_controller_page_background_wifi": "Samo na WiFi", "backup_controller_page_backup": "Napravi rezervnu kopiju", - "backup_controller_page_backup_selected": "Odabrano:", + "backup_controller_page_backup_selected": "Odabrano: ", "backup_controller_page_backup_sub": "Završeno pravljenje rezervne kopije fotografija i videa", "backup_controller_page_created": "Napravljeno:{}", "backup_controller_page_desc_backup": "Uključi pravljenje rezervnih kopija u prvom planu da automatski napravite rezervne kopije kada otvorite aplikaciju.", - "backup_controller_page_excluded": "Isključeno:", + "backup_controller_page_excluded": "Isključeno: ", "backup_controller_page_failed": "Neuspešno ({})", "backup_controller_page_filename": "Ime fajla:{} [{}]", "backup_controller_page_id": "ID:{}", @@ -992,6 +996,7 @@ "filetype": "Vrsta dokumenta", "filter": "Filter", "filter_people": "Filtriranje osoba", + "filter_places": "Filtrirajte mesta", "find_them_fast": "Brzo ih pronađite po imenu pomoću pretrage", "fix_incorrect_match": "Ispravite netačno podudaranje", "folder": "Folder", @@ -1282,6 +1287,7 @@ "onboarding_welcome_user": "Dobrodošli, {user}", "online": "Dostupan (Online)", "only_favorites": "Samo favoriti", + "open": "Otvori", "open_in_map_view": "Otvorite u prikaz karte", "open_in_openstreetmap": "Otvorite u OpenStreetMap-u", "open_the_search_filters": "Otvorite filtere za pretragu", diff --git a/i18n/sv.json b/i18n/sv.json index a727aa68e2..22f97a1cb0 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "Är du säker på att du vill inaktivera alla inloggningsmetoder? Inloggning kommer att helt inaktiveras.", "authentication_settings_reenable": "För att återaktivera, använd Server Command.", "background_task_job": "Bakgrundsaktiviteter", - "backup_database": "Databassäkerhetskopia", - "backup_database_enable_description": "Aktivera säkerhetskopiering av databas", - "backup_keep_last_amount": "Antal säkerhetskopior att behålla", - "backup_settings": "Säkerhetskopieringsinställningar", - "backup_settings_description": "Hantera inställningar för säkerhetskopiering av databas", + "backup_database": "Skapa Databasdump", + "backup_database_enable_description": "Aktivera dumpning av databas", + "backup_keep_last_amount": "Antal databasdumpar att behålla", + "backup_settings": "Inställningar databasdump", + "backup_settings_description": "Hantera inställningar för databasdumpning. Observera: Dessa jobb övervakas inte och du blir inte notifierad om misslyckanden.", "check_all": "Välj alla", "cleanup": "Uppstädning", "cleared_jobs": "Rensade jobben för:{job}", diff --git a/i18n/th.json b/i18n/th.json index d797dab583..939ab431a9 100644 --- a/i18n/th.json +++ b/i18n/th.json @@ -524,11 +524,11 @@ "backup_controller_page_background_turn_on": "เปิดบริการเบื้องหลัง", "backup_controller_page_background_wifi": "บน WiFi เท่านั้น", "backup_controller_page_backup": "สำรองข้อมูล", - "backup_controller_page_backup_selected": "ที่เลือก:", + "backup_controller_page_backup_selected": "ที่เลือก: ", "backup_controller_page_backup_sub": "รูปภาพและวิดีโอที่สำรองแล้ว", "backup_controller_page_created": "สร้างเมื่อ: {}", "backup_controller_page_desc_backup": "เปิดการสำรองข้อมูลในฉากหน้าเพื่อที่จะอัพโหลดทรัพยากรใหม่ไปยังเซิร์ฟเวอร์เมื่อเปิดแอพ", - "backup_controller_page_excluded": "ถูกยกเว้น:", + "backup_controller_page_excluded": "ถูกยกเว้น: ", "backup_controller_page_failed": "ล้มเหลว ({})", "backup_controller_page_filename": "ชื่อไฟล์: {} [{}]", "backup_controller_page_id": "ID: {}", diff --git a/i18n/tr.json b/i18n/tr.json index 35c90b7f90..db9088f5e7 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -524,11 +524,11 @@ "backup_controller_page_background_turn_on": "Arka plan hizmetini aç", "backup_controller_page_background_wifi": "Sadece Wi-Fi", "backup_controller_page_backup": "Yedekle", - "backup_controller_page_backup_selected": "Seçili:", + "backup_controller_page_backup_selected": "Seçili: ", "backup_controller_page_backup_sub": "Yedeklenen öğeler", "backup_controller_page_created": "Oluşturma tarihi: {}", "backup_controller_page_desc_backup": "Uygulamayı açtığınızda yeni öğelerin sunucuya otomatik olarak yüklenmesi için ön planda yedeklemeyi açın.", - "backup_controller_page_excluded": "Hariç tutuldu:", + "backup_controller_page_excluded": "Hariç tutuldu: ", "backup_controller_page_failed": "Başarısız ({})", "backup_controller_page_filename": "Dosya adı: {} [{}]", "backup_controller_page_id": "ID: {}", @@ -1842,7 +1842,7 @@ "version_announcement_message": "Merhaba! Immich'in yeni bir sürümü mevcut. Lütfen yapılandırmanızın güncel olduğundan emin olmak için sürüm notlarını okumak için biraz zaman ayırın, özellikle WatchTower veya Immich kurulumunuzu otomatik olarak güncelleyen bir mekanizma kullanıyorsanız yanlış yapılandırmaların önüne geçmek adına bu önemlidir.", "version_announcement_overlay_release_notes": "sürüm notları", "version_announcement_overlay_text_1": "Merhaba arkadaşım, yeni bir sürüm mevcut", - "version_announcement_overlay_text_2": "lütfen biraz zaman ayırın ve inceleyin:", + "version_announcement_overlay_text_2": "lütfen biraz zaman ayırın ve inceleyin: ", "version_announcement_overlay_text_3": "ve özellikle WatchTower veya sunucu uygulamanızı otomatik olarak güncelleyen herhangi bir mekanizma kullanıyorsanız, herhangi bir yanlış yapılandırmayı önlemek için docker-compose ve .env kurulumunuzun güncel olduğundan emin olun.", "version_announcement_overlay_title": "Yeni Sunucu Sürümü Mevcut 🎉", "version_history": "Versiyon geçmişi", diff --git a/i18n/uk.json b/i18n/uk.json index 5c6a809f26..061d9bd78f 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "Ви впевнені, що хочете вимкнути всі методи входу? Вхід буде повністю вимкнений.", "authentication_settings_reenable": "Для повторного ввімкнення використовуйте Команду сервера.", "background_task_job": "Фонові Завдання", - "backup_database": "Резервна копія бази даних", - "backup_database_enable_description": "Увімкнути резервне копіювання бази даних", - "backup_keep_last_amount": "Кількість резервних копій для зберігання", - "backup_settings": "Налаштування резервного копіювання", - "backup_settings_description": "Керування налаштуваннями резервного копіювання бази даних", + "backup_database": "Створити дамп бази даних", + "backup_database_enable_description": "Увімкнути дампи бази даних", + "backup_keep_last_amount": "Кількість попередніх дампів, які зберігати", + "backup_settings": "Налаштування дампа бази даних", + "backup_settings_description": "Керувати налаштуваннями дампа бази даних. Примітка: ці завдання не контролюються, і ви не отримаєте сповіщення про помилки.", "check_all": "Перевірити все", "cleanup": "Очищення", "cleared_jobs": "Очищені завдання для: {job}", @@ -371,13 +371,17 @@ "admin_password": "Пароль адміністратора", "administration": "Адміністрування", "advanced": "Розширені", + "advanced_settings_enable_alternate_media_filter_subtitle": "Використовуйте цей варіант для фільтрації медіафайлів під час синхронізації за альтернативними критеріями. Спробуйте це, якщо у вас виникають проблеми з тим, що додаток не виявляє всі альбоми.", + "advanced_settings_enable_alternate_media_filter_title": "[ЕКСПЕРИМЕНТАЛЬНИЙ] Використовуйте альтернативний фільтр синхронізації альбомів пристрою", "advanced_settings_log_level_title": "Рівень логування: {}", "advanced_settings_prefer_remote_subtitle": "Деякі пристрої вельми повільно завантажують мініатюри із елементів на пристрої. Активуйте для завантаження віддалених мініатюр натомість.", "advanced_settings_prefer_remote_title": "Перевага віддаленим зображенням", - "advanced_settings_proxy_headers_subtitle": "Визначте заголовки проксі-сервера, які Immich має надсилати з кожним мережевим запитом.", + "advanced_settings_proxy_headers_subtitle": "Визначте заголовки проксі-сервера, які Immich має надсилати з кожним мережевим запитом", "advanced_settings_proxy_headers_title": "Проксі-заголовки", "advanced_settings_self_signed_ssl_subtitle": "Пропускає перевірку SSL-сертифіката сервера. Потрібне для самопідписаних сертифікатів.", "advanced_settings_self_signed_ssl_title": "Дозволити самопідписані SSL-сертифікати", + "advanced_settings_sync_remote_deletions_subtitle": "Автоматично видаляти або відновлювати ресурс на цьому пристрої, коли ця дія виконується в веб-інтерфейсі", + "advanced_settings_sync_remote_deletions_title": "Синхронізація видалених видалень [ЕКСПЕРИМЕНТАЛЬНО]", "advanced_settings_tile_subtitle": "Розширені користувацькі налаштування", "advanced_settings_troubleshooting_subtitle": "Увімкніть додаткові функції для усунення несправностей", "advanced_settings_troubleshooting_title": "Усунення несправностей", @@ -498,18 +502,18 @@ "background_location_permission": "Дозвіл до місцезнаходження у фоні", "background_location_permission_content": "Щоб перемикати мережі у фоновому режимі, Immich має *завжди* мати доступ до точної геолокації, щоб зчитувати назву Wi-Fi мережі", "backup_album_selection_page_albums_device": "Альбоми на пристрої ({})", - "backup_album_selection_page_albums_tap": "Торкніться, щоб включити,\nторкніться двічі, щоб виключити", + "backup_album_selection_page_albums_tap": "Торкніться, щоб включити, двічі, щоб виключити", "backup_album_selection_page_assets_scatter": "Елементи можуть належати до кількох альбомів водночас. Таким чином, альбоми можуть бути включені або вилучені під час резервного копіювання.", "backup_album_selection_page_select_albums": "Оберіть альбоми", "backup_album_selection_page_selection_info": "Інформація про обране", "backup_album_selection_page_total_assets": "Загальна кількість унікальних елементів", "backup_all": "Усі", - "backup_background_service_backup_failed_message": "Не вдалося зробити резервну копію елементів. Повторюю...", - "backup_background_service_connection_failed_message": "Не вдалося зв'язатися із сервером. Повторюю...", + "backup_background_service_backup_failed_message": "Не вдалося зробити резервну копію елементів. Повторюю…", + "backup_background_service_connection_failed_message": "Не вдалося зв'язатися із сервером. Повторюю…", "backup_background_service_current_upload_notification": "Завантажується {}", "backup_background_service_default_notification": "Перевіряю наявність нових елементів…", "backup_background_service_error_title": "Помилка резервного копіювання", - "backup_background_service_in_progress_notification": "Резервне копіювання ваших елементів...", + "backup_background_service_in_progress_notification": "Резервне копіювання ваших елементів…", "backup_background_service_upload_failure_notification": "Не вдалося завантажити {}", "backup_controller_page_albums": "Резервне копіювання альбомів", "backup_controller_page_background_app_refresh_disabled_content": "Для фонового резервного копіювання увімкніть фонове оновлення в меню \"Налаштування > Загальні > Фонове оновлення програми\".", @@ -521,7 +525,7 @@ "backup_controller_page_background_battery_info_title": "Оптимізація батареї", "backup_controller_page_background_charging": "Лише під час заряджання", "backup_controller_page_background_configure_error": "Не вдалося налаштувати фоновий сервіс", - "backup_controller_page_background_delay": "Затримка перед резервним копіюванням нових елементів: {}", + "backup_controller_page_background_delay": "Затримка резервного копіювання нових елементів: {}", "backup_controller_page_background_description": "Увімкніть фонову службу, щоб автоматично створювати резервні копії будь-яких нових елементів без необхідності відкривати програму", "backup_controller_page_background_is_off": "Автоматичне фонове резервне копіювання вимкнено", "backup_controller_page_background_is_on": "Автоматичне фонове резервне копіювання ввімкнено", @@ -529,11 +533,11 @@ "backup_controller_page_background_turn_on": "Увімкнути фоновий сервіс", "backup_controller_page_background_wifi": "Лише на WiFi", "backup_controller_page_backup": "Резервне копіювання", - "backup_controller_page_backup_selected": "Обрано:", + "backup_controller_page_backup_selected": "Обрано: ", "backup_controller_page_backup_sub": "Резервні копії знімків та відео", "backup_controller_page_created": "Створено: {}", "backup_controller_page_desc_backup": "Увімкніть резервне копіювання на передньому плані, щоб автоматично завантажувати нові елементи на сервер під час відкриття програми.", - "backup_controller_page_excluded": "Вилучено:", + "backup_controller_page_excluded": "Вилучено: ", "backup_controller_page_failed": "Невдалі ({})", "backup_controller_page_filename": "Назва файлу: {} [{}]", "backup_controller_page_id": "ID: {}", @@ -545,7 +549,7 @@ "backup_controller_page_start_backup": "Почати резервне копіювання", "backup_controller_page_status_off": "Автоматичне резервне копіювання в активному режимі вимкнено", "backup_controller_page_status_on": "Автоматичне резервне копіювання в активному режимі ввімкнено", - "backup_controller_page_storage_format": "{} із {} спожито", + "backup_controller_page_storage_format": "Використано: {} з {}", "backup_controller_page_to_backup": "Альбоми до резервного копіювання", "backup_controller_page_total_sub": "Усі унікальні знімки та відео з вибраних альбомів", "backup_controller_page_turn_off": "Вимкнути резервне копіювання в активному режимі", @@ -606,7 +610,7 @@ "change_password": "Змінити пароль", "change_password_description": "Це або перший раз, коли ви увійшли в систему, або було зроблено запит на зміну вашого пароля. Будь ласка, введіть новий пароль нижче.", "change_password_form_confirm_password": "Підтвердити пароль", - "change_password_form_description": "Привіт {name},\n\nВи або або вперше входите у систему, або було зроблено запит на зміну вашого пароля. \nВведіть ваш новий пароль.", + "change_password_form_description": "Привіт, {name},\n\nЦе або ваш перший вхід у систему, або було надіслано запит на зміну пароля. Будь ласка, введіть новий пароль нижче.", "change_password_form_new_password": "Новий пароль", "change_password_form_password_mismatch": "Паролі не співпадають", "change_password_form_reenter_new_password": "Повторіть новий пароль", @@ -630,7 +634,7 @@ "client_cert_import_success_msg": "Клієнтський сертифікат імпортовано", "client_cert_invalid_msg": "Недійсний файл сертифіката або неправильний пароль", "client_cert_remove_msg": "Клієнтський сертифікат видалено", - "client_cert_subtitle": "Підтримується лише формат PKCS12 (.p12, .pfx). Імпорт/видалення сертифіката доступне лише перед входом у систему.", + "client_cert_subtitle": "Підтримується лише формат PKCS12 (.p12, .pfx). Імпорт/видалення сертифіката доступні лише до входу в систему", "client_cert_title": "Клієнтський SSL-сертифікат", "clockwise": "По годинниковій стрілці", "close": "Закрити", @@ -719,8 +723,8 @@ "delete_album": "Видалити альбом", "delete_api_key_prompt": "Ви впевнені, що хочете видалити цей ключ API?", "delete_dialog_alert": "Ці елементи будуть остаточно видалені з серверу Immich та вашого пристрою", - "delete_dialog_alert_local": "Ці елементи будуть видалені видалені з Вашого пристрою, але залишаться доступними на сервері Immich", - "delete_dialog_alert_local_non_backed_up": "Резервні копії деяких елементів не були завантажені в Immich і будуть видалені видалені з Вашого пристрою", + "delete_dialog_alert_local": "Ці елементи будуть остаточно видалені з вашого пристрою, але залишаться доступними на сервері Immich", + "delete_dialog_alert_local_non_backed_up": "Деякі елементи не були збережені на сервері Immich і будуть остаточно видалені з вашого пристрою", "delete_dialog_alert_remote": "Ці елементи будуть назавжди видалені з серверу Immich", "delete_dialog_ok_force": "Все одно видалити", "delete_dialog_title": "Видалити остаточно", @@ -992,6 +996,7 @@ "filetype": "Тип файлу", "filter": "Фільтр", "filter_people": "Фільтр по людях", + "filter_places": "Фільтр по місцях", "find_them_fast": "Швидко знаходьте їх за назвою за допомогою пошуку", "fix_incorrect_match": "Виправити неправильний збіг", "folder": "Папка", @@ -1020,7 +1025,7 @@ "header_settings_field_validator_msg": "Значення не може бути порожнім", "header_settings_header_name_input": "Ім'я заголовку", "header_settings_header_value_input": "Значення заголовку", - "headers_settings_tile_subtitle": "Визначте заголовки проксі, які програма має надсилати з кожним мережевим запитом.", + "headers_settings_tile_subtitle": "Визначте заголовки проксі, які програма має надсилати з кожним мережевим запитом", "headers_settings_tile_title": "Користувальницькі заголовки проксі", "hi_user": "Привіт {name} ({email})", "hide_all_people": "Сховати всіх", @@ -1040,7 +1045,7 @@ "home_page_delete_remote_err_local": "Локальні елемент(и) вже в процесі видалення з сервера, пропущено", "home_page_favorite_err_local": "Поки що не можна додати до улюблених локальні елементи, пропущено", "home_page_favorite_err_partner": "Поки що не можна додати до улюблених елементи партнера, пропущено", - "home_page_first_time_notice": "Якщо ви вперше користуєтеся програмою, переконайтеся, що ви вибрали альбоми для резервування, щоб могти заповнювати хронологію знімків та відео в альбомах.", + "home_page_first_time_notice": "Якщо ви користуєтеся додатком вперше, будь ласка, оберіть альбом для резервного копіювання, щоб на шкалі часу з’явилися фото та відео", "home_page_share_err_local": "Неможливо поділитися локальними елементами через посилання, пропущено", "home_page_upload_err_limit": "Можна вантажити не більше 30 елементів водночас, пропущено", "host": "Хост", @@ -1132,7 +1137,7 @@ "logged_out_device": "Вихід з пристрою", "login": "Вхід", "login_disabled": "Авторизація була відключена", - "login_form_api_exception": "Помилка API. Перевірте адресу сервера і спробуйте знову", + "login_form_api_exception": "Помилка API. Перевірте адресу сервера і спробуйте знову.", "login_form_back_button_text": "Назад", "login_form_email_hint": "youremail@email.com", "login_form_endpoint_hint": "http://your-server-ip:port", @@ -1149,7 +1154,7 @@ "login_form_password_hint": "пароль", "login_form_save_login": "Запам'ятати вхід", "login_form_server_empty": "Введіть URL-адресу сервера.", - "login_form_server_error": "Неможливо з'єднатися із сервером", + "login_form_server_error": "Не вдалося підключитися до сервера.", "login_has_been_disabled": "Вхід було вимкнено.", "login_password_changed_error": "Помилка у оновлені вашого пароля", "login_password_changed_success": "Пароль оновлено успішно", @@ -1282,6 +1287,7 @@ "onboarding_welcome_user": "Ласкаво просимо, {user}", "online": "Доступний", "only_favorites": "Лише обрані", + "open": "Відкрити", "open_in_map_view": "Відкрити у перегляді мапи", "open_in_openstreetmap": "Відкрити в OpenStreetMap", "open_the_search_filters": "Відкрийте фільтри пошуку", @@ -1304,7 +1310,7 @@ "partner_page_no_more_users": "Більше немає кого додати", "partner_page_partner_add_failed": "Не вдалося додати партнера", "partner_page_select_partner": "Обрати партнера", - "partner_page_shared_to_title": "Спільне із ", + "partner_page_shared_to_title": "Спільне із", "partner_page_stop_sharing_content": "{} втратить доступ до ваших знімків.", "partner_sharing": "Спільне використання", "partners": "Партнери", @@ -1338,9 +1344,9 @@ "permission_onboarding_continue_anyway": "Все одно продовжити", "permission_onboarding_get_started": "Розпочати", "permission_onboarding_go_to_settings": "Перейти до налаштувань", - "permission_onboarding_permission_denied": "Доступ заборонено. Аби користуватися Immich, надайте доступ до знімків та відео у Налаштуваннях.", + "permission_onboarding_permission_denied": "Доступ заборонено. Для використання Immich надайте дозволи до \"Фото та відео\" в налаштуваннях.", "permission_onboarding_permission_granted": "Доступ надано! Все готово.", - "permission_onboarding_permission_limited": "Обмежений доступ. Аби дозволити Immich резервне копіювання та керування вашою галереєю, надайте доступ до знімків та відео у Налаштуваннях", + "permission_onboarding_permission_limited": "Обмежений доступ. Аби дозволити Immich резервне копіювання та керування вашою галереєю, надайте доступ до знімків та відео у Налаштуваннях.", "permission_onboarding_request": "Immich потребує доступу до ваших знімків та відео.", "person": "Людина", "person_birthdate": "Народився {date}", @@ -1540,7 +1546,7 @@ "search_result_page_new_search_hint": "Новий пошук", "search_settings": "Налаштування пошуку", "search_state": "Пошук регіону...", - "search_suggestion_list_smart_search_hint_1": "Інтелектуальний пошук увімкнено за замовчуванням, для пошуку метаданих використовуйте синтаксис", + "search_suggestion_list_smart_search_hint_1": "Розумний пошук увімкнено за замовчуванням, для пошуку за метаданими використовуйте синтаксис. ", "search_suggestion_list_smart_search_hint_2": "m:ваш-пошуковий-термін", "search_tags": "Пошук тегів...", "search_timezone": "Пошук часового поясу...", @@ -1582,9 +1588,9 @@ "set_profile_picture": "Встановити зображення профілю", "set_slideshow_to_fullscreen": "Встановити слайд-шоу на весь екран", "setting_image_viewer_help": "Повноекранний переглядач спочатку завантажує зображення для попереднього перегляду в низькій роздільній здатності, потім завантажує зображення в зменшеній роздільній здатності відносно оригіналу (якщо включено) і зрештою завантажує оригінал (якщо включено).", - "setting_image_viewer_original_subtitle": "Увімкніть для завантаження оригінального зображення з повною роздільною здатністю (велике!).\nВимкніть, щоб зменшити використання даних (мережі та кешу пристрою).", + "setting_image_viewer_original_subtitle": "Увімкнути для завантаження оригінального зображення з повною роздільною здатністю (велике!). Вимкнути, щоб зменшити використання даних (як через мережу, так і на кеші пристрою).", "setting_image_viewer_original_title": "Завантажувати оригінальне зображення", - "setting_image_viewer_preview_subtitle": "Увімкніть для завантаження зображень середньої роздільної здатності.\nВимкніть для безпосереднього завантаження оригіналу або використовувати лише мініатюру.", + "setting_image_viewer_preview_subtitle": "Увімкнути для завантаження зображення середньої роздільної здатності. Вимкнути, щоб завантажувати оригінал або використовувати тільки ескіз.", "setting_image_viewer_preview_title": "Завантажувати зображення попереднього перегляду", "setting_image_viewer_title": "Зображення", "setting_languages_apply": "Застосувати", @@ -1783,7 +1789,7 @@ "trash_emptied": "Кошик очищений", "trash_no_results_message": "Тут з'являтимуться видалені фото та відео.", "trash_page_delete_all": "Видалити усі", - "trash_page_empty_trash_dialog_content": "Бажаєте очистити ваші елементи в кошику? Ці елементи буде остаточно видалено з Immich.", + "trash_page_empty_trash_dialog_content": "Ви хочете очистити кошик? Ці елементи будуть остаточно видалені з Immich", "trash_page_info": "Поміщені у кошик елементи буде остаточно видалено через {} днів", "trash_page_no_assets": "Віддалені елементи відсутні", "trash_page_restore_all": "Відновити усі", @@ -1851,9 +1857,9 @@ "version_announcement_closing": "Твій друг, Алекс", "version_announcement_message": "Привіт! Доступна нова версія Immich. Будь ласка, приділіть трохи часу для ознайомлення з примітками до випуску, щоб переконатися, що ваша установка оновлена і уникнути можливих помилок у налаштуваннях, особливо якщо ви використовуєте WatchTower або будь-який інший механізм, який автоматично оновлює ваш екземпляр Immich.", "version_announcement_overlay_release_notes": "примітки до випуску", - "version_announcement_overlay_text_1": "Вітаємо, є новий випуск ", + "version_announcement_overlay_text_1": "Вітаємо, є новий випуск", "version_announcement_overlay_text_2": "знайдіть хвильку навідатися на ", - "version_announcement_overlay_text_3": "і переконайтеся, що ваші налаштування docker-compose та .env оновлені, аби запобігти будь-якій неправильній конфігурації, особливо, якщо ви використовуєте WatchTower або інший механізм, для автоматичних оновлень вашої серверної частини.", + "version_announcement_overlay_text_3": " і переконайтеся, що ваші налаштування docker-compose та .env оновлені, аби запобігти будь-якій неправильній конфігурації, особливо, якщо ви використовуєте WatchTower або інший механізм, для автоматичних оновлень вашої серверної частини.", "version_announcement_overlay_title": "Доступна нова версія сервера 🎉", "version_history": "Історія версій", "version_history_item": "Встановлено {version} {date}", diff --git a/i18n/vi.json b/i18n/vi.json index fdcea7772c..9262d8a6b3 100644 --- a/i18n/vi.json +++ b/i18n/vi.json @@ -525,7 +525,7 @@ "backup_controller_page_backup_sub": "Ảnh và video đã sao lưu", "backup_controller_page_created": "Tạo vào: {}", "backup_controller_page_desc_backup": "Bật sao lưu khi ứng dụng hoạt động để tự động sao lưu ảnh mới lên máy chủ khi mở ứng dụng.", - "backup_controller_page_excluded": "Đã bỏ qua:", + "backup_controller_page_excluded": "Đã bỏ qua: ", "backup_controller_page_failed": "Thất bại ({})", "backup_controller_page_filename": "Tên tệp: {} [{}]", "backup_controller_page_id": "ID: {}", @@ -537,7 +537,7 @@ "backup_controller_page_start_backup": "Bắt đầu sao lưu", "backup_controller_page_status_off": "Sao lưu tự động khi ứng dụng hoạt động đang tắt", "backup_controller_page_status_on": "Sao lưu tự động khi ứng dụng hoạt động đang bật", - "backup_controller_page_storage_format": "Đã sử dụng {} của {} ", + "backup_controller_page_storage_format": "Đã sử dụng {} của {}", "backup_controller_page_to_backup": "Các album cần được sao lưu", "backup_controller_page_total_sub": "Tất cả ảnh và video không trùng lập từ các album được chọn", "backup_controller_page_turn_off": "Tắt sao lưu khi ứng dụng hoạt động", diff --git a/i18n/zh_Hant.json b/i18n/zh_Hant.json index bb5094e352..7758052880 100644 --- a/i18n/zh_Hant.json +++ b/i18n/zh_Hant.json @@ -400,9 +400,9 @@ "album_remove_user_confirmation": "確定要移除 {user} 嗎?", "album_share_no_users": "看來您與所有使用者共享了這本相簿,或沒有其他使用者可供分享。", "album_thumbnail_card_item": "1 項", - "album_thumbnail_card_items": "{} 項", + "album_thumbnail_card_items": "{} 項", "album_thumbnail_card_shared": " · 已共享", - "album_thumbnail_shared_by": "由 {} 共享", + "album_thumbnail_shared_by": "由 {} 共享", "album_updated": "更新相簿時", "album_updated_setting_description": "當共享相簿有新檔案時,用電子郵件通知我", "album_user_left": "已離開 {album}", @@ -440,7 +440,7 @@ "archive": "封存", "archive_or_unarchive_photo": "封存或取消封存照片", "archive_page_no_archived_assets": "未找到歸檔項目", - "archive_page_title": "歸檔( {} )", + "archive_page_title": "封存 ({})", "archive_size": "封存量", "archive_size_description": "設定要下載的封存量(單位:GiB)", "archived": "已存檔", @@ -506,11 +506,11 @@ "backup_all": "全部", "backup_background_service_backup_failed_message": "備份失敗,正在重試…", "backup_background_service_connection_failed_message": "連接伺服器失敗,正在重試…", - "backup_background_service_current_upload_notification": "正在上傳 {} ", + "backup_background_service_current_upload_notification": "正在上傳 {}", "backup_background_service_default_notification": "正在檢查新項目…", "backup_background_service_error_title": "備份失敗", "backup_background_service_in_progress_notification": "正在備份…", - "backup_background_service_upload_failure_notification": "上傳失敗 {} ", + "backup_background_service_upload_failure_notification": "上傳失敗 {}", "backup_controller_page_albums": "備份相簿", "backup_controller_page_background_app_refresh_disabled_content": "要使用背景備份功能,請在「設定」>「備份」>「背景套用更新」中啓用背本程式更新。", "backup_controller_page_background_app_refresh_disabled_title": "背景套用更新已禁用", @@ -531,12 +531,12 @@ "backup_controller_page_backup": "備份", "backup_controller_page_backup_selected": "已選中:", "backup_controller_page_backup_sub": "已備份的照片和短片", - "backup_controller_page_created": "新增時間: {} ", + "backup_controller_page_created": "新增時間: {}", "backup_controller_page_desc_backup": "打開前台備份,以本程式運行時自動備份新項目。", "backup_controller_page_excluded": "已排除:", "backup_controller_page_failed": "失敗( {} )", "backup_controller_page_filename": "文件名稱: {} [ {} ]", - "backup_controller_page_id": "ID: {} ", + "backup_controller_page_id": "ID: {}", "backup_controller_page_info": "備份資訊", "backup_controller_page_none_selected": "未選擇", "backup_controller_page_remainder": "剩餘", @@ -545,7 +545,7 @@ "backup_controller_page_start_backup": "開始備份", "backup_controller_page_status_off": "前台自動備份已關閉", "backup_controller_page_status_on": "前台自動備份已開啓", - "backup_controller_page_storage_format": " {} / {} 已使用", + "backup_controller_page_storage_format": "{} / {} 已使用", "backup_controller_page_to_backup": "要備份的相簿", "backup_controller_page_total_sub": "選中相簿中所有不重複的短片和圖片", "backup_controller_page_turn_off": "關閉前台備份", @@ -578,7 +578,7 @@ "cache_settings_duplicated_assets_title": "重複項目( {} )", "cache_settings_image_cache_size": "圖片緩存大小( {} 項)", "cache_settings_statistics_album": "圖庫縮圖", - "cache_settings_statistics_assets": " {} 項( {} )", + "cache_settings_statistics_assets": "{} 項( {} )", "cache_settings_statistics_full": "完整圖片", "cache_settings_statistics_shared": "共享相簿縮圖", "cache_settings_statistics_thumbnail": "縮圖", @@ -654,7 +654,7 @@ "contain": "包含", "context": "情境", "continue": "繼續", - "control_bottom_app_bar_album_info_shared": " {} 項 · 已共享", + "control_bottom_app_bar_album_info_shared": "{} 項 · 已共享", "control_bottom_app_bar_create_new_album": "新增相簿", "control_bottom_app_bar_delete_from_immich": "從Immich伺服器中刪除", "control_bottom_app_bar_delete_from_local": "從移動裝置中刪除", @@ -763,7 +763,7 @@ "download_enqueue": "已加入下載隊列", "download_error": "下載出錯", "download_failed": "下載失敗", - "download_filename": "文件: {} ", + "download_filename": "文件: {}", "download_finished": "下載完成", "download_include_embedded_motion_videos": "嵌入影片", "download_include_embedded_motion_videos_description": "把嵌入動態照片的影片作為單獨的檔案包含在內", @@ -819,7 +819,7 @@ "error_change_sort_album": "Failed to change album sort order", "error_delete_face": "從項目中刪除臉孔時發生錯誤", "error_loading_image": "載入圖片時出錯", - "error_saving_image": "錯誤: {} ", + "error_saving_image": "錯誤: {}", "error_title": "錯誤 - 出問題了", "errors": { "cannot_navigate_next_asset": "無法瀏覽下一個檔案", @@ -1170,8 +1170,8 @@ "manage_your_devices": "管理已登入的裝置", "manage_your_oauth_connection": "管理您的 OAuth 連接", "map": "地圖", - "map_assets_in_bound": " {} 張照片", - "map_assets_in_bounds": " {} 張照片", + "map_assets_in_bound": "{} 張照片", + "map_assets_in_bounds": "{} 張照片", "map_cannot_get_user_location": "無法獲取用戶位置", "map_location_dialog_yes": "確定", "map_location_picker_page_use_location": "使用此位置", @@ -1185,9 +1185,9 @@ "map_settings": "地圖設定", "map_settings_dark_mode": "深色模式", "map_settings_date_range_option_day": "過去24小時", - "map_settings_date_range_option_days": " {} 天前", + "map_settings_date_range_option_days": "{} 天前", "map_settings_date_range_option_year": "1年前", - "map_settings_date_range_option_years": " {} 年前", + "map_settings_date_range_option_years": "{} 年前", "map_settings_dialog_title": "地圖設定", "map_settings_include_show_archived": "包括已歸檔項目", "map_settings_include_show_partners": "包含夥伴", @@ -1203,7 +1203,7 @@ "memories_start_over": "再看一次", "memories_swipe_to_close": "上滑關閉", "memories_year_ago": "1年前", - "memories_years_ago": " {} 年前", + "memories_years_ago": "{} 年前", "memory": "回憶", "memory_lane_title": "回憶長廊{title}", "menu": "選單", @@ -1305,7 +1305,7 @@ "partner_page_partner_add_failed": "新增同伴失敗", "partner_page_select_partner": "選擇同伴", "partner_page_shared_to_title": "共享給", - "partner_page_stop_sharing_content": " {} 將無法再存取您的照片。", + "partner_page_stop_sharing_content": "{} 將無法再存取您的照片。", "partner_sharing": "夥伴分享", "partners": "夥伴", "password": "密碼", @@ -1590,12 +1590,12 @@ "setting_languages_apply": "套用", "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "語言", - "setting_notifications_notify_failures_grace_period": "背景備份失敗通知: {} ", - "setting_notifications_notify_hours": " {} 小時", + "setting_notifications_notify_failures_grace_period": "背景備份失敗通知: {}", + "setting_notifications_notify_hours": "{} 小時", "setting_notifications_notify_immediately": "立即", - "setting_notifications_notify_minutes": " {} 分鐘", + "setting_notifications_notify_minutes": "{} 分鐘", "setting_notifications_notify_never": "從不", - "setting_notifications_notify_seconds": " {} 秒", + "setting_notifications_notify_seconds": "{} 秒", "setting_notifications_single_progress_subtitle": "每項的詳細上傳進度資訊", "setting_notifications_single_progress_title": "顯示背景備份詳細進度", "setting_notifications_subtitle": "調整通知選項", @@ -1609,7 +1609,7 @@ "settings_saved": "設定已儲存", "share": "分享", "share_add_photos": "新增項目", - "share_assets_selected": " {} 已選擇", + "share_assets_selected": "{} 已選擇", "share_dialog_preparing": "正在準備...", "shared": "共享", "shared_album_activities_input_disable": "已禁用評論", @@ -1626,28 +1626,28 @@ "shared_intent_upload_button_progress_text": "{} / {} Uploaded", "shared_link_app_bar_title": "共享鏈接", "shared_link_clipboard_copied_massage": "複製到剪貼板", - "shared_link_clipboard_text": "鏈接: {} \n密碼: {} ", + "shared_link_clipboard_text": "鏈接: {} \n密碼: {}", "shared_link_create_error": "新增共享鏈接出錯", "shared_link_edit_description_hint": "編輯共享描述", "shared_link_edit_expire_after_option_day": "1天", - "shared_link_edit_expire_after_option_days": " {} 天", + "shared_link_edit_expire_after_option_days": "{} 天", "shared_link_edit_expire_after_option_hour": "1小時", - "shared_link_edit_expire_after_option_hours": " {} 小時", + "shared_link_edit_expire_after_option_hours": "{} 小時", "shared_link_edit_expire_after_option_minute": "1分鐘", - "shared_link_edit_expire_after_option_minutes": " {} 分鐘", - "shared_link_edit_expire_after_option_months": " {} 個月", - "shared_link_edit_expire_after_option_year": " {} 年", + "shared_link_edit_expire_after_option_minutes": "{} 分鐘", + "shared_link_edit_expire_after_option_months": "{} 個月", + "shared_link_edit_expire_after_option_year": "{} 年", "shared_link_edit_password_hint": "輸入共享密碼", "shared_link_edit_submit_button": "更新鏈接", "shared_link_error_server_url_fetch": "無法獲取伺服器地址", - "shared_link_expires_day": " {} 天後過期", - "shared_link_expires_days": " {} 天後過期", - "shared_link_expires_hour": " {} 小時後過期", - "shared_link_expires_hours": " {} 小時後過期", - "shared_link_expires_minute": " {} 分鐘後過期", + "shared_link_expires_day": "{} 天後過期", + "shared_link_expires_days": "{} 天後過期", + "shared_link_expires_hour": "{} 小時後過期", + "shared_link_expires_hours": "{} 小時後過期", + "shared_link_expires_minute": "{} 分鐘後過期", "shared_link_expires_minutes": "將在 {} 分鐘後過期", "shared_link_expires_never": "永不過期", - "shared_link_expires_second": " {} 秒後過期", + "shared_link_expires_second": "{} 秒後過期", "shared_link_expires_seconds": "將在 {} 秒後過期", "shared_link_individual_shared": "個人共享", "shared_link_info_chip_metadata": "EXIF", diff --git a/i18n/zh_SIMPLIFIED.json b/i18n/zh_SIMPLIFIED.json index 20b906ced3..573fbb7a24 100644 --- a/i18n/zh_SIMPLIFIED.json +++ b/i18n/zh_SIMPLIFIED.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "确定要禁用所有的登录方式?该操作将完全禁止登录。", "authentication_settings_reenable": "如需再次启用,使用 服务器指令。", "background_task_job": "后台任务", - "backup_database": "备份数据库", - "backup_database_enable_description": "启用数据库备份", - "backup_keep_last_amount": "要保留的历史备份数量", - "backup_settings": "备份设置", - "backup_settings_description": "管理数据库备份设置", + "backup_database": "创建数据库备份", + "backup_database_enable_description": "启用数据库导出备份", + "backup_keep_last_amount": "要保留的历史导出数量", + "backup_settings": "数据库导出设置", + "backup_settings_description": "管理数据库备份设置。注意:这些任务不会被监控,失败也不会通知您。", "check_all": "检查全部", "cleanup": "清理", "cleared_jobs": "已清理任务:{job}", @@ -371,13 +371,17 @@ "admin_password": "管理员密码", "administration": "系统管理", "advanced": "高级", - "advanced_settings_log_level_title": "日志等级:{}", - "advanced_settings_prefer_remote_subtitle": "在某些设备上,从本地的项目加载缩略图的速度非常慢。\n启用此选项以加载远程项目。", + "advanced_settings_enable_alternate_media_filter_subtitle": "使用此选项可在同步过程中根据备用条件筛选项目。仅当您在应用程序检测所有相册均遇到问题时才尝试此功能。", + "advanced_settings_enable_alternate_media_filter_title": "[实验] 使用备用的设备相册同步筛选条件", + "advanced_settings_log_level_title": "日志等级: {}", + "advanced_settings_prefer_remote_subtitle": "在某些设备上,从本地的项目加载缩略图的速度非常慢。启用此选项以加载远程项目。", "advanced_settings_prefer_remote_title": "优先远程项目", "advanced_settings_proxy_headers_subtitle": "定义代理标头,应用于Immich的每次网络请求", "advanced_settings_proxy_headers_title": "代理标头", "advanced_settings_self_signed_ssl_subtitle": "跳过服务器终结点的 SSL 证书验证(该选项适用于使用自签名证书的服务器)。", "advanced_settings_self_signed_ssl_title": "允许自签名 SSL 证书", + "advanced_settings_sync_remote_deletions_subtitle": "在网页上执行操作时,自动删除或还原该设备中的项目", + "advanced_settings_sync_remote_deletions_title": "远程同步删除 [实验]", "advanced_settings_tile_subtitle": "高级用户设置", "advanced_settings_troubleshooting_subtitle": "启用用于故障排除的额外功能", "advanced_settings_troubleshooting_title": "故障排除", @@ -477,18 +481,18 @@ "assets_added_to_album_count": "已添加{count, plural, one {#个项目} other {#个项目}}到相册", "assets_added_to_name_count": "已添加{count, plural, one {#个项目} other {#个项目}}到{hasName, select, true {{name}} other {新相册}}", "assets_count": "{count, plural, one {#个项目} other {#个项目}}", - "assets_deleted_permanently": "{}个项目已被永久删除", - "assets_deleted_permanently_from_server": "已从服务器中永久移除{}个项目", + "assets_deleted_permanently": "{} 个项目已被永久删除", + "assets_deleted_permanently_from_server": "已永久移除 {} 个项目", "assets_moved_to_trash_count": "已移动{count, plural, one {#个项目} other {#个项目}}到回收站", "assets_permanently_deleted_count": "已永久删除{count, plural, one {#个项目} other {#个项目}}", "assets_removed_count": "已移除{count, plural, one {#个项目} other {#个项目}}", - "assets_removed_permanently_from_device": "已从设备中永久移除{}个项目", + "assets_removed_permanently_from_device": "已从设备中永久移除 {} 个项目", "assets_restore_confirmation": "确定要恢复回收站中的所有项目吗?该操作无法撤消!请注意,脱机项目无法通过这种方式恢复。", "assets_restored_count": "已恢复{count, plural, one {#个项目} other {#个项目}}", "assets_restored_successfully": "已成功恢复{}个项目", - "assets_trashed": "{}个回收站项目", + "assets_trashed": "{} 个项目放入回收站", "assets_trashed_count": "{count, plural, one {#个项目} other {#个项目}}已放入回收站", - "assets_trashed_from_server": "{}个项目已放入回收站", + "assets_trashed_from_server": "{} 个项目已放入回收站", "assets_were_part_of_album_count": "{count, plural, one {项目} other {项目}}已经在相册中", "authorized_devices": "已授权设备", "automatic_endpoint_switching_subtitle": "在可用的情况下,通过指定的 Wi-Fi 进行本地连接,并在其它地方使用替代连接", @@ -521,7 +525,7 @@ "backup_controller_page_background_battery_info_title": "电池优化", "backup_controller_page_background_charging": "仅充电时", "backup_controller_page_background_configure_error": "配置后台服务失败", - "backup_controller_page_background_delay": "延迟 {} 后备份", + "backup_controller_page_background_delay": "延迟备份的新项目:{}", "backup_controller_page_background_description": "打开后台服务以自动备份任何新项目,且无需打开应用", "backup_controller_page_background_is_off": "后台自动备份已关闭", "backup_controller_page_background_is_on": "后台自动备份已开启", @@ -529,14 +533,14 @@ "backup_controller_page_background_turn_on": "开启后台服务", "backup_controller_page_background_wifi": "仅 WiFi", "backup_controller_page_backup": "备份", - "backup_controller_page_backup_selected": "已选中:", + "backup_controller_page_backup_selected": "已选中: ", "backup_controller_page_backup_sub": "已备份的照片和视频", - "backup_controller_page_created": "创建时间: {}", + "backup_controller_page_created": "创建时间:{}", "backup_controller_page_desc_backup": "打开前台备份,以在程序运行时自动备份新项目。", - "backup_controller_page_excluded": "已排除:", + "backup_controller_page_excluded": "已排除: ", "backup_controller_page_failed": "失败({})", - "backup_controller_page_filename": "文件名称: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_filename": "文件名称:{} [{}]", + "backup_controller_page_id": "ID:{}", "backup_controller_page_info": "备份信息", "backup_controller_page_none_selected": "未选择", "backup_controller_page_remainder": "剩余", @@ -570,7 +574,7 @@ "bulk_keep_duplicates_confirmation": "您确定要保留{count, plural, one {#个重复项目} other {#个重复项目}}吗?这将清空所有重复记录,但不会删除任何内容。", "bulk_trash_duplicates_confirmation": "您确定要批量删除{count, plural, one {#个重复项目} other {#个重复项目}}吗?这将保留每组中最大的项目并删除所有其它重复项目。", "buy": "购买 Immich", - "cache_settings_album_thumbnails": "图库缩略图({} 项)", + "cache_settings_album_thumbnails": "图库页面缩略图({} 项)", "cache_settings_clear_cache_button": "清除缓存", "cache_settings_clear_cache_button_title": "清除应用缓存。在重新生成缓存之前,将显著影响应用的性能。", "cache_settings_duplicated_assets_clear_button": "清除", @@ -606,7 +610,7 @@ "change_password": "修改密码", "change_password_description": "这是你的第一次登录亦或有人要求更改你的密码。请在下面输入新密码。", "change_password_form_confirm_password": "确认密码", - "change_password_form_description": "{name} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。", + "change_password_form_description": "{name} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。请在下方输入新密码。", "change_password_form_new_password": "新密码", "change_password_form_password_mismatch": "密码不匹配", "change_password_form_reenter_new_password": "再次输入新密码", @@ -654,13 +658,13 @@ "contain": "包含", "context": "以文搜图", "continue": "继续", - "control_bottom_app_bar_album_info_shared": "{} 项 · 已共享", + "control_bottom_app_bar_album_info_shared": "已共享 {} 项", "control_bottom_app_bar_create_new_album": "新建相册", "control_bottom_app_bar_delete_from_immich": "从Immich服务器中删除", "control_bottom_app_bar_delete_from_local": "从移动设备中删除", "control_bottom_app_bar_edit_location": "编辑位置信息", "control_bottom_app_bar_edit_time": "编辑日期和时间", - "control_bottom_app_bar_share_link": "Share Link", + "control_bottom_app_bar_share_link": "分享链接", "control_bottom_app_bar_share_to": "发送给", "control_bottom_app_bar_trash_from_immich": "放入回收站", "copied_image_to_clipboard": "已复制图片至剪切板。", @@ -953,10 +957,10 @@ "exif_bottom_sheet_location": "位置", "exif_bottom_sheet_people": "人物", "exif_bottom_sheet_person_add_person": "添加姓名", - "exif_bottom_sheet_person_age": "年龄 {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", + "exif_bottom_sheet_person_age": "{} 岁", + "exif_bottom_sheet_person_age_months": "{} 月龄", + "exif_bottom_sheet_person_age_year_months": "1岁 {} 个月", + "exif_bottom_sheet_person_age_years": "{} 岁", "exit_slideshow": "退出幻灯片放映", "expand_all": "全部展开", "experimental_settings_new_asset_list_subtitle": "正在处理", @@ -992,6 +996,7 @@ "filetype": "文件类型", "filter": "筛选", "filter_people": "过滤人物", + "filter_places": "筛选地点", "find_them_fast": "按名称快速搜索", "fix_incorrect_match": "修复不正确的匹配", "folder": "文件夹", @@ -1029,9 +1034,9 @@ "hide_password": "隐藏密码", "hide_person": "隐藏人物", "hide_unnamed_people": "隐藏未命名的人物", - "home_page_add_to_album_conflicts": "已向相册 {album} 中添加 {added} 项。\n其中 {failed} 项在相册中已存在。", + "home_page_add_to_album_conflicts": "已向相册 {album} 中添加 {added} 项。其中 {failed} 项在相册中已存在。", "home_page_add_to_album_err_local": "暂不能将本地项目添加到相册中,跳过", - "home_page_add_to_album_success": "已向相册 {album} 中添加 {added} 项。", + "home_page_add_to_album_success": "已向相册 {album} 中添加 {added} 项。", "home_page_album_err_partner": "暂无法将同伴的项目添加到相册,跳过", "home_page_archive_err_local": "暂无法归档本地项目,跳过", "home_page_archive_err_partner": "无法存档同伴的项目,跳过", @@ -1151,7 +1156,7 @@ "login_form_server_empty": "输入服务器地址", "login_form_server_error": "无法连接到服务器。", "login_has_been_disabled": "登录已禁用。", - "login_password_changed_error": "更新密码时出错\n", + "login_password_changed_error": "更新密码时出错", "login_password_changed_success": "密码更新成功", "logout_all_device_confirmation": "确定要从所有设备注销?", "logout_this_device_confirmation": "确定要从本设备注销?", @@ -1170,8 +1175,8 @@ "manage_your_devices": "管理已登录设备", "manage_your_oauth_connection": "管理你的 OAuth 绑定", "map": "地图", - "map_assets_in_bound": "{}张照片", - "map_assets_in_bounds": "{}张照片", + "map_assets_in_bound": "{} 张照片", + "map_assets_in_bounds": "{} 张照片", "map_cannot_get_user_location": "无法获取用户位置", "map_location_dialog_yes": "是", "map_location_picker_page_use_location": "使用此位置", @@ -1185,9 +1190,9 @@ "map_settings": "地图设置", "map_settings_dark_mode": "深色模式", "map_settings_date_range_option_day": "过去24小时", - "map_settings_date_range_option_days": "{}天前", + "map_settings_date_range_option_days": "{} 天前", "map_settings_date_range_option_year": "1年前", - "map_settings_date_range_option_years": "{}年前", + "map_settings_date_range_option_years": "{} 年前", "map_settings_dialog_title": "地图设置", "map_settings_include_show_archived": "包括已归档项目", "map_settings_include_show_partners": "包含伙伴", @@ -1203,7 +1208,7 @@ "memories_start_over": "再看一次", "memories_swipe_to_close": "上划关闭", "memories_year_ago": "1年前", - "memories_years_ago": "{}年前", + "memories_years_ago": "{} 年前", "memory": "回忆", "memory_lane_title": "记忆线{title}", "menu": "菜单", @@ -1282,6 +1287,7 @@ "onboarding_welcome_user": "欢迎你,{user}", "online": "在线", "only_favorites": "仅显示已收藏", + "open": "打开", "open_in_map_view": "在地图视图中打开", "open_in_openstreetmap": "在 OpenStreetMap 中打开", "open_the_search_filters": "打开搜索过滤器", @@ -1540,7 +1546,7 @@ "search_result_page_new_search_hint": "搜索新的", "search_settings": "搜索设置", "search_state": "按省份查找...", - "search_suggestion_list_smart_search_hint_1": "默认情况下启用智能搜索,要搜索元数据,请使用相关语法", + "search_suggestion_list_smart_search_hint_1": "默认情况下启用智能搜索,要搜索元数据,请使用相关语法 ", "search_suggestion_list_smart_search_hint_2": "m:您的搜索关键词", "search_tags": "按标签查找…", "search_timezone": "按时区查找...", @@ -1630,25 +1636,25 @@ "shared_link_create_error": "创建共享链接出错", "shared_link_edit_description_hint": "编辑共享描述", "shared_link_edit_expire_after_option_day": "1天", - "shared_link_edit_expire_after_option_days": "{}天", + "shared_link_edit_expire_after_option_days": "{} 天", "shared_link_edit_expire_after_option_hour": "1小时", - "shared_link_edit_expire_after_option_hours": "{}小时", + "shared_link_edit_expire_after_option_hours": "{} 小时", "shared_link_edit_expire_after_option_minute": "1分钟", - "shared_link_edit_expire_after_option_minutes": "{}分钟", + "shared_link_edit_expire_after_option_minutes": "{} 分钟", "shared_link_edit_expire_after_option_months": "{} 个月", "shared_link_edit_expire_after_option_year": "{} 年", "shared_link_edit_password_hint": "输入共享密码", "shared_link_edit_submit_button": "更新链接", "shared_link_error_server_url_fetch": "无法获取服务器地址", - "shared_link_expires_day": "{}天后过期", - "shared_link_expires_days": "{}天后过期", - "shared_link_expires_hour": "{}小时后过期", - "shared_link_expires_hours": "{}小时后过期", - "shared_link_expires_minute": "{}分钟后过期", - "shared_link_expires_minutes": "将在{}分钟后过期", + "shared_link_expires_day": "{} 天后过期", + "shared_link_expires_days": "{} 天后过期", + "shared_link_expires_hour": "{} 小时后过期", + "shared_link_expires_hours": "{} 小时后过期", + "shared_link_expires_minute": "{} 分钟后过期", + "shared_link_expires_minutes": "{} 分钟后过期", "shared_link_expires_never": "过期时间 ∞", - "shared_link_expires_second": "{}秒后过期", - "shared_link_expires_seconds": "将在{}秒后过期", + "shared_link_expires_second": "{} 秒后过期", + "shared_link_expires_seconds": "{} 秒后过期", "shared_link_individual_shared": "个人共享", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "管理共享链接", @@ -1784,7 +1790,7 @@ "trash_no_results_message": "删除的照片和视频将在此处展示。", "trash_page_delete_all": "删除全部", "trash_page_empty_trash_dialog_content": "是否清空回收站?这些项目将被从Immich中永久删除", - "trash_page_info": "回收站中项目将在{}天后永久删除", + "trash_page_info": "回收站中项目将在 {} 天后永久删除", "trash_page_no_assets": "暂无已删除项目", "trash_page_restore_all": "恢复全部", "trash_page_select_assets_btn": "选择项目", @@ -1826,7 +1832,7 @@ "upload_status_errors": "错误", "upload_status_uploaded": "已上传", "upload_success": "上传成功,刷新页面查看新上传的项目。", - "upload_to_immich": "上传至Immich ({})", + "upload_to_immich": "上传至Immich({})", "uploading": "正在上传", "url": "URL", "usage": "用量", @@ -1852,8 +1858,8 @@ "version_announcement_message": "你好!已经检测到Immich有新版本。请抽空阅读一下发行说明,以确保您的配置文件是最新的,避免存在配置错误,特别是当你是使用WatchTower或其它类似的自动升级工具时。", "version_announcement_overlay_release_notes": "发行说明", "version_announcement_overlay_text_1": "号外号外,有新版本的", - "version_announcement_overlay_text_2": "请花点时间访问", - "version_announcement_overlay_text_3": "并检查您的 docker-compose 和 .env 是否为最新且正确的配置,特别是您在使用 WatchTower 或者其他自动更新的程序时,您需要更加细致的检查。", + "version_announcement_overlay_text_2": "请花点时间访问 ", + "version_announcement_overlay_text_3": " 并检查您的 docker-compose 和 .env 是否为最新且正确的配置,特别是您在使用 WatchTower 或者其他自动更新的程序时,您需要更加细致的检查。", "version_announcement_overlay_title": "服务端有新版本啦 🎉", "version_history": "版本更新历史记录", "version_history_item": "在 {date} 安装 {version} 版本", From 699fdd0d1be17ef2c15fd056628fd8ff3837c664 Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Wed, 23 Apr 2025 07:38:25 -0400 Subject: [PATCH 033/356] fix(mobile): recently added -> taken (#17780) --- i18n/en.json | 2 ++ mobile/lib/interfaces/asset.interface.dart | 2 +- ...ntly_added.page.dart => recently_taken.page.dart} | 10 +++++----- mobile/lib/pages/search/search.page.dart | 4 ++-- ...vider.dart => recently_taken_asset.provider.dart} | 4 ++-- mobile/lib/repositories/asset.repository.dart | 2 +- mobile/lib/routing/router.dart | 4 ++-- mobile/lib/routing/router.gr.dart | 12 ++++++------ mobile/lib/services/asset.service.dart | 4 ++-- 9 files changed, 23 insertions(+), 21 deletions(-) rename mobile/lib/pages/search/{recently_added.page.dart => recently_taken.page.dart} (74%) rename mobile/lib/providers/search/{recently_added_asset.provider.dart => recently_taken_asset.provider.dart} (68%) diff --git a/i18n/en.json b/i18n/en.json index 883b69dff5..eafb3415d5 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1432,6 +1432,8 @@ "recent_searches": "Recent searches", "recently_added": "Recently added", "recently_added_page_title": "Recently Added", + "recently_taken": "Recently taken", + "recently_taken_page_title": "Recently Taken", "refresh": "Refresh", "refresh_encoded_videos": "Refresh encoded videos", "refresh_faces": "Refresh faces", diff --git a/mobile/lib/interfaces/asset.interface.dart b/mobile/lib/interfaces/asset.interface.dart index 76744c9172..ca9e9d64fb 100644 --- a/mobile/lib/interfaces/asset.interface.dart +++ b/mobile/lib/interfaces/asset.interface.dart @@ -61,7 +61,7 @@ abstract interface class IAssetRepository implements IDatabaseRepository { Future> getTrashAssets(String userId); - Future> getRecentlyAddedAssets(String userId); + Future> getRecentlyTakenAssets(String userId); Future> getMotionAssets(String userId); } diff --git a/mobile/lib/pages/search/recently_added.page.dart b/mobile/lib/pages/search/recently_taken.page.dart similarity index 74% rename from mobile/lib/pages/search/recently_added.page.dart rename to mobile/lib/pages/search/recently_taken.page.dart index b79527e222..cc1eb7086e 100644 --- a/mobile/lib/pages/search/recently_added.page.dart +++ b/mobile/lib/pages/search/recently_taken.page.dart @@ -4,19 +4,19 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; -import 'package:immich_mobile/providers/search/recently_added_asset.provider.dart'; +import 'package:immich_mobile/providers/search/recently_taken_asset.provider.dart'; @RoutePage() -class RecentlyAddedPage extends HookConsumerWidget { - const RecentlyAddedPage({super.key}); +class RecentlyTakenPage extends HookConsumerWidget { + const RecentlyTakenPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final recents = ref.watch(recentlyAddedAssetProvider); + final recents = ref.watch(recentlyTakenAssetProvider); return Scaffold( appBar: AppBar( - title: const Text('recently_added_page_title').tr(), + title: const Text('recently_taken_page_title').tr(), leading: IconButton( onPressed: () => context.maybePop(), icon: const Icon(Icons.arrow_back_ios_rounded), diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index 62a62d6c98..f7a87803de 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -843,10 +843,10 @@ class QuickLinkList extends StatelessWidget { physics: const NeverScrollableScrollPhysics(), children: [ QuickLink( - title: 'recently_added'.tr(), + title: 'recently_taken'.tr(), icon: Icons.schedule_outlined, isTop: true, - onTap: () => context.pushRoute(const RecentlyAddedRoute()), + onTap: () => context.pushRoute(const RecentlyTakenRoute()), ), QuickLink( title: 'videos'.tr(), diff --git a/mobile/lib/providers/search/recently_added_asset.provider.dart b/mobile/lib/providers/search/recently_taken_asset.provider.dart similarity index 68% rename from mobile/lib/providers/search/recently_added_asset.provider.dart rename to mobile/lib/providers/search/recently_taken_asset.provider.dart index c4819d9d44..157e7c2a74 100644 --- a/mobile/lib/providers/search/recently_added_asset.provider.dart +++ b/mobile/lib/providers/search/recently_taken_asset.provider.dart @@ -2,8 +2,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/services/asset.service.dart'; -final recentlyAddedAssetProvider = FutureProvider>((ref) async { +final recentlyTakenAssetProvider = FutureProvider>((ref) async { final assetService = ref.read(assetServiceProvider); - return assetService.getRecentlyAddedAssets(); + return assetService.getRecentlyTakenAssets(); }); diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart index cda2b25e4d..60e5d09bcd 100644 --- a/mobile/lib/repositories/asset.repository.dart +++ b/mobile/lib/repositories/asset.repository.dart @@ -225,7 +225,7 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { } @override - Future> getRecentlyAddedAssets(String userId) { + Future> getRecentlyTakenAssets(String userId) { return db.assets .where() .ownerIdEqualToAnyChecksum(fastHash(userId)) diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index d7edc6fd28..fcfe7e59bd 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -58,7 +58,7 @@ import 'package:immich_mobile/pages/search/all_videos.page.dart'; import 'package:immich_mobile/pages/search/map/map.page.dart'; import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart'; import 'package:immich_mobile/pages/search/person_result.page.dart'; -import 'package:immich_mobile/pages/search/recently_added.page.dart'; +import 'package:immich_mobile/pages/search/recently_taken.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -160,7 +160,7 @@ class AppRouter extends RootStackRouter { guards: [_authGuard, _duplicateGuard], ), AutoRoute( - page: RecentlyAddedRoute.page, + page: RecentlyTakenRoute.page, guards: [_authGuard, _duplicateGuard], ), CustomRoute( diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 89e83e8159..01ab3fa13c 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1407,20 +1407,20 @@ class PlacesCollectionRouteArgs { } /// generated route for -/// [RecentlyAddedPage] -class RecentlyAddedRoute extends PageRouteInfo { - const RecentlyAddedRoute({List? children}) +/// [RecentlyTakenPage] +class RecentlyTakenRoute extends PageRouteInfo { + const RecentlyTakenRoute({List? children}) : super( - RecentlyAddedRoute.name, + RecentlyTakenRoute.name, initialChildren: children, ); - static const String name = 'RecentlyAddedRoute'; + static const String name = 'RecentlyTakenRoute'; static PageInfo page = PageInfo( name, builder: (data) { - return const RecentlyAddedPage(); + return const RecentlyTakenPage(); }, ); } diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index d187284d07..4bf62eca31 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -514,9 +514,9 @@ class AssetService { return _assetRepository.watchAsset(id, fireImmediately: fireImmediately); } - Future> getRecentlyAddedAssets() { + Future> getRecentlyTakenAssets() { final me = _userService.getMyUser(); - return _assetRepository.getRecentlyAddedAssets(me.id); + return _assetRepository.getRecentlyTakenAssets(me.id); } Future> getMotionAssets() { From a493dab294868283ef6e0fc6c60ab56840eda7cf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:41:51 +0000 Subject: [PATCH 034/356] chore(deps): update github-actions (#17766) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/prepare-release.yml | 4 ++-- .github/workflows/test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 7971f7574a..dc171597e9 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -44,7 +44,7 @@ jobs: persist-credentials: true - name: Install uv - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 - name: Bump version run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}" @@ -95,7 +95,7 @@ jobs: name: release-apk-signed - name: Create draft release - uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2 + uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2 with: draft: true tag_name: ${{ env.IMMICH_VERSION }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 91389c25ff..2d7e07c383 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -431,7 +431,7 @@ jobs: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 # TODO: add caching when supported (https://github.com/actions/setup-python/pull/818) # with: From 2c3658e642d87911c28d89118f533c11e2a5d39b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 07:44:30 -0400 Subject: [PATCH 035/356] fix(deps): update machine-learning (#17769) --- machine-learning/Dockerfile | 8 +- machine-learning/uv.lock | 2759 ++++++++++++++++++----------------- 2 files changed, 1384 insertions(+), 1383 deletions(-) diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 25f3c44d9e..6e31cfe60e 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:0a9d314ae6e976351bd37b702bf6b0a89bb58e6304e5df35b960059b12531419 AS builder-cpu +FROM python:3.11-bookworm@sha256:a3e280261e448b95d49423532ccd6e5329c39d171c10df1457891ff7c5e2301b AS builder-cpu FROM builder-cpu AS builder-openvino @@ -54,7 +54,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ RUN apt-get update && apt-get install -y --no-install-recommends g++ -COPY --from=ghcr.io/astral-sh/uv:latest@sha256:0b6dc79013b689f3bc0cbf12807cb1c901beaafe80f2ee10a1d76aa3842afb92 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:latest@sha256:db305ce8edc1c2df4988b9d23471465d90d599cc55571e6501421c173a33bb0b /uv /uvx /bin/ RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ @@ -63,11 +63,11 @@ RUN if [ "$DEVICE" = "rocm" ]; then \ uv pip install /opt/onnxruntime_rocm-*.whl; \ fi -FROM python:3.11-slim-bookworm@sha256:49d73c49616929b0a4f37c50fee0056eb4b0f15de624591e8d9bf84b4dfdd3ce AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:82c07f2f6e35255b92eb16f38dbd22679d5e8fb523064138d7c6468e7bf0c15b AS prod-cpu ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 -FROM python:3.11-slim-bookworm@sha256:49d73c49616929b0a4f37c50fee0056eb4b0f15de624591e8d9bf84b4dfdd3ce AS prod-openvino +FROM python:3.11-slim-bookworm@sha256:82c07f2f6e35255b92eb16f38dbd22679d5e8fb523064138d7c6468e7bf0c15b AS prod-openvino RUN apt-get update && \ apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index 65011f9cab..eeb48ea666 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 1 +revision = 2 requires-python = ">=3.10, <4.0" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'darwin'", @@ -23,9 +23,9 @@ resolution-markers = [ name = "aiocache" version = "0.12.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7a/64/b945b8025a9d1e6e2138845f4022165d3b337f55f50984fbc6a4c0a1e355/aiocache-0.12.3.tar.gz", hash = "sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713", size = 132196 } +sdist = { url = "https://files.pythonhosted.org/packages/7a/64/b945b8025a9d1e6e2138845f4022165d3b337f55f50984fbc6a4c0a1e355/aiocache-0.12.3.tar.gz", hash = "sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713", size = 132196, upload_time = "2024-09-25T13:20:23.823Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/d7/15d67e05b235d1ed8c3ce61688fe4d84130e72af1657acadfaac3479f4cf/aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d", size = 28199 }, + { url = "https://files.pythonhosted.org/packages/37/d7/15d67e05b235d1ed8c3ce61688fe4d84130e72af1657acadfaac3479f4cf/aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d", size = 28199, upload_time = "2024-09-25T13:20:22.688Z" }, ] [[package]] @@ -40,18 +40,18 @@ dependencies = [ { name = "scikit-image" }, { name = "scipy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/14/d6/8dd5b690d28a332a0b2c3179a345808b5d4c7ad5ddc079b7e116098dff35/albumentations-1.3.1.tar.gz", hash = "sha256:a6a38388fe546c568071e8c82f414498e86c9ed03c08b58e7a88b31cf7a244c6", size = 176371 } +sdist = { url = "https://files.pythonhosted.org/packages/14/d6/8dd5b690d28a332a0b2c3179a345808b5d4c7ad5ddc079b7e116098dff35/albumentations-1.3.1.tar.gz", hash = "sha256:a6a38388fe546c568071e8c82f414498e86c9ed03c08b58e7a88b31cf7a244c6", size = 176371, upload_time = "2023-06-10T07:44:32.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/f6/c486cedb4f75147232f32ec4c97026714cfef7c7e247a1f0427bc5489f66/albumentations-1.3.1-py3-none-any.whl", hash = "sha256:6b641d13733181d9ecdc29550e6ad580d1bfa9d25e2213a66940062f25e291bd", size = 125706 }, + { url = "https://files.pythonhosted.org/packages/9b/f6/c486cedb4f75147232f32ec4c97026714cfef7c7e247a1f0427bc5489f66/albumentations-1.3.1-py3-none-any.whl", hash = "sha256:6b641d13733181d9ecdc29550e6ad580d1bfa9d25e2213a66940062f25e291bd", size = 125706, upload_time = "2023-06-10T07:44:30.373Z" }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload_time = "2024-05-20T21:33:25.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload_time = "2024-05-20T21:33:24.1Z" }, ] [[package]] @@ -64,9 +64,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f", size = 158770 } +sdist = { url = "https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f", size = 158770, upload_time = "2023-12-16T17:06:57.709Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/cd/d6d9bb1dadf73e7af02d18225cbd2c93f8552e13130484f1c8dcfece292b/anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee", size = 85481 }, + { url = "https://files.pythonhosted.org/packages/bf/cd/d6d9bb1dadf73e7af02d18225cbd2c93f8552e13130484f1c8dcfece292b/anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee", size = 85481, upload_time = "2023-12-16T17:06:55.989Z" }, ] [[package]] @@ -82,113 +82,113 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 } +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload_time = "2025-01-29T04:15:40.373Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419 }, - { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080 }, - { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886 }, - { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404 }, - { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372 }, - { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865 }, - { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699 }, - { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028 }, - { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 }, - { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 }, - { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 }, - { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 }, - { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 }, - { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 }, - { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 }, - { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, + { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419, upload_time = "2025-01-29T05:37:06.642Z" }, + { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080, upload_time = "2025-01-29T05:37:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886, upload_time = "2025-01-29T04:18:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404, upload_time = "2025-01-29T04:19:04.296Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload_time = "2025-01-29T05:37:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload_time = "2025-01-29T05:37:14.309Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload_time = "2025-01-29T04:18:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload_time = "2025-01-29T04:18:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload_time = "2025-01-29T05:37:16.707Z" }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload_time = "2025-01-29T05:37:18.273Z" }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload_time = "2025-01-29T04:18:33.823Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload_time = "2025-01-29T04:19:12.944Z" }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload_time = "2025-01-29T05:37:20.574Z" }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload_time = "2025-01-29T05:37:22.106Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload_time = "2025-01-29T04:18:58.564Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload_time = "2025-01-29T04:19:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload_time = "2025-01-29T04:15:38.082Z" }, ] [[package]] name = "blinker" version = "1.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/13/6df5fc090ff4e5d246baf1f45fe9e5623aa8565757dfa5bd243f6a545f9e/blinker-1.7.0.tar.gz", hash = "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182", size = 28134 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/13/6df5fc090ff4e5d246baf1f45fe9e5623aa8565757dfa5bd243f6a545f9e/blinker-1.7.0.tar.gz", hash = "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182", size = 28134, upload_time = "2023-11-01T22:06:01.588Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/2a/7f3714cbc6356a0efec525ce7a0613d581072ed6eb53eb7b9754f33db807/blinker-1.7.0-py3-none-any.whl", hash = "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9", size = 13068 }, + { url = "https://files.pythonhosted.org/packages/fa/2a/7f3714cbc6356a0efec525ce7a0613d581072ed6eb53eb7b9754f33db807/blinker-1.7.0-py3-none-any.whl", hash = "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9", size = 13068, upload_time = "2023-11-01T22:06:00.162Z" }, ] [[package]] name = "brotli" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270, upload_time = "2023-09-07T14:05:41.643Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/3a/dbf4fb970c1019a57b5e492e1e0eae745d32e59ba4d6161ab5422b08eefe/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", size = 873045 }, - { url = "https://files.pythonhosted.org/packages/dd/11/afc14026ea7f44bd6eb9316d800d439d092c8d508752055ce8d03086079a/Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", size = 446218 }, - { url = "https://files.pythonhosted.org/packages/36/83/7545a6e7729db43cb36c4287ae388d6885c85a86dd251768a47015dfde32/Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", size = 2903872 }, - { url = "https://files.pythonhosted.org/packages/32/23/35331c4d9391fcc0f29fd9bec2c76e4b4eeab769afbc4b11dd2e1098fb13/Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", size = 2941254 }, - { url = "https://files.pythonhosted.org/packages/3b/24/1671acb450c902edb64bd765d73603797c6c7280a9ada85a195f6b78c6e5/Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", size = 2857293 }, - { url = "https://files.pythonhosted.org/packages/d5/00/40f760cc27007912b327fe15bf6bfd8eaecbe451687f72a8abc587d503b3/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", size = 3002385 }, - { url = "https://files.pythonhosted.org/packages/b8/cb/8aaa83f7a4caa131757668c0fb0c4b6384b09ffa77f2fba9570d87ab587d/Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", size = 2911104 }, - { url = "https://files.pythonhosted.org/packages/bc/c4/65456561d89d3c49f46b7fbeb8fe6e449f13bdc8ea7791832c5d476b2faf/Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", size = 2809981 }, - { url = "https://files.pythonhosted.org/packages/05/1b/cf49528437bae28abce5f6e059f0d0be6fecdcc1d3e33e7c54b3ca498425/Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", size = 2935297 }, - { url = "https://files.pythonhosted.org/packages/81/ff/190d4af610680bf0c5a09eb5d1eac6e99c7c8e216440f9c7cfd42b7adab5/Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", size = 2930735 }, - { url = "https://files.pythonhosted.org/packages/80/7d/f1abbc0c98f6e09abd3cad63ec34af17abc4c44f308a7a539010f79aae7a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", size = 2933107 }, - { url = "https://files.pythonhosted.org/packages/34/ce/5a5020ba48f2b5a4ad1c0522d095ad5847a0be508e7d7569c8630ce25062/Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", size = 2845400 }, - { url = "https://files.pythonhosted.org/packages/44/89/fa2c4355ab1eecf3994e5a0a7f5492c6ff81dfcb5f9ba7859bd534bb5c1a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", size = 3031985 }, - { url = "https://files.pythonhosted.org/packages/af/a4/79196b4a1674143d19dca400866b1a4d1a089040df7b93b88ebae81f3447/Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", size = 2927099 }, - { url = "https://files.pythonhosted.org/packages/e9/54/1c0278556a097f9651e657b873ab08f01b9a9ae4cac128ceb66427d7cd20/Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", size = 333172 }, - { url = "https://files.pythonhosted.org/packages/f7/65/b785722e941193fd8b571afd9edbec2a9b838ddec4375d8af33a50b8dab9/Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", size = 357255 }, - { url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068 }, - { url = "https://files.pythonhosted.org/packages/95/4e/5afab7b2b4b61a84e9c75b17814198ce515343a44e2ed4488fac314cd0a9/Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", size = 446244 }, - { url = "https://files.pythonhosted.org/packages/9d/e6/f305eb61fb9a8580c525478a4a34c5ae1a9bcb12c3aee619114940bc513d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", size = 2906500 }, - { url = "https://files.pythonhosted.org/packages/3e/4f/af6846cfbc1550a3024e5d3775ede1e00474c40882c7bf5b37a43ca35e91/Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", size = 2943950 }, - { url = "https://files.pythonhosted.org/packages/b3/e7/ca2993c7682d8629b62630ebf0d1f3bb3d579e667ce8e7ca03a0a0576a2d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", size = 2918527 }, - { url = "https://files.pythonhosted.org/packages/b3/96/da98e7bedc4c51104d29cc61e5f449a502dd3dbc211944546a4cc65500d3/Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", size = 2845489 }, - { url = "https://files.pythonhosted.org/packages/e8/ef/ccbc16947d6ce943a7f57e1a40596c75859eeb6d279c6994eddd69615265/Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", size = 2914080 }, - { url = "https://files.pythonhosted.org/packages/80/d6/0bd38d758d1afa62a5524172f0b18626bb2392d717ff94806f741fcd5ee9/Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", size = 2813051 }, - { url = "https://files.pythonhosted.org/packages/14/56/48859dd5d129d7519e001f06dcfbb6e2cf6db92b2702c0c2ce7d97e086c1/Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", size = 2938172 }, - { url = "https://files.pythonhosted.org/packages/3d/77/a236d5f8cd9e9f4348da5acc75ab032ab1ab2c03cc8f430d24eea2672888/Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", size = 2933023 }, - { url = "https://files.pythonhosted.org/packages/f1/87/3b283efc0f5cb35f7f84c0c240b1e1a1003a5e47141a4881bf87c86d0ce2/Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", size = 2935871 }, - { url = "https://files.pythonhosted.org/packages/f3/eb/2be4cc3e2141dc1a43ad4ca1875a72088229de38c68e842746b342667b2a/Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", size = 2847784 }, - { url = "https://files.pythonhosted.org/packages/66/13/b58ddebfd35edde572ccefe6890cf7c493f0c319aad2a5badee134b4d8ec/Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", size = 3034905 }, - { url = "https://files.pythonhosted.org/packages/84/9c/bc96b6c7db824998a49ed3b38e441a2cae9234da6fa11f6ed17e8cf4f147/Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", size = 2929467 }, - { url = "https://files.pythonhosted.org/packages/e7/71/8f161dee223c7ff7fea9d44893fba953ce97cf2c3c33f78ba260a91bcff5/Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", size = 333169 }, - { url = "https://files.pythonhosted.org/packages/02/8a/fece0ee1057643cb2a5bbf59682de13f1725f8482b2c057d4e799d7ade75/Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", size = 357253 }, - { url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693 }, - { url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489 }, - { url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081 }, - { url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244 }, - { url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505 }, - { url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152 }, - { url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252 }, - { url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955 }, - { url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304 }, - { url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452 }, - { url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751 }, - { url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757 }, - { url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146 }, - { url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055 }, - { url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102 }, - { url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029 }, - { url = "https://files.pythonhosted.org/packages/5f/3b/4e3fd1893eb3bbfef8e5a80d4508bec17a57bb92d586c85c12d28666bb13/Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", size = 333276 }, - { url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255 }, - { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681 }, - { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475 }, - { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173 }, - { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803 }, - { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946 }, - { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707 }, - { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231 }, - { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157 }, - { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122 }, - { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206 }, - { url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804 }, - { url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517 }, + { url = "https://files.pythonhosted.org/packages/6d/3a/dbf4fb970c1019a57b5e492e1e0eae745d32e59ba4d6161ab5422b08eefe/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", size = 873045, upload_time = "2023-09-07T14:03:16.894Z" }, + { url = "https://files.pythonhosted.org/packages/dd/11/afc14026ea7f44bd6eb9316d800d439d092c8d508752055ce8d03086079a/Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", size = 446218, upload_time = "2023-09-07T14:03:18.917Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/7545a6e7729db43cb36c4287ae388d6885c85a86dd251768a47015dfde32/Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", size = 2903872, upload_time = "2023-09-07T14:03:20.398Z" }, + { url = "https://files.pythonhosted.org/packages/32/23/35331c4d9391fcc0f29fd9bec2c76e4b4eeab769afbc4b11dd2e1098fb13/Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", size = 2941254, upload_time = "2023-09-07T14:03:21.914Z" }, + { url = "https://files.pythonhosted.org/packages/3b/24/1671acb450c902edb64bd765d73603797c6c7280a9ada85a195f6b78c6e5/Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", size = 2857293, upload_time = "2023-09-07T14:03:24Z" }, + { url = "https://files.pythonhosted.org/packages/d5/00/40f760cc27007912b327fe15bf6bfd8eaecbe451687f72a8abc587d503b3/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", size = 3002385, upload_time = "2023-09-07T14:03:26.248Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/8aaa83f7a4caa131757668c0fb0c4b6384b09ffa77f2fba9570d87ab587d/Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", size = 2911104, upload_time = "2023-09-07T14:03:27.849Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c4/65456561d89d3c49f46b7fbeb8fe6e449f13bdc8ea7791832c5d476b2faf/Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", size = 2809981, upload_time = "2023-09-07T14:03:29.92Z" }, + { url = "https://files.pythonhosted.org/packages/05/1b/cf49528437bae28abce5f6e059f0d0be6fecdcc1d3e33e7c54b3ca498425/Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", size = 2935297, upload_time = "2023-09-07T14:03:32.035Z" }, + { url = "https://files.pythonhosted.org/packages/81/ff/190d4af610680bf0c5a09eb5d1eac6e99c7c8e216440f9c7cfd42b7adab5/Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", size = 2930735, upload_time = "2023-09-07T14:03:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/80/7d/f1abbc0c98f6e09abd3cad63ec34af17abc4c44f308a7a539010f79aae7a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", size = 2933107, upload_time = "2024-10-18T12:32:09.016Z" }, + { url = "https://files.pythonhosted.org/packages/34/ce/5a5020ba48f2b5a4ad1c0522d095ad5847a0be508e7d7569c8630ce25062/Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", size = 2845400, upload_time = "2024-10-18T12:32:11.134Z" }, + { url = "https://files.pythonhosted.org/packages/44/89/fa2c4355ab1eecf3994e5a0a7f5492c6ff81dfcb5f9ba7859bd534bb5c1a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", size = 3031985, upload_time = "2024-10-18T12:32:12.813Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/79196b4a1674143d19dca400866b1a4d1a089040df7b93b88ebae81f3447/Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", size = 2927099, upload_time = "2024-10-18T12:32:14.733Z" }, + { url = "https://files.pythonhosted.org/packages/e9/54/1c0278556a097f9651e657b873ab08f01b9a9ae4cac128ceb66427d7cd20/Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", size = 333172, upload_time = "2023-09-07T14:03:35.212Z" }, + { url = "https://files.pythonhosted.org/packages/f7/65/b785722e941193fd8b571afd9edbec2a9b838ddec4375d8af33a50b8dab9/Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", size = 357255, upload_time = "2023-09-07T14:03:36.447Z" }, + { url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068, upload_time = "2023-09-07T14:03:37.779Z" }, + { url = "https://files.pythonhosted.org/packages/95/4e/5afab7b2b4b61a84e9c75b17814198ce515343a44e2ed4488fac314cd0a9/Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", size = 446244, upload_time = "2023-09-07T14:03:39.223Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e6/f305eb61fb9a8580c525478a4a34c5ae1a9bcb12c3aee619114940bc513d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", size = 2906500, upload_time = "2023-09-07T14:03:40.858Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4f/af6846cfbc1550a3024e5d3775ede1e00474c40882c7bf5b37a43ca35e91/Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", size = 2943950, upload_time = "2023-09-07T14:03:42.896Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e7/ca2993c7682d8629b62630ebf0d1f3bb3d579e667ce8e7ca03a0a0576a2d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", size = 2918527, upload_time = "2023-09-07T14:03:44.552Z" }, + { url = "https://files.pythonhosted.org/packages/b3/96/da98e7bedc4c51104d29cc61e5f449a502dd3dbc211944546a4cc65500d3/Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", size = 2845489, upload_time = "2023-09-07T14:03:46.594Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/ccbc16947d6ce943a7f57e1a40596c75859eeb6d279c6994eddd69615265/Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", size = 2914080, upload_time = "2023-09-07T14:03:48.204Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/0bd38d758d1afa62a5524172f0b18626bb2392d717ff94806f741fcd5ee9/Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", size = 2813051, upload_time = "2023-09-07T14:03:50.348Z" }, + { url = "https://files.pythonhosted.org/packages/14/56/48859dd5d129d7519e001f06dcfbb6e2cf6db92b2702c0c2ce7d97e086c1/Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", size = 2938172, upload_time = "2023-09-07T14:03:52.395Z" }, + { url = "https://files.pythonhosted.org/packages/3d/77/a236d5f8cd9e9f4348da5acc75ab032ab1ab2c03cc8f430d24eea2672888/Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", size = 2933023, upload_time = "2023-09-07T14:03:53.96Z" }, + { url = "https://files.pythonhosted.org/packages/f1/87/3b283efc0f5cb35f7f84c0c240b1e1a1003a5e47141a4881bf87c86d0ce2/Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", size = 2935871, upload_time = "2024-10-18T12:32:16.688Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/2be4cc3e2141dc1a43ad4ca1875a72088229de38c68e842746b342667b2a/Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", size = 2847784, upload_time = "2024-10-18T12:32:18.459Z" }, + { url = "https://files.pythonhosted.org/packages/66/13/b58ddebfd35edde572ccefe6890cf7c493f0c319aad2a5badee134b4d8ec/Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", size = 3034905, upload_time = "2024-10-18T12:32:20.192Z" }, + { url = "https://files.pythonhosted.org/packages/84/9c/bc96b6c7db824998a49ed3b38e441a2cae9234da6fa11f6ed17e8cf4f147/Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", size = 2929467, upload_time = "2024-10-18T12:32:21.774Z" }, + { url = "https://files.pythonhosted.org/packages/e7/71/8f161dee223c7ff7fea9d44893fba953ce97cf2c3c33f78ba260a91bcff5/Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", size = 333169, upload_time = "2023-09-07T14:03:55.404Z" }, + { url = "https://files.pythonhosted.org/packages/02/8a/fece0ee1057643cb2a5bbf59682de13f1725f8482b2c057d4e799d7ade75/Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", size = 357253, upload_time = "2023-09-07T14:03:56.643Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693, upload_time = "2024-10-18T12:32:23.824Z" }, + { url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489, upload_time = "2024-10-18T12:32:25.641Z" }, + { url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081, upload_time = "2023-09-07T14:03:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244, upload_time = "2023-09-07T14:03:59.319Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505, upload_time = "2023-09-07T14:04:01.327Z" }, + { url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152, upload_time = "2023-09-07T14:04:03.033Z" }, + { url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252, upload_time = "2023-09-07T14:04:04.675Z" }, + { url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955, upload_time = "2023-09-07T14:04:06.585Z" }, + { url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304, upload_time = "2023-09-07T14:04:08.668Z" }, + { url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452, upload_time = "2023-09-07T14:04:10.736Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751, upload_time = "2023-09-07T14:04:12.875Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757, upload_time = "2023-09-07T14:04:14.551Z" }, + { url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146, upload_time = "2024-10-18T12:32:27.257Z" }, + { url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055, upload_time = "2024-10-18T12:32:29.376Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102, upload_time = "2024-10-18T12:32:31.371Z" }, + { url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029, upload_time = "2024-10-18T12:32:33.293Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3b/4e3fd1893eb3bbfef8e5a80d4508bec17a57bb92d586c85c12d28666bb13/Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", size = 333276, upload_time = "2023-09-07T14:04:16.49Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255, upload_time = "2023-09-07T14:04:17.83Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681, upload_time = "2024-10-18T12:32:34.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475, upload_time = "2024-10-18T12:32:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173, upload_time = "2024-10-18T12:32:37.978Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803, upload_time = "2024-10-18T12:32:39.606Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946, upload_time = "2024-10-18T12:32:41.679Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707, upload_time = "2024-10-18T12:32:43.478Z" }, + { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231, upload_time = "2024-10-18T12:32:45.224Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157, upload_time = "2024-10-18T12:32:46.894Z" }, + { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122, upload_time = "2024-10-18T12:32:48.844Z" }, + { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206, upload_time = "2024-10-18T12:32:51.198Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804, upload_time = "2024-10-18T12:32:52.661Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517, upload_time = "2024-10-18T12:32:54.066Z" }, ] [[package]] name = "certifi" version = "2023.11.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/91/c89518dd4fe1f3a4e3f6ab7ff23cb00ef2e8c9adf99dacc618ad5e068e28/certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", size = 163637 } +sdist = { url = "https://files.pythonhosted.org/packages/d4/91/c89518dd4fe1f3a4e3f6ab7ff23cb00ef2e8c9adf99dacc618ad5e068e28/certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", size = 163637, upload_time = "2023-11-18T02:54:02.397Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/62/428ef076be88fa93716b576e4a01f919d25968913e817077a386fcbe4f42/certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474", size = 162530 }, + { url = "https://files.pythonhosted.org/packages/64/62/428ef076be88fa93716b576e4a01f919d25968913e817077a386fcbe4f42/certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474", size = 162530, upload_time = "2023-11-18T02:54:00.083Z" }, ] [[package]] @@ -198,108 +198,108 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload_time = "2024-09-04T20:45:21.852Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, - { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, - { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, - { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload_time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload_time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload_time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload_time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload_time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload_time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload_time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload_time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload_time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload_time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload_time = "2024-09-04T20:43:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload_time = "2024-09-04T20:43:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload_time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload_time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload_time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload_time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload_time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload_time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload_time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload_time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload_time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload_time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload_time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload_time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload_time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload_time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload_time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload_time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload_time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload_time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload_time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload_time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload_time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload_time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload_time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload_time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload_time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload_time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload_time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload_time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload_time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload_time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload_time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload_time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload_time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload_time = "2024-09-04T20:44:45.309Z" }, ] [[package]] name = "charset-normalizer" version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } +sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809, upload_time = "2023-11-01T04:04:59.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", size = 194219 }, - { url = "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", size = 122521 }, - { url = "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", size = 120383 }, - { url = "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", size = 138223 }, - { url = "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", size = 148101 }, - { url = "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", size = 140699 }, - { url = "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", size = 142065 }, - { url = "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", size = 144505 }, - { url = "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", size = 139425 }, - { url = "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", size = 145287 }, - { url = "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", size = 149929 }, - { url = "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", size = 141605 }, - { url = "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", size = 142646 }, - { url = "https://files.pythonhosted.org/packages/ae/d5/4fecf1d58bedb1340a50f165ba1c7ddc0400252d6832ff619c4568b36cc0/charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", size = 92846 }, - { url = "https://files.pythonhosted.org/packages/a2/a0/4af29e22cb5942488cf45630cbdd7cefd908768e69bdd90280842e4e8529/charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", size = 100343 }, - { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, - { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, - { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, - { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, - { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, - { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, - { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, - { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, - { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, - { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, - { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, - { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, - { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, - { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, - { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, - { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 }, - { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 }, - { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 }, - { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 }, - { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 }, - { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 }, - { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 }, - { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 }, - { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 }, - { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 }, - { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 }, - { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 }, - { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 }, - { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 }, - { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 }, - { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, + { url = "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", size = 194219, upload_time = "2023-11-01T04:02:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", size = 122521, upload_time = "2023-11-01T04:02:32.452Z" }, + { url = "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", size = 120383, upload_time = "2023-11-01T04:02:34.11Z" }, + { url = "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", size = 138223, upload_time = "2023-11-01T04:02:36.213Z" }, + { url = "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", size = 148101, upload_time = "2023-11-01T04:02:38.067Z" }, + { url = "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", size = 140699, upload_time = "2023-11-01T04:02:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", size = 142065, upload_time = "2023-11-01T04:02:41.357Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", size = 144505, upload_time = "2023-11-01T04:02:43.108Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", size = 139425, upload_time = "2023-11-01T04:02:45.427Z" }, + { url = "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", size = 145287, upload_time = "2023-11-01T04:02:46.705Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", size = 149929, upload_time = "2023-11-01T04:02:48.098Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", size = 141605, upload_time = "2023-11-01T04:02:49.605Z" }, + { url = "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", size = 142646, upload_time = "2023-11-01T04:02:51.35Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/4fecf1d58bedb1340a50f165ba1c7ddc0400252d6832ff619c4568b36cc0/charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", size = 92846, upload_time = "2023-11-01T04:02:52.679Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a0/4af29e22cb5942488cf45630cbdd7cefd908768e69bdd90280842e4e8529/charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", size = 100343, upload_time = "2023-11-01T04:02:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647, upload_time = "2023-11-01T04:02:55.329Z" }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434, upload_time = "2023-11-01T04:02:57.173Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979, upload_time = "2023-11-01T04:02:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582, upload_time = "2023-11-01T04:02:59.776Z" }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645, upload_time = "2023-11-01T04:03:02.186Z" }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398, upload_time = "2023-11-01T04:03:04.255Z" }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273, upload_time = "2023-11-01T04:03:05.983Z" }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577, upload_time = "2023-11-01T04:03:07.567Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747, upload_time = "2023-11-01T04:03:08.886Z" }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375, upload_time = "2023-11-01T04:03:10.613Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474, upload_time = "2023-11-01T04:03:11.973Z" }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232, upload_time = "2023-11-01T04:03:13.505Z" }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859, upload_time = "2023-11-01T04:03:17.362Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509, upload_time = "2023-11-01T04:03:21.453Z" }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870, upload_time = "2023-11-01T04:03:22.723Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892, upload_time = "2023-11-01T04:03:24.135Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213, upload_time = "2023-11-01T04:03:25.66Z" }, + { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404, upload_time = "2023-11-01T04:03:27.04Z" }, + { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275, upload_time = "2023-11-01T04:03:28.466Z" }, + { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518, upload_time = "2023-11-01T04:03:29.82Z" }, + { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182, upload_time = "2023-11-01T04:03:31.511Z" }, + { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869, upload_time = "2023-11-01T04:03:32.887Z" }, + { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042, upload_time = "2023-11-01T04:03:34.412Z" }, + { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275, upload_time = "2023-11-01T04:03:35.759Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819, upload_time = "2023-11-01T04:03:37.216Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415, upload_time = "2023-11-01T04:03:38.694Z" }, + { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212, upload_time = "2023-11-01T04:03:40.07Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167, upload_time = "2023-11-01T04:03:41.491Z" }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041, upload_time = "2023-11-01T04:03:42.836Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397, upload_time = "2023-11-01T04:03:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543, upload_time = "2023-11-01T04:04:58.622Z" }, ] [[package]] @@ -309,18 +309,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121, upload_time = "2023-08-17T17:29:11.868Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941, upload_time = "2023-08-17T17:29:10.08Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload_time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload_time = "2022-10-25T02:36:20.889Z" }, ] [[package]] @@ -330,18 +330,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "humanfriendly" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520 } +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload_time = "2021-06-11T10:22:45.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018 }, + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload_time = "2021-06-11T10:22:42.561Z" }, ] [[package]] name = "configargparse" version = "1.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/8a/73f1008adfad01cb923255b924b1528727b8270e67cb4ef41eabdc7d783e/ConfigArgParse-1.7.tar.gz", hash = "sha256:e7067471884de5478c58a511e529f0f9bd1c66bfef1dea90935438d6c23306d1", size = 43817 } +sdist = { url = "https://files.pythonhosted.org/packages/70/8a/73f1008adfad01cb923255b924b1528727b8270e67cb4ef41eabdc7d783e/ConfigArgParse-1.7.tar.gz", hash = "sha256:e7067471884de5478c58a511e529f0f9bd1c66bfef1dea90935438d6c23306d1", size = 43817, upload_time = "2023-07-23T16:20:04.95Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/b3/b4ac838711fd74a2b4e6f746703cf9dd2cf5462d17dac07e349234e21b97/ConfigArgParse-1.7-py3-none-any.whl", hash = "sha256:d249da6591465c6c26df64a9f73d2536e743be2f244eb3ebe61114af2f94f86b", size = 25489 }, + { url = "https://files.pythonhosted.org/packages/6f/b3/b4ac838711fd74a2b4e6f746703cf9dd2cf5462d17dac07e349234e21b97/ConfigArgParse-1.7-py3-none-any.whl", hash = "sha256:d249da6591465c6c26df64a9f73d2536e743be2f244eb3ebe61114af2f94f86b", size = 25489, upload_time = "2023-07-23T16:20:03.27Z" }, ] [[package]] @@ -351,97 +351,97 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/a3/48ddc7ae832b000952cf4be64452381d150a41a2299c2eb19237168528d1/contourpy-1.2.0.tar.gz", hash = "sha256:171f311cb758de7da13fc53af221ae47a5877be5a0843a9fe150818c51ed276a", size = 13455881 } +sdist = { url = "https://files.pythonhosted.org/packages/11/a3/48ddc7ae832b000952cf4be64452381d150a41a2299c2eb19237168528d1/contourpy-1.2.0.tar.gz", hash = "sha256:171f311cb758de7da13fc53af221ae47a5877be5a0843a9fe150818c51ed276a", size = 13455881, upload_time = "2023-11-03T17:01:03.144Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/ea/f6e90933d82cc5aacf52f886a1c01f47f96eba99108ca2929c7b3ef45f82/contourpy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0274c1cb63625972c0c007ab14dd9ba9e199c36ae1a231ce45d725cbcbfd10a8", size = 256873 }, - { url = "https://files.pythonhosted.org/packages/fe/26/43821d61b7ee62c1809ec852bc572aaf4c27f101ebcebbbcce29a5ee0445/contourpy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab459a1cbbf18e8698399c595a01f6dcc5c138220ca3ea9e7e6126232d102bb4", size = 242211 }, - { url = "https://files.pythonhosted.org/packages/9b/99/c8fb63072a7573fe7682e1786a021f29f9c5f660a3aafcdce80b9ee8348d/contourpy-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdd887f17c2f4572ce548461e4f96396681212d858cae7bd52ba3310bc6f00f", size = 293195 }, - { url = "https://files.pythonhosted.org/packages/c7/a7/ae0b4bb8e0c865270d02ee619981413996dc10ddf1fd2689c938173ff62f/contourpy-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d16edfc3fc09968e09ddffada434b3bf989bf4911535e04eada58469873e28e", size = 332279 }, - { url = "https://files.pythonhosted.org/packages/94/7c/682228b9085ff323fb7e946fe139072e5f21b71360cf91f36ea079d4ea95/contourpy-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c203f617abc0dde5792beb586f827021069fb6d403d7f4d5c2b543d87edceb9", size = 305326 }, - { url = "https://files.pythonhosted.org/packages/58/56/e2c43dcfa1f9c7db4d5e3d6f5134b24ed953f4e2133a4b12f0062148db58/contourpy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b69303ceb2e4d4f146bf82fda78891ef7bcd80c41bf16bfca3d0d7eb545448aa", size = 310732 }, - { url = "https://files.pythonhosted.org/packages/94/0b/8495c4582057abc8377f945f6e11a86f1c07ad7b32fd4fdc968478cd0324/contourpy-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:884c3f9d42d7218304bc74a8a7693d172685c84bd7ab2bab1ee567b769696df9", size = 803420 }, - { url = "https://files.pythonhosted.org/packages/d5/1f/40399c7da649297147d404aedaa675cc60018f48ad284630c0d1406133e3/contourpy-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4a1b1208102be6e851f20066bf0e7a96b7d48a07c9b0cfe6d0d4545c2f6cadab", size = 829204 }, - { url = "https://files.pythonhosted.org/packages/8b/01/4be433b60dce7cbce8315cbcdfc016e7d25430a8b94e272355dff79cc3a8/contourpy-1.2.0-cp310-cp310-win32.whl", hash = "sha256:34b9071c040d6fe45d9826cbbe3727d20d83f1b6110d219b83eb0e2a01d79488", size = 165434 }, - { url = "https://files.pythonhosted.org/packages/fd/7c/168f8343f33d861305e18c56901ef1bb675d3c7f977f435ec72751a71a54/contourpy-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:bd2f1ae63998da104f16a8b788f685e55d65760cd1929518fd94cd682bf03e41", size = 186652 }, - { url = "https://files.pythonhosted.org/packages/9b/54/1dafec3c84df1d29119037330f7289db84a679cb2d5283af4ef24d89f532/contourpy-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd10c26b4eadae44783c45ad6655220426f971c61d9b239e6f7b16d5cdaaa727", size = 258243 }, - { url = "https://files.pythonhosted.org/packages/5b/ac/26fa1057f62beaa2af4c55c6ac733b114a403b746cfe0ce3dc6e4aec921a/contourpy-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c6b28956b7b232ae801406e529ad7b350d3f09a4fde958dfdf3c0520cdde0dd", size = 243408 }, - { url = "https://files.pythonhosted.org/packages/b7/33/cd0ecc80123f499d76d2fe2807cb4d5638ef8730735c580c8a8a03e1928e/contourpy-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebeac59e9e1eb4b84940d076d9f9a6cec0064e241818bcb6e32124cc5c3e377a", size = 294142 }, - { url = "https://files.pythonhosted.org/packages/6d/75/1b7bf20bf6394e01df2c4b4b3d44d3dc280c16ddaff72724639100bd4314/contourpy-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:139d8d2e1c1dd52d78682f505e980f592ba53c9f73bd6be102233e358b401063", size = 333129 }, - { url = "https://files.pythonhosted.org/packages/22/5b/fedd961dff1877e5d3b83c5201295cfdcdc2438884c2851aa7ecf6cec045/contourpy-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e9dc350fb4c58adc64df3e0703ab076f60aac06e67d48b3848c23647ae4310e", size = 307461 }, - { url = "https://files.pythonhosted.org/packages/e2/83/29a63bbc72839cc6b24b5a0e3d004d4ed4e8439f26460ad9a34e39251904/contourpy-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18fc2b4ed8e4a8fe849d18dce4bd3c7ea637758c6343a1f2bae1e9bd4c9f4686", size = 313352 }, - { url = "https://files.pythonhosted.org/packages/4b/c7/4bac0fc4f1e802ab47e75076d83d2e1448e0668ba6cc9000cf4e9d5bd94a/contourpy-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:16a7380e943a6d52472096cb7ad5264ecee36ed60888e2a3d3814991a0107286", size = 804127 }, - { url = "https://files.pythonhosted.org/packages/e3/47/b3fd5bdc2f6ec13502d57a5bc390ffe62648605ed1689c93b0015150a784/contourpy-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8d8faf05be5ec8e02a4d86f616fc2a0322ff4a4ce26c0f09d9f7fb5330a35c95", size = 829561 }, - { url = "https://files.pythonhosted.org/packages/5c/04/be16038e754169caea4d02d82f8e5cd97dece593e5ac9e05735da0afd0c5/contourpy-1.2.0-cp311-cp311-win32.whl", hash = "sha256:67b7f17679fa62ec82b7e3e611c43a016b887bd64fb933b3ae8638583006c6d6", size = 166197 }, - { url = "https://files.pythonhosted.org/packages/ca/2a/d197a412ec474391ee878b1218cf2fe9c6e963903755887fc5654c06636a/contourpy-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:99ad97258985328b4f207a5e777c1b44a83bfe7cf1f87b99f9c11d4ee477c4de", size = 187556 }, - { url = "https://files.pythonhosted.org/packages/4f/03/839da46999173226bead08794cbd7b4d37c9e6b02686ca74c93556b43258/contourpy-1.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:575bcaf957a25d1194903a10bc9f316c136c19f24e0985a2b9b5608bdf5dbfe0", size = 259253 }, - { url = "https://files.pythonhosted.org/packages/f3/9e/8fb3f53144269d3fecdd8786d3a4686eeff55b9b35a3c0772a3f62f71e36/contourpy-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9e6c93b5b2dbcedad20a2f18ec22cae47da0d705d454308063421a3b290d9ea4", size = 242555 }, - { url = "https://files.pythonhosted.org/packages/a6/85/9815ccb5a18ee8c9a46bd5ef20d02b292cd4a99c62553f38c87015f16d59/contourpy-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:464b423bc2a009088f19bdf1f232299e8b6917963e2b7e1d277da5041f33a779", size = 288108 }, - { url = "https://files.pythonhosted.org/packages/5a/d9/4df5c26bd0f496c8cd7940fd53db95d07deeb98518f02f805ce570590da8/contourpy-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68ce4788b7d93e47f84edd3f1f95acdcd142ae60bc0e5493bfd120683d2d4316", size = 330810 }, - { url = "https://files.pythonhosted.org/packages/67/d4/8aae9793a0cfde72959312521ebd3aa635c260c3d580448e8db6bdcdd1aa/contourpy-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d7d1f8871998cdff5d2ff6a087e5e1780139abe2838e85b0b46b7ae6cc25399", size = 305290 }, - { url = "https://files.pythonhosted.org/packages/20/84/ffddcdcc579cbf7213fd92a3578ca08a931a3bf879a22deb5a83ffc5002c/contourpy-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e739530c662a8d6d42c37c2ed52a6f0932c2d4a3e8c1f90692ad0ce1274abe0", size = 303937 }, - { url = "https://files.pythonhosted.org/packages/d8/ad/6e570cf525f909da94559ed716189f92f529bc7b5f78645733c44619a0e2/contourpy-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:247b9d16535acaa766d03037d8e8fb20866d054d3c7fbf6fd1f993f11fc60ca0", size = 801977 }, - { url = "https://files.pythonhosted.org/packages/36/b4/55f23482c596eca36d16fc668b147865c56fcf90353f4c57f073d8d5e532/contourpy-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:461e3ae84cd90b30f8d533f07d87c00379644205b1d33a5ea03381edc4b69431", size = 827442 }, - { url = "https://files.pythonhosted.org/packages/e9/47/9c081b1f11d6053cb0aa4c46b7de2ea2849a4a8d40de81c7bc3f99773b02/contourpy-1.2.0-cp312-cp312-win32.whl", hash = "sha256:1c2559d6cffc94890b0529ea7eeecc20d6fadc1539273aa27faf503eb4656d8f", size = 165363 }, - { url = "https://files.pythonhosted.org/packages/8e/ae/a6353db548bff1a592b85ae6bb80275f0a51dc25a0410d059e5b33183e36/contourpy-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:491b1917afdd8638a05b611a56d46587d5a632cabead889a5440f7c638bc6ed9", size = 187731 }, + { url = "https://files.pythonhosted.org/packages/e8/ea/f6e90933d82cc5aacf52f886a1c01f47f96eba99108ca2929c7b3ef45f82/contourpy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0274c1cb63625972c0c007ab14dd9ba9e199c36ae1a231ce45d725cbcbfd10a8", size = 256873, upload_time = "2023-11-03T16:56:34.548Z" }, + { url = "https://files.pythonhosted.org/packages/fe/26/43821d61b7ee62c1809ec852bc572aaf4c27f101ebcebbbcce29a5ee0445/contourpy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab459a1cbbf18e8698399c595a01f6dcc5c138220ca3ea9e7e6126232d102bb4", size = 242211, upload_time = "2023-11-03T16:56:38.028Z" }, + { url = "https://files.pythonhosted.org/packages/9b/99/c8fb63072a7573fe7682e1786a021f29f9c5f660a3aafcdce80b9ee8348d/contourpy-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdd887f17c2f4572ce548461e4f96396681212d858cae7bd52ba3310bc6f00f", size = 293195, upload_time = "2023-11-03T16:56:41.598Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a7/ae0b4bb8e0c865270d02ee619981413996dc10ddf1fd2689c938173ff62f/contourpy-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d16edfc3fc09968e09ddffada434b3bf989bf4911535e04eada58469873e28e", size = 332279, upload_time = "2023-11-03T16:56:46.08Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/682228b9085ff323fb7e946fe139072e5f21b71360cf91f36ea079d4ea95/contourpy-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c203f617abc0dde5792beb586f827021069fb6d403d7f4d5c2b543d87edceb9", size = 305326, upload_time = "2023-11-03T16:56:49.647Z" }, + { url = "https://files.pythonhosted.org/packages/58/56/e2c43dcfa1f9c7db4d5e3d6f5134b24ed953f4e2133a4b12f0062148db58/contourpy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b69303ceb2e4d4f146bf82fda78891ef7bcd80c41bf16bfca3d0d7eb545448aa", size = 310732, upload_time = "2023-11-03T16:56:53.773Z" }, + { url = "https://files.pythonhosted.org/packages/94/0b/8495c4582057abc8377f945f6e11a86f1c07ad7b32fd4fdc968478cd0324/contourpy-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:884c3f9d42d7218304bc74a8a7693d172685c84bd7ab2bab1ee567b769696df9", size = 803420, upload_time = "2023-11-03T16:57:00.669Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1f/40399c7da649297147d404aedaa675cc60018f48ad284630c0d1406133e3/contourpy-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4a1b1208102be6e851f20066bf0e7a96b7d48a07c9b0cfe6d0d4545c2f6cadab", size = 829204, upload_time = "2023-11-03T16:57:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/8b/01/4be433b60dce7cbce8315cbcdfc016e7d25430a8b94e272355dff79cc3a8/contourpy-1.2.0-cp310-cp310-win32.whl", hash = "sha256:34b9071c040d6fe45d9826cbbe3727d20d83f1b6110d219b83eb0e2a01d79488", size = 165434, upload_time = "2023-11-03T16:57:10.601Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7c/168f8343f33d861305e18c56901ef1bb675d3c7f977f435ec72751a71a54/contourpy-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:bd2f1ae63998da104f16a8b788f685e55d65760cd1929518fd94cd682bf03e41", size = 186652, upload_time = "2023-11-03T16:57:13.57Z" }, + { url = "https://files.pythonhosted.org/packages/9b/54/1dafec3c84df1d29119037330f7289db84a679cb2d5283af4ef24d89f532/contourpy-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd10c26b4eadae44783c45ad6655220426f971c61d9b239e6f7b16d5cdaaa727", size = 258243, upload_time = "2023-11-03T16:57:16.604Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ac/26fa1057f62beaa2af4c55c6ac733b114a403b746cfe0ce3dc6e4aec921a/contourpy-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c6b28956b7b232ae801406e529ad7b350d3f09a4fde958dfdf3c0520cdde0dd", size = 243408, upload_time = "2023-11-03T16:57:20.021Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/cd0ecc80123f499d76d2fe2807cb4d5638ef8730735c580c8a8a03e1928e/contourpy-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebeac59e9e1eb4b84940d076d9f9a6cec0064e241818bcb6e32124cc5c3e377a", size = 294142, upload_time = "2023-11-03T16:57:23.48Z" }, + { url = "https://files.pythonhosted.org/packages/6d/75/1b7bf20bf6394e01df2c4b4b3d44d3dc280c16ddaff72724639100bd4314/contourpy-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:139d8d2e1c1dd52d78682f505e980f592ba53c9f73bd6be102233e358b401063", size = 333129, upload_time = "2023-11-03T16:57:27.141Z" }, + { url = "https://files.pythonhosted.org/packages/22/5b/fedd961dff1877e5d3b83c5201295cfdcdc2438884c2851aa7ecf6cec045/contourpy-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e9dc350fb4c58adc64df3e0703ab076f60aac06e67d48b3848c23647ae4310e", size = 307461, upload_time = "2023-11-03T16:57:30.537Z" }, + { url = "https://files.pythonhosted.org/packages/e2/83/29a63bbc72839cc6b24b5a0e3d004d4ed4e8439f26460ad9a34e39251904/contourpy-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18fc2b4ed8e4a8fe849d18dce4bd3c7ea637758c6343a1f2bae1e9bd4c9f4686", size = 313352, upload_time = "2023-11-03T16:57:34.937Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c7/4bac0fc4f1e802ab47e75076d83d2e1448e0668ba6cc9000cf4e9d5bd94a/contourpy-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:16a7380e943a6d52472096cb7ad5264ecee36ed60888e2a3d3814991a0107286", size = 804127, upload_time = "2023-11-03T16:57:42.201Z" }, + { url = "https://files.pythonhosted.org/packages/e3/47/b3fd5bdc2f6ec13502d57a5bc390ffe62648605ed1689c93b0015150a784/contourpy-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8d8faf05be5ec8e02a4d86f616fc2a0322ff4a4ce26c0f09d9f7fb5330a35c95", size = 829561, upload_time = "2023-11-03T16:57:49.667Z" }, + { url = "https://files.pythonhosted.org/packages/5c/04/be16038e754169caea4d02d82f8e5cd97dece593e5ac9e05735da0afd0c5/contourpy-1.2.0-cp311-cp311-win32.whl", hash = "sha256:67b7f17679fa62ec82b7e3e611c43a016b887bd64fb933b3ae8638583006c6d6", size = 166197, upload_time = "2023-11-03T16:57:52.682Z" }, + { url = "https://files.pythonhosted.org/packages/ca/2a/d197a412ec474391ee878b1218cf2fe9c6e963903755887fc5654c06636a/contourpy-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:99ad97258985328b4f207a5e777c1b44a83bfe7cf1f87b99f9c11d4ee477c4de", size = 187556, upload_time = "2023-11-03T16:57:55.286Z" }, + { url = "https://files.pythonhosted.org/packages/4f/03/839da46999173226bead08794cbd7b4d37c9e6b02686ca74c93556b43258/contourpy-1.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:575bcaf957a25d1194903a10bc9f316c136c19f24e0985a2b9b5608bdf5dbfe0", size = 259253, upload_time = "2023-11-03T16:57:58.572Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9e/8fb3f53144269d3fecdd8786d3a4686eeff55b9b35a3c0772a3f62f71e36/contourpy-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9e6c93b5b2dbcedad20a2f18ec22cae47da0d705d454308063421a3b290d9ea4", size = 242555, upload_time = "2023-11-03T16:58:01.48Z" }, + { url = "https://files.pythonhosted.org/packages/a6/85/9815ccb5a18ee8c9a46bd5ef20d02b292cd4a99c62553f38c87015f16d59/contourpy-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:464b423bc2a009088f19bdf1f232299e8b6917963e2b7e1d277da5041f33a779", size = 288108, upload_time = "2023-11-03T16:58:05.546Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d9/4df5c26bd0f496c8cd7940fd53db95d07deeb98518f02f805ce570590da8/contourpy-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68ce4788b7d93e47f84edd3f1f95acdcd142ae60bc0e5493bfd120683d2d4316", size = 330810, upload_time = "2023-11-03T16:58:09.568Z" }, + { url = "https://files.pythonhosted.org/packages/67/d4/8aae9793a0cfde72959312521ebd3aa635c260c3d580448e8db6bdcdd1aa/contourpy-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d7d1f8871998cdff5d2ff6a087e5e1780139abe2838e85b0b46b7ae6cc25399", size = 305290, upload_time = "2023-11-03T16:58:13.017Z" }, + { url = "https://files.pythonhosted.org/packages/20/84/ffddcdcc579cbf7213fd92a3578ca08a931a3bf879a22deb5a83ffc5002c/contourpy-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e739530c662a8d6d42c37c2ed52a6f0932c2d4a3e8c1f90692ad0ce1274abe0", size = 303937, upload_time = "2023-11-03T16:58:16.426Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ad/6e570cf525f909da94559ed716189f92f529bc7b5f78645733c44619a0e2/contourpy-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:247b9d16535acaa766d03037d8e8fb20866d054d3c7fbf6fd1f993f11fc60ca0", size = 801977, upload_time = "2023-11-03T16:58:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/36/b4/55f23482c596eca36d16fc668b147865c56fcf90353f4c57f073d8d5e532/contourpy-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:461e3ae84cd90b30f8d533f07d87c00379644205b1d33a5ea03381edc4b69431", size = 827442, upload_time = "2023-11-03T16:58:30.724Z" }, + { url = "https://files.pythonhosted.org/packages/e9/47/9c081b1f11d6053cb0aa4c46b7de2ea2849a4a8d40de81c7bc3f99773b02/contourpy-1.2.0-cp312-cp312-win32.whl", hash = "sha256:1c2559d6cffc94890b0529ea7eeecc20d6fadc1539273aa27faf503eb4656d8f", size = 165363, upload_time = "2023-11-03T16:58:33.54Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ae/a6353db548bff1a592b85ae6bb80275f0a51dc25a0410d059e5b33183e36/contourpy-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:491b1917afdd8638a05b611a56d46587d5a632cabead889a5440f7c638bc6ed9", size = 187731, upload_time = "2023-11-03T16:58:36.585Z" }, ] [[package]] name = "coverage" version = "7.6.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/12/3669b6382792783e92046730ad3327f53b2726f0603f4c311c4da4824222/coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", size = 798716 } +sdist = { url = "https://files.pythonhosted.org/packages/52/12/3669b6382792783e92046730ad3327f53b2726f0603f4c311c4da4824222/coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", size = 798716, upload_time = "2024-10-20T22:57:39.682Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/93/4ad92f71e28ece5c0326e5f4a6630aa4928a8846654a65cfff69b49b95b9/coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07", size = 206713 }, - { url = "https://files.pythonhosted.org/packages/01/ae/747a580b1eda3f2e431d87de48f0604bd7bc92e52a1a95185a4aa585bc47/coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0", size = 207149 }, - { url = "https://files.pythonhosted.org/packages/07/1a/1f573f8a6145f6d4c9130bbc120e0024daf1b24cf2a78d7393fa6eb6aba7/coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72", size = 235584 }, - { url = "https://files.pythonhosted.org/packages/40/42/c8523f2e4db34aa9389caee0d3688b6ada7a84fcc782e943a868a7f302bd/coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51", size = 233486 }, - { url = "https://files.pythonhosted.org/packages/8d/95/565c310fffa16ede1a042e9ea1ca3962af0d8eb5543bc72df6b91dc0c3d5/coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491", size = 234649 }, - { url = "https://files.pythonhosted.org/packages/d5/81/3b550674d98968ec29c92e3e8650682be6c8b1fa7581a059e7e12e74c431/coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b", size = 233744 }, - { url = "https://files.pythonhosted.org/packages/0d/70/d66c7f51b3e33aabc5ea9f9624c1c9d9655472962270eb5e7b0d32707224/coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea", size = 232204 }, - { url = "https://files.pythonhosted.org/packages/23/2d/2b3a2dbed7a5f40693404c8a09e779d7c1a5fbed089d3e7224c002129ec8/coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a", size = 233335 }, - { url = "https://files.pythonhosted.org/packages/5a/4f/92d1d2ad720d698a4e71c176eacf531bfb8e0721d5ad560556f2c484a513/coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa", size = 209435 }, - { url = "https://files.pythonhosted.org/packages/c7/b9/cdf158e7991e2287bcf9082670928badb73d310047facac203ff8dcd5ff3/coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172", size = 210243 }, - { url = "https://files.pythonhosted.org/packages/87/31/9c0cf84f0dfcbe4215b7eb95c31777cdc0483c13390e69584c8150c85175/coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", size = 206819 }, - { url = "https://files.pythonhosted.org/packages/53/ed/a38401079ad320ad6e054a01ec2b61d270511aeb3c201c80e99c841229d5/coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", size = 207263 }, - { url = "https://files.pythonhosted.org/packages/20/e7/c3ad33b179ab4213f0d70da25a9c214d52464efa11caeab438592eb1d837/coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", size = 239205 }, - { url = "https://files.pythonhosted.org/packages/36/91/fc02e8d8e694f557752120487fd982f654ba1421bbaa5560debf96ddceda/coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", size = 236612 }, - { url = "https://files.pythonhosted.org/packages/cc/57/cb08f0eda0389a9a8aaa4fc1f9fec7ac361c3e2d68efd5890d7042c18aa3/coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", size = 238479 }, - { url = "https://files.pythonhosted.org/packages/d5/c9/2c7681a9b3ca6e6f43d489c2e6653a53278ed857fd6e7010490c307b0a47/coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", size = 237405 }, - { url = "https://files.pythonhosted.org/packages/b5/4e/ebfc6944b96317df8b537ae875d2e57c27b84eb98820bc0a1055f358f056/coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", size = 236038 }, - { url = "https://files.pythonhosted.org/packages/13/f2/3a0bf1841a97c0654905e2ef531170f02c89fad2555879db8fe41a097871/coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", size = 236812 }, - { url = "https://files.pythonhosted.org/packages/b9/9c/66bf59226b52ce6ed9541b02d33e80a6e816a832558fbdc1111a7bd3abd4/coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", size = 209400 }, - { url = "https://files.pythonhosted.org/packages/2a/a0/b0790934c04dfc8d658d4a62acb8f7ca0efdf3818456fcad757b11c6479d/coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", size = 210243 }, - { url = "https://files.pythonhosted.org/packages/7d/e7/9291de916d084f41adddfd4b82246e68d61d6a75747f075f7e64628998d2/coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", size = 207013 }, - { url = "https://files.pythonhosted.org/packages/27/03/932c2c5717a7fa80cd43c6a07d3177076d97b79f12f40f882f9916db0063/coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", size = 207251 }, - { url = "https://files.pythonhosted.org/packages/d5/3f/0af47dcb9327f65a45455fbca846fe96eb57c153af46c4754a3ba678938a/coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", size = 240268 }, - { url = "https://files.pythonhosted.org/packages/8a/3c/37a9d81bbd4b23bc7d46ca820e16174c613579c66342faa390a271d2e18b/coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", size = 237298 }, - { url = "https://files.pythonhosted.org/packages/c0/70/6b0627e5bd68204ee580126ed3513140b2298995c1233bd67404b4e44d0e/coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", size = 239367 }, - { url = "https://files.pythonhosted.org/packages/3c/eb/634d7dfab24ac3b790bebaf9da0f4a5352cbc125ce6a9d5c6cf4c6cae3c7/coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", size = 238853 }, - { url = "https://files.pythonhosted.org/packages/d9/0d/8e3ed00f1266ef7472a4e33458f42e39492e01a64281084fb3043553d3f1/coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", size = 237160 }, - { url = "https://files.pythonhosted.org/packages/ce/9c/4337f468ef0ab7a2e0887a9c9da0e58e2eada6fc6cbee637a4acd5dfd8a9/coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", size = 238824 }, - { url = "https://files.pythonhosted.org/packages/5e/09/3e94912b8dd37251377bb02727a33a67ee96b84bbbe092f132b401ca5dd9/coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", size = 209639 }, - { url = "https://files.pythonhosted.org/packages/01/69/d4f3a4101171f32bc5b3caec8ff94c2c60f700107a6aaef7244b2c166793/coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", size = 210428 }, - { url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039 }, - { url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298 }, - { url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813 }, - { url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959 }, - { url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950 }, - { url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610 }, - { url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697 }, - { url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541 }, - { url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707 }, - { url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439 }, - { url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784 }, - { url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058 }, - { url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772 }, - { url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490 }, - { url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848 }, - { url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340 }, - { url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229 }, - { url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510 }, - { url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353 }, - { url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502 }, - { url = "https://files.pythonhosted.org/packages/cc/56/e1d75e8981a2a92c2a777e67c26efa96c66da59d645423146eb9ff3a851b/coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e", size = 198954 }, + { url = "https://files.pythonhosted.org/packages/a5/93/4ad92f71e28ece5c0326e5f4a6630aa4928a8846654a65cfff69b49b95b9/coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07", size = 206713, upload_time = "2024-10-20T22:56:03.877Z" }, + { url = "https://files.pythonhosted.org/packages/01/ae/747a580b1eda3f2e431d87de48f0604bd7bc92e52a1a95185a4aa585bc47/coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0", size = 207149, upload_time = "2024-10-20T22:56:06.511Z" }, + { url = "https://files.pythonhosted.org/packages/07/1a/1f573f8a6145f6d4c9130bbc120e0024daf1b24cf2a78d7393fa6eb6aba7/coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72", size = 235584, upload_time = "2024-10-20T22:56:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/40/42/c8523f2e4db34aa9389caee0d3688b6ada7a84fcc782e943a868a7f302bd/coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51", size = 233486, upload_time = "2024-10-20T22:56:09.496Z" }, + { url = "https://files.pythonhosted.org/packages/8d/95/565c310fffa16ede1a042e9ea1ca3962af0d8eb5543bc72df6b91dc0c3d5/coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491", size = 234649, upload_time = "2024-10-20T22:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/3b550674d98968ec29c92e3e8650682be6c8b1fa7581a059e7e12e74c431/coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b", size = 233744, upload_time = "2024-10-20T22:56:12.481Z" }, + { url = "https://files.pythonhosted.org/packages/0d/70/d66c7f51b3e33aabc5ea9f9624c1c9d9655472962270eb5e7b0d32707224/coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea", size = 232204, upload_time = "2024-10-20T22:56:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/23/2d/2b3a2dbed7a5f40693404c8a09e779d7c1a5fbed089d3e7224c002129ec8/coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a", size = 233335, upload_time = "2024-10-20T22:56:15.521Z" }, + { url = "https://files.pythonhosted.org/packages/5a/4f/92d1d2ad720d698a4e71c176eacf531bfb8e0721d5ad560556f2c484a513/coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa", size = 209435, upload_time = "2024-10-20T22:56:17.309Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/cdf158e7991e2287bcf9082670928badb73d310047facac203ff8dcd5ff3/coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172", size = 210243, upload_time = "2024-10-20T22:56:18.366Z" }, + { url = "https://files.pythonhosted.org/packages/87/31/9c0cf84f0dfcbe4215b7eb95c31777cdc0483c13390e69584c8150c85175/coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", size = 206819, upload_time = "2024-10-20T22:56:20.132Z" }, + { url = "https://files.pythonhosted.org/packages/53/ed/a38401079ad320ad6e054a01ec2b61d270511aeb3c201c80e99c841229d5/coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", size = 207263, upload_time = "2024-10-20T22:56:21.88Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/c3ad33b179ab4213f0d70da25a9c214d52464efa11caeab438592eb1d837/coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", size = 239205, upload_time = "2024-10-20T22:56:23.03Z" }, + { url = "https://files.pythonhosted.org/packages/36/91/fc02e8d8e694f557752120487fd982f654ba1421bbaa5560debf96ddceda/coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", size = 236612, upload_time = "2024-10-20T22:56:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/cc/57/cb08f0eda0389a9a8aaa4fc1f9fec7ac361c3e2d68efd5890d7042c18aa3/coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", size = 238479, upload_time = "2024-10-20T22:56:26.749Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c9/2c7681a9b3ca6e6f43d489c2e6653a53278ed857fd6e7010490c307b0a47/coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", size = 237405, upload_time = "2024-10-20T22:56:27.958Z" }, + { url = "https://files.pythonhosted.org/packages/b5/4e/ebfc6944b96317df8b537ae875d2e57c27b84eb98820bc0a1055f358f056/coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", size = 236038, upload_time = "2024-10-20T22:56:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/13/f2/3a0bf1841a97c0654905e2ef531170f02c89fad2555879db8fe41a097871/coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", size = 236812, upload_time = "2024-10-20T22:56:31.654Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/66bf59226b52ce6ed9541b02d33e80a6e816a832558fbdc1111a7bd3abd4/coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", size = 209400, upload_time = "2024-10-20T22:56:33.569Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a0/b0790934c04dfc8d658d4a62acb8f7ca0efdf3818456fcad757b11c6479d/coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", size = 210243, upload_time = "2024-10-20T22:56:34.863Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e7/9291de916d084f41adddfd4b82246e68d61d6a75747f075f7e64628998d2/coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", size = 207013, upload_time = "2024-10-20T22:56:36.034Z" }, + { url = "https://files.pythonhosted.org/packages/27/03/932c2c5717a7fa80cd43c6a07d3177076d97b79f12f40f882f9916db0063/coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", size = 207251, upload_time = "2024-10-20T22:56:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/d5/3f/0af47dcb9327f65a45455fbca846fe96eb57c153af46c4754a3ba678938a/coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", size = 240268, upload_time = "2024-10-20T22:56:40.051Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3c/37a9d81bbd4b23bc7d46ca820e16174c613579c66342faa390a271d2e18b/coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", size = 237298, upload_time = "2024-10-20T22:56:41.929Z" }, + { url = "https://files.pythonhosted.org/packages/c0/70/6b0627e5bd68204ee580126ed3513140b2298995c1233bd67404b4e44d0e/coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", size = 239367, upload_time = "2024-10-20T22:56:43.141Z" }, + { url = "https://files.pythonhosted.org/packages/3c/eb/634d7dfab24ac3b790bebaf9da0f4a5352cbc125ce6a9d5c6cf4c6cae3c7/coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", size = 238853, upload_time = "2024-10-20T22:56:44.33Z" }, + { url = "https://files.pythonhosted.org/packages/d9/0d/8e3ed00f1266ef7472a4e33458f42e39492e01a64281084fb3043553d3f1/coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", size = 237160, upload_time = "2024-10-20T22:56:46.258Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9c/4337f468ef0ab7a2e0887a9c9da0e58e2eada6fc6cbee637a4acd5dfd8a9/coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", size = 238824, upload_time = "2024-10-20T22:56:48.666Z" }, + { url = "https://files.pythonhosted.org/packages/5e/09/3e94912b8dd37251377bb02727a33a67ee96b84bbbe092f132b401ca5dd9/coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", size = 209639, upload_time = "2024-10-20T22:56:50.664Z" }, + { url = "https://files.pythonhosted.org/packages/01/69/d4f3a4101171f32bc5b3caec8ff94c2c60f700107a6aaef7244b2c166793/coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", size = 210428, upload_time = "2024-10-20T22:56:52.468Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039, upload_time = "2024-10-20T22:56:53.656Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298, upload_time = "2024-10-20T22:56:54.979Z" }, + { url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813, upload_time = "2024-10-20T22:56:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959, upload_time = "2024-10-20T22:56:58.06Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950, upload_time = "2024-10-20T22:56:59.329Z" }, + { url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610, upload_time = "2024-10-20T22:57:00.645Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697, upload_time = "2024-10-20T22:57:01.944Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541, upload_time = "2024-10-20T22:57:03.848Z" }, + { url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707, upload_time = "2024-10-20T22:57:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439, upload_time = "2024-10-20T22:57:06.35Z" }, + { url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784, upload_time = "2024-10-20T22:57:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058, upload_time = "2024-10-20T22:57:09.845Z" }, + { url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772, upload_time = "2024-10-20T22:57:11.147Z" }, + { url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490, upload_time = "2024-10-20T22:57:13.02Z" }, + { url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848, upload_time = "2024-10-20T22:57:14.927Z" }, + { url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340, upload_time = "2024-10-20T22:57:16.246Z" }, + { url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229, upload_time = "2024-10-20T22:57:17.546Z" }, + { url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510, upload_time = "2024-10-20T22:57:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353, upload_time = "2024-10-20T22:57:20.891Z" }, + { url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502, upload_time = "2024-10-20T22:57:22.21Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/e1d75e8981a2a92c2a777e67c26efa96c66da59d645423146eb9ff3a851b/coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e", size = 198954, upload_time = "2024-10-20T22:57:38.28Z" }, ] [package.optional-dependencies] @@ -453,57 +453,57 @@ toml = [ name = "cycler" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload_time = "2023-10-07T05:32:18.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload_time = "2023-10-07T05:32:16.783Z" }, ] [[package]] name = "cython" version = "3.0.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/09/ffb61f29b8e3d207c444032b21328327d753e274ea081bc74e009827cc81/Cython-3.0.8.tar.gz", hash = "sha256:8333423d8fd5765e7cceea3a9985dd1e0a5dfeb2734629e1a2ed2d6233d39de6", size = 2744096 } +sdist = { url = "https://files.pythonhosted.org/packages/68/09/ffb61f29b8e3d207c444032b21328327d753e274ea081bc74e009827cc81/Cython-3.0.8.tar.gz", hash = "sha256:8333423d8fd5765e7cceea3a9985dd1e0a5dfeb2734629e1a2ed2d6233d39de6", size = 2744096, upload_time = "2024-01-10T11:01:02.155Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/f4/d2542e186fe33ec1cc542770fb17466421ed54f4ffe04d00fe9549d0a467/Cython-3.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a846e0a38e2b24e9a5c5dc74b0e54c6e29420d88d1dafabc99e0fc0f3e338636", size = 3100459 }, - { url = "https://files.pythonhosted.org/packages/fc/27/2652f395aa708fb3081148e0df3ab700bd7288636c65332ef7febad6a380/Cython-3.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45523fdc2b78d79b32834cc1cc12dc2ca8967af87e22a3ee1bff20e77c7f5520", size = 3456626 }, - { url = "https://files.pythonhosted.org/packages/f9/bd/e8a1d26d04c08a67bcc383f2ea5493a4e77f37a8770ead00a238b08ad729/Cython-3.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa0b7f3f841fe087410cab66778e2d3fb20ae2d2078a2be3dffe66c6574be39", size = 3621379 }, - { url = "https://files.pythonhosted.org/packages/03/ae/ead7ec03d0062d439879d41b7830e4f2480213f7beabf2f7052a191cc6f7/Cython-3.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e87294e33e40c289c77a135f491cd721bd089f193f956f7b8ed5aa2d0b8c558f", size = 3671873 }, - { url = "https://files.pythonhosted.org/packages/63/b0/81dad725604d7b529c492f873a7fa1b5800704a9f26e100ed25e9fd8d057/Cython-3.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a1df7a129344b1215c20096d33c00193437df1a8fcca25b71f17c23b1a44f782", size = 3463832 }, - { url = "https://files.pythonhosted.org/packages/13/cd/72b8e0af597ac1b376421847acf6d6fa252e60059a2a00dcf05ceb16d28f/Cython-3.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:13c2a5e57a0358da467d97667297bf820b62a1a87ae47c5f87938b9bb593acbd", size = 3618325 }, - { url = "https://files.pythonhosted.org/packages/ef/73/11a4355d8b8966504c751e5bcb25916c4140de27bb2ba1b54ff21994d7fe/Cython-3.0.8-cp310-cp310-win32.whl", hash = "sha256:96b028f044f5880e3cb18ecdcfc6c8d3ce9d0af28418d5ab464509f26d8adf12", size = 2571305 }, - { url = "https://files.pythonhosted.org/packages/18/15/fdc0c3552d20f9337b134a36d786da24e47998fc39f62cb61c1534f26123/Cython-3.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:8140597a8b5cc4f119a1190f5a2228a84f5ca6d8d9ec386cfce24663f48b2539", size = 2776113 }, - { url = "https://files.pythonhosted.org/packages/db/a7/f4a0bc9a80e23b380daa2ebb4879bf434aaa0b3b91f7ad8a7f9762b4bd1b/Cython-3.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aae26f9663e50caf9657148403d9874eea41770ecdd6caf381d177c2b1bb82ba", size = 3113615 }, - { url = "https://files.pythonhosted.org/packages/e9/e9/e9295df74246c165b91253a473bfa179debf739c9bee961cbb3ae56c2b79/Cython-3.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:547eb3cdb2f8c6f48e6865d5a741d9dd051c25b3ce076fbca571727977b28ac3", size = 3436320 }, - { url = "https://files.pythonhosted.org/packages/26/2c/6a887c957aa53e44f928119dea628a5dfacc8e875424034f5fecac9daba4/Cython-3.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a567d4b9ba70b26db89d75b243529de9e649a2f56384287533cf91512705bee", size = 3591755 }, - { url = "https://files.pythonhosted.org/packages/ba/b8/f9c97bae6281da50b3ecb1f7fef0f7f7851eae084609b364717a2b366bf1/Cython-3.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51d1426263b0e82fb22bda8ea60dc77a428581cc19e97741011b938445d383f1", size = 3636099 }, - { url = "https://files.pythonhosted.org/packages/17/ae/cd055c2c081c67a6fcad1d8d17d82bd6395b14c6741e3a938f40318c8bc5/Cython-3.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c26daaeccda072459b48d211415fd1e5507c06bcd976fa0d5b8b9f1063467d7b", size = 3458119 }, - { url = "https://files.pythonhosted.org/packages/72/ab/ac6f5548d6194f4bb2fc8c6c996aa7369f0fa1403e4d4de787d9e9309b27/Cython-3.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:289ce7838208211cd166e975865fd73b0649bf118170b6cebaedfbdaf4a37795", size = 3614418 }, - { url = "https://files.pythonhosted.org/packages/70/e2/3e3e448b7a94887bec3235bcb71957b6681dc42b4536459f8f54d46fa936/Cython-3.0.8-cp311-cp311-win32.whl", hash = "sha256:c8aa05f5e17f8042a3be052c24f2edc013fb8af874b0bf76907d16c51b4e7871", size = 2572819 }, - { url = "https://files.pythonhosted.org/packages/85/7d/58635941dfbb5b4e197adb88080b9cbfb230dc3b75683698a530a1989bdb/Cython-3.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:000dc9e135d0eec6ecb2b40a5b02d0868a2f8d2e027a41b0fe16a908a9e6de02", size = 2784167 }, - { url = "https://files.pythonhosted.org/packages/3d/8e/28f8c6109990eef7317ab7e43644092b49a88a39f9373dcd19318946df09/Cython-3.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:90d3fe31db55685d8cb97d43b0ec39ef614fcf660f83c77ed06aa670cb0e164f", size = 3135638 }, - { url = "https://files.pythonhosted.org/packages/83/1f/4720cb682b8ed1ab9749dea35351a66dd29b6a022628cce038415660c384/Cython-3.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e24791ddae2324e88e3c902a765595c738f19ae34ee66bfb1a6dac54b1833419", size = 3340052 }, - { url = "https://files.pythonhosted.org/packages/8a/47/ec3fceb9e8f7d6fa130216b8740038e1df7c8e5f215bba363fcf1272a6c1/Cython-3.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f020fa1c0552052e0660790b8153b79e3fc9a15dbd8f1d0b841fe5d204a6ae6", size = 3510079 }, - { url = "https://files.pythonhosted.org/packages/71/31/b458127851e248effb909e2791b55870914863cde7c60b94db5ee65d7867/Cython-3.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18bfa387d7a7f77d7b2526af69a65dbd0b731b8d941aaff5becff8e21f6d7717", size = 3573972 }, - { url = "https://files.pythonhosted.org/packages/6b/d5/ca6513844d0634abd05ba12304053a454bb70441a9520afa9897d4300156/Cython-3.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fe81b339cffd87c0069c6049b4d33e28bdd1874625ee515785bf42c9fdff3658", size = 3356158 }, - { url = "https://files.pythonhosted.org/packages/33/59/98a87b6264f4ad45c820db13c4ec657567476efde020c49443cc842a86af/Cython-3.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:80fd94c076e1e1b1ee40a309be03080b75f413e8997cddcf401a118879863388", size = 3522312 }, - { url = "https://files.pythonhosted.org/packages/2b/cb/132115d07a0b9d4f075e0741db70a5416b424dcd875b2bb0dd805e818222/Cython-3.0.8-cp312-cp312-win32.whl", hash = "sha256:85077915a93e359a9b920280d214dc0cf8a62773e1f3d7d30fab8ea4daed670c", size = 2602579 }, - { url = "https://files.pythonhosted.org/packages/b4/69/cb4620287cd9ef461103e122c0a2ae7f7ecf183e02510676fb5a15c95b05/Cython-3.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:0cb2dcc565c7851f75d496f724a384a790fab12d1b82461b663e66605bec429a", size = 2791268 }, - { url = "https://files.pythonhosted.org/packages/e3/7f/f584f5d15323feb897d42ef0e9d910649e2150d7a30cf7e7a8cc1d236e6f/Cython-3.0.8-py2.py3-none-any.whl", hash = "sha256:171b27051253d3f9108e9759e504ba59ff06e7f7ba944457f94deaf9c21bf0b6", size = 1168213 }, + { url = "https://files.pythonhosted.org/packages/63/f4/d2542e186fe33ec1cc542770fb17466421ed54f4ffe04d00fe9549d0a467/Cython-3.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a846e0a38e2b24e9a5c5dc74b0e54c6e29420d88d1dafabc99e0fc0f3e338636", size = 3100459, upload_time = "2024-01-10T11:33:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/fc/27/2652f395aa708fb3081148e0df3ab700bd7288636c65332ef7febad6a380/Cython-3.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45523fdc2b78d79b32834cc1cc12dc2ca8967af87e22a3ee1bff20e77c7f5520", size = 3456626, upload_time = "2024-01-10T11:01:44.897Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bd/e8a1d26d04c08a67bcc383f2ea5493a4e77f37a8770ead00a238b08ad729/Cython-3.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa0b7f3f841fe087410cab66778e2d3fb20ae2d2078a2be3dffe66c6574be39", size = 3621379, upload_time = "2024-01-10T11:01:48.777Z" }, + { url = "https://files.pythonhosted.org/packages/03/ae/ead7ec03d0062d439879d41b7830e4f2480213f7beabf2f7052a191cc6f7/Cython-3.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e87294e33e40c289c77a135f491cd721bd089f193f956f7b8ed5aa2d0b8c558f", size = 3671873, upload_time = "2024-01-10T11:01:51.858Z" }, + { url = "https://files.pythonhosted.org/packages/63/b0/81dad725604d7b529c492f873a7fa1b5800704a9f26e100ed25e9fd8d057/Cython-3.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a1df7a129344b1215c20096d33c00193437df1a8fcca25b71f17c23b1a44f782", size = 3463832, upload_time = "2024-01-10T11:01:55.364Z" }, + { url = "https://files.pythonhosted.org/packages/13/cd/72b8e0af597ac1b376421847acf6d6fa252e60059a2a00dcf05ceb16d28f/Cython-3.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:13c2a5e57a0358da467d97667297bf820b62a1a87ae47c5f87938b9bb593acbd", size = 3618325, upload_time = "2024-01-10T11:01:59.03Z" }, + { url = "https://files.pythonhosted.org/packages/ef/73/11a4355d8b8966504c751e5bcb25916c4140de27bb2ba1b54ff21994d7fe/Cython-3.0.8-cp310-cp310-win32.whl", hash = "sha256:96b028f044f5880e3cb18ecdcfc6c8d3ce9d0af28418d5ab464509f26d8adf12", size = 2571305, upload_time = "2024-01-10T11:02:02.589Z" }, + { url = "https://files.pythonhosted.org/packages/18/15/fdc0c3552d20f9337b134a36d786da24e47998fc39f62cb61c1534f26123/Cython-3.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:8140597a8b5cc4f119a1190f5a2228a84f5ca6d8d9ec386cfce24663f48b2539", size = 2776113, upload_time = "2024-01-10T11:02:05.581Z" }, + { url = "https://files.pythonhosted.org/packages/db/a7/f4a0bc9a80e23b380daa2ebb4879bf434aaa0b3b91f7ad8a7f9762b4bd1b/Cython-3.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aae26f9663e50caf9657148403d9874eea41770ecdd6caf381d177c2b1bb82ba", size = 3113615, upload_time = "2024-01-10T11:34:05.899Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/e9295df74246c165b91253a473bfa179debf739c9bee961cbb3ae56c2b79/Cython-3.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:547eb3cdb2f8c6f48e6865d5a741d9dd051c25b3ce076fbca571727977b28ac3", size = 3436320, upload_time = "2024-01-10T11:02:08.689Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/6a887c957aa53e44f928119dea628a5dfacc8e875424034f5fecac9daba4/Cython-3.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a567d4b9ba70b26db89d75b243529de9e649a2f56384287533cf91512705bee", size = 3591755, upload_time = "2024-01-10T11:02:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b8/f9c97bae6281da50b3ecb1f7fef0f7f7851eae084609b364717a2b366bf1/Cython-3.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51d1426263b0e82fb22bda8ea60dc77a428581cc19e97741011b938445d383f1", size = 3636099, upload_time = "2024-01-10T11:02:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/17/ae/cd055c2c081c67a6fcad1d8d17d82bd6395b14c6741e3a938f40318c8bc5/Cython-3.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c26daaeccda072459b48d211415fd1e5507c06bcd976fa0d5b8b9f1063467d7b", size = 3458119, upload_time = "2024-01-10T11:02:19.103Z" }, + { url = "https://files.pythonhosted.org/packages/72/ab/ac6f5548d6194f4bb2fc8c6c996aa7369f0fa1403e4d4de787d9e9309b27/Cython-3.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:289ce7838208211cd166e975865fd73b0649bf118170b6cebaedfbdaf4a37795", size = 3614418, upload_time = "2024-01-10T11:02:22.732Z" }, + { url = "https://files.pythonhosted.org/packages/70/e2/3e3e448b7a94887bec3235bcb71957b6681dc42b4536459f8f54d46fa936/Cython-3.0.8-cp311-cp311-win32.whl", hash = "sha256:c8aa05f5e17f8042a3be052c24f2edc013fb8af874b0bf76907d16c51b4e7871", size = 2572819, upload_time = "2024-01-10T11:02:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/85/7d/58635941dfbb5b4e197adb88080b9cbfb230dc3b75683698a530a1989bdb/Cython-3.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:000dc9e135d0eec6ecb2b40a5b02d0868a2f8d2e027a41b0fe16a908a9e6de02", size = 2784167, upload_time = "2024-01-10T11:02:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8e/28f8c6109990eef7317ab7e43644092b49a88a39f9373dcd19318946df09/Cython-3.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:90d3fe31db55685d8cb97d43b0ec39ef614fcf660f83c77ed06aa670cb0e164f", size = 3135638, upload_time = "2024-01-10T11:34:22.889Z" }, + { url = "https://files.pythonhosted.org/packages/83/1f/4720cb682b8ed1ab9749dea35351a66dd29b6a022628cce038415660c384/Cython-3.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e24791ddae2324e88e3c902a765595c738f19ae34ee66bfb1a6dac54b1833419", size = 3340052, upload_time = "2024-01-10T11:02:32.471Z" }, + { url = "https://files.pythonhosted.org/packages/8a/47/ec3fceb9e8f7d6fa130216b8740038e1df7c8e5f215bba363fcf1272a6c1/Cython-3.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f020fa1c0552052e0660790b8153b79e3fc9a15dbd8f1d0b841fe5d204a6ae6", size = 3510079, upload_time = "2024-01-10T11:02:35.312Z" }, + { url = "https://files.pythonhosted.org/packages/71/31/b458127851e248effb909e2791b55870914863cde7c60b94db5ee65d7867/Cython-3.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18bfa387d7a7f77d7b2526af69a65dbd0b731b8d941aaff5becff8e21f6d7717", size = 3573972, upload_time = "2024-01-10T11:02:39.044Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d5/ca6513844d0634abd05ba12304053a454bb70441a9520afa9897d4300156/Cython-3.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fe81b339cffd87c0069c6049b4d33e28bdd1874625ee515785bf42c9fdff3658", size = 3356158, upload_time = "2024-01-10T11:02:42.125Z" }, + { url = "https://files.pythonhosted.org/packages/33/59/98a87b6264f4ad45c820db13c4ec657567476efde020c49443cc842a86af/Cython-3.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:80fd94c076e1e1b1ee40a309be03080b75f413e8997cddcf401a118879863388", size = 3522312, upload_time = "2024-01-10T11:02:45.056Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cb/132115d07a0b9d4f075e0741db70a5416b424dcd875b2bb0dd805e818222/Cython-3.0.8-cp312-cp312-win32.whl", hash = "sha256:85077915a93e359a9b920280d214dc0cf8a62773e1f3d7d30fab8ea4daed670c", size = 2602579, upload_time = "2024-01-10T11:02:48.368Z" }, + { url = "https://files.pythonhosted.org/packages/b4/69/cb4620287cd9ef461103e122c0a2ae7f7ecf183e02510676fb5a15c95b05/Cython-3.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:0cb2dcc565c7851f75d496f724a384a790fab12d1b82461b663e66605bec429a", size = 2791268, upload_time = "2024-01-10T11:02:51.483Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7f/f584f5d15323feb897d42ef0e9d910649e2150d7a30cf7e7a8cc1d236e6f/Cython-3.0.8-py2.py3-none-any.whl", hash = "sha256:171b27051253d3f9108e9759e504ba59ff06e7f7ba944457f94deaf9c21bf0b6", size = 1168213, upload_time = "2024-01-10T11:00:56.857Z" }, ] [[package]] name = "easydict" version = "1.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d2/deb3296d08097fedd622d423c0ec8b68b78c1704b3f1545326f6ce05c75c/easydict-1.11.tar.gz", hash = "sha256:dcb1d2ed28eb300c8e46cd371340373abc62f7c14d6dea74fdfc6f1069061c78", size = 6644 } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d2/deb3296d08097fedd622d423c0ec8b68b78c1704b3f1545326f6ce05c75c/easydict-1.11.tar.gz", hash = "sha256:dcb1d2ed28eb300c8e46cd371340373abc62f7c14d6dea74fdfc6f1069061c78", size = 6644, upload_time = "2023-10-23T23:01:37.686Z" } [[package]] name = "exceptiongroup" version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/1c/beef724eaf5b01bb44b6338c8c3494eff7cab376fab4904cfbbc3585dc79/exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68", size = 26264 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/1c/beef724eaf5b01bb44b6338c8c3494eff7cab376fab4904cfbbc3585dc79/exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68", size = 26264, upload_time = "2023-11-21T08:42:17.407Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/9a/5028fd52db10e600f1c4674441b968cf2ea4959085bfb5b99fb1250e5f68/exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", size = 16210 }, + { url = "https://files.pythonhosted.org/packages/b8/9a/5028fd52db10e600f1c4674441b968cf2ea4959085bfb5b99fb1250e5f68/exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", size = 16210, upload_time = "2023-11-21T08:42:15.525Z" }, ] [[package]] @@ -515,18 +515,18 @@ dependencies = [ { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload_time = "2025-03-23T22:55:43.822Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload_time = "2025-03-23T22:55:42.101Z" }, ] [[package]] name = "filelock" version = "3.13.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/70/41905c80dcfe71b22fb06827b8eae65781783d4a14194bce79d16a013263/filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e", size = 14553 } +sdist = { url = "https://files.pythonhosted.org/packages/70/70/41905c80dcfe71b22fb06827b8eae65781783d4a14194bce79d16a013263/filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e", size = 14553, upload_time = "2023-10-30T18:29:39.035Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/54/84d42a0bee35edba99dee7b59a8d4970eccdd44b99fe728ed912106fc781/filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c", size = 11740 }, + { url = "https://files.pythonhosted.org/packages/81/54/84d42a0bee35edba99dee7b59a8d4970eccdd44b99fe728ed912106fc781/filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c", size = 11740, upload_time = "2023-10-30T18:29:37.267Z" }, ] [[package]] @@ -540,9 +540,9 @@ dependencies = [ { name = "jinja2" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/09/c1a7354d3925a3c6c8cfdebf4245bae67d633ffda1ba415add06ffc839c5/flask-3.0.0.tar.gz", hash = "sha256:cfadcdb638b609361d29ec22360d6070a77d7463dcb3ab08d2c2f2f168845f58", size = 674171 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/09/c1a7354d3925a3c6c8cfdebf4245bae67d633ffda1ba415add06ffc839c5/flask-3.0.0.tar.gz", hash = "sha256:cfadcdb638b609361d29ec22360d6070a77d7463dcb3ab08d2c2f2f168845f58", size = 674171, upload_time = "2023-09-30T14:36:12.918Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl", hash = "sha256:21128f47e4e3b9d597a3e8521a329bf56909b690fcc3fa3e477725aa81367638", size = 99724 }, + { url = "https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl", hash = "sha256:21128f47e4e3b9d597a3e8521a329bf56909b690fcc3fa3e477725aa81367638", size = 99724, upload_time = "2023-09-30T14:36:10.961Z" }, ] [[package]] @@ -552,9 +552,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flask" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/6a/a8d56d60bcfa1ec3e4fdad81b45aafd508c3bd5c244a16526fa29139d7d4/flask_cors-4.0.1.tar.gz", hash = "sha256:eeb69b342142fdbf4766ad99357a7f3876a2ceb77689dc10ff912aac06c389e4", size = 30306 } +sdist = { url = "https://files.pythonhosted.org/packages/40/6a/a8d56d60bcfa1ec3e4fdad81b45aafd508c3bd5c244a16526fa29139d7d4/flask_cors-4.0.1.tar.gz", hash = "sha256:eeb69b342142fdbf4766ad99357a7f3876a2ceb77689dc10ff912aac06c389e4", size = 30306, upload_time = "2024-05-04T19:49:43.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/52/2aa6285f104616f73ee1ad7905a16b2b35af0143034ad0cf7b64bcba715c/Flask_Cors-4.0.1-py2.py3-none-any.whl", hash = "sha256:f2a704e4458665580c074b714c4627dd5a306b333deb9074d0b1794dfa2fb677", size = 14290 }, + { url = "https://files.pythonhosted.org/packages/8b/52/2aa6285f104616f73ee1ad7905a16b2b35af0143034ad0cf7b64bcba715c/Flask_Cors-4.0.1-py2.py3-none-any.whl", hash = "sha256:f2a704e4458665580c074b714c4627dd5a306b333deb9074d0b1794dfa2fb677", size = 14290, upload_time = "2024-05-04T19:49:41.721Z" }, ] [[package]] @@ -565,60 +565,60 @@ dependencies = [ { name = "flask" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/6e/2f4e13e373bb49e68c02c51ceadd22d172715a06716f9299d9df01b6ddb2/Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333", size = 48834 } +sdist = { url = "https://files.pythonhosted.org/packages/c3/6e/2f4e13e373bb49e68c02c51ceadd22d172715a06716f9299d9df01b6ddb2/Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333", size = 48834, upload_time = "2023-10-30T14:53:21.151Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/f5/67e9cc5c2036f58115f9fe0f00d203cf6780c3ff8ae0e705e7a9d9e8ff9e/Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d", size = 17303 }, + { url = "https://files.pythonhosted.org/packages/59/f5/67e9cc5c2036f58115f9fe0f00d203cf6780c3ff8ae0e705e7a9d9e8ff9e/Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d", size = 17303, upload_time = "2023-10-30T14:53:19.636Z" }, ] [[package]] name = "flatbuffers" version = "23.5.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/6e/3e52cd294d8e7a61e010973cce076a0cb2c6c0dfd4d0b7a13648c1b98329/flatbuffers-23.5.26.tar.gz", hash = "sha256:9ea1144cac05ce5d86e2859f431c6cd5e66cd9c78c558317c7955fb8d4c78d89", size = 22114 } +sdist = { url = "https://files.pythonhosted.org/packages/0c/6e/3e52cd294d8e7a61e010973cce076a0cb2c6c0dfd4d0b7a13648c1b98329/flatbuffers-23.5.26.tar.gz", hash = "sha256:9ea1144cac05ce5d86e2859f431c6cd5e66cd9c78c558317c7955fb8d4c78d89", size = 22114, upload_time = "2023-05-26T17:35:16.034Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/12/d5c79ee252793ffe845d58a913197bfa02ae9a0b5c9bc3dc4b58d477b9e7/flatbuffers-23.5.26-py2.py3-none-any.whl", hash = "sha256:c0ff356da363087b915fde4b8b45bdda73432fc17cddb3c8157472eab1422ad1", size = 26744 }, + { url = "https://files.pythonhosted.org/packages/6f/12/d5c79ee252793ffe845d58a913197bfa02ae9a0b5c9bc3dc4b58d477b9e7/flatbuffers-23.5.26-py2.py3-none-any.whl", hash = "sha256:c0ff356da363087b915fde4b8b45bdda73432fc17cddb3c8157472eab1422ad1", size = 26744, upload_time = "2023-05-26T17:35:14.269Z" }, ] [[package]] name = "fonttools" version = "4.47.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/cd/75d24afa673edf92fd04657fad7d3b5e20c4abc3cad5bc14e5e30051c1f0/fonttools-4.47.2.tar.gz", hash = "sha256:7df26dd3650e98ca45f1e29883c96a0b9f5bb6af8d632a6a108bc744fa0bd9b3", size = 3410067 } +sdist = { url = "https://files.pythonhosted.org/packages/e5/cd/75d24afa673edf92fd04657fad7d3b5e20c4abc3cad5bc14e5e30051c1f0/fonttools-4.47.2.tar.gz", hash = "sha256:7df26dd3650e98ca45f1e29883c96a0b9f5bb6af8d632a6a108bc744fa0bd9b3", size = 3410067, upload_time = "2024-01-11T11:22:45.293Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/30/02de0b7f3d72f2c4fce3e512b166c1bdbe5a687408474b61eb0114be921c/fonttools-4.47.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b629108351d25512d4ea1a8393a2dba325b7b7d7308116b605ea3f8e1be88df", size = 2779949 }, - { url = "https://files.pythonhosted.org/packages/9a/52/1a5e1373afb78a040ea0c371ab8a79da121060a8e518968bb8f41457ca90/fonttools-4.47.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c19044256c44fe299d9a73456aabee4b4d06c6b930287be93b533b4737d70aa1", size = 2281336 }, - { url = "https://files.pythonhosted.org/packages/c5/ce/9d3b5bf51aafee024566ebb374f5b040381d92660cb04647af3c5860c611/fonttools-4.47.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8be28c036b9f186e8c7eaf8a11b42373e7e4949f9e9f370202b9da4c4c3f56c", size = 4541692 }, - { url = "https://files.pythonhosted.org/packages/e8/68/af41b7cfd35c7418e17b6a43bb106be4b0f0e5feb405a88dee29b186f2a7/fonttools-4.47.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f83a4daef6d2a202acb9bf572958f91cfde5b10c8ee7fb1d09a4c81e5d851fd8", size = 4600529 }, - { url = "https://files.pythonhosted.org/packages/ab/7e/428dbb4cfc342b7a05cbc9d349e134e7fad6588f4ce2a7128e8e3e58ad3b/fonttools-4.47.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a5a5318ba5365d992666ac4fe35365f93004109d18858a3e18ae46f67907670", size = 4524215 }, - { url = "https://files.pythonhosted.org/packages/a6/61/762fad1cc1debc4626f2eb373fa999591c63c231fce53d5073574a639531/fonttools-4.47.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8f57ecd742545362a0f7186774b2d1c53423ed9ece67689c93a1055b236f638c", size = 4584778 }, - { url = "https://files.pythonhosted.org/packages/04/30/170ca22284c1d825470e8b5871d6b25d3a70e2f5b185ffb1647d5e11ee4d/fonttools-4.47.2-cp310-cp310-win32.whl", hash = "sha256:a1c154bb85dc9a4cf145250c88d112d88eb414bad81d4cb524d06258dea1bdc0", size = 2131876 }, - { url = "https://files.pythonhosted.org/packages/df/07/4a30437bed355b838b8ce31d14c5983334c31adc97e70c6ecff90c60d6d2/fonttools-4.47.2-cp310-cp310-win_amd64.whl", hash = "sha256:3e2b95dce2ead58fb12524d0ca7d63a63459dd489e7e5838c3cd53557f8933e1", size = 2177937 }, - { url = "https://files.pythonhosted.org/packages/dd/1d/670372323642eada0f7743cfcdd156de6a28d37769c916421fec2f32c814/fonttools-4.47.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:29495d6d109cdbabe73cfb6f419ce67080c3ef9ea1e08d5750240fd4b0c4763b", size = 2782908 }, - { url = "https://files.pythonhosted.org/packages/c1/36/5f0bb863a6575db4c4b67fa9be7f98e4c551dd87638ef327bc180b988998/fonttools-4.47.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0a1d313a415eaaba2b35d6cd33536560deeebd2ed758b9bfb89ab5d97dc5deac", size = 2283501 }, - { url = "https://files.pythonhosted.org/packages/bd/1e/95de682a86567426bcc40a56c9b118ffa97de6cbfcc293addf20994e329d/fonttools-4.47.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90f898cdd67f52f18049250a6474185ef6544c91f27a7bee70d87d77a8daf89c", size = 4848039 }, - { url = "https://files.pythonhosted.org/packages/ef/95/92a0b5fc844c1db734752f8a51431de519cd6b02e7e561efa9e9fd415544/fonttools-4.47.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3480eeb52770ff75140fe7d9a2ec33fb67b07efea0ab5129c7e0c6a639c40c70", size = 4893166 }, - { url = "https://files.pythonhosted.org/packages/ff/e6/ed9dd7ee1afd6cd70eb7237688118fe489dbde962e3765c91c86c095f84b/fonttools-4.47.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0255dbc128fee75fb9be364806b940ed450dd6838672a150d501ee86523ac61e", size = 4815529 }, - { url = "https://files.pythonhosted.org/packages/6b/67/cdffa0b3cd8f863b45125c335bbd3d9dc16ec42f5a8d5b64dd1244c5ce6b/fonttools-4.47.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f791446ff297fd5f1e2247c188de53c1bfb9dd7f0549eba55b73a3c2087a2703", size = 4875414 }, - { url = "https://files.pythonhosted.org/packages/b8/fb/41638e748c8f20f5483987afcf9be746d3ccb9e9600ca62128a27c791a82/fonttools-4.47.2-cp311-cp311-win32.whl", hash = "sha256:740947906590a878a4bde7dd748e85fefa4d470a268b964748403b3ab2aeed6c", size = 2130073 }, - { url = "https://files.pythonhosted.org/packages/a0/ef/93321cf55180a778b4d97919b28739874c0afab90e7b9f5b232db70f47c2/fonttools-4.47.2-cp311-cp311-win_amd64.whl", hash = "sha256:63fbed184979f09a65aa9c88b395ca539c94287ba3a364517698462e13e457c9", size = 2178744 }, - { url = "https://files.pythonhosted.org/packages/c0/bd/4dd1e8a9e632f325d9203ce543402f912f26efd213c8d9efec0180fbac64/fonttools-4.47.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4ec558c543609e71b2275c4894e93493f65d2f41c15fe1d089080c1d0bb4d635", size = 2754076 }, - { url = "https://files.pythonhosted.org/packages/e6/4d/c2ebaac81dadbc3fc3c3c2fa5fe7b16429dc713b1b8ace49e11e92904d78/fonttools-4.47.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e040f905d542362e07e72e03612a6270c33d38281fd573160e1003e43718d68d", size = 2263784 }, - { url = "https://files.pythonhosted.org/packages/d3/f6/9d484cd275845c7e503a8669a5952a7fa089c7a881babb4dce5ebe6fc5d1/fonttools-4.47.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dd58cc03016b281bd2c74c84cdaa6bd3ce54c5a7f47478b7657b930ac3ed8eb", size = 4769142 }, - { url = "https://files.pythonhosted.org/packages/7a/bf/c6ae0768a531b38245aac0bb8d30bc05d53d499e09fccdc5d72e7c8d28b6/fonttools-4.47.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32ab2e9702dff0dd4510c7bb958f265a8d3dd5c0e2547e7b5f7a3df4979abb07", size = 4853241 }, - { url = "https://files.pythonhosted.org/packages/2b/f0/c06709666cb7722447efb70ea456c302bd6eb3b997d30076401fb32bca4b/fonttools-4.47.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a808f3c1d1df1f5bf39be869b6e0c263570cdafb5bdb2df66087733f566ea71", size = 4730447 }, - { url = "https://files.pythonhosted.org/packages/3e/71/4c758ae5f4f8047904fc1c6bbbb828248c94cc7aa6406af3a62ede766f25/fonttools-4.47.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ac71e2e201df041a2891067dc36256755b1229ae167edbdc419b16da78732c2f", size = 4809265 }, - { url = "https://files.pythonhosted.org/packages/81/f6/a6912c11280607d48947341e2167502605a3917925c835afcd7dfcabc289/fonttools-4.47.2-cp312-cp312-win32.whl", hash = "sha256:69731e8bea0578b3c28fdb43dbf95b9386e2d49a399e9a4ad736b8e479b08085", size = 2118363 }, - { url = "https://files.pythonhosted.org/packages/81/4b/42d0488765ea5aa308b4e8197cb75366b2124240a73e86f98b6107ccf282/fonttools-4.47.2-cp312-cp312-win_amd64.whl", hash = "sha256:b3e1304e5f19ca861d86a72218ecce68f391646d85c851742d265787f55457a4", size = 2165866 }, - { url = "https://files.pythonhosted.org/packages/af/2f/c34b0f99d46766cf49566d1ee2ee3606e4c9880b5a7d734257dc61c804e9/fonttools-4.47.2-py3-none-any.whl", hash = "sha256:7eb7ad665258fba68fd22228a09f347469d95a97fb88198e133595947a20a184", size = 1063011 }, + { url = "https://files.pythonhosted.org/packages/19/30/02de0b7f3d72f2c4fce3e512b166c1bdbe5a687408474b61eb0114be921c/fonttools-4.47.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b629108351d25512d4ea1a8393a2dba325b7b7d7308116b605ea3f8e1be88df", size = 2779949, upload_time = "2024-01-11T11:19:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/9a/52/1a5e1373afb78a040ea0c371ab8a79da121060a8e518968bb8f41457ca90/fonttools-4.47.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c19044256c44fe299d9a73456aabee4b4d06c6b930287be93b533b4737d70aa1", size = 2281336, upload_time = "2024-01-11T11:20:08.835Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ce/9d3b5bf51aafee024566ebb374f5b040381d92660cb04647af3c5860c611/fonttools-4.47.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8be28c036b9f186e8c7eaf8a11b42373e7e4949f9e9f370202b9da4c4c3f56c", size = 4541692, upload_time = "2024-01-11T11:20:13.378Z" }, + { url = "https://files.pythonhosted.org/packages/e8/68/af41b7cfd35c7418e17b6a43bb106be4b0f0e5feb405a88dee29b186f2a7/fonttools-4.47.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f83a4daef6d2a202acb9bf572958f91cfde5b10c8ee7fb1d09a4c81e5d851fd8", size = 4600529, upload_time = "2024-01-11T11:20:17.27Z" }, + { url = "https://files.pythonhosted.org/packages/ab/7e/428dbb4cfc342b7a05cbc9d349e134e7fad6588f4ce2a7128e8e3e58ad3b/fonttools-4.47.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a5a5318ba5365d992666ac4fe35365f93004109d18858a3e18ae46f67907670", size = 4524215, upload_time = "2024-01-11T11:20:21.061Z" }, + { url = "https://files.pythonhosted.org/packages/a6/61/762fad1cc1debc4626f2eb373fa999591c63c231fce53d5073574a639531/fonttools-4.47.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8f57ecd742545362a0f7186774b2d1c53423ed9ece67689c93a1055b236f638c", size = 4584778, upload_time = "2024-01-11T11:20:25.815Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/170ca22284c1d825470e8b5871d6b25d3a70e2f5b185ffb1647d5e11ee4d/fonttools-4.47.2-cp310-cp310-win32.whl", hash = "sha256:a1c154bb85dc9a4cf145250c88d112d88eb414bad81d4cb524d06258dea1bdc0", size = 2131876, upload_time = "2024-01-11T11:20:30.261Z" }, + { url = "https://files.pythonhosted.org/packages/df/07/4a30437bed355b838b8ce31d14c5983334c31adc97e70c6ecff90c60d6d2/fonttools-4.47.2-cp310-cp310-win_amd64.whl", hash = "sha256:3e2b95dce2ead58fb12524d0ca7d63a63459dd489e7e5838c3cd53557f8933e1", size = 2177937, upload_time = "2024-01-11T11:20:33.814Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1d/670372323642eada0f7743cfcdd156de6a28d37769c916421fec2f32c814/fonttools-4.47.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:29495d6d109cdbabe73cfb6f419ce67080c3ef9ea1e08d5750240fd4b0c4763b", size = 2782908, upload_time = "2024-01-11T11:20:37.495Z" }, + { url = "https://files.pythonhosted.org/packages/c1/36/5f0bb863a6575db4c4b67fa9be7f98e4c551dd87638ef327bc180b988998/fonttools-4.47.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0a1d313a415eaaba2b35d6cd33536560deeebd2ed758b9bfb89ab5d97dc5deac", size = 2283501, upload_time = "2024-01-11T11:20:42.027Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1e/95de682a86567426bcc40a56c9b118ffa97de6cbfcc293addf20994e329d/fonttools-4.47.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90f898cdd67f52f18049250a6474185ef6544c91f27a7bee70d87d77a8daf89c", size = 4848039, upload_time = "2024-01-11T11:20:47.038Z" }, + { url = "https://files.pythonhosted.org/packages/ef/95/92a0b5fc844c1db734752f8a51431de519cd6b02e7e561efa9e9fd415544/fonttools-4.47.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3480eeb52770ff75140fe7d9a2ec33fb67b07efea0ab5129c7e0c6a639c40c70", size = 4893166, upload_time = "2024-01-11T11:20:50.855Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/ed9dd7ee1afd6cd70eb7237688118fe489dbde962e3765c91c86c095f84b/fonttools-4.47.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0255dbc128fee75fb9be364806b940ed450dd6838672a150d501ee86523ac61e", size = 4815529, upload_time = "2024-01-11T11:20:54.696Z" }, + { url = "https://files.pythonhosted.org/packages/6b/67/cdffa0b3cd8f863b45125c335bbd3d9dc16ec42f5a8d5b64dd1244c5ce6b/fonttools-4.47.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f791446ff297fd5f1e2247c188de53c1bfb9dd7f0549eba55b73a3c2087a2703", size = 4875414, upload_time = "2024-01-11T11:20:58.435Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fb/41638e748c8f20f5483987afcf9be746d3ccb9e9600ca62128a27c791a82/fonttools-4.47.2-cp311-cp311-win32.whl", hash = "sha256:740947906590a878a4bde7dd748e85fefa4d470a268b964748403b3ab2aeed6c", size = 2130073, upload_time = "2024-01-11T11:21:02.056Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ef/93321cf55180a778b4d97919b28739874c0afab90e7b9f5b232db70f47c2/fonttools-4.47.2-cp311-cp311-win_amd64.whl", hash = "sha256:63fbed184979f09a65aa9c88b395ca539c94287ba3a364517698462e13e457c9", size = 2178744, upload_time = "2024-01-11T11:21:05.88Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bd/4dd1e8a9e632f325d9203ce543402f912f26efd213c8d9efec0180fbac64/fonttools-4.47.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4ec558c543609e71b2275c4894e93493f65d2f41c15fe1d089080c1d0bb4d635", size = 2754076, upload_time = "2024-01-11T11:21:09.745Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4d/c2ebaac81dadbc3fc3c3c2fa5fe7b16429dc713b1b8ace49e11e92904d78/fonttools-4.47.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e040f905d542362e07e72e03612a6270c33d38281fd573160e1003e43718d68d", size = 2263784, upload_time = "2024-01-11T11:21:13.367Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f6/9d484cd275845c7e503a8669a5952a7fa089c7a881babb4dce5ebe6fc5d1/fonttools-4.47.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dd58cc03016b281bd2c74c84cdaa6bd3ce54c5a7f47478b7657b930ac3ed8eb", size = 4769142, upload_time = "2024-01-11T11:21:17.615Z" }, + { url = "https://files.pythonhosted.org/packages/7a/bf/c6ae0768a531b38245aac0bb8d30bc05d53d499e09fccdc5d72e7c8d28b6/fonttools-4.47.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32ab2e9702dff0dd4510c7bb958f265a8d3dd5c0e2547e7b5f7a3df4979abb07", size = 4853241, upload_time = "2024-01-11T11:21:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f0/c06709666cb7722447efb70ea456c302bd6eb3b997d30076401fb32bca4b/fonttools-4.47.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a808f3c1d1df1f5bf39be869b6e0c263570cdafb5bdb2df66087733f566ea71", size = 4730447, upload_time = "2024-01-11T11:21:24.755Z" }, + { url = "https://files.pythonhosted.org/packages/3e/71/4c758ae5f4f8047904fc1c6bbbb828248c94cc7aa6406af3a62ede766f25/fonttools-4.47.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ac71e2e201df041a2891067dc36256755b1229ae167edbdc419b16da78732c2f", size = 4809265, upload_time = "2024-01-11T11:21:28.586Z" }, + { url = "https://files.pythonhosted.org/packages/81/f6/a6912c11280607d48947341e2167502605a3917925c835afcd7dfcabc289/fonttools-4.47.2-cp312-cp312-win32.whl", hash = "sha256:69731e8bea0578b3c28fdb43dbf95b9386e2d49a399e9a4ad736b8e479b08085", size = 2118363, upload_time = "2024-01-11T11:21:33.245Z" }, + { url = "https://files.pythonhosted.org/packages/81/4b/42d0488765ea5aa308b4e8197cb75366b2124240a73e86f98b6107ccf282/fonttools-4.47.2-cp312-cp312-win_amd64.whl", hash = "sha256:b3e1304e5f19ca861d86a72218ecce68f391646d85c851742d265787f55457a4", size = 2165866, upload_time = "2024-01-11T11:21:37.23Z" }, + { url = "https://files.pythonhosted.org/packages/af/2f/c34b0f99d46766cf49566d1ee2ee3606e4c9880b5a7d734257dc61c804e9/fonttools-4.47.2-py3-none-any.whl", hash = "sha256:7eb7ad665258fba68fd22228a09f347469d95a97fb88198e133595947a20a184", size = 1063011, upload_time = "2024-01-11T11:22:41.676Z" }, ] [[package]] name = "fsspec" version = "2023.12.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/08/cac914ff6ff46c4500fc4323a939dbe7a0f528cca04e7fd3e859611dea41/fsspec-2023.12.2.tar.gz", hash = "sha256:8548d39e8810b59c38014934f6b31e57f40c1b20f911f4cc2b85389c7e9bf0cb", size = 167507 } +sdist = { url = "https://files.pythonhosted.org/packages/fa/08/cac914ff6ff46c4500fc4323a939dbe7a0f528cca04e7fd3e859611dea41/fsspec-2023.12.2.tar.gz", hash = "sha256:8548d39e8810b59c38014934f6b31e57f40c1b20f911f4cc2b85389c7e9bf0cb", size = 167507, upload_time = "2023-12-11T21:19:54.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/25/fab23259a52ece5670dcb8452e1af34b89e6135ecc17cd4b54b4b479eac6/fsspec-2023.12.2-py3-none-any.whl", hash = "sha256:d800d87f72189a745fa3d6b033b9dc4a34ad069f60ca60b943a63599f5501960", size = 168979 }, + { url = "https://files.pythonhosted.org/packages/70/25/fab23259a52ece5670dcb8452e1af34b89e6135ecc17cd4b54b4b479eac6/fsspec-2023.12.2-py3-none-any.whl", hash = "sha256:d800d87f72189a745fa3d6b033b9dc4a34ad069f60ca60b943a63599f5501960", size = 168979, upload_time = "2023-12-11T21:19:52.446Z" }, ] [[package]] @@ -628,9 +628,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/d3/8650919bc3c7c6e90ee3fa7fd618bf373cbbe55dff043bd67353dbb20cd8/ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec", size = 308927 } +sdist = { url = "https://files.pythonhosted.org/packages/a5/d3/8650919bc3c7c6e90ee3fa7fd618bf373cbbe55dff043bd67353dbb20cd8/ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec", size = 308927, upload_time = "2024-10-26T00:50:35.149Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821 }, + { url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821, upload_time = "2024-10-26T00:50:33.425Z" }, ] [[package]] @@ -643,41 +643,41 @@ dependencies = [ { name = "zope-event" }, { name = "zope-interface" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/f0/be10ed5d7721ed2317d7feb59e167603217156c2a6d57f128523e24e673d/gevent-24.10.3.tar.gz", hash = "sha256:aa7ee1bd5cabb2b7ef35105f863b386c8d5e332f754b60cfc354148bd70d35d1", size = 6108837 } +sdist = { url = "https://files.pythonhosted.org/packages/70/f0/be10ed5d7721ed2317d7feb59e167603217156c2a6d57f128523e24e673d/gevent-24.10.3.tar.gz", hash = "sha256:aa7ee1bd5cabb2b7ef35105f863b386c8d5e332f754b60cfc354148bd70d35d1", size = 6108837, upload_time = "2024-10-18T16:06:25.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/6f/a2100e7883c7bdfc2b45cb60b310ca748762a21596258b9dd01c5c093dbc/gevent-24.10.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d7a1ad0f2da582f5bd238bca067e1c6c482c30c15a6e4d14aaa3215cbb2232f3", size = 3014382 }, - { url = "https://files.pythonhosted.org/packages/7a/b1/460e4884ed6185d9eb9c4c2e9639d2b254197e46513301c0f63dec22dc90/gevent-24.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4e526fdc279c655c1e809b0c34b45844182c2a6b219802da5e411bd2cf5a8ad", size = 4853460 }, - { url = "https://files.pythonhosted.org/packages/ca/f6/7ded98760d381229183ecce8db2edcce96f13e23807d31a90c66dae85304/gevent-24.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57a5c4e0bdac482c5f02f240d0354e61362df73501ef6ebafce8ef635cad7527", size = 4977636 }, - { url = "https://files.pythonhosted.org/packages/7d/21/7b928e6029eedb93ef94fc0aee701f497af2e601f0ec00aac0e72e3f450e/gevent-24.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d67daed8383326dc8b5e58d88e148d29b6b52274a489e383530b0969ae7b9cb9", size = 5058031 }, - { url = "https://files.pythonhosted.org/packages/00/98/12c03fd004fbeeca01276ffc589f5a368fd741d02582ab7006d1bdef57e7/gevent-24.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e24ffea72e27987979c009536fd0868e52239b44afe6cf7135ce8aafd0f108e", size = 6683694 }, - { url = "https://files.pythonhosted.org/packages/64/4c/ea14d971452d3da09e49267e052d8312f112c7835120aed78d22ef14efee/gevent-24.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c1d80090485da1ea3d99205fe97908b31188c1f4857f08b333ffaf2de2e89d18", size = 5286063 }, - { url = "https://files.pythonhosted.org/packages/39/3f/397efff27e637d7306caa00d1560512c44028c25c70be1e72c46b79b1b66/gevent-24.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0c129f81d60cda614acb4b0c5731997ca05b031fb406fcb58ad53a7ade53b13", size = 6817462 }, - { url = "https://files.pythonhosted.org/packages/aa/5d/19939eaa7c5b7c0f37e0a0665a911ddfe1e35c25c512446fc356a065c16e/gevent-24.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:26ca7a6b42d35129617025ac801135118333cad75856ffc3217b38e707383eba", size = 1566631 }, - { url = "https://files.pythonhosted.org/packages/6e/01/1be5cf013826d8baae235976d6a94f3628014fd2db7c071aeec13f82b4d1/gevent-24.10.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:68c3a0d8402755eba7f69022e42e8021192a721ca8341908acc222ea597029b6", size = 2966909 }, - { url = "https://files.pythonhosted.org/packages/fe/3e/7fa9ab023f24d8689e2c77951981f8ea1f25089e0349a0bf8b35ee9b9277/gevent-24.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d850a453d66336272be4f1d3a8126777f3efdaea62d053b4829857f91e09755", size = 4913247 }, - { url = "https://files.pythonhosted.org/packages/db/63/6e40eaaa3c2abd1561faff11dc3e6781f8c25e975354b8835762834415af/gevent-24.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8e58ee3723f1fbe07d66892f1caa7481c306f653a6829b6fd16cb23d618a5915", size = 5049036 }, - { url = "https://files.pythonhosted.org/packages/94/89/158bc32cdc898dda0481040ac18650022e73133d93460c5af56ca622fe9a/gevent-24.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b52382124eca13135a3abe4f65c6bd428656975980a48e51b17aeab68bdb14db", size = 5107299 }, - { url = "https://files.pythonhosted.org/packages/64/91/1abe62ee350fdfac186d33f615d0d3a0b3b140e7ccf23c73547aa0deec44/gevent-24.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ca2266e08f43c0e22c028801dff7d92a0b102ef20e4caeb6a46abfb95f6a328", size = 6819625 }, - { url = "https://files.pythonhosted.org/packages/92/8b/0b2fe0d36b7c4d463e46cc68eaf6c14488bd7d86cc37e995c64a0ff7d02f/gevent-24.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d758f0d4dbf32502ec87bb9b536ca8055090a16f8305f0ada3ce6f34e70f2fd7", size = 5474079 }, - { url = "https://files.pythonhosted.org/packages/12/7b/9f5abbf0021a50321314f850697e0f46d2e5081168223af2d8544af9d19f/gevent-24.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0de6eb3d55c03138fda567d9bfed28487ce5d0928c5107549767a93efdf2be26", size = 6901323 }, - { url = "https://files.pythonhosted.org/packages/8a/63/607715c621ae78ed581b7ba36d076df63feeb352993d521327f865056771/gevent-24.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:385710355eadecdb70428a5ae3e7e5a45dcf888baa1426884588be9d25ac4290", size = 1549468 }, - { url = "https://files.pythonhosted.org/packages/d9/e4/4edbe17001bb3e6fade4ad2d85ca8f9e4eabcbde4aa29aa6889281616e3e/gevent-24.10.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3ad8fb70aa0ebc935729c9699ac31b210a49b689a7b27b7ac9f91676475f3f53", size = 2970952 }, - { url = "https://files.pythonhosted.org/packages/3c/a6/ce0824fe9398ba6b00028a74840f12be1165d5feaacdc028ea953db3d6c3/gevent-24.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f18689f7a70d2ed0e75bad5036ec3c89690a493d4cfac8d7cdb258ac04b132bd", size = 5172230 }, - { url = "https://files.pythonhosted.org/packages/25/d4/9002cfb585bfa52c860ed4b1349d1a6400bdf2df9f1bd21df5ff33eea33c/gevent-24.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f4f171d4d2018170454d84c934842e1b5f6ce7468ba298f6e7f7cff15000a3", size = 5338394 }, - { url = "https://files.pythonhosted.org/packages/0c/98/222f1a14f22ad2d1cbcc37edb74095264c1f9c7ab49e6423693383462b8a/gevent-24.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7021e26d70189b33c27173d4173f27bf4685d6b6f1c0ea50e5335f8491cb110c", size = 5437989 }, - { url = "https://files.pythonhosted.org/packages/bf/e8/cbb46afea3c7ecdc7289e15cb4a6f89903f4f9754a27ca320d3e465abc78/gevent-24.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34aea15f9c79f27a8faeaa361bc1e72c773a9b54a1996a2ec4eefc8bcd59a824", size = 6838539 }, - { url = "https://files.pythonhosted.org/packages/69/c3/e43e348f23da404a6d4368a14453ed097cdfca97d5212eaceb987d04a0e1/gevent-24.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8af65a4d4feaec6042c666d22c322a310fba3b47e841ad52f724b9c3ce5da48e", size = 5513842 }, - { url = "https://files.pythonhosted.org/packages/c2/76/84b7c19c072a80900118717a85236859127d630cdf8b079fe42f19649f12/gevent-24.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:89c4115e3f5ada55f92b61701a46043fe42f702b5af863b029e4c1a76f6cc2d4", size = 6927374 }, - { url = "https://files.pythonhosted.org/packages/5e/69/0ab1b04c363547058fb5035275c144957b80b36cb6aee715fe6181b0cee9/gevent-24.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:1ce6dab94c0b0d24425ba55712de2f8c9cb21267150ca63f5bb3a0e1f165da99", size = 1546701 }, - { url = "https://files.pythonhosted.org/packages/f7/2d/c783583d7999cd2f2e7aa2d6a1c333d663003ca61255a89ff6a891be95f4/gevent-24.10.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f147e38423fbe96e8731f60a63475b3d2cab2f3d10578d8ee9d10c507c58a2ff", size = 2962857 }, - { url = "https://files.pythonhosted.org/packages/f3/77/d3ce96fd49406f61976e9a3b6c742b97bb274d3b30c68ff190c5b5f81afd/gevent-24.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e6984ec96fc95fd67488555c38ece3015be1f38b1bcceb27b7d6c36b343008", size = 5141676 }, - { url = "https://files.pythonhosted.org/packages/49/f4/f99f893770c316b9d2f03bd684947126cbed0321b89fe5423838974c2025/gevent-24.10.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:051b22e2758accfddb0457728bfc9abf8c3f2ce6bca43f1ff6e07b5ed9e49bf4", size = 5310248 }, - { url = "https://files.pythonhosted.org/packages/e3/0c/67257ba906f76ed82e8f0bd8c00c2a0687b360a1050b70db7e58dff749ab/gevent-24.10.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb5edb6433764119a664bbb148d2aea9990950aa89cc3498f475c2408d523ea3", size = 5407304 }, - { url = "https://files.pythonhosted.org/packages/35/6c/3a72da7c224b0111728130c0f1abc3ee07feff91b37e0ea83db98f4a3eaf/gevent-24.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce417bcaaab496bc9c77f75566531e9d93816262037b8b2dbb88b0fdcd66587c", size = 6818624 }, - { url = "https://files.pythonhosted.org/packages/a3/96/cc5f6ecba032a45fc312fe0db2908a893057fd81361eea93845d6c325556/gevent-24.10.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:1c3a828b033fb02b7c31da4d75014a1f82e6c072fc0523456569a57f8b025861", size = 5484356 }, - { url = "https://files.pythonhosted.org/packages/7c/97/e680b2b2f0c291ae4db9813ffbf02c22c2a0f14c8f1a613971385e29ef67/gevent-24.10.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f2ae3efbbd120cdf4a68b7abc27a37e61e6f443c5a06ec2c6ad94c37cd8471ec", size = 6903191 }, - { url = "https://files.pythonhosted.org/packages/1b/1c/b4181957da062d1c060974ec6cb798cc24aeeb28e8cd2ece84eb4b4991f7/gevent-24.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:9e1210334a9bc9f76c3d008e0785ca62214f8a54e1325f6c2ecab3b6a572a015", size = 1545117 }, - { url = "https://files.pythonhosted.org/packages/89/2b/bf4af9950b8f9abd5b4025858f6311930de550e3498bbfeb47c914701a1d/gevent-24.10.3-pp310-pypy310_pp73-macosx_11_0_universal2.whl", hash = "sha256:e534e6a968d74463b11de6c9c67f4b4bf61775fb00f2e6e0f7fcdd412ceade18", size = 1271541 }, + { url = "https://files.pythonhosted.org/packages/6b/6f/a2100e7883c7bdfc2b45cb60b310ca748762a21596258b9dd01c5c093dbc/gevent-24.10.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d7a1ad0f2da582f5bd238bca067e1c6c482c30c15a6e4d14aaa3215cbb2232f3", size = 3014382, upload_time = "2024-10-18T15:37:34.041Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b1/460e4884ed6185d9eb9c4c2e9639d2b254197e46513301c0f63dec22dc90/gevent-24.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4e526fdc279c655c1e809b0c34b45844182c2a6b219802da5e411bd2cf5a8ad", size = 4853460, upload_time = "2024-10-18T16:19:39.515Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f6/7ded98760d381229183ecce8db2edcce96f13e23807d31a90c66dae85304/gevent-24.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57a5c4e0bdac482c5f02f240d0354e61362df73501ef6ebafce8ef635cad7527", size = 4977636, upload_time = "2024-10-18T16:18:45.464Z" }, + { url = "https://files.pythonhosted.org/packages/7d/21/7b928e6029eedb93ef94fc0aee701f497af2e601f0ec00aac0e72e3f450e/gevent-24.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d67daed8383326dc8b5e58d88e148d29b6b52274a489e383530b0969ae7b9cb9", size = 5058031, upload_time = "2024-10-18T16:23:10.719Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/12c03fd004fbeeca01276ffc589f5a368fd741d02582ab7006d1bdef57e7/gevent-24.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e24ffea72e27987979c009536fd0868e52239b44afe6cf7135ce8aafd0f108e", size = 6683694, upload_time = "2024-10-18T15:59:35.475Z" }, + { url = "https://files.pythonhosted.org/packages/64/4c/ea14d971452d3da09e49267e052d8312f112c7835120aed78d22ef14efee/gevent-24.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c1d80090485da1ea3d99205fe97908b31188c1f4857f08b333ffaf2de2e89d18", size = 5286063, upload_time = "2024-10-18T16:38:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/39/3f/397efff27e637d7306caa00d1560512c44028c25c70be1e72c46b79b1b66/gevent-24.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0c129f81d60cda614acb4b0c5731997ca05b031fb406fcb58ad53a7ade53b13", size = 6817462, upload_time = "2024-10-18T16:02:48.427Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/19939eaa7c5b7c0f37e0a0665a911ddfe1e35c25c512446fc356a065c16e/gevent-24.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:26ca7a6b42d35129617025ac801135118333cad75856ffc3217b38e707383eba", size = 1566631, upload_time = "2024-10-18T16:08:38.489Z" }, + { url = "https://files.pythonhosted.org/packages/6e/01/1be5cf013826d8baae235976d6a94f3628014fd2db7c071aeec13f82b4d1/gevent-24.10.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:68c3a0d8402755eba7f69022e42e8021192a721ca8341908acc222ea597029b6", size = 2966909, upload_time = "2024-10-18T15:37:31.43Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3e/7fa9ab023f24d8689e2c77951981f8ea1f25089e0349a0bf8b35ee9b9277/gevent-24.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d850a453d66336272be4f1d3a8126777f3efdaea62d053b4829857f91e09755", size = 4913247, upload_time = "2024-10-18T16:19:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/db/63/6e40eaaa3c2abd1561faff11dc3e6781f8c25e975354b8835762834415af/gevent-24.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8e58ee3723f1fbe07d66892f1caa7481c306f653a6829b6fd16cb23d618a5915", size = 5049036, upload_time = "2024-10-18T16:18:47.419Z" }, + { url = "https://files.pythonhosted.org/packages/94/89/158bc32cdc898dda0481040ac18650022e73133d93460c5af56ca622fe9a/gevent-24.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b52382124eca13135a3abe4f65c6bd428656975980a48e51b17aeab68bdb14db", size = 5107299, upload_time = "2024-10-18T16:23:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/1abe62ee350fdfac186d33f615d0d3a0b3b140e7ccf23c73547aa0deec44/gevent-24.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ca2266e08f43c0e22c028801dff7d92a0b102ef20e4caeb6a46abfb95f6a328", size = 6819625, upload_time = "2024-10-18T15:59:38.226Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/0b2fe0d36b7c4d463e46cc68eaf6c14488bd7d86cc37e995c64a0ff7d02f/gevent-24.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d758f0d4dbf32502ec87bb9b536ca8055090a16f8305f0ada3ce6f34e70f2fd7", size = 5474079, upload_time = "2024-10-18T16:38:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/12/7b/9f5abbf0021a50321314f850697e0f46d2e5081168223af2d8544af9d19f/gevent-24.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0de6eb3d55c03138fda567d9bfed28487ce5d0928c5107549767a93efdf2be26", size = 6901323, upload_time = "2024-10-18T16:02:50.066Z" }, + { url = "https://files.pythonhosted.org/packages/8a/63/607715c621ae78ed581b7ba36d076df63feeb352993d521327f865056771/gevent-24.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:385710355eadecdb70428a5ae3e7e5a45dcf888baa1426884588be9d25ac4290", size = 1549468, upload_time = "2024-10-18T16:01:30.331Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e4/4edbe17001bb3e6fade4ad2d85ca8f9e4eabcbde4aa29aa6889281616e3e/gevent-24.10.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3ad8fb70aa0ebc935729c9699ac31b210a49b689a7b27b7ac9f91676475f3f53", size = 2970952, upload_time = "2024-10-18T15:37:31.389Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a6/ce0824fe9398ba6b00028a74840f12be1165d5feaacdc028ea953db3d6c3/gevent-24.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f18689f7a70d2ed0e75bad5036ec3c89690a493d4cfac8d7cdb258ac04b132bd", size = 5172230, upload_time = "2024-10-18T16:19:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/25/d4/9002cfb585bfa52c860ed4b1349d1a6400bdf2df9f1bd21df5ff33eea33c/gevent-24.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f4f171d4d2018170454d84c934842e1b5f6ce7468ba298f6e7f7cff15000a3", size = 5338394, upload_time = "2024-10-18T16:18:49.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/98/222f1a14f22ad2d1cbcc37edb74095264c1f9c7ab49e6423693383462b8a/gevent-24.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7021e26d70189b33c27173d4173f27bf4685d6b6f1c0ea50e5335f8491cb110c", size = 5437989, upload_time = "2024-10-18T16:23:13.851Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e8/cbb46afea3c7ecdc7289e15cb4a6f89903f4f9754a27ca320d3e465abc78/gevent-24.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34aea15f9c79f27a8faeaa361bc1e72c773a9b54a1996a2ec4eefc8bcd59a824", size = 6838539, upload_time = "2024-10-18T15:59:40.489Z" }, + { url = "https://files.pythonhosted.org/packages/69/c3/e43e348f23da404a6d4368a14453ed097cdfca97d5212eaceb987d04a0e1/gevent-24.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8af65a4d4feaec6042c666d22c322a310fba3b47e841ad52f724b9c3ce5da48e", size = 5513842, upload_time = "2024-10-18T16:38:29.538Z" }, + { url = "https://files.pythonhosted.org/packages/c2/76/84b7c19c072a80900118717a85236859127d630cdf8b079fe42f19649f12/gevent-24.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:89c4115e3f5ada55f92b61701a46043fe42f702b5af863b029e4c1a76f6cc2d4", size = 6927374, upload_time = "2024-10-18T16:02:51.669Z" }, + { url = "https://files.pythonhosted.org/packages/5e/69/0ab1b04c363547058fb5035275c144957b80b36cb6aee715fe6181b0cee9/gevent-24.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:1ce6dab94c0b0d24425ba55712de2f8c9cb21267150ca63f5bb3a0e1f165da99", size = 1546701, upload_time = "2024-10-18T15:54:53.562Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/c783583d7999cd2f2e7aa2d6a1c333d663003ca61255a89ff6a891be95f4/gevent-24.10.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f147e38423fbe96e8731f60a63475b3d2cab2f3d10578d8ee9d10c507c58a2ff", size = 2962857, upload_time = "2024-10-18T15:37:33.098Z" }, + { url = "https://files.pythonhosted.org/packages/f3/77/d3ce96fd49406f61976e9a3b6c742b97bb274d3b30c68ff190c5b5f81afd/gevent-24.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e6984ec96fc95fd67488555c38ece3015be1f38b1bcceb27b7d6c36b343008", size = 5141676, upload_time = "2024-10-18T16:19:45.484Z" }, + { url = "https://files.pythonhosted.org/packages/49/f4/f99f893770c316b9d2f03bd684947126cbed0321b89fe5423838974c2025/gevent-24.10.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:051b22e2758accfddb0457728bfc9abf8c3f2ce6bca43f1ff6e07b5ed9e49bf4", size = 5310248, upload_time = "2024-10-18T16:18:51.175Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0c/67257ba906f76ed82e8f0bd8c00c2a0687b360a1050b70db7e58dff749ab/gevent-24.10.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb5edb6433764119a664bbb148d2aea9990950aa89cc3498f475c2408d523ea3", size = 5407304, upload_time = "2024-10-18T16:23:15.348Z" }, + { url = "https://files.pythonhosted.org/packages/35/6c/3a72da7c224b0111728130c0f1abc3ee07feff91b37e0ea83db98f4a3eaf/gevent-24.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce417bcaaab496bc9c77f75566531e9d93816262037b8b2dbb88b0fdcd66587c", size = 6818624, upload_time = "2024-10-18T15:59:42.068Z" }, + { url = "https://files.pythonhosted.org/packages/a3/96/cc5f6ecba032a45fc312fe0db2908a893057fd81361eea93845d6c325556/gevent-24.10.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:1c3a828b033fb02b7c31da4d75014a1f82e6c072fc0523456569a57f8b025861", size = 5484356, upload_time = "2024-10-18T16:38:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/7c/97/e680b2b2f0c291ae4db9813ffbf02c22c2a0f14c8f1a613971385e29ef67/gevent-24.10.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f2ae3efbbd120cdf4a68b7abc27a37e61e6f443c5a06ec2c6ad94c37cd8471ec", size = 6903191, upload_time = "2024-10-18T16:02:53.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1c/b4181957da062d1c060974ec6cb798cc24aeeb28e8cd2ece84eb4b4991f7/gevent-24.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:9e1210334a9bc9f76c3d008e0785ca62214f8a54e1325f6c2ecab3b6a572a015", size = 1545117, upload_time = "2024-10-18T15:45:47.375Z" }, + { url = "https://files.pythonhosted.org/packages/89/2b/bf4af9950b8f9abd5b4025858f6311930de550e3498bbfeb47c914701a1d/gevent-24.10.3-pp310-pypy310_pp73-macosx_11_0_universal2.whl", hash = "sha256:e534e6a968d74463b11de6c9c67f4b4bf61775fb00f2e6e0f7fcdd412ceade18", size = 1271541, upload_time = "2024-10-18T15:37:53.146Z" }, ] [[package]] @@ -690,103 +690,103 @@ dependencies = [ { name = "gevent" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/14/d4eddae757de44985718a9e38d9e6f2a923d764ed97d0f1cbc1a8aa2b0ef/geventhttpclient-2.3.1.tar.gz", hash = "sha256:b40ddac8517c456818942c7812f555f84702105c82783238c9fcb8dc12675185", size = 69345 } +sdist = { url = "https://files.pythonhosted.org/packages/8c/14/d4eddae757de44985718a9e38d9e6f2a923d764ed97d0f1cbc1a8aa2b0ef/geventhttpclient-2.3.1.tar.gz", hash = "sha256:b40ddac8517c456818942c7812f555f84702105c82783238c9fcb8dc12675185", size = 69345, upload_time = "2024-04-18T21:39:50.83Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/a5/5e49d6a581b3f1399425e22961c6e341e90c12fa2193ed0adee9afbd864c/geventhttpclient-2.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da22ab7bf5af4ba3d07cffee6de448b42696e53e7ac1fe97ed289037733bf1c2", size = 71729 }, - { url = "https://files.pythonhosted.org/packages/eb/23/4ff584e5f344dae64b5bc588b65c4ea81083f9d662b9f64cf5f28e5ae9cc/geventhttpclient-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2399e3d4e2fae8bbd91756189da6e9d84adf8f3eaace5eef0667874a705a29f8", size = 52062 }, - { url = "https://files.pythonhosted.org/packages/bb/60/6bd8badb97b31a49f4c2b79466abce208a97dad95d447893c7546063fc8a/geventhttpclient-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3e33e87d0d5b9f5782c4e6d3cb7e3592fea41af52713137d04776df7646d71b", size = 51645 }, - { url = "https://files.pythonhosted.org/packages/e1/62/47d431bf05f74aa683d63163a11432bda8f576c86dec8c3bc9d6a156ee03/geventhttpclient-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c071db313866c3d0510feb6c0f40ec086ccf7e4a845701b6316c82c06e8b9b29", size = 117838 }, - { url = "https://files.pythonhosted.org/packages/6c/8b/e7c9ae813bb41883a96ad9afcf86465219c3bb682daa8b09448481edef8a/geventhttpclient-2.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f36f0c6ef88a27e60af8369d9c2189fe372c6f2943182a7568e0f2ad33bb69f1", size = 123272 }, - { url = "https://files.pythonhosted.org/packages/4d/26/71e9b2526009faadda9f588dac04f8bf837a5b97628ab44145efc3fa796e/geventhttpclient-2.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4624843c03a5337282a42247d987c2531193e57255ee307b36eeb4f243a0c21", size = 114319 }, - { url = "https://files.pythonhosted.org/packages/34/8c/1da2960293c42b7a6b01dbe3204b569e4cdb55b8289cb1c7154826500f19/geventhttpclient-2.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d614573621ba827c417786057e1e20e9f96c4f6b3878c55b1b7b54e1026693bc", size = 112705 }, - { url = "https://files.pythonhosted.org/packages/a7/a1/4d08ecf0f213fdc63f78a217f87c07c1cb9891e68cdf74c8cbca76298bdb/geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5d51330a40ac9762879d0e296c279c1beae8cfa6484bb196ac829242c416b709", size = 121236 }, - { url = "https://files.pythonhosted.org/packages/4f/f7/42ece3e1f54602c518d74364a214da3b35b6be267b335564b7e9f0d37705/geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc9f2162d4e8cb86bb5322d99bfd552088a3eacd540a841298f06bb8bc1f1f03", size = 117859 }, - { url = "https://files.pythonhosted.org/packages/1f/8e/de026b3697bffe5fa1a4938a3882107e378eea826905acf8e46c69b71ffd/geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:06e59d3397e63c65ecc7a7561a5289f0cf2e2c2252e29632741e792f57f5d124", size = 127268 }, - { url = "https://files.pythonhosted.org/packages/54/bf/1ee99a322467e6825a24612d306a46ca94b51088170d1b5de0df1c82ab2a/geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4436eef515b3e0c1d4a453ae32e047290e780a623c1eddb11026ae9d5fb03d42", size = 116426 }, - { url = "https://files.pythonhosted.org/packages/72/54/10c8ec745b3dcbfd52af62977fec85829749c0325e1a5429d050a4b45e75/geventhttpclient-2.3.1-cp310-cp310-win32.whl", hash = "sha256:5d1cf7d8a4f8e15cc8fd7d88ac4cdb058d6274203a42587e594cc9f0850ac862", size = 47599 }, - { url = "https://files.pythonhosted.org/packages/da/0d/36a47cdeaa83c3b4efdbd18d77720fa27dc40600998f4dedd7c4a1259862/geventhttpclient-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:4deaebc121036f7ea95430c2d0f80ab085b15280e6ab677a6360b70e57020e7f", size = 48302 }, - { url = "https://files.pythonhosted.org/packages/56/ad/1fcbbea0465f04d4425960e3737d4d8ae6407043cfc88688fb17b9064160/geventhttpclient-2.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0ae055b9ce1704f2ce72c0847df28f4e14dbb3eea79256cda6c909d82688ea3", size = 71733 }, - { url = "https://files.pythonhosted.org/packages/06/1a/10e547adb675beea407ff7117ecb4e5063534569ac14bb4360279d2888dd/geventhttpclient-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f087af2ac439495b5388841d6f3c4de8d2573ca9870593d78f7b554aa5cfa7f5", size = 52060 }, - { url = "https://files.pythonhosted.org/packages/e0/c0/9960ac6e8818a00702743cd2a9637d6f26909ac7ac59ca231f446e367b20/geventhttpclient-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76c367d175810facfe56281e516c9a5a4a191eff76641faaa30aa33882ed4b2f", size = 51649 }, - { url = "https://files.pythonhosted.org/packages/58/3a/b032cd8f885dafdfa002a8a0e4e21b633713798ec08e19010b815fbfead6/geventhttpclient-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a58376d0d461fe0322ff2ad362553b437daee1eeb92b4c0e3b1ffef9e77defbe", size = 117987 }, - { url = "https://files.pythonhosted.org/packages/94/36/6493a5cbc20c269a51186946947f3ca2eae687e05831289891027bd038c3/geventhttpclient-2.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f440cc704f8a9869848a109b2c401805c17c070539b2014e7b884ecfc8591e33", size = 123356 }, - { url = "https://files.pythonhosted.org/packages/2f/07/b66d9a13b97a7e59d84b4faf704113aa963aaf3a0f71c9138c8740d57d5c/geventhttpclient-2.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f10c62994f9052f23948c19de930b2d1f063240462c8bd7077c2b3290e61f4fa", size = 114460 }, - { url = "https://files.pythonhosted.org/packages/4e/72/1467b9e1ef63aecfe3b42333fb7607f66129dffaeca231f97e4be6f71803/geventhttpclient-2.3.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c45d9f3dd9627844c12e9ca347258c7be585bed54046336220e25ea6eac155", size = 112808 }, - { url = "https://files.pythonhosted.org/packages/ce/ef/64894efd67cb3459074c734736ecacff398cd841a5538dc70e3e77d35500/geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:77c1a2c6e3854bf87cd5588b95174640c8a881716bd07fa0d131d082270a6795", size = 122049 }, - { url = "https://files.pythonhosted.org/packages/c5/c8/1b13b4ea4bb88d7c2db56d070a52daf4757b3139afd83885e81455cb422f/geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ce649d4e25c2d56023471df0bf1e8e2ab67dfe4ff12ce3e8fe7e6fae30cd672a", size = 118755 }, - { url = "https://files.pythonhosted.org/packages/d1/06/95ac63fa1ee118a4d5824aa0a6b0dc3a2934a2f4ce695bf6747e1744d813/geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:265d9f31b4ac8f688eebef0bd4c814ffb37a16f769ad0c8c8b8c24a84db8eab5", size = 128053 }, - { url = "https://files.pythonhosted.org/packages/8a/27/3d6dbbd128e1b965bae198bffa4b5552cd635397e3d2bbcc7d9592890ca9/geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2de436a9d61dae877e4e811fb3e2594e2a1df1b18f4280878f318aef48a562b9", size = 117316 }, - { url = "https://files.pythonhosted.org/packages/ed/9a/8b65daf417ff982fa1928ebc6ebdfb081750d426f877f0056288aaa689e8/geventhttpclient-2.3.1-cp311-cp311-win32.whl", hash = "sha256:83e22178b9480b0a95edf0053d4f30b717d0b696b3c262beabe6964d9c5224b1", size = 47598 }, - { url = "https://files.pythonhosted.org/packages/ab/83/ed0d14787861cf30beddd3aadc29ad07d75555de43c629ba514ddd2978d0/geventhttpclient-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:97b072a282233384c1302a7dee88ad8bfedc916f06b1bc1da54f84980f1406a9", size = 48301 }, - { url = "https://files.pythonhosted.org/packages/82/ee/bf3d26170a518d2b1254f44202f2fa4490496b476ee24046ff6c34e79c08/geventhttpclient-2.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e1c90abcc2735cd8dd2d2572a13da32f6625392dc04862decb5c6476a3ddee22", size = 71742 }, - { url = "https://files.pythonhosted.org/packages/77/72/bd64b2a491094a3fbf7f3c314bb3c3918afb652783a8a9db07b86072da35/geventhttpclient-2.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5deb41c2f51247b4e568c14964f59d7b8e537eff51900564c88af3200004e678", size = 52070 }, - { url = "https://files.pythonhosted.org/packages/85/96/e25becfde16c5551ba04ed2beac1f018e2efc70275ec19ae3765ff634ff2/geventhttpclient-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c6f1a56a66a90c4beae2f009b5e9d42db9a58ced165aa35441ace04d69cb7b37", size = 51650 }, - { url = "https://files.pythonhosted.org/packages/5d/b8/fe6e938a369b3742103d04e5771e1ec7b18c047ac30b06a8e9704e2d34fc/geventhttpclient-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ee6e741849c29e3129b1ec3828ac3a5e5dcb043402f852ea92c52334fb8cabf", size = 118507 }, - { url = "https://files.pythonhosted.org/packages/68/0b/381d01de049b02dc70addbcc1c8e24d15500bff6a9e89103c4aa8eb352c3/geventhttpclient-2.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d0972096a63b1ddaa73fa3dab2c7a136e3ab8bf7999a2f85a5dee851fa77cdd", size = 124061 }, - { url = "https://files.pythonhosted.org/packages/c6/e6/7c97b5bf41cc403b2936a0887a85550b3153aa4b60c0c5062c49cd6286f2/geventhttpclient-2.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00675ba682fb7d19d659c14686fa8a52a65e3f301b56c2a4ee6333b380dd9467", size = 115060 }, - { url = "https://files.pythonhosted.org/packages/45/1f/3e02464449c74a8146f27218471578c1dfabf18731cf047520b76e1b6331/geventhttpclient-2.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea77b67c186df90473416f4403839728f70ef6cf1689cec97b4f6bbde392a8a8", size = 113762 }, - { url = "https://files.pythonhosted.org/packages/4f/a4/08551776f7d6b219d6f73ca25be88806007b16af51a1dbfed7192528e1c3/geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ddcc3f0fdffd9a3801e1005b73026202cffed8199863fdef9315bea9a860a032", size = 122018 }, - { url = "https://files.pythonhosted.org/packages/70/14/ba91417ac7cbce8d553f72c885a19c6b9d7f9dc7de81b7814551cf020a57/geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:c9f1ef4ec048563cc621a47ff01a4f10048ff8b676d7a4d75e5433ed8e703e56", size = 118884 }, - { url = "https://files.pythonhosted.org/packages/7c/78/e1f2c30e11bda8347a74b3a7254f727ff53ea260244da77d76b96779a006/geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:a364b30bec7a0a00dbe256e2b6807e4dc866bead7ac84aaa51ca5e2c3d15c258", size = 128224 }, - { url = "https://files.pythonhosted.org/packages/ac/2f/b7fd96e9cfa9d9719b0c9feb50b4cbb341d1940e34fd3305006efa8c3e33/geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:25d255383d3d6a6fbd643bb51ae1a7e4f6f7b0dbd5f3225b537d0bd0432eaf39", size = 117758 }, - { url = "https://files.pythonhosted.org/packages/fb/e0/1384c9a76379ab257b75df92283797861dcae592dd98e471df254f87c635/geventhttpclient-2.3.1-cp312-cp312-win32.whl", hash = "sha256:ad0b507e354d2f398186dcb12fe526d0594e7c9387b514fb843f7a14fdf1729a", size = 47595 }, - { url = "https://files.pythonhosted.org/packages/54/e3/6b8dbb24e3941e20abbe7736e59290c5d4182057ea1d984d46c853208bcd/geventhttpclient-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:7924e0883bc2b177cfe27aa65af6bb9dd57f3e26905c7675a2d1f3ef69df7cca", size = 48271 }, - { url = "https://files.pythonhosted.org/packages/ee/9f/251b1b7e665523137a8711f0f0029196cf18b57741135f01aea80a56f16c/geventhttpclient-2.3.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c31431e38df45b3c79bf3c9427c796adb8263d622bc6fa25e2f6ba916c2aad93", size = 49827 }, - { url = "https://files.pythonhosted.org/packages/74/c7/ad4c23de669191e1c83cfa28c51d3b50fc246d72e1ee40d4d5b330532492/geventhttpclient-2.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:855ab1e145575769b180b57accb0573a77cd6a7392f40a6ef7bc9a4926ebd77b", size = 54017 }, - { url = "https://files.pythonhosted.org/packages/04/7b/59fc8c8fbd10596abfc46dc103654e3d9676de64229d8eee4b0a4ac2e890/geventhttpclient-2.3.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a374aad77c01539e786d0c7829bec2eba034ccd45733c1bf9811ad18d2a8ecd", size = 58359 }, - { url = "https://files.pythonhosted.org/packages/94/b7/743552b0ecda75458c83d55d62937e29c9ee9a42598f57d4025d5de70004/geventhttpclient-2.3.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c1e97460608304f400485ac099736fff3566d3d8db2038533d466f8cf5de5a", size = 54262 }, - { url = "https://files.pythonhosted.org/packages/18/60/10f6215b6cc76b5845a7f4b9c3d1f47d7ecd84ce8769b1e27e0482d605d7/geventhttpclient-2.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4f843f81ee44ba4c553a1b3f73115e0ad8f00044023c24db29f5b1df3da08465", size = 48343 }, + { url = "https://files.pythonhosted.org/packages/a2/a5/5e49d6a581b3f1399425e22961c6e341e90c12fa2193ed0adee9afbd864c/geventhttpclient-2.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da22ab7bf5af4ba3d07cffee6de448b42696e53e7ac1fe97ed289037733bf1c2", size = 71729, upload_time = "2024-04-18T21:38:06.866Z" }, + { url = "https://files.pythonhosted.org/packages/eb/23/4ff584e5f344dae64b5bc588b65c4ea81083f9d662b9f64cf5f28e5ae9cc/geventhttpclient-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2399e3d4e2fae8bbd91756189da6e9d84adf8f3eaace5eef0667874a705a29f8", size = 52062, upload_time = "2024-04-18T21:38:08.433Z" }, + { url = "https://files.pythonhosted.org/packages/bb/60/6bd8badb97b31a49f4c2b79466abce208a97dad95d447893c7546063fc8a/geventhttpclient-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3e33e87d0d5b9f5782c4e6d3cb7e3592fea41af52713137d04776df7646d71b", size = 51645, upload_time = "2024-04-18T21:38:10.139Z" }, + { url = "https://files.pythonhosted.org/packages/e1/62/47d431bf05f74aa683d63163a11432bda8f576c86dec8c3bc9d6a156ee03/geventhttpclient-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c071db313866c3d0510feb6c0f40ec086ccf7e4a845701b6316c82c06e8b9b29", size = 117838, upload_time = "2024-04-18T21:38:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8b/e7c9ae813bb41883a96ad9afcf86465219c3bb682daa8b09448481edef8a/geventhttpclient-2.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f36f0c6ef88a27e60af8369d9c2189fe372c6f2943182a7568e0f2ad33bb69f1", size = 123272, upload_time = "2024-04-18T21:38:13.704Z" }, + { url = "https://files.pythonhosted.org/packages/4d/26/71e9b2526009faadda9f588dac04f8bf837a5b97628ab44145efc3fa796e/geventhttpclient-2.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4624843c03a5337282a42247d987c2531193e57255ee307b36eeb4f243a0c21", size = 114319, upload_time = "2024-04-18T21:38:15.097Z" }, + { url = "https://files.pythonhosted.org/packages/34/8c/1da2960293c42b7a6b01dbe3204b569e4cdb55b8289cb1c7154826500f19/geventhttpclient-2.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d614573621ba827c417786057e1e20e9f96c4f6b3878c55b1b7b54e1026693bc", size = 112705, upload_time = "2024-04-18T21:38:17.005Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a1/4d08ecf0f213fdc63f78a217f87c07c1cb9891e68cdf74c8cbca76298bdb/geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5d51330a40ac9762879d0e296c279c1beae8cfa6484bb196ac829242c416b709", size = 121236, upload_time = "2024-04-18T21:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f7/42ece3e1f54602c518d74364a214da3b35b6be267b335564b7e9f0d37705/geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc9f2162d4e8cb86bb5322d99bfd552088a3eacd540a841298f06bb8bc1f1f03", size = 117859, upload_time = "2024-04-18T21:38:20.917Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8e/de026b3697bffe5fa1a4938a3882107e378eea826905acf8e46c69b71ffd/geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:06e59d3397e63c65ecc7a7561a5289f0cf2e2c2252e29632741e792f57f5d124", size = 127268, upload_time = "2024-04-18T21:38:22.676Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/1ee99a322467e6825a24612d306a46ca94b51088170d1b5de0df1c82ab2a/geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4436eef515b3e0c1d4a453ae32e047290e780a623c1eddb11026ae9d5fb03d42", size = 116426, upload_time = "2024-04-18T21:38:24.228Z" }, + { url = "https://files.pythonhosted.org/packages/72/54/10c8ec745b3dcbfd52af62977fec85829749c0325e1a5429d050a4b45e75/geventhttpclient-2.3.1-cp310-cp310-win32.whl", hash = "sha256:5d1cf7d8a4f8e15cc8fd7d88ac4cdb058d6274203a42587e594cc9f0850ac862", size = 47599, upload_time = "2024-04-18T21:38:26.385Z" }, + { url = "https://files.pythonhosted.org/packages/da/0d/36a47cdeaa83c3b4efdbd18d77720fa27dc40600998f4dedd7c4a1259862/geventhttpclient-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:4deaebc121036f7ea95430c2d0f80ab085b15280e6ab677a6360b70e57020e7f", size = 48302, upload_time = "2024-04-18T21:38:28.297Z" }, + { url = "https://files.pythonhosted.org/packages/56/ad/1fcbbea0465f04d4425960e3737d4d8ae6407043cfc88688fb17b9064160/geventhttpclient-2.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0ae055b9ce1704f2ce72c0847df28f4e14dbb3eea79256cda6c909d82688ea3", size = 71733, upload_time = "2024-04-18T21:38:30.357Z" }, + { url = "https://files.pythonhosted.org/packages/06/1a/10e547adb675beea407ff7117ecb4e5063534569ac14bb4360279d2888dd/geventhttpclient-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f087af2ac439495b5388841d6f3c4de8d2573ca9870593d78f7b554aa5cfa7f5", size = 52060, upload_time = "2024-04-18T21:38:32.561Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c0/9960ac6e8818a00702743cd2a9637d6f26909ac7ac59ca231f446e367b20/geventhttpclient-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76c367d175810facfe56281e516c9a5a4a191eff76641faaa30aa33882ed4b2f", size = 51649, upload_time = "2024-04-18T21:38:34.265Z" }, + { url = "https://files.pythonhosted.org/packages/58/3a/b032cd8f885dafdfa002a8a0e4e21b633713798ec08e19010b815fbfead6/geventhttpclient-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a58376d0d461fe0322ff2ad362553b437daee1eeb92b4c0e3b1ffef9e77defbe", size = 117987, upload_time = "2024-04-18T21:38:37.661Z" }, + { url = "https://files.pythonhosted.org/packages/94/36/6493a5cbc20c269a51186946947f3ca2eae687e05831289891027bd038c3/geventhttpclient-2.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f440cc704f8a9869848a109b2c401805c17c070539b2014e7b884ecfc8591e33", size = 123356, upload_time = "2024-04-18T21:38:39.705Z" }, + { url = "https://files.pythonhosted.org/packages/2f/07/b66d9a13b97a7e59d84b4faf704113aa963aaf3a0f71c9138c8740d57d5c/geventhttpclient-2.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f10c62994f9052f23948c19de930b2d1f063240462c8bd7077c2b3290e61f4fa", size = 114460, upload_time = "2024-04-18T21:38:40.947Z" }, + { url = "https://files.pythonhosted.org/packages/4e/72/1467b9e1ef63aecfe3b42333fb7607f66129dffaeca231f97e4be6f71803/geventhttpclient-2.3.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c45d9f3dd9627844c12e9ca347258c7be585bed54046336220e25ea6eac155", size = 112808, upload_time = "2024-04-18T21:38:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/64894efd67cb3459074c734736ecacff398cd841a5538dc70e3e77d35500/geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:77c1a2c6e3854bf87cd5588b95174640c8a881716bd07fa0d131d082270a6795", size = 122049, upload_time = "2024-04-18T21:38:44.184Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c8/1b13b4ea4bb88d7c2db56d070a52daf4757b3139afd83885e81455cb422f/geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ce649d4e25c2d56023471df0bf1e8e2ab67dfe4ff12ce3e8fe7e6fae30cd672a", size = 118755, upload_time = "2024-04-18T21:38:45.654Z" }, + { url = "https://files.pythonhosted.org/packages/d1/06/95ac63fa1ee118a4d5824aa0a6b0dc3a2934a2f4ce695bf6747e1744d813/geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:265d9f31b4ac8f688eebef0bd4c814ffb37a16f769ad0c8c8b8c24a84db8eab5", size = 128053, upload_time = "2024-04-18T21:38:47.247Z" }, + { url = "https://files.pythonhosted.org/packages/8a/27/3d6dbbd128e1b965bae198bffa4b5552cd635397e3d2bbcc7d9592890ca9/geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2de436a9d61dae877e4e811fb3e2594e2a1df1b18f4280878f318aef48a562b9", size = 117316, upload_time = "2024-04-18T21:38:49.086Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/8b65daf417ff982fa1928ebc6ebdfb081750d426f877f0056288aaa689e8/geventhttpclient-2.3.1-cp311-cp311-win32.whl", hash = "sha256:83e22178b9480b0a95edf0053d4f30b717d0b696b3c262beabe6964d9c5224b1", size = 47598, upload_time = "2024-04-18T21:38:50.919Z" }, + { url = "https://files.pythonhosted.org/packages/ab/83/ed0d14787861cf30beddd3aadc29ad07d75555de43c629ba514ddd2978d0/geventhttpclient-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:97b072a282233384c1302a7dee88ad8bfedc916f06b1bc1da54f84980f1406a9", size = 48301, upload_time = "2024-04-18T21:38:52.14Z" }, + { url = "https://files.pythonhosted.org/packages/82/ee/bf3d26170a518d2b1254f44202f2fa4490496b476ee24046ff6c34e79c08/geventhttpclient-2.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e1c90abcc2735cd8dd2d2572a13da32f6625392dc04862decb5c6476a3ddee22", size = 71742, upload_time = "2024-04-18T21:38:54.167Z" }, + { url = "https://files.pythonhosted.org/packages/77/72/bd64b2a491094a3fbf7f3c314bb3c3918afb652783a8a9db07b86072da35/geventhttpclient-2.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5deb41c2f51247b4e568c14964f59d7b8e537eff51900564c88af3200004e678", size = 52070, upload_time = "2024-04-18T21:38:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/85/96/e25becfde16c5551ba04ed2beac1f018e2efc70275ec19ae3765ff634ff2/geventhttpclient-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c6f1a56a66a90c4beae2f009b5e9d42db9a58ced165aa35441ace04d69cb7b37", size = 51650, upload_time = "2024-04-18T21:38:57.022Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b8/fe6e938a369b3742103d04e5771e1ec7b18c047ac30b06a8e9704e2d34fc/geventhttpclient-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ee6e741849c29e3129b1ec3828ac3a5e5dcb043402f852ea92c52334fb8cabf", size = 118507, upload_time = "2024-04-18T21:38:58.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/0b/381d01de049b02dc70addbcc1c8e24d15500bff6a9e89103c4aa8eb352c3/geventhttpclient-2.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d0972096a63b1ddaa73fa3dab2c7a136e3ab8bf7999a2f85a5dee851fa77cdd", size = 124061, upload_time = "2024-04-18T21:39:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e6/7c97b5bf41cc403b2936a0887a85550b3153aa4b60c0c5062c49cd6286f2/geventhttpclient-2.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00675ba682fb7d19d659c14686fa8a52a65e3f301b56c2a4ee6333b380dd9467", size = 115060, upload_time = "2024-04-18T21:39:02.323Z" }, + { url = "https://files.pythonhosted.org/packages/45/1f/3e02464449c74a8146f27218471578c1dfabf18731cf047520b76e1b6331/geventhttpclient-2.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea77b67c186df90473416f4403839728f70ef6cf1689cec97b4f6bbde392a8a8", size = 113762, upload_time = "2024-04-18T21:39:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a4/08551776f7d6b219d6f73ca25be88806007b16af51a1dbfed7192528e1c3/geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ddcc3f0fdffd9a3801e1005b73026202cffed8199863fdef9315bea9a860a032", size = 122018, upload_time = "2024-04-18T21:39:05.781Z" }, + { url = "https://files.pythonhosted.org/packages/70/14/ba91417ac7cbce8d553f72c885a19c6b9d7f9dc7de81b7814551cf020a57/geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:c9f1ef4ec048563cc621a47ff01a4f10048ff8b676d7a4d75e5433ed8e703e56", size = 118884, upload_time = "2024-04-18T21:39:08.001Z" }, + { url = "https://files.pythonhosted.org/packages/7c/78/e1f2c30e11bda8347a74b3a7254f727ff53ea260244da77d76b96779a006/geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:a364b30bec7a0a00dbe256e2b6807e4dc866bead7ac84aaa51ca5e2c3d15c258", size = 128224, upload_time = "2024-04-18T21:39:09.31Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2f/b7fd96e9cfa9d9719b0c9feb50b4cbb341d1940e34fd3305006efa8c3e33/geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:25d255383d3d6a6fbd643bb51ae1a7e4f6f7b0dbd5f3225b537d0bd0432eaf39", size = 117758, upload_time = "2024-04-18T21:39:11.287Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e0/1384c9a76379ab257b75df92283797861dcae592dd98e471df254f87c635/geventhttpclient-2.3.1-cp312-cp312-win32.whl", hash = "sha256:ad0b507e354d2f398186dcb12fe526d0594e7c9387b514fb843f7a14fdf1729a", size = 47595, upload_time = "2024-04-18T21:39:12.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/6b8dbb24e3941e20abbe7736e59290c5d4182057ea1d984d46c853208bcd/geventhttpclient-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:7924e0883bc2b177cfe27aa65af6bb9dd57f3e26905c7675a2d1f3ef69df7cca", size = 48271, upload_time = "2024-04-18T21:39:14.479Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9f/251b1b7e665523137a8711f0f0029196cf18b57741135f01aea80a56f16c/geventhttpclient-2.3.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c31431e38df45b3c79bf3c9427c796adb8263d622bc6fa25e2f6ba916c2aad93", size = 49827, upload_time = "2024-04-18T21:39:36.14Z" }, + { url = "https://files.pythonhosted.org/packages/74/c7/ad4c23de669191e1c83cfa28c51d3b50fc246d72e1ee40d4d5b330532492/geventhttpclient-2.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:855ab1e145575769b180b57accb0573a77cd6a7392f40a6ef7bc9a4926ebd77b", size = 54017, upload_time = "2024-04-18T21:39:37.577Z" }, + { url = "https://files.pythonhosted.org/packages/04/7b/59fc8c8fbd10596abfc46dc103654e3d9676de64229d8eee4b0a4ac2e890/geventhttpclient-2.3.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a374aad77c01539e786d0c7829bec2eba034ccd45733c1bf9811ad18d2a8ecd", size = 58359, upload_time = "2024-04-18T21:39:39.437Z" }, + { url = "https://files.pythonhosted.org/packages/94/b7/743552b0ecda75458c83d55d62937e29c9ee9a42598f57d4025d5de70004/geventhttpclient-2.3.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c1e97460608304f400485ac099736fff3566d3d8db2038533d466f8cf5de5a", size = 54262, upload_time = "2024-04-18T21:39:40.866Z" }, + { url = "https://files.pythonhosted.org/packages/18/60/10f6215b6cc76b5845a7f4b9c3d1f47d7ecd84ce8769b1e27e0482d605d7/geventhttpclient-2.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4f843f81ee44ba4c553a1b3f73115e0ad8f00044023c24db29f5b1df3da08465", size = 48343, upload_time = "2024-04-18T21:39:42.173Z" }, ] [[package]] name = "greenlet" version = "3.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022, upload_time = "2024-09-20T18:21:04.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235 }, - { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168 }, - { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826 }, - { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443 }, - { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295 }, - { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544 }, - { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456 }, - { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111 }, - { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392 }, - { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479 }, - { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404 }, - { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813 }, - { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517 }, - { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831 }, - { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413 }, - { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619 }, - { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198 }, - { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 }, - { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 }, - { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 }, - { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 }, - { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 }, - { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 }, - { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 }, - { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 }, - { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 }, - { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 }, - { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 }, - { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 }, - { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 }, - { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 }, - { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 }, - { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 }, - { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 }, - { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 }, - { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 }, - { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 }, - { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 }, - { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 }, - { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 }, - { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 }, - { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 }, - { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, + { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235, upload_time = "2024-09-20T17:07:18.761Z" }, + { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168, upload_time = "2024-09-20T17:36:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826, upload_time = "2024-09-20T17:39:16.921Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443, upload_time = "2024-09-20T17:44:21.896Z" }, + { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295, upload_time = "2024-09-20T17:08:37.951Z" }, + { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544, upload_time = "2024-09-20T17:08:27.894Z" }, + { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456, upload_time = "2024-09-20T17:44:11.755Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111, upload_time = "2024-09-20T17:09:22.104Z" }, + { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392, upload_time = "2024-09-20T17:28:51.988Z" }, + { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479, upload_time = "2024-09-20T17:07:22.332Z" }, + { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404, upload_time = "2024-09-20T17:36:45.588Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813, upload_time = "2024-09-20T17:39:19.052Z" }, + { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517, upload_time = "2024-09-20T17:44:24.101Z" }, + { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831, upload_time = "2024-09-20T17:08:40.577Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413, upload_time = "2024-09-20T17:08:31.728Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619, upload_time = "2024-09-20T17:44:14.222Z" }, + { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198, upload_time = "2024-09-20T17:09:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930, upload_time = "2024-09-20T17:25:18.656Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260, upload_time = "2024-09-20T17:08:07.301Z" }, + { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064, upload_time = "2024-09-20T17:36:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420, upload_time = "2024-09-20T17:39:21.258Z" }, + { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035, upload_time = "2024-09-20T17:44:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105, upload_time = "2024-09-20T17:08:42.048Z" }, + { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077, upload_time = "2024-09-20T17:08:33.707Z" }, + { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975, upload_time = "2024-09-20T17:44:15.989Z" }, + { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955, upload_time = "2024-09-20T17:09:25.539Z" }, + { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655, upload_time = "2024-09-20T17:21:22.427Z" }, + { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990, upload_time = "2024-09-20T17:08:26.312Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175, upload_time = "2024-09-20T17:36:48.983Z" }, + { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425, upload_time = "2024-09-20T17:39:22.705Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736, upload_time = "2024-09-20T17:44:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347, upload_time = "2024-09-20T17:08:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583, upload_time = "2024-09-20T17:08:36.85Z" }, + { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039, upload_time = "2024-09-20T17:44:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716, upload_time = "2024-09-20T17:09:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490, upload_time = "2024-09-20T17:17:09.501Z" }, + { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731, upload_time = "2024-09-20T17:36:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304, upload_time = "2024-09-20T17:39:24.55Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537, upload_time = "2024-09-20T17:44:31.102Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506, upload_time = "2024-09-20T17:08:47.852Z" }, + { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753, upload_time = "2024-09-20T17:08:38.079Z" }, + { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731, upload_time = "2024-09-20T17:44:20.556Z" }, + { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112, upload_time = "2024-09-20T17:09:28.753Z" }, ] [[package]] @@ -796,18 +796,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031 } +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload_time = "2024-08-10T20:25:27.378Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029 }, + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload_time = "2024-08-10T20:25:24.996Z" }, ] [[package]] name = "h11" version = "0.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418, upload_time = "2022-09-25T15:40:01.519Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259, upload_time = "2022-09-25T15:39:59.68Z" }, ] [[package]] @@ -818,45 +818,45 @@ dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/56/78a38490b834fa0942cbe6d39bd8a7fd76316e8940319305a98d2b320366/httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535", size = 81036 } +sdist = { url = "https://files.pythonhosted.org/packages/18/56/78a38490b834fa0942cbe6d39bd8a7fd76316e8940319305a98d2b320366/httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535", size = 81036, upload_time = "2023-11-10T13:37:42.496Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/ba/78b0a99c4da0ff8b0f59defa2f13ca4668189b134bd9840b6202a93d9a0f/httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7", size = 76943 }, + { url = "https://files.pythonhosted.org/packages/56/ba/78b0a99c4da0ff8b0f59defa2f13ca4668189b134bd9840b6202a93d9a0f/httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7", size = 76943, upload_time = "2023-11-10T13:37:40.937Z" }, ] [[package]] name = "httptools" version = "0.6.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload_time = "2024-10-16T19:45:08.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", size = 198780 }, - { url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", size = 103297 }, - { url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", size = 443130 }, - { url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", size = 442148 }, - { url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", size = 415949 }, - { url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", size = 417591 }, - { url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", size = 88344 }, - { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029 }, - { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492 }, - { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891 }, - { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788 }, - { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214 }, - { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120 }, - { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565 }, - { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 }, - { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 }, - { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 }, - { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 }, - { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 }, - { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 }, - { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 }, - { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, - { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, - { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, - { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 }, - { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 }, - { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 }, - { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, + { url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", size = 198780, upload_time = "2024-10-16T19:44:06.882Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", size = 103297, upload_time = "2024-10-16T19:44:08.129Z" }, + { url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", size = 443130, upload_time = "2024-10-16T19:44:09.45Z" }, + { url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", size = 442148, upload_time = "2024-10-16T19:44:11.539Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", size = 415949, upload_time = "2024-10-16T19:44:13.388Z" }, + { url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", size = 417591, upload_time = "2024-10-16T19:44:15.258Z" }, + { url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", size = 88344, upload_time = "2024-10-16T19:44:16.54Z" }, + { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029, upload_time = "2024-10-16T19:44:18.427Z" }, + { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492, upload_time = "2024-10-16T19:44:19.515Z" }, + { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891, upload_time = "2024-10-16T19:44:21.067Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788, upload_time = "2024-10-16T19:44:22.958Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214, upload_time = "2024-10-16T19:44:24.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120, upload_time = "2024-10-16T19:44:26.295Z" }, + { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565, upload_time = "2024-10-16T19:44:29.188Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683, upload_time = "2024-10-16T19:44:30.175Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337, upload_time = "2024-10-16T19:44:31.786Z" }, + { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796, upload_time = "2024-10-16T19:44:32.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837, upload_time = "2024-10-16T19:44:33.974Z" }, + { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289, upload_time = "2024-10-16T19:44:35.111Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779, upload_time = "2024-10-16T19:44:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634, upload_time = "2024-10-16T19:44:37.357Z" }, + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload_time = "2024-10-16T19:44:38.738Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload_time = "2024-10-16T19:44:39.818Z" }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload_time = "2024-10-16T19:44:41.189Z" }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload_time = "2024-10-16T19:44:42.384Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload_time = "2024-10-16T19:44:43.959Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload_time = "2024-10-16T19:44:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload_time = "2024-10-16T19:44:46.46Z" }, ] [[package]] @@ -869,9 +869,9 @@ dependencies = [ { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload_time = "2024-12-06T15:37:23.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload_time = "2024-12-06T15:37:21.509Z" }, ] [[package]] @@ -887,9 +887,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/22/8eb91736b1dcb83d879bd49050a09df29a57cc5cd9f38e48a4b1c45ee890/huggingface_hub-0.30.2.tar.gz", hash = "sha256:9a7897c5b6fd9dad3168a794a8998d6378210f5b9688d0dfc180b1a228dc2466", size = 400868 } +sdist = { url = "https://files.pythonhosted.org/packages/df/22/8eb91736b1dcb83d879bd49050a09df29a57cc5cd9f38e48a4b1c45ee890/huggingface_hub-0.30.2.tar.gz", hash = "sha256:9a7897c5b6fd9dad3168a794a8998d6378210f5b9688d0dfc180b1a228dc2466", size = 400868, upload_time = "2025-04-08T08:32:45.26Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/27/1fb384a841e9661faad1c31cbfa62864f59632e876df5d795234da51c395/huggingface_hub-0.30.2-py3-none-any.whl", hash = "sha256:68ff05969927058cfa41df4f2155d4bb48f5f54f719dd0390103eefa9b191e28", size = 481433 }, + { url = "https://files.pythonhosted.org/packages/93/27/1fb384a841e9661faad1c31cbfa62864f59632e876df5d795234da51c395/huggingface_hub-0.30.2-py3-none-any.whl", hash = "sha256:68ff05969927058cfa41df4f2155d4bb48f5f54f719dd0390103eefa9b191e28", size = 481433, upload_time = "2025-04-08T08:32:43.305Z" }, ] [[package]] @@ -899,18 +899,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyreadline3", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702 } +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload_time = "2021-09-17T21:40:43.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 }, + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload_time = "2021-09-17T21:40:39.897Z" }, ] [[package]] name = "idna" version = "3.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } +sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload_time = "2023-11-25T15:40:54.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload_time = "2023-11-25T15:40:52.604Z" }, ] [[package]] @@ -921,9 +921,9 @@ dependencies = [ { name = "numpy" }, { name = "pillow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/38/f4c568318c656352d211eec6954460dc3af0b7583a6682308f8a66e4c19b/imageio-2.33.1.tar.gz", hash = "sha256:78722d40b137bd98f5ec7312119f8aea9ad2049f76f434748eb306b6937cc1ce", size = 387374 } +sdist = { url = "https://files.pythonhosted.org/packages/25/38/f4c568318c656352d211eec6954460dc3af0b7583a6682308f8a66e4c19b/imageio-2.33.1.tar.gz", hash = "sha256:78722d40b137bd98f5ec7312119f8aea9ad2049f76f434748eb306b6937cc1ce", size = 387374, upload_time = "2023-12-11T02:26:44.715Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/69/3aaa69cb0748e33e644fda114c9abd3186ce369edd4fca11107e9f39c6a7/imageio-2.33.1-py3-none-any.whl", hash = "sha256:c5094c48ccf6b2e6da8b4061cd95e1209380afafcbeae4a4e280938cce227e1d", size = 313345 }, + { url = "https://files.pythonhosted.org/packages/c0/69/3aaa69cb0748e33e644fda114c9abd3186ce369edd4fca11107e9f39c6a7/imageio-2.33.1-py3-none-any.whl", hash = "sha256:c5094c48ccf6b2e6da8b4061cd95e1209380afafcbeae4a4e280938cce227e1d", size = 313345, upload_time = "2023-12-11T02:26:42.724Z" }, ] [[package]] @@ -1080,9 +1080,9 @@ types = [ name = "iniconfig" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload_time = "2023-01-07T11:08:11.254Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload_time = "2023-01-07T11:08:09.864Z" }, ] [[package]] @@ -1104,15 +1104,15 @@ dependencies = [ { name = "scipy" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/8d/0f4af90999ca96cf8cb846eb5ae27c5ef5b390f9c090dd19e4fa76364c13/insightface-0.7.3.tar.gz", hash = "sha256:f191f719612ebb37018f41936814500544cd0f86e6fcd676c023f354c668ddf7", size = 439490 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/8d/0f4af90999ca96cf8cb846eb5ae27c5ef5b390f9c090dd19e4fa76364c13/insightface-0.7.3.tar.gz", hash = "sha256:f191f719612ebb37018f41936814500544cd0f86e6fcd676c023f354c668ddf7", size = 439490, upload_time = "2023-04-02T08:01:54.541Z" } [[package]] name = "itsdangerous" version = "2.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7f/a1/d3fb83e7a61fa0c0d3d08ad0a94ddbeff3731c05212617dff3a94e097f08/itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a", size = 56143 } +sdist = { url = "https://files.pythonhosted.org/packages/7f/a1/d3fb83e7a61fa0c0d3d08ad0a94ddbeff3731c05212617dff3a94e097f08/itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a", size = 56143, upload_time = "2022-03-24T15:12:15.102Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5f/447e04e828f47465eeab35b5d408b7ebaaaee207f48b7136c5a7267a30ae/itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", size = 15749 }, + { url = "https://files.pythonhosted.org/packages/68/5f/447e04e828f47465eeab35b5d408b7ebaaaee207f48b7136c5a7267a30ae/itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", size = 15749, upload_time = "2022-03-24T15:12:13.2Z" }, ] [[package]] @@ -1122,85 +1122,85 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245, upload_time = "2024-05-05T23:42:02.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271, upload_time = "2024-05-05T23:41:59.928Z" }, ] [[package]] name = "joblib" version = "1.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/0f/d3b33b9f106dddef461f6df1872b7881321b247f3d255b87f61a7636f7fe/joblib-1.3.2.tar.gz", hash = "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1", size = 1987720 } +sdist = { url = "https://files.pythonhosted.org/packages/15/0f/d3b33b9f106dddef461f6df1872b7881321b247f3d255b87f61a7636f7fe/joblib-1.3.2.tar.gz", hash = "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1", size = 1987720, upload_time = "2023-08-09T09:23:40.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/40/d551139c85db202f1f384ba8bcf96aca2f329440a844f924c8a0040b6d02/joblib-1.3.2-py3-none-any.whl", hash = "sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9", size = 302207 }, + { url = "https://files.pythonhosted.org/packages/10/40/d551139c85db202f1f384ba8bcf96aca2f329440a844f924c8a0040b6d02/joblib-1.3.2-py3-none-any.whl", hash = "sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9", size = 302207, upload_time = "2023-08-09T09:23:34.583Z" }, ] [[package]] name = "kiwisolver" version = "1.4.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/2d/226779e405724344fc678fcc025b812587617ea1a48b9442628b688e85ea/kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec", size = 97552 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2d/226779e405724344fc678fcc025b812587617ea1a48b9442628b688e85ea/kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec", size = 97552, upload_time = "2023-08-24T09:30:39.861Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/56/cb02dcefdaab40df636b91e703b172966b444605a0ea313549f3ffc05bd3/kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af", size = 127397 }, - { url = "https://files.pythonhosted.org/packages/0e/c1/d084f8edb26533a191415d5173157080837341f9a06af9dd1a75f727abb4/kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3", size = 68125 }, - { url = "https://files.pythonhosted.org/packages/23/11/6fb190bae4b279d712a834e7b1da89f6dcff6791132f7399aa28a57c3565/kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4", size = 66211 }, - { url = "https://files.pythonhosted.org/packages/b3/13/5e9e52feb33e9e063f76b2c5eb09cb977f5bba622df3210081bfb26ec9a3/kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1", size = 1637145 }, - { url = "https://files.pythonhosted.org/packages/6f/40/4ab1fdb57fced80ce5903f04ae1aed7c1d5939dda4fd0c0aa526c12fe28a/kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff", size = 1617849 }, - { url = "https://files.pythonhosted.org/packages/49/ca/61ef43bd0832c7253b370735b0c38972c140c8774889b884372a629a8189/kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a", size = 1400921 }, - { url = "https://files.pythonhosted.org/packages/68/6f/854f6a845c00b4257482468e08d8bc386f4929ee499206142378ba234419/kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa", size = 1513009 }, - { url = "https://files.pythonhosted.org/packages/50/65/76f303377167d12eb7a9b423d6771b39fe5c4373e4a42f075805b1f581ae/kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c", size = 1444819 }, - { url = "https://files.pythonhosted.org/packages/7e/ee/98cdf9dde129551467138b6e18cc1cc901e75ecc7ffb898c6f49609f33b1/kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b", size = 1817054 }, - { url = "https://files.pythonhosted.org/packages/e6/5b/ab569016ec4abc7b496f6cb8a3ab511372c99feb6a23d948cda97e0db6da/kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770", size = 1918613 }, - { url = "https://files.pythonhosted.org/packages/93/ac/39b9f99d2474b1ac7af1ddfe5756ddf9b6a8f24c5f3a32cd4c010317fc6b/kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0", size = 1872650 }, - { url = "https://files.pythonhosted.org/packages/40/5b/be568548266516b114d1776120281ea9236c732fb6032a1f8f3b1e5e921c/kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525", size = 1827415 }, - { url = "https://files.pythonhosted.org/packages/d4/80/c0c13d2a17a12937a19ef378bf35e94399fd171ed6ec05bcee0f038e1eaf/kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b", size = 1838094 }, - { url = "https://files.pythonhosted.org/packages/70/d1/5ab93ee00ca5af708929cc12fbe665b6f1ed4ad58088e70dc00e87e0d107/kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238", size = 46585 }, - { url = "https://files.pythonhosted.org/packages/4a/a1/8a9c9be45c642fa12954855d8b3a02d9fd8551165a558835a19508fec2e6/kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276", size = 56095 }, - { url = "https://files.pythonhosted.org/packages/2a/eb/9e099ad7c47c279995d2d20474e1821100a5f10f847739bd65b1c1f02442/kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5", size = 127403 }, - { url = "https://files.pythonhosted.org/packages/a6/94/695922e71288855fc7cace3bdb52edda9d7e50edba77abb0c9d7abb51e96/kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90", size = 68156 }, - { url = "https://files.pythonhosted.org/packages/4a/fe/23d7fa78f7c66086d196406beb1fb2eaf629dd7adc01c3453033303d17fa/kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797", size = 66166 }, - { url = "https://files.pythonhosted.org/packages/f1/68/f472bf16c9141bb1bea5c0b8c66c68fc1ccb048efdbd8f0872b92125724e/kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9", size = 1334300 }, - { url = "https://files.pythonhosted.org/packages/8d/26/b4569d1f29751fca22ee915b4ebfef5974f4ef239b3335fc072882bd62d9/kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437", size = 1426579 }, - { url = "https://files.pythonhosted.org/packages/f3/a3/804fc7c8bf233806ec0321c9da35971578620f2ab4fafe67d76100b3ce52/kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9", size = 1541360 }, - { url = "https://files.pythonhosted.org/packages/07/ef/286e1d26524854f6fbd6540e8364d67a8857d61038ac743e11edc42fe217/kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da", size = 1470091 }, - { url = "https://files.pythonhosted.org/packages/17/ba/17a706b232308e65f57deeccae503c268292e6a091313f6ce833a23093ea/kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e", size = 1426259 }, - { url = "https://files.pythonhosted.org/packages/d0/f3/a0925611c9d6c2f37c5935a39203cadec6883aa914e013b46c84c4c2e641/kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8", size = 1847516 }, - { url = "https://files.pythonhosted.org/packages/da/85/82d59bb8f7c4c9bb2785138b72462cb1b161668f8230c58bbb28c0403cd5/kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d", size = 1946228 }, - { url = "https://files.pythonhosted.org/packages/34/3c/6a37f444c0233993881e5db3a6a1775925d4d9d2f2609bb325bb1348ed94/kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0", size = 1901716 }, - { url = "https://files.pythonhosted.org/packages/cd/7e/180425790efc00adfd47db14e1e341cb4826516982334129012b971121a6/kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f", size = 1852871 }, - { url = "https://files.pythonhosted.org/packages/1b/9a/13c68b2edb1fa74321e60893a9a5829788e135138e68060cf44e2d92d2c3/kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f", size = 1870265 }, - { url = "https://files.pythonhosted.org/packages/9f/0a/fa56a0fdee5da2b4c79899c0f6bd1aefb29d9438c2d66430e78793571c6b/kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac", size = 46649 }, - { url = "https://files.pythonhosted.org/packages/1e/37/d3c2d4ba2719059a0f12730947bbe1ad5ee8bff89e8c35319dcb2c9ddb4c/kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355", size = 56116 }, - { url = "https://files.pythonhosted.org/packages/f3/7a/debbce859be1a2711eb8437818107137192007b88d17b5cfdb556f457b42/kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a", size = 125484 }, - { url = "https://files.pythonhosted.org/packages/2d/e0/bf8df75ba93b9e035cc6757dd5dcaf63084fdc1c846ae134e818bd7e0f03/kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192", size = 67332 }, - { url = "https://files.pythonhosted.org/packages/26/61/58bb691f6880588be3a4801d199bd776032ece07203faf3e4a8b377f7d9b/kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45", size = 64987 }, - { url = "https://files.pythonhosted.org/packages/8e/a3/96ac5413068b237c006f54dd8d70114e8756d70e3da7613c5aef20627e22/kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7", size = 1370613 }, - { url = "https://files.pythonhosted.org/packages/4d/12/f48539e6e17068b59c7f12f4d6214b973431b8e3ac83af525cafd27cebec/kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db", size = 1463183 }, - { url = "https://files.pythonhosted.org/packages/f3/70/26c99be8eb034cc8e3f62e0760af1fbdc97a842a7cbc252f7978507d41c2/kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff", size = 1581248 }, - { url = "https://files.pythonhosted.org/packages/17/f6/f75f20e543639b09b2de7fc864274a5a9b96cda167a6210a1d9d19306b9d/kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228", size = 1508815 }, - { url = "https://files.pythonhosted.org/packages/e3/d5/bc0f22ac108743062ab703f8d6d71c9c7b077b8839fa358700bfb81770b8/kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16", size = 1466042 }, - { url = "https://files.pythonhosted.org/packages/75/18/98142500f21d6838bcab49ec919414a1f0c6d049d21ddadf139124db6a70/kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9", size = 1885159 }, - { url = "https://files.pythonhosted.org/packages/21/49/a241eff9e0ee013368c1d17957f9d345b0957493c3a43d82ebb558c90b0a/kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162", size = 1981694 }, - { url = "https://files.pythonhosted.org/packages/90/90/9490c3de4788123041b1d600d64434f1eed809a2ce9f688075a22166b289/kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4", size = 1941579 }, - { url = "https://files.pythonhosted.org/packages/b7/bb/a0cc488ef2aa92d7d304318c8549d3ec8dfe6dd3c2c67a44e3922b77bc4f/kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3", size = 1888168 }, - { url = "https://files.pythonhosted.org/packages/4f/e9/9c0de8e45fef3d63f85eed3b1757f9aa511065942866331ef8b99421f433/kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a", size = 1908464 }, - { url = "https://files.pythonhosted.org/packages/a3/60/4f0fd50b08f5be536ea0cef518ac7255d9dab43ca40f3b93b60e3ddf80dd/kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20", size = 46473 }, - { url = "https://files.pythonhosted.org/packages/63/50/2746566bdf4a6a842d117367d05c90cfb87ac04e9e2845aa1fa21f071362/kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9", size = 56004 }, + { url = "https://files.pythonhosted.org/packages/f1/56/cb02dcefdaab40df636b91e703b172966b444605a0ea313549f3ffc05bd3/kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af", size = 127397, upload_time = "2023-08-24T09:28:18.105Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c1/d084f8edb26533a191415d5173157080837341f9a06af9dd1a75f727abb4/kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3", size = 68125, upload_time = "2023-08-24T09:28:19.218Z" }, + { url = "https://files.pythonhosted.org/packages/23/11/6fb190bae4b279d712a834e7b1da89f6dcff6791132f7399aa28a57c3565/kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4", size = 66211, upload_time = "2023-08-24T09:28:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/b3/13/5e9e52feb33e9e063f76b2c5eb09cb977f5bba622df3210081bfb26ec9a3/kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1", size = 1637145, upload_time = "2023-08-24T09:28:21.439Z" }, + { url = "https://files.pythonhosted.org/packages/6f/40/4ab1fdb57fced80ce5903f04ae1aed7c1d5939dda4fd0c0aa526c12fe28a/kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff", size = 1617849, upload_time = "2023-08-24T09:28:23.004Z" }, + { url = "https://files.pythonhosted.org/packages/49/ca/61ef43bd0832c7253b370735b0c38972c140c8774889b884372a629a8189/kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a", size = 1400921, upload_time = "2023-08-24T09:28:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/68/6f/854f6a845c00b4257482468e08d8bc386f4929ee499206142378ba234419/kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa", size = 1513009, upload_time = "2023-08-24T09:28:25.636Z" }, + { url = "https://files.pythonhosted.org/packages/50/65/76f303377167d12eb7a9b423d6771b39fe5c4373e4a42f075805b1f581ae/kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c", size = 1444819, upload_time = "2023-08-24T09:28:27.547Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ee/98cdf9dde129551467138b6e18cc1cc901e75ecc7ffb898c6f49609f33b1/kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b", size = 1817054, upload_time = "2023-08-24T09:28:28.839Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5b/ab569016ec4abc7b496f6cb8a3ab511372c99feb6a23d948cda97e0db6da/kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770", size = 1918613, upload_time = "2023-08-24T09:28:30.351Z" }, + { url = "https://files.pythonhosted.org/packages/93/ac/39b9f99d2474b1ac7af1ddfe5756ddf9b6a8f24c5f3a32cd4c010317fc6b/kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0", size = 1872650, upload_time = "2023-08-24T09:28:32.303Z" }, + { url = "https://files.pythonhosted.org/packages/40/5b/be568548266516b114d1776120281ea9236c732fb6032a1f8f3b1e5e921c/kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525", size = 1827415, upload_time = "2023-08-24T09:28:34.141Z" }, + { url = "https://files.pythonhosted.org/packages/d4/80/c0c13d2a17a12937a19ef378bf35e94399fd171ed6ec05bcee0f038e1eaf/kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b", size = 1838094, upload_time = "2023-08-24T09:28:35.97Z" }, + { url = "https://files.pythonhosted.org/packages/70/d1/5ab93ee00ca5af708929cc12fbe665b6f1ed4ad58088e70dc00e87e0d107/kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238", size = 46585, upload_time = "2023-08-24T09:28:37.326Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a1/8a9c9be45c642fa12954855d8b3a02d9fd8551165a558835a19508fec2e6/kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276", size = 56095, upload_time = "2023-08-24T09:28:38.325Z" }, + { url = "https://files.pythonhosted.org/packages/2a/eb/9e099ad7c47c279995d2d20474e1821100a5f10f847739bd65b1c1f02442/kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5", size = 127403, upload_time = "2023-08-24T09:28:39.3Z" }, + { url = "https://files.pythonhosted.org/packages/a6/94/695922e71288855fc7cace3bdb52edda9d7e50edba77abb0c9d7abb51e96/kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90", size = 68156, upload_time = "2023-08-24T09:28:40.301Z" }, + { url = "https://files.pythonhosted.org/packages/4a/fe/23d7fa78f7c66086d196406beb1fb2eaf629dd7adc01c3453033303d17fa/kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797", size = 66166, upload_time = "2023-08-24T09:28:41.235Z" }, + { url = "https://files.pythonhosted.org/packages/f1/68/f472bf16c9141bb1bea5c0b8c66c68fc1ccb048efdbd8f0872b92125724e/kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9", size = 1334300, upload_time = "2023-08-24T09:28:42.409Z" }, + { url = "https://files.pythonhosted.org/packages/8d/26/b4569d1f29751fca22ee915b4ebfef5974f4ef239b3335fc072882bd62d9/kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437", size = 1426579, upload_time = "2023-08-24T09:28:43.677Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a3/804fc7c8bf233806ec0321c9da35971578620f2ab4fafe67d76100b3ce52/kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9", size = 1541360, upload_time = "2023-08-24T09:28:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/07/ef/286e1d26524854f6fbd6540e8364d67a8857d61038ac743e11edc42fe217/kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da", size = 1470091, upload_time = "2023-08-24T09:28:47.959Z" }, + { url = "https://files.pythonhosted.org/packages/17/ba/17a706b232308e65f57deeccae503c268292e6a091313f6ce833a23093ea/kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e", size = 1426259, upload_time = "2023-08-24T09:28:49.224Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f3/a0925611c9d6c2f37c5935a39203cadec6883aa914e013b46c84c4c2e641/kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8", size = 1847516, upload_time = "2023-08-24T09:28:50.979Z" }, + { url = "https://files.pythonhosted.org/packages/da/85/82d59bb8f7c4c9bb2785138b72462cb1b161668f8230c58bbb28c0403cd5/kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d", size = 1946228, upload_time = "2023-08-24T09:28:52.812Z" }, + { url = "https://files.pythonhosted.org/packages/34/3c/6a37f444c0233993881e5db3a6a1775925d4d9d2f2609bb325bb1348ed94/kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0", size = 1901716, upload_time = "2023-08-24T09:28:54.115Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7e/180425790efc00adfd47db14e1e341cb4826516982334129012b971121a6/kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f", size = 1852871, upload_time = "2023-08-24T09:28:55.433Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9a/13c68b2edb1fa74321e60893a9a5829788e135138e68060cf44e2d92d2c3/kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f", size = 1870265, upload_time = "2023-08-24T09:28:56.855Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0a/fa56a0fdee5da2b4c79899c0f6bd1aefb29d9438c2d66430e78793571c6b/kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac", size = 46649, upload_time = "2023-08-24T09:28:58.021Z" }, + { url = "https://files.pythonhosted.org/packages/1e/37/d3c2d4ba2719059a0f12730947bbe1ad5ee8bff89e8c35319dcb2c9ddb4c/kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355", size = 56116, upload_time = "2023-08-24T09:28:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7a/debbce859be1a2711eb8437818107137192007b88d17b5cfdb556f457b42/kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a", size = 125484, upload_time = "2023-08-24T09:28:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e0/bf8df75ba93b9e035cc6757dd5dcaf63084fdc1c846ae134e818bd7e0f03/kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192", size = 67332, upload_time = "2023-08-24T09:29:01.733Z" }, + { url = "https://files.pythonhosted.org/packages/26/61/58bb691f6880588be3a4801d199bd776032ece07203faf3e4a8b377f7d9b/kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45", size = 64987, upload_time = "2023-08-24T09:29:02.789Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a3/96ac5413068b237c006f54dd8d70114e8756d70e3da7613c5aef20627e22/kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7", size = 1370613, upload_time = "2023-08-24T09:29:03.912Z" }, + { url = "https://files.pythonhosted.org/packages/4d/12/f48539e6e17068b59c7f12f4d6214b973431b8e3ac83af525cafd27cebec/kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db", size = 1463183, upload_time = "2023-08-24T09:29:05.244Z" }, + { url = "https://files.pythonhosted.org/packages/f3/70/26c99be8eb034cc8e3f62e0760af1fbdc97a842a7cbc252f7978507d41c2/kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff", size = 1581248, upload_time = "2023-08-24T09:29:06.531Z" }, + { url = "https://files.pythonhosted.org/packages/17/f6/f75f20e543639b09b2de7fc864274a5a9b96cda167a6210a1d9d19306b9d/kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228", size = 1508815, upload_time = "2023-08-24T09:29:07.867Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d5/bc0f22ac108743062ab703f8d6d71c9c7b077b8839fa358700bfb81770b8/kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16", size = 1466042, upload_time = "2023-08-24T09:29:09.403Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/98142500f21d6838bcab49ec919414a1f0c6d049d21ddadf139124db6a70/kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9", size = 1885159, upload_time = "2023-08-24T09:29:10.66Z" }, + { url = "https://files.pythonhosted.org/packages/21/49/a241eff9e0ee013368c1d17957f9d345b0957493c3a43d82ebb558c90b0a/kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162", size = 1981694, upload_time = "2023-08-24T09:29:12.469Z" }, + { url = "https://files.pythonhosted.org/packages/90/90/9490c3de4788123041b1d600d64434f1eed809a2ce9f688075a22166b289/kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4", size = 1941579, upload_time = "2023-08-24T09:29:13.743Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bb/a0cc488ef2aa92d7d304318c8549d3ec8dfe6dd3c2c67a44e3922b77bc4f/kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3", size = 1888168, upload_time = "2023-08-24T09:29:15.097Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e9/9c0de8e45fef3d63f85eed3b1757f9aa511065942866331ef8b99421f433/kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a", size = 1908464, upload_time = "2023-08-24T09:29:16.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/60/4f0fd50b08f5be536ea0cef518ac7255d9dab43ca40f3b93b60e3ddf80dd/kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20", size = 46473, upload_time = "2023-08-24T09:29:17.956Z" }, + { url = "https://files.pythonhosted.org/packages/63/50/2746566bdf4a6a842d117367d05c90cfb87ac04e9e2845aa1fa21f071362/kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9", size = 56004, upload_time = "2023-08-24T09:29:19.329Z" }, ] [[package]] name = "lazy-loader" version = "0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0e/3a/1630a735bfdf9eb857a3b9a53317a1e1658ea97a1b4b39dcb0f71dae81f8/lazy_loader-0.3.tar.gz", hash = "sha256:3b68898e34f5b2a29daaaac172c6555512d0f32074f147e2254e4a6d9d838f37", size = 12268 } +sdist = { url = "https://files.pythonhosted.org/packages/0e/3a/1630a735bfdf9eb857a3b9a53317a1e1658ea97a1b4b39dcb0f71dae81f8/lazy_loader-0.3.tar.gz", hash = "sha256:3b68898e34f5b2a29daaaac172c6555512d0f32074f147e2254e4a6d9d838f37", size = 12268, upload_time = "2023-06-30T21:12:55.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/c3/65b3814e155836acacf720e5be3b5757130346670ac454fee29d3eda1381/lazy_loader-0.3-py3-none-any.whl", hash = "sha256:1e9e76ee8631e264c62ce10006718e80b2cfc74340d17d1031e0f84af7478554", size = 9087 }, + { url = "https://files.pythonhosted.org/packages/a1/c3/65b3814e155836acacf720e5be3b5757130346670ac454fee29d3eda1381/lazy_loader-0.3-py3-none-any.whl", hash = "sha256:1e9e76ee8631e264c62ce10006718e80b2cfc74340d17d1031e0f84af7478554", size = 9087, upload_time = "2023-06-30T21:12:51.09Z" }, ] [[package]] name = "locust" -version = "2.34.1" +version = "2.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "configargparse" }, @@ -1219,9 +1219,9 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/21/c2bfe4f9482f8754e9a1ff2b1840a1abe63640576fc918a67a02fff7d961/locust-2.34.1.tar.gz", hash = "sha256:184a6ffcb0d6c543bbeae4de65cbb198c7e0739d569d48a2b8bf5db962077733", size = 2240533 } +sdist = { url = "https://files.pythonhosted.org/packages/79/21/d5aeeee74173d73d7d8d392e307ec24d8281fca69a2bf1f19199bd84c498/locust-2.35.0.tar.gz", hash = "sha256:97f83e591646ca3227644cfb6d4fa590e9a3e3d791ab18b216ca98be235b9b24", size = 2240690, upload_time = "2025-04-16T12:10:25.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/e4/0944fbfb1ce0bf09cb400ed9349d4cbaed1230114e4018ac28805097f1c6/locust-2.34.1-py3-none-any.whl", hash = "sha256:487bfadd584e3320f9862adf5aa1cfa1023e030a6af414f4e0a92e62617ce451", size = 2257910 }, + { url = "https://files.pythonhosted.org/packages/3d/15/9e92757c08af3f0c0168ab6480315ed6374396d635f70055df5c42cfa672/locust-2.35.0-py3-none-any.whl", hash = "sha256:fb9e0ec25c5db3ed6a3c6d48e7236d7c2c370b0ddae102e9badcb2d3d101abde", size = 2258054, upload_time = "2025-04-16T12:10:22.608Z" }, ] [[package]] @@ -1231,47 +1231,47 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload_time = "2023-06-03T06:41:14.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload_time = "2023-06-03T06:41:11.019Z" }, ] [[package]] name = "markupsafe" version = "2.1.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/7c/59a3248f411813f8ccba92a55feaac4bf360d29e2ff05ee7d8e1ef2d7dbf/MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", size = 19132 } +sdist = { url = "https://files.pythonhosted.org/packages/6d/7c/59a3248f411813f8ccba92a55feaac4bf360d29e2ff05ee7d8e1ef2d7dbf/MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", size = 19132, upload_time = "2023-06-02T21:43:45.578Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/1d/713d443799d935f4d26a4f1510c9e61b1d288592fb869845e5cc92a1e055/MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", size = 17846 }, - { url = "https://files.pythonhosted.org/packages/f7/9c/86cbd8e0e1d81f0ba420f20539dd459c50537c7751e28102dbfee2b6f28c/MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", size = 13720 }, - { url = "https://files.pythonhosted.org/packages/a6/56/f1d4ee39e898a9e63470cbb7fae1c58cce6874f25f54220b89213a47f273/MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", size = 26498 }, - { url = "https://files.pythonhosted.org/packages/12/b3/d9ed2c0971e1435b8a62354b18d3060b66c8cb1d368399ec0b9baa7c0ee5/MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", size = 25691 }, - { url = "https://files.pythonhosted.org/packages/bf/b7/c5ba9b7ad9ad21fc4a60df226615cf43ead185d328b77b0327d603d00cc5/MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", size = 25366 }, - { url = "https://files.pythonhosted.org/packages/71/61/f5673d7aac2cf7f203859008bb3fc2b25187aa330067c5e9955e5c5ebbab/MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", size = 30505 }, - { url = "https://files.pythonhosted.org/packages/47/26/932140621773bfd4df3223fbdd9e78de3477f424f0d2987c313b1cb655ff/MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", size = 29616 }, - { url = "https://files.pythonhosted.org/packages/3c/c8/74d13c999cbb49e3460bf769025659a37ef4a8e884de629720ab4e42dcdb/MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", size = 29891 }, - { url = "https://files.pythonhosted.org/packages/96/e4/4db3b1abc5a1fe7295aa0683eafd13832084509c3b8236f3faf8dd4eff75/MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", size = 16525 }, - { url = "https://files.pythonhosted.org/packages/84/a8/c4aebb8a14a1d39d5135eb8233a0b95831cdc42c4088358449c3ed657044/MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", size = 17083 }, - { url = "https://files.pythonhosted.org/packages/fe/09/c31503cb8150cf688c1534a7135cc39bb9092f8e0e6369ec73494d16ee0e/MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", size = 17862 }, - { url = "https://files.pythonhosted.org/packages/c0/c7/171f5ac6b065e1425e8fabf4a4dfbeca76fd8070072c6a41bd5c07d90d8b/MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", size = 13738 }, - { url = "https://files.pythonhosted.org/packages/a2/f7/9175ad1b8152092f7c3b78c513c1bdfe9287e0564447d1c2d3d1a2471540/MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", size = 28891 }, - { url = "https://files.pythonhosted.org/packages/fe/21/2eff1de472ca6c99ec3993eab11308787b9879af9ca8bbceb4868cf4f2ca/MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", size = 28096 }, - { url = "https://files.pythonhosted.org/packages/f4/a0/103f94793c3bf829a18d2415117334ece115aeca56f2df1c47fa02c6dbd6/MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", size = 27631 }, - { url = "https://files.pythonhosted.org/packages/43/70/f24470f33b2035b035ef0c0ffebf57006beb2272cf3df068fc5154e04ead/MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", size = 33863 }, - { url = "https://files.pythonhosted.org/packages/32/d4/ce98c4ca713d91c4a17c1a184785cc00b9e9c25699d618956c2b9999500a/MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", size = 32591 }, - { url = "https://files.pythonhosted.org/packages/bb/82/f88ccb3ca6204a4536cf7af5abdad7c3657adac06ab33699aa67279e0744/MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", size = 33186 }, - { url = "https://files.pythonhosted.org/packages/44/53/93405d37bb04a10c43b1bdd6f548097478d494d7eadb4b364e3e1337f0cc/MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", size = 16537 }, - { url = "https://files.pythonhosted.org/packages/be/bb/08b85bc194034efbf572e70c3951549c8eca0ada25363afc154386b5390a/MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", size = 17089 }, - { url = "https://files.pythonhosted.org/packages/89/5a/ee546f2aa73a1d6fcfa24272f356fe06d29acca81e76b8d32ca53e429a2e/MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc", size = 17849 }, - { url = "https://files.pythonhosted.org/packages/3a/72/9f683a059bde096776e8acf9aa34cbbba21ddc399861fe3953790d4f2cde/MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823", size = 13700 }, - { url = "https://files.pythonhosted.org/packages/9d/78/92f15eb9b1e8f1668a9787ba103cf6f8d19a9efed8150245404836145c24/MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11", size = 29319 }, - { url = "https://files.pythonhosted.org/packages/51/94/9a04085114ff2c24f7424dbc890a281d73c5a74ea935dc2e69c66a3bd558/MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd", size = 28314 }, - { url = "https://files.pythonhosted.org/packages/ec/53/fcb3214bd370185e223b209ce6bb010fb887ea57173ca4f75bd211b24e10/MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939", size = 27696 }, - { url = "https://files.pythonhosted.org/packages/e7/33/54d29854716725d7826079b8984dd235fac76dab1c32321e555d493e61f5/MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c", size = 33746 }, - { url = "https://files.pythonhosted.org/packages/11/40/ea7f85e2681d29bc9301c757257de561923924f24de1802d9c3baa396bb4/MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c", size = 32131 }, - { url = "https://files.pythonhosted.org/packages/41/f1/bc770c37ecd58638c18f8ec85df205dacb818ccf933692082fd93010a4bc/MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1", size = 32878 }, - { url = "https://files.pythonhosted.org/packages/49/74/bf95630aab0a9ed6a67556cd4e54f6aeb0e74f4cb0fd2f229154873a4be4/MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007", size = 16426 }, - { url = "https://files.pythonhosted.org/packages/44/44/dbaf65876e258facd65f586dde158387ab89963e7f2235551afc9c2e24c2/MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", size = 16979 }, + { url = "https://files.pythonhosted.org/packages/20/1d/713d443799d935f4d26a4f1510c9e61b1d288592fb869845e5cc92a1e055/MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", size = 17846, upload_time = "2023-06-02T21:42:33.954Z" }, + { url = "https://files.pythonhosted.org/packages/f7/9c/86cbd8e0e1d81f0ba420f20539dd459c50537c7751e28102dbfee2b6f28c/MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", size = 13720, upload_time = "2023-06-02T21:42:35.102Z" }, + { url = "https://files.pythonhosted.org/packages/a6/56/f1d4ee39e898a9e63470cbb7fae1c58cce6874f25f54220b89213a47f273/MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", size = 26498, upload_time = "2023-06-02T21:42:36.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/d9ed2c0971e1435b8a62354b18d3060b66c8cb1d368399ec0b9baa7c0ee5/MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", size = 25691, upload_time = "2023-06-02T21:42:37.778Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b7/c5ba9b7ad9ad21fc4a60df226615cf43ead185d328b77b0327d603d00cc5/MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", size = 25366, upload_time = "2023-06-02T21:42:39.441Z" }, + { url = "https://files.pythonhosted.org/packages/71/61/f5673d7aac2cf7f203859008bb3fc2b25187aa330067c5e9955e5c5ebbab/MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", size = 30505, upload_time = "2023-06-02T21:42:41.088Z" }, + { url = "https://files.pythonhosted.org/packages/47/26/932140621773bfd4df3223fbdd9e78de3477f424f0d2987c313b1cb655ff/MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", size = 29616, upload_time = "2023-06-02T21:42:42.273Z" }, + { url = "https://files.pythonhosted.org/packages/3c/c8/74d13c999cbb49e3460bf769025659a37ef4a8e884de629720ab4e42dcdb/MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", size = 29891, upload_time = "2023-06-02T21:42:43.635Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/4db3b1abc5a1fe7295aa0683eafd13832084509c3b8236f3faf8dd4eff75/MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", size = 16525, upload_time = "2023-06-02T21:42:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/84/a8/c4aebb8a14a1d39d5135eb8233a0b95831cdc42c4088358449c3ed657044/MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", size = 17083, upload_time = "2023-06-02T21:42:46.948Z" }, + { url = "https://files.pythonhosted.org/packages/fe/09/c31503cb8150cf688c1534a7135cc39bb9092f8e0e6369ec73494d16ee0e/MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", size = 17862, upload_time = "2023-06-02T21:42:48.569Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/171f5ac6b065e1425e8fabf4a4dfbeca76fd8070072c6a41bd5c07d90d8b/MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", size = 13738, upload_time = "2023-06-02T21:42:49.727Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f7/9175ad1b8152092f7c3b78c513c1bdfe9287e0564447d1c2d3d1a2471540/MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", size = 28891, upload_time = "2023-06-02T21:42:51.33Z" }, + { url = "https://files.pythonhosted.org/packages/fe/21/2eff1de472ca6c99ec3993eab11308787b9879af9ca8bbceb4868cf4f2ca/MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", size = 28096, upload_time = "2023-06-02T21:42:52.966Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a0/103f94793c3bf829a18d2415117334ece115aeca56f2df1c47fa02c6dbd6/MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", size = 27631, upload_time = "2023-06-02T21:42:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/43/70/f24470f33b2035b035ef0c0ffebf57006beb2272cf3df068fc5154e04ead/MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", size = 33863, upload_time = "2023-06-02T21:42:55.777Z" }, + { url = "https://files.pythonhosted.org/packages/32/d4/ce98c4ca713d91c4a17c1a184785cc00b9e9c25699d618956c2b9999500a/MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", size = 32591, upload_time = "2023-06-02T21:42:57.415Z" }, + { url = "https://files.pythonhosted.org/packages/bb/82/f88ccb3ca6204a4536cf7af5abdad7c3657adac06ab33699aa67279e0744/MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", size = 33186, upload_time = "2023-06-02T21:42:59.107Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/93405d37bb04a10c43b1bdd6f548097478d494d7eadb4b364e3e1337f0cc/MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", size = 16537, upload_time = "2023-06-02T21:43:00.927Z" }, + { url = "https://files.pythonhosted.org/packages/be/bb/08b85bc194034efbf572e70c3951549c8eca0ada25363afc154386b5390a/MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", size = 17089, upload_time = "2023-06-02T21:43:02.355Z" }, + { url = "https://files.pythonhosted.org/packages/89/5a/ee546f2aa73a1d6fcfa24272f356fe06d29acca81e76b8d32ca53e429a2e/MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc", size = 17849, upload_time = "2023-09-07T16:00:43.795Z" }, + { url = "https://files.pythonhosted.org/packages/3a/72/9f683a059bde096776e8acf9aa34cbbba21ddc399861fe3953790d4f2cde/MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823", size = 13700, upload_time = "2023-09-07T16:00:45.384Z" }, + { url = "https://files.pythonhosted.org/packages/9d/78/92f15eb9b1e8f1668a9787ba103cf6f8d19a9efed8150245404836145c24/MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11", size = 29319, upload_time = "2023-09-07T16:00:46.48Z" }, + { url = "https://files.pythonhosted.org/packages/51/94/9a04085114ff2c24f7424dbc890a281d73c5a74ea935dc2e69c66a3bd558/MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd", size = 28314, upload_time = "2023-09-07T16:00:47.64Z" }, + { url = "https://files.pythonhosted.org/packages/ec/53/fcb3214bd370185e223b209ce6bb010fb887ea57173ca4f75bd211b24e10/MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939", size = 27696, upload_time = "2023-09-07T16:00:48.92Z" }, + { url = "https://files.pythonhosted.org/packages/e7/33/54d29854716725d7826079b8984dd235fac76dab1c32321e555d493e61f5/MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c", size = 33746, upload_time = "2023-09-07T16:00:50.081Z" }, + { url = "https://files.pythonhosted.org/packages/11/40/ea7f85e2681d29bc9301c757257de561923924f24de1802d9c3baa396bb4/MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c", size = 32131, upload_time = "2023-09-07T16:00:51.822Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/bc770c37ecd58638c18f8ec85df205dacb818ccf933692082fd93010a4bc/MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1", size = 32878, upload_time = "2023-09-07T16:00:53.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/74/bf95630aab0a9ed6a67556cd4e54f6aeb0e74f4cb0fd2f229154873a4be4/MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007", size = 16426, upload_time = "2023-09-07T16:00:55.987Z" }, + { url = "https://files.pythonhosted.org/packages/44/44/dbaf65876e258facd65f586dde158387ab89963e7f2235551afc9c2e24c2/MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", size = 16979, upload_time = "2023-09-07T16:00:57.77Z" }, ] [[package]] @@ -1289,85 +1289,85 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/ab/38a0e94cb01dacb50f06957c2bed1c83b8f9dac6618988a37b2487862944/matplotlib-3.8.2.tar.gz", hash = "sha256:01a978b871b881ee76017152f1f1a0cbf6bd5f7b8ff8c96df0df1bd57d8755a1", size = 35866957 } +sdist = { url = "https://files.pythonhosted.org/packages/fb/ab/38a0e94cb01dacb50f06957c2bed1c83b8f9dac6618988a37b2487862944/matplotlib-3.8.2.tar.gz", hash = "sha256:01a978b871b881ee76017152f1f1a0cbf6bd5f7b8ff8c96df0df1bd57d8755a1", size = 35866957, upload_time = "2023-11-17T21:16:40.15Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/d0/fc5f6796a1956f5b9a33555611d01a3cec038f000c3d70ecb051b1631ac4/matplotlib-3.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:09796f89fb71a0c0e1e2f4bdaf63fb2cefc84446bb963ecdeb40dfee7dfa98c7", size = 7590640 }, - { url = "https://files.pythonhosted.org/packages/57/44/007b592809f50883c910db9ec4b81b16dfa0136407250fb581824daabf03/matplotlib-3.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9c6976748a25e8b9be51ea028df49b8e561eed7809146da7a47dbecebab367", size = 7484350 }, - { url = "https://files.pythonhosted.org/packages/01/87/c7b24f3048234fe10184560263be2173311376dc3d1fa329de7f012d6ce5/matplotlib-3.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78e4f2cedf303869b782071b55fdde5987fda3038e9d09e58c91cc261b5ad18", size = 11382388 }, - { url = "https://files.pythonhosted.org/packages/19/e5/a4ea514515f270224435c69359abb7a3d152ed31b9ee3ba5e63017461945/matplotlib-3.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e208f46cf6576a7624195aa047cb344a7f802e113bb1a06cfd4bee431de5e31", size = 11611959 }, - { url = "https://files.pythonhosted.org/packages/09/23/ab5a562c9acb81e351b084bea39f65b153918417fb434619cf5a19f44a55/matplotlib-3.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46a569130ff53798ea5f50afce7406e91fdc471ca1e0e26ba976a8c734c9427a", size = 9536938 }, - { url = "https://files.pythonhosted.org/packages/46/37/b5e27ab30ecc0a3694c8a78287b5ef35dad0c3095c144fcc43081170bfd6/matplotlib-3.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:830f00640c965c5b7f6bc32f0d4ce0c36dfe0379f7dd65b07a00c801713ec40a", size = 7643836 }, - { url = "https://files.pythonhosted.org/packages/a9/0d/53afb186adafc7326d093b8333e8a79974c495095771659f4304626c4bc7/matplotlib-3.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d86593ccf546223eb75a39b44c32788e6f6440d13cfc4750c1c15d0fcb850b63", size = 7593458 }, - { url = "https://files.pythonhosted.org/packages/ce/25/a557ee10ac9dce1300850024707ce1850a6958f1673a9194be878b99d631/matplotlib-3.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a5430836811b7652991939012f43d2808a2db9b64ee240387e8c43e2e5578c8", size = 7486840 }, - { url = "https://files.pythonhosted.org/packages/e7/3d/72712b3895ee180f6e342638a8591c31912fbcc09ce9084cc256da16d0a0/matplotlib-3.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9576723858a78751d5aacd2497b8aef29ffea6d1c95981505877f7ac28215c6", size = 11387332 }, - { url = "https://files.pythonhosted.org/packages/92/1a/cd3e0c90d1a763ad90073e13b189b4702f11becf4e71dbbad70a7a149811/matplotlib-3.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ba9cbd8ac6cf422f3102622b20f8552d601bf8837e49a3afed188d560152788", size = 11616911 }, - { url = "https://files.pythonhosted.org/packages/78/4a/bad239071477305a3758eb4810615e310a113399cddd7682998be9f01e97/matplotlib-3.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:03f9d160a29e0b65c0790bb07f4f45d6a181b1ac33eb1bb0dd225986450148f0", size = 9549260 }, - { url = "https://files.pythonhosted.org/packages/26/5a/27fd341e4510257789f19a4b4be8bb90d1113b8f176c3dab562b4f21466e/matplotlib-3.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:3773002da767f0a9323ba1a9b9b5d00d6257dbd2a93107233167cfb581f64717", size = 7645742 }, - { url = "https://files.pythonhosted.org/packages/e4/1b/864d28d5a72d586ac137f4ca54d5afc8b869720e30d508dbd9adcce4d231/matplotlib-3.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4c318c1e95e2f5926fba326f68177dee364aa791d6df022ceb91b8221bd0a627", size = 7590988 }, - { url = "https://files.pythonhosted.org/packages/9a/b0/dd2b60f2dd90fbc21d1d3129c36a453c322d7995d5e3589f5b3c59ee528d/matplotlib-3.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:091275d18d942cf1ee9609c830a1bc36610607d8223b1b981c37d5c9fc3e46a4", size = 7483594 }, - { url = "https://files.pythonhosted.org/packages/33/da/9942533ad9f96753bde0e5a5d48eacd6c21de8ea1ad16570e31bda8a017f/matplotlib-3.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b0f3b8ea0e99e233a4bcc44590f01604840d833c280ebb8fe5554fd3e6cfe8d", size = 11380843 }, - { url = "https://files.pythonhosted.org/packages/fc/52/bfd36eb4745a3b21b3946c2c3a15679b620e14574fe2b98e9451b65ef578/matplotlib-3.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7b1704a530395aaf73912be741c04d181f82ca78084fbd80bc737be04848331", size = 11604608 }, - { url = "https://files.pythonhosted.org/packages/6d/8c/0cdfbf604d4ea3dfa77435176c51e233cc408ad8f3efbf8d2c9f57cbdafb/matplotlib-3.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533b0e3b0c6768eef8cbe4b583731ce25a91ab54a22f830db2b031e83cca9213", size = 9545252 }, - { url = "https://files.pythonhosted.org/packages/2e/51/c77a14869b7eb9d6fb440e811b754fc3950d6868c38ace57d0632b674415/matplotlib-3.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:0f4fc5d72b75e2c18e55eb32292659cf731d9d5b312a6eb036506304f4675630", size = 7645067 }, + { url = "https://files.pythonhosted.org/packages/92/d0/fc5f6796a1956f5b9a33555611d01a3cec038f000c3d70ecb051b1631ac4/matplotlib-3.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:09796f89fb71a0c0e1e2f4bdaf63fb2cefc84446bb963ecdeb40dfee7dfa98c7", size = 7590640, upload_time = "2023-11-17T21:17:02.834Z" }, + { url = "https://files.pythonhosted.org/packages/57/44/007b592809f50883c910db9ec4b81b16dfa0136407250fb581824daabf03/matplotlib-3.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9c6976748a25e8b9be51ea028df49b8e561eed7809146da7a47dbecebab367", size = 7484350, upload_time = "2023-11-17T21:17:12.281Z" }, + { url = "https://files.pythonhosted.org/packages/01/87/c7b24f3048234fe10184560263be2173311376dc3d1fa329de7f012d6ce5/matplotlib-3.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78e4f2cedf303869b782071b55fdde5987fda3038e9d09e58c91cc261b5ad18", size = 11382388, upload_time = "2023-11-17T21:17:26.461Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/a4ea514515f270224435c69359abb7a3d152ed31b9ee3ba5e63017461945/matplotlib-3.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e208f46cf6576a7624195aa047cb344a7f802e113bb1a06cfd4bee431de5e31", size = 11611959, upload_time = "2023-11-17T21:17:40.541Z" }, + { url = "https://files.pythonhosted.org/packages/09/23/ab5a562c9acb81e351b084bea39f65b153918417fb434619cf5a19f44a55/matplotlib-3.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46a569130ff53798ea5f50afce7406e91fdc471ca1e0e26ba976a8c734c9427a", size = 9536938, upload_time = "2023-11-17T21:17:49.925Z" }, + { url = "https://files.pythonhosted.org/packages/46/37/b5e27ab30ecc0a3694c8a78287b5ef35dad0c3095c144fcc43081170bfd6/matplotlib-3.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:830f00640c965c5b7f6bc32f0d4ce0c36dfe0379f7dd65b07a00c801713ec40a", size = 7643836, upload_time = "2023-11-17T21:17:58.379Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0d/53afb186adafc7326d093b8333e8a79974c495095771659f4304626c4bc7/matplotlib-3.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d86593ccf546223eb75a39b44c32788e6f6440d13cfc4750c1c15d0fcb850b63", size = 7593458, upload_time = "2023-11-17T21:18:06.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/25/a557ee10ac9dce1300850024707ce1850a6958f1673a9194be878b99d631/matplotlib-3.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a5430836811b7652991939012f43d2808a2db9b64ee240387e8c43e2e5578c8", size = 7486840, upload_time = "2023-11-17T21:18:13.706Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/72712b3895ee180f6e342638a8591c31912fbcc09ce9084cc256da16d0a0/matplotlib-3.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9576723858a78751d5aacd2497b8aef29ffea6d1c95981505877f7ac28215c6", size = 11387332, upload_time = "2023-11-17T21:18:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/92/1a/cd3e0c90d1a763ad90073e13b189b4702f11becf4e71dbbad70a7a149811/matplotlib-3.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ba9cbd8ac6cf422f3102622b20f8552d601bf8837e49a3afed188d560152788", size = 11616911, upload_time = "2023-11-17T21:18:35.27Z" }, + { url = "https://files.pythonhosted.org/packages/78/4a/bad239071477305a3758eb4810615e310a113399cddd7682998be9f01e97/matplotlib-3.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:03f9d160a29e0b65c0790bb07f4f45d6a181b1ac33eb1bb0dd225986450148f0", size = 9549260, upload_time = "2023-11-17T21:18:44.836Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/27fd341e4510257789f19a4b4be8bb90d1113b8f176c3dab562b4f21466e/matplotlib-3.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:3773002da767f0a9323ba1a9b9b5d00d6257dbd2a93107233167cfb581f64717", size = 7645742, upload_time = "2023-11-17T21:18:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1b/864d28d5a72d586ac137f4ca54d5afc8b869720e30d508dbd9adcce4d231/matplotlib-3.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4c318c1e95e2f5926fba326f68177dee364aa791d6df022ceb91b8221bd0a627", size = 7590988, upload_time = "2023-11-17T21:19:01.119Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b0/dd2b60f2dd90fbc21d1d3129c36a453c322d7995d5e3589f5b3c59ee528d/matplotlib-3.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:091275d18d942cf1ee9609c830a1bc36610607d8223b1b981c37d5c9fc3e46a4", size = 7483594, upload_time = "2023-11-17T21:19:09.865Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/9942533ad9f96753bde0e5a5d48eacd6c21de8ea1ad16570e31bda8a017f/matplotlib-3.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b0f3b8ea0e99e233a4bcc44590f01604840d833c280ebb8fe5554fd3e6cfe8d", size = 11380843, upload_time = "2023-11-17T21:19:20.46Z" }, + { url = "https://files.pythonhosted.org/packages/fc/52/bfd36eb4745a3b21b3946c2c3a15679b620e14574fe2b98e9451b65ef578/matplotlib-3.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7b1704a530395aaf73912be741c04d181f82ca78084fbd80bc737be04848331", size = 11604608, upload_time = "2023-11-17T21:19:31.363Z" }, + { url = "https://files.pythonhosted.org/packages/6d/8c/0cdfbf604d4ea3dfa77435176c51e233cc408ad8f3efbf8d2c9f57cbdafb/matplotlib-3.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533b0e3b0c6768eef8cbe4b583731ce25a91ab54a22f830db2b031e83cca9213", size = 9545252, upload_time = "2023-11-17T21:19:42.271Z" }, + { url = "https://files.pythonhosted.org/packages/2e/51/c77a14869b7eb9d6fb440e811b754fc3950d6868c38ace57d0632b674415/matplotlib-3.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:0f4fc5d72b75e2c18e55eb32292659cf731d9d5b312a6eb036506304f4675630", size = 7645067, upload_time = "2023-11-17T21:19:50.091Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload_time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload_time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "mpmath" version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload_time = "2023-03-07T16:47:11.061Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload_time = "2023-03-07T16:47:09.197Z" }, ] [[package]] name = "msgpack" version = "1.0.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/d5/5662032db1571110b5b51647aed4b56dfbd01bfae789fa566a2be1f385d1/msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87", size = 166311 } +sdist = { url = "https://files.pythonhosted.org/packages/c2/d5/5662032db1571110b5b51647aed4b56dfbd01bfae789fa566a2be1f385d1/msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87", size = 166311, upload_time = "2023-09-28T13:20:36.726Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/3a/2e2e902afcd751738e38d88af976fc4010b16e8e821945f4cbf32f75f9c3/msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862", size = 304827 }, - { url = "https://files.pythonhosted.org/packages/86/a6/490792a524a82e855bdf3885ecb73d7b3a0b17744b3cf4a40aea13ceca38/msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329", size = 234959 }, - { url = "https://files.pythonhosted.org/packages/ad/72/d39ed43bfb2ec6968d768318477adb90c474bdc59b2437170c6697ee4115/msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b", size = 231970 }, - { url = "https://files.pythonhosted.org/packages/a2/90/2d769e693654f036acfb462b54dacb3ae345699999897ca34f6bd9534fe9/msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6", size = 522440 }, - { url = "https://files.pythonhosted.org/packages/46/95/d0440400485eab1bf50f1efe5118967b539f3191d994c3dfc220657594cd/msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee", size = 530797 }, - { url = "https://files.pythonhosted.org/packages/76/33/35df717bc095c6e938b3c65ed117b95048abc24d1614427685123fb2f0af/msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d", size = 520372 }, - { url = "https://files.pythonhosted.org/packages/af/d1/abbdd58a43827fbec5d98427a7a535c620890289b9d927154465313d6967/msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d", size = 527287 }, - { url = "https://files.pythonhosted.org/packages/0c/ac/66625b05091b97ca2c7418eb2d2af152f033d969519f9315556a4ed800fe/msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1", size = 560715 }, - { url = "https://files.pythonhosted.org/packages/de/4e/a0e8611f94bac32d2c1c4ad05bb1c0ae61132e3398e0b44a93e6d7830968/msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681", size = 532614 }, - { url = "https://files.pythonhosted.org/packages/9b/07/0b3f089684ca330602b2994248eda2898a7232e4b63882b9271164ef672e/msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9", size = 216340 }, - { url = "https://files.pythonhosted.org/packages/4b/14/c62fbc8dff118f1558e43b9469d56a1f37bbb35febadc3163efaedd01500/msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415", size = 222828 }, - { url = "https://files.pythonhosted.org/packages/f9/b3/309de40dc7406b7f3492332c5ee2b492a593c2a9bb97ea48ebf2f5279999/msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84", size = 305096 }, - { url = "https://files.pythonhosted.org/packages/15/56/a677cd761a2cefb2e3ffe7e684633294dccb161d78e8ea6da9277e45b4a2/msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93", size = 235210 }, - { url = "https://files.pythonhosted.org/packages/f5/4e/1ab4a982cbd90f988e49f849fc1212f2c04a59870c59daabf8950617e2aa/msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8", size = 231952 }, - { url = "https://files.pythonhosted.org/packages/6d/74/bd02044eb628c7361ad2bd8c1a6147af5c6c2bbceb77b3b1da20f4a8a9c5/msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46", size = 549511 }, - { url = "https://files.pythonhosted.org/packages/df/09/dee50913ba5cc047f7fd7162f09453a676e7935c84b3bf3a398e12108677/msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b", size = 557980 }, - { url = "https://files.pythonhosted.org/packages/26/a5/78a7d87f5f8ffe4c32167afa15d4957db649bab4822f909d8d765339bbab/msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e", size = 545547 }, - { url = "https://files.pythonhosted.org/packages/d4/53/698c10913947f97f6fe7faad86a34e6aa1b66cea2df6f99105856bd346d9/msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002", size = 554669 }, - { url = "https://files.pythonhosted.org/packages/f5/3f/9730c6cb574b15d349b80cd8523a7df4b82058528339f952ea1c32ac8a10/msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c", size = 583353 }, - { url = "https://files.pythonhosted.org/packages/4c/bc/dc184d943692671149848438fb3bed3a3de288ce7998cb91bc98f40f201b/msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e", size = 557455 }, - { url = "https://files.pythonhosted.org/packages/cf/7b/1bc69d4a56c8d2f4f2dfbe4722d40344af9a85b6fb3b09cfb350ba6a42f6/msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1", size = 216367 }, - { url = "https://files.pythonhosted.org/packages/b4/3d/c8dd23050eefa3d9b9c5b8329ed3308c2f2f80f65825e9ea4b7fa621cdab/msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82", size = 222860 }, - { url = "https://files.pythonhosted.org/packages/d7/47/20dff6b4512cf3575550c8801bc53fe7d540f4efef9c5c37af51760fcdcf/msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b", size = 305759 }, - { url = "https://files.pythonhosted.org/packages/6f/8a/34f1726d2c9feccec3d946776e9bce8f20ae09d8b91899fc20b296c942af/msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4", size = 235330 }, - { url = "https://files.pythonhosted.org/packages/9c/f6/e64c72577d6953789c3cb051b059a4b56317056b3c65013952338ed8a34e/msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee", size = 232537 }, - { url = "https://files.pythonhosted.org/packages/89/75/1ed3a96e12941873fd957e016cc40c0c178861a872bd45e75b9a188eb422/msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5", size = 546561 }, - { url = "https://files.pythonhosted.org/packages/e5/0a/c6a1390f9c6a31da0fecbbfdb86b1cb39ad302d9e24f9cca3d9e14c364f0/msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672", size = 559009 }, - { url = "https://files.pythonhosted.org/packages/a5/74/99f6077754665613ea1f37b3d91c10129f6976b7721ab4d0973023808e5a/msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075", size = 543882 }, - { url = "https://files.pythonhosted.org/packages/9c/7e/dc0dc8de2bf27743b31691149258f9b1bd4bf3c44c105df3df9b97081cd1/msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba", size = 546949 }, - { url = "https://files.pythonhosted.org/packages/78/61/91bae9474def032f6c333d62889bbeda9e1554c6b123375ceeb1767efd78/msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c", size = 579836 }, - { url = "https://files.pythonhosted.org/packages/5d/4d/d98592099d4f18945f89cf3e634dc0cb128bb33b1b93f85a84173d35e181/msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5", size = 556587 }, - { url = "https://files.pythonhosted.org/packages/5e/44/6556ffe169bf2c0e974e2ea25fb82a7e55ebcf52a81b03a5e01820de5f84/msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9", size = 216509 }, - { url = "https://files.pythonhosted.org/packages/dc/c1/63903f30d51d165e132e5221a2a4a1bbfab7508b68131c871d70bffac78a/msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf", size = 223287 }, + { url = "https://files.pythonhosted.org/packages/41/3a/2e2e902afcd751738e38d88af976fc4010b16e8e821945f4cbf32f75f9c3/msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862", size = 304827, upload_time = "2023-09-28T13:18:30.258Z" }, + { url = "https://files.pythonhosted.org/packages/86/a6/490792a524a82e855bdf3885ecb73d7b3a0b17744b3cf4a40aea13ceca38/msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329", size = 234959, upload_time = "2023-09-28T13:18:32.146Z" }, + { url = "https://files.pythonhosted.org/packages/ad/72/d39ed43bfb2ec6968d768318477adb90c474bdc59b2437170c6697ee4115/msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b", size = 231970, upload_time = "2023-09-28T13:18:34.134Z" }, + { url = "https://files.pythonhosted.org/packages/a2/90/2d769e693654f036acfb462b54dacb3ae345699999897ca34f6bd9534fe9/msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6", size = 522440, upload_time = "2023-09-28T13:18:35.866Z" }, + { url = "https://files.pythonhosted.org/packages/46/95/d0440400485eab1bf50f1efe5118967b539f3191d994c3dfc220657594cd/msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee", size = 530797, upload_time = "2023-09-28T13:18:37.653Z" }, + { url = "https://files.pythonhosted.org/packages/76/33/35df717bc095c6e938b3c65ed117b95048abc24d1614427685123fb2f0af/msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d", size = 520372, upload_time = "2023-09-28T13:18:39.685Z" }, + { url = "https://files.pythonhosted.org/packages/af/d1/abbdd58a43827fbec5d98427a7a535c620890289b9d927154465313d6967/msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d", size = 527287, upload_time = "2023-09-28T13:18:41.051Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ac/66625b05091b97ca2c7418eb2d2af152f033d969519f9315556a4ed800fe/msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1", size = 560715, upload_time = "2023-09-28T13:18:42.883Z" }, + { url = "https://files.pythonhosted.org/packages/de/4e/a0e8611f94bac32d2c1c4ad05bb1c0ae61132e3398e0b44a93e6d7830968/msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681", size = 532614, upload_time = "2023-09-28T13:18:44.679Z" }, + { url = "https://files.pythonhosted.org/packages/9b/07/0b3f089684ca330602b2994248eda2898a7232e4b63882b9271164ef672e/msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9", size = 216340, upload_time = "2023-09-28T13:18:46.588Z" }, + { url = "https://files.pythonhosted.org/packages/4b/14/c62fbc8dff118f1558e43b9469d56a1f37bbb35febadc3163efaedd01500/msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415", size = 222828, upload_time = "2023-09-28T13:18:47.875Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b3/309de40dc7406b7f3492332c5ee2b492a593c2a9bb97ea48ebf2f5279999/msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84", size = 305096, upload_time = "2023-09-28T13:18:49.678Z" }, + { url = "https://files.pythonhosted.org/packages/15/56/a677cd761a2cefb2e3ffe7e684633294dccb161d78e8ea6da9277e45b4a2/msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93", size = 235210, upload_time = "2023-09-28T13:18:51.039Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4e/1ab4a982cbd90f988e49f849fc1212f2c04a59870c59daabf8950617e2aa/msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8", size = 231952, upload_time = "2023-09-28T13:18:52.871Z" }, + { url = "https://files.pythonhosted.org/packages/6d/74/bd02044eb628c7361ad2bd8c1a6147af5c6c2bbceb77b3b1da20f4a8a9c5/msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46", size = 549511, upload_time = "2023-09-28T13:18:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/df/09/dee50913ba5cc047f7fd7162f09453a676e7935c84b3bf3a398e12108677/msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b", size = 557980, upload_time = "2023-09-28T13:18:56.058Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/78a7d87f5f8ffe4c32167afa15d4957db649bab4822f909d8d765339bbab/msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e", size = 545547, upload_time = "2023-09-28T13:18:57.396Z" }, + { url = "https://files.pythonhosted.org/packages/d4/53/698c10913947f97f6fe7faad86a34e6aa1b66cea2df6f99105856bd346d9/msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002", size = 554669, upload_time = "2023-09-28T13:18:58.957Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3f/9730c6cb574b15d349b80cd8523a7df4b82058528339f952ea1c32ac8a10/msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c", size = 583353, upload_time = "2023-09-28T13:19:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/4c/bc/dc184d943692671149848438fb3bed3a3de288ce7998cb91bc98f40f201b/msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e", size = 557455, upload_time = "2023-09-28T13:19:03.201Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7b/1bc69d4a56c8d2f4f2dfbe4722d40344af9a85b6fb3b09cfb350ba6a42f6/msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1", size = 216367, upload_time = "2023-09-28T13:19:04.554Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/c8dd23050eefa3d9b9c5b8329ed3308c2f2f80f65825e9ea4b7fa621cdab/msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82", size = 222860, upload_time = "2023-09-28T13:19:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/20dff6b4512cf3575550c8801bc53fe7d540f4efef9c5c37af51760fcdcf/msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b", size = 305759, upload_time = "2023-09-28T13:19:08.148Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8a/34f1726d2c9feccec3d946776e9bce8f20ae09d8b91899fc20b296c942af/msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4", size = 235330, upload_time = "2023-09-28T13:19:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f6/e64c72577d6953789c3cb051b059a4b56317056b3c65013952338ed8a34e/msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee", size = 232537, upload_time = "2023-09-28T13:19:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/89/75/1ed3a96e12941873fd957e016cc40c0c178861a872bd45e75b9a188eb422/msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5", size = 546561, upload_time = "2023-09-28T13:19:12.779Z" }, + { url = "https://files.pythonhosted.org/packages/e5/0a/c6a1390f9c6a31da0fecbbfdb86b1cb39ad302d9e24f9cca3d9e14c364f0/msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672", size = 559009, upload_time = "2023-09-28T13:19:14.373Z" }, + { url = "https://files.pythonhosted.org/packages/a5/74/99f6077754665613ea1f37b3d91c10129f6976b7721ab4d0973023808e5a/msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075", size = 543882, upload_time = "2023-09-28T13:19:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7e/dc0dc8de2bf27743b31691149258f9b1bd4bf3c44c105df3df9b97081cd1/msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba", size = 546949, upload_time = "2023-09-28T13:19:18.114Z" }, + { url = "https://files.pythonhosted.org/packages/78/61/91bae9474def032f6c333d62889bbeda9e1554c6b123375ceeb1767efd78/msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c", size = 579836, upload_time = "2023-09-28T13:19:19.729Z" }, + { url = "https://files.pythonhosted.org/packages/5d/4d/d98592099d4f18945f89cf3e634dc0cb128bb33b1b93f85a84173d35e181/msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5", size = 556587, upload_time = "2023-09-28T13:19:21.666Z" }, + { url = "https://files.pythonhosted.org/packages/5e/44/6556ffe169bf2c0e974e2ea25fb82a7e55ebcf52a81b03a5e01820de5f84/msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9", size = 216509, upload_time = "2023-09-28T13:19:23.161Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c1/63903f30d51d165e132e5221a2a4a1bbfab7508b68131c871d70bffac78a/msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf", size = 223287, upload_time = "2023-09-28T13:19:25.097Z" }, ] [[package]] @@ -1379,83 +1379,83 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } +sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload_time = "2025-02-05T03:50:34.655Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433 }, - { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472 }, - { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424 }, - { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450 }, - { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765 }, - { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701 }, - { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 }, - { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 }, - { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 }, - { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 }, - { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 }, - { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 }, - { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, - { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, - { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, - { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, - { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, - { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, - { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, - { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, - { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, - { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, - { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, - { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, - { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, + { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433, upload_time = "2025-02-05T03:49:29.145Z" }, + { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472, upload_time = "2025-02-05T03:49:16.986Z" }, + { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424, upload_time = "2025-02-05T03:49:46.908Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450, upload_time = "2025-02-05T03:50:05.89Z" }, + { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765, upload_time = "2025-02-05T03:49:33.56Z" }, + { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701, upload_time = "2025-02-05T03:49:38.981Z" }, + { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338, upload_time = "2025-02-05T03:50:17.287Z" }, + { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540, upload_time = "2025-02-05T03:49:51.21Z" }, + { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051, upload_time = "2025-02-05T03:50:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751, upload_time = "2025-02-05T03:49:42.408Z" }, + { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783, upload_time = "2025-02-05T03:49:07.707Z" }, + { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618, upload_time = "2025-02-05T03:49:54.581Z" }, + { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981, upload_time = "2025-02-05T03:50:28.25Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175, upload_time = "2025-02-05T03:50:13.411Z" }, + { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675, upload_time = "2025-02-05T03:50:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020, upload_time = "2025-02-05T03:48:48.705Z" }, + { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582, upload_time = "2025-02-05T03:49:03.628Z" }, + { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614, upload_time = "2025-02-05T03:50:00.313Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592, upload_time = "2025-02-05T03:48:55.789Z" }, + { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611, upload_time = "2025-02-05T03:48:44.581Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443, upload_time = "2025-02-05T03:49:25.514Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541, upload_time = "2025-02-05T03:49:57.623Z" }, + { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348, upload_time = "2025-02-05T03:48:52.361Z" }, + { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648, upload_time = "2025-02-05T03:49:11.395Z" }, + { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload_time = "2025-02-05T03:50:08.348Z" }, ] [[package]] name = "mypy-extensions" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload_time = "2023-02-04T12:11:27.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload_time = "2023-02-04T12:11:25.002Z" }, ] [[package]] name = "networkx" version = "3.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c4/80/a84676339aaae2f1cfdf9f418701dd634aef9cc76f708ef55c36ff39c3ca/networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6", size = 2073928 } +sdist = { url = "https://files.pythonhosted.org/packages/c4/80/a84676339aaae2f1cfdf9f418701dd634aef9cc76f708ef55c36ff39c3ca/networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6", size = 2073928, upload_time = "2023-10-28T08:41:39.364Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/f0/8fbc882ca80cf077f1b246c0e3c3465f7f415439bdea6b899f6b19f61f70/networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2", size = 1647772 }, + { url = "https://files.pythonhosted.org/packages/d5/f0/8fbc882ca80cf077f1b246c0e3c3465f7f415439bdea6b899f6b19f61f70/networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2", size = 1647772, upload_time = "2023-10-28T08:41:36.945Z" }, ] [[package]] name = "numpy" version = "1.26.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129 } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload_time = "2024-02-06T00:26:44.495Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468 }, - { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411 }, - { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016 }, - { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889 }, - { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746 }, - { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620 }, - { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659 }, - { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905 }, - { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554 }, - { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127 }, - { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994 }, - { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005 }, - { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297 }, - { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567 }, - { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812 }, - { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913 }, - { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901 }, - { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868 }, - { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109 }, - { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613 }, - { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172 }, - { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643 }, - { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803 }, - { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754 }, + { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload_time = "2024-02-05T23:48:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload_time = "2024-02-05T23:48:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload_time = "2024-02-05T23:48:54.098Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload_time = "2024-02-05T23:49:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload_time = "2024-02-05T23:49:51.983Z" }, + { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload_time = "2024-02-05T23:50:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload_time = "2024-02-05T23:50:35.834Z" }, + { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload_time = "2024-02-05T23:51:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload_time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload_time = "2024-02-05T23:52:15.314Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload_time = "2024-02-05T23:52:47.569Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload_time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload_time = "2024-02-05T23:53:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload_time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload_time = "2024-02-05T23:54:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload_time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload_time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload_time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload_time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload_time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload_time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload_time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload_time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload_time = "2024-02-05T23:58:36.364Z" }, ] [[package]] @@ -1466,26 +1466,26 @@ dependencies = [ { name = "numpy" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fe/0978403c8d710ece2f34006367e78de80410743fe0e7680c8f33f2dab20d/onnx-1.16.0.tar.gz", hash = "sha256:237c6987c6c59d9f44b6136f5819af79574f8d96a760a1fa843bede11f3822f7", size = 12303017 } +sdist = { url = "https://files.pythonhosted.org/packages/b3/fe/0978403c8d710ece2f34006367e78de80410743fe0e7680c8f33f2dab20d/onnx-1.16.0.tar.gz", hash = "sha256:237c6987c6c59d9f44b6136f5819af79574f8d96a760a1fa843bede11f3822f7", size = 12303017, upload_time = "2024-03-25T15:33:46.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/0b/f4705e4a3fa6fd0de971302fdae17ad176b024eca8c24360f0e37c00f9df/onnx-1.16.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:9eadbdce25b19d6216f426d6d99b8bc877a65ed92cbef9707751c6669190ba4f", size = 16514483 }, - { url = "https://files.pythonhosted.org/packages/b8/1c/50310a559857951fc6e069cf5d89deebe34287997d1c5928bca435456f62/onnx-1.16.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:034ae21a2aaa2e9c14119a840d2926d213c27aad29e5e3edaa30145a745048e1", size = 15012939 }, - { url = "https://files.pythonhosted.org/packages/ef/6e/96be6692ebcd8da568084d753f386ce08efa1f99b216f346ee281edd6cc3/onnx-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec22a43d74eb1f2303373e2fbe7fbcaa45fb225f4eb146edfed1356ada7a9aea", size = 15791856 }, - { url = "https://files.pythonhosted.org/packages/49/5f/d8e1a24247f506a77cbe22341c72ca91bea3b468c5d6bca2047d885ea3c6/onnx-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:298f28a2b5ac09145fa958513d3d1e6b349ccf86a877dbdcccad57713fe360b3", size = 15922279 }, - { url = "https://files.pythonhosted.org/packages/cb/14/562e4ac22cdf41f4465e3b114ef1a9467d513eeff0b9c2285c2da5db6ed1/onnx-1.16.0-cp310-cp310-win32.whl", hash = "sha256:66300197b52beca08bc6262d43c103289c5d45fde43fb51922ed1eb83658cf0c", size = 14335703 }, - { url = "https://files.pythonhosted.org/packages/3b/e2/471ff83b3862967791d67f630000afce038756afbdf0665a3d767677c851/onnx-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:ae0029f5e47bf70a1a62e7f88c80bca4ef39b844a89910039184221775df5e43", size = 14435099 }, - { url = "https://files.pythonhosted.org/packages/a4/b8/7accf3f93eee498711f0b7f07f6e93906e031622473e85ce9cd3578f6a92/onnx-1.16.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:f51179d4af3372b4f3800c558d204b592c61e4b4a18b8f61e0eea7f46211221a", size = 16514376 }, - { url = "https://files.pythonhosted.org/packages/cc/24/a328236b594d5fea23f70a3a8139e730cb43334f0b24693831c47c9064f0/onnx-1.16.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:5202559070afec5144332db216c20f2fff8323cf7f6512b0ca11b215eacc5bf3", size = 15012839 }, - { url = "https://files.pythonhosted.org/packages/80/12/57187bab3f830a47fa65eafe4fbaef01dfdf5042cf82a41fa440fab68766/onnx-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77579e7c15b4df39d29465b216639a5f9b74026bdd9e4b6306cd19a32dcfe67c", size = 15791944 }, - { url = "https://files.pythonhosted.org/packages/df/48/63f68b65d041aedffab41eea930563ca52aab70dbaa7d4820501618c1a70/onnx-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e60ca76ac24b65c25860d0f2d2cdd96d6320d062a01dd8ce87c5743603789b8", size = 15922450 }, - { url = "https://files.pythonhosted.org/packages/08/1b/4bdf4534f5ff08973725ba5409f95bbf64e2789cd20be615880dae689973/onnx-1.16.0-cp311-cp311-win32.whl", hash = "sha256:81b4ee01bc554e8a2b11ac6439882508a5377a1c6b452acd69a1eebb83571117", size = 14335808 }, - { url = "https://files.pythonhosted.org/packages/aa/d0/0514d02d2e84e7bb48a105877eae4065e54d7dabb60d0b60214fe2677346/onnx-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:7449241e70b847b9c3eb8dae622df8c1b456d11032a9d7e26e0ee8a698d5bf86", size = 14434905 }, - { url = "https://files.pythonhosted.org/packages/42/87/577adadda30ee08041e81ef02a331ca9d1a8df93a2e4c4c53ec56fbbc2ac/onnx-1.16.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:03a627488b1a9975d95d6a55582af3e14c7f3bb87444725b999935ddd271d352", size = 16516304 }, - { url = "https://files.pythonhosted.org/packages/e3/1b/6e1ea37e081cc49a28f0e4d3830b4c8525081354cf9f5529c6c92268fc77/onnx-1.16.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c392faeabd9283ee344ccb4b067d1fea9dfc614fa1f0de7c47589efd79e15e78", size = 15016538 }, - { url = "https://files.pythonhosted.org/packages/6d/07/f8fefd5eb0984be42ef677f0b7db7527edc4529224a34a3c31f7b12ec80d/onnx-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0efeb46985de08f0efe758cb54ad3457e821a05c2eaf5ba2ccb8cd1602c08084", size = 15790415 }, - { url = "https://files.pythonhosted.org/packages/11/71/c219ce6d4b5205c77405af7f2de2511ad4eeffbfeb77a422151e893de0ea/onnx-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddf14a3d32234f23e44abb73a755cb96a423fac7f004e8f046f36b10214151ee", size = 15922224 }, - { url = "https://files.pythonhosted.org/packages/8e/a4/554a6e5741b42406c5b1970d04685d7f2012019d4178408ed4b3ec953033/onnx-1.16.0-cp312-cp312-win32.whl", hash = "sha256:62a2e27ae8ba5fc9b4a2620301446a517b5ffaaf8566611de7a7c2160f5bcf4c", size = 14336234 }, - { url = "https://files.pythonhosted.org/packages/e9/a1/8aecec497010ad34e7656408df1868d94483c5c56bc991f4088c06150896/onnx-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:3e0860fea94efde777e81a6f68f65761ed5e5f3adea2e050d7fbe373a9ae05b3", size = 14436591 }, + { url = "https://files.pythonhosted.org/packages/c8/0b/f4705e4a3fa6fd0de971302fdae17ad176b024eca8c24360f0e37c00f9df/onnx-1.16.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:9eadbdce25b19d6216f426d6d99b8bc877a65ed92cbef9707751c6669190ba4f", size = 16514483, upload_time = "2024-03-25T15:25:07.947Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1c/50310a559857951fc6e069cf5d89deebe34287997d1c5928bca435456f62/onnx-1.16.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:034ae21a2aaa2e9c14119a840d2926d213c27aad29e5e3edaa30145a745048e1", size = 15012939, upload_time = "2024-03-25T15:25:11.632Z" }, + { url = "https://files.pythonhosted.org/packages/ef/6e/96be6692ebcd8da568084d753f386ce08efa1f99b216f346ee281edd6cc3/onnx-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec22a43d74eb1f2303373e2fbe7fbcaa45fb225f4eb146edfed1356ada7a9aea", size = 15791856, upload_time = "2024-03-25T15:25:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/49/5f/d8e1a24247f506a77cbe22341c72ca91bea3b468c5d6bca2047d885ea3c6/onnx-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:298f28a2b5ac09145fa958513d3d1e6b349ccf86a877dbdcccad57713fe360b3", size = 15922279, upload_time = "2024-03-25T15:25:18.939Z" }, + { url = "https://files.pythonhosted.org/packages/cb/14/562e4ac22cdf41f4465e3b114ef1a9467d513eeff0b9c2285c2da5db6ed1/onnx-1.16.0-cp310-cp310-win32.whl", hash = "sha256:66300197b52beca08bc6262d43c103289c5d45fde43fb51922ed1eb83658cf0c", size = 14335703, upload_time = "2024-03-25T15:25:22.611Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e2/471ff83b3862967791d67f630000afce038756afbdf0665a3d767677c851/onnx-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:ae0029f5e47bf70a1a62e7f88c80bca4ef39b844a89910039184221775df5e43", size = 14435099, upload_time = "2024-03-25T15:25:25.05Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/7accf3f93eee498711f0b7f07f6e93906e031622473e85ce9cd3578f6a92/onnx-1.16.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:f51179d4af3372b4f3800c558d204b592c61e4b4a18b8f61e0eea7f46211221a", size = 16514376, upload_time = "2024-03-25T15:25:27.899Z" }, + { url = "https://files.pythonhosted.org/packages/cc/24/a328236b594d5fea23f70a3a8139e730cb43334f0b24693831c47c9064f0/onnx-1.16.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:5202559070afec5144332db216c20f2fff8323cf7f6512b0ca11b215eacc5bf3", size = 15012839, upload_time = "2024-03-25T15:25:31.16Z" }, + { url = "https://files.pythonhosted.org/packages/80/12/57187bab3f830a47fa65eafe4fbaef01dfdf5042cf82a41fa440fab68766/onnx-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77579e7c15b4df39d29465b216639a5f9b74026bdd9e4b6306cd19a32dcfe67c", size = 15791944, upload_time = "2024-03-25T15:25:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/df/48/63f68b65d041aedffab41eea930563ca52aab70dbaa7d4820501618c1a70/onnx-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e60ca76ac24b65c25860d0f2d2cdd96d6320d062a01dd8ce87c5743603789b8", size = 15922450, upload_time = "2024-03-25T15:25:37.983Z" }, + { url = "https://files.pythonhosted.org/packages/08/1b/4bdf4534f5ff08973725ba5409f95bbf64e2789cd20be615880dae689973/onnx-1.16.0-cp311-cp311-win32.whl", hash = "sha256:81b4ee01bc554e8a2b11ac6439882508a5377a1c6b452acd69a1eebb83571117", size = 14335808, upload_time = "2024-03-25T15:25:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d0/0514d02d2e84e7bb48a105877eae4065e54d7dabb60d0b60214fe2677346/onnx-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:7449241e70b847b9c3eb8dae622df8c1b456d11032a9d7e26e0ee8a698d5bf86", size = 14434905, upload_time = "2024-03-25T15:25:42.905Z" }, + { url = "https://files.pythonhosted.org/packages/42/87/577adadda30ee08041e81ef02a331ca9d1a8df93a2e4c4c53ec56fbbc2ac/onnx-1.16.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:03a627488b1a9975d95d6a55582af3e14c7f3bb87444725b999935ddd271d352", size = 16516304, upload_time = "2024-03-25T15:25:45.875Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1b/6e1ea37e081cc49a28f0e4d3830b4c8525081354cf9f5529c6c92268fc77/onnx-1.16.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c392faeabd9283ee344ccb4b067d1fea9dfc614fa1f0de7c47589efd79e15e78", size = 15016538, upload_time = "2024-03-25T15:25:49.396Z" }, + { url = "https://files.pythonhosted.org/packages/6d/07/f8fefd5eb0984be42ef677f0b7db7527edc4529224a34a3c31f7b12ec80d/onnx-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0efeb46985de08f0efe758cb54ad3457e821a05c2eaf5ba2ccb8cd1602c08084", size = 15790415, upload_time = "2024-03-25T15:25:51.929Z" }, + { url = "https://files.pythonhosted.org/packages/11/71/c219ce6d4b5205c77405af7f2de2511ad4eeffbfeb77a422151e893de0ea/onnx-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddf14a3d32234f23e44abb73a755cb96a423fac7f004e8f046f36b10214151ee", size = 15922224, upload_time = "2024-03-25T15:25:55.049Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/554a6e5741b42406c5b1970d04685d7f2012019d4178408ed4b3ec953033/onnx-1.16.0-cp312-cp312-win32.whl", hash = "sha256:62a2e27ae8ba5fc9b4a2620301446a517b5ffaaf8566611de7a7c2160f5bcf4c", size = 14336234, upload_time = "2024-03-25T15:25:57.998Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/8aecec497010ad34e7656408df1868d94483c5c56bc991f4088c06150896/onnx-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:3e0860fea94efde777e81a6f68f65761ed5e5f3adea2e050d7fbe373a9ae05b3", size = 14436591, upload_time = "2024-03-25T15:26:01.252Z" }, ] [[package]] @@ -1501,27 +1501,27 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/28/99f903b0eb1cd6f3faa0e343217d9fb9f47b84bca98bd9859884631336ee/onnxruntime-1.20.1-cp310-cp310-macosx_13_0_universal2.whl", hash = "sha256:e50ba5ff7fed4f7d9253a6baf801ca2883cc08491f9d32d78a80da57256a5439", size = 30996314 }, - { url = "https://files.pythonhosted.org/packages/6d/c6/c4c0860bee2fde6037bdd9dcd12d323f6e38cf00fcc9a5065b394337fc55/onnxruntime-1.20.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b2908b50101a19e99c4d4e97ebb9905561daf61829403061c1adc1b588bc0de", size = 11954010 }, - { url = "https://files.pythonhosted.org/packages/63/47/3dc0b075ab539f16b3d8b09df6b504f51836086ee709690a6278d791737d/onnxruntime-1.20.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d82daaec24045a2e87598b8ac2b417b1cce623244e80e663882e9fe1aae86410", size = 13330452 }, - { url = "https://files.pythonhosted.org/packages/27/ef/80fab86289ecc01a734b7ddf115dfb93d8b2e004bd1e1977e12881c72b12/onnxruntime-1.20.1-cp310-cp310-win32.whl", hash = "sha256:4c4b251a725a3b8cf2aab284f7d940c26094ecd9d442f07dd81ab5470e99b83f", size = 9813849 }, - { url = "https://files.pythonhosted.org/packages/a9/e6/33ab10066c9875a29d55e66ae97c3bf91b9b9b987179455d67c32261a49c/onnxruntime-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:d3b616bb53a77a9463707bb313637223380fc327f5064c9a782e8ec69c22e6a2", size = 11329702 }, - { url = "https://files.pythonhosted.org/packages/95/8d/2634e2959b34aa8a0037989f4229e9abcfa484e9c228f99633b3241768a6/onnxruntime-1.20.1-cp311-cp311-macosx_13_0_universal2.whl", hash = "sha256:06bfbf02ca9ab5f28946e0f912a562a5f005301d0c419283dc57b3ed7969bb7b", size = 30998725 }, - { url = "https://files.pythonhosted.org/packages/a5/da/c44bf9bd66cd6d9018a921f053f28d819445c4d84b4dd4777271b0fe52a2/onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6243e34d74423bdd1edf0ae9596dd61023b260f546ee17d701723915f06a9f7", size = 11955227 }, - { url = "https://files.pythonhosted.org/packages/11/ac/4120dfb74c8e45cce1c664fc7f7ce010edd587ba67ac41489f7432eb9381/onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5eec64c0269dcdb8d9a9a53dc4d64f87b9e0c19801d9321246a53b7eb5a7d1bc", size = 13331703 }, - { url = "https://files.pythonhosted.org/packages/12/f1/cefacac137f7bb7bfba57c50c478150fcd3c54aca72762ac2c05ce0532c1/onnxruntime-1.20.1-cp311-cp311-win32.whl", hash = "sha256:a19bc6e8c70e2485a1725b3d517a2319603acc14c1f1a017dda0afe6d4665b41", size = 9813977 }, - { url = "https://files.pythonhosted.org/packages/2c/2d/2d4d202c0bcfb3a4cc2b171abb9328672d7f91d7af9ea52572722c6d8d96/onnxruntime-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:8508887eb1c5f9537a4071768723ec7c30c28eb2518a00d0adcd32c89dea3221", size = 11329895 }, - { url = "https://files.pythonhosted.org/packages/e5/39/9335e0874f68f7d27103cbffc0e235e32e26759202df6085716375c078bb/onnxruntime-1.20.1-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:22b0655e2bf4f2161d52706e31f517a0e54939dc393e92577df51808a7edc8c9", size = 31007580 }, - { url = "https://files.pythonhosted.org/packages/c5/9d/a42a84e10f1744dd27c6f2f9280cc3fb98f869dd19b7cd042e391ee2ab61/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f56e898815963d6dc4ee1c35fc6c36506466eff6d16f3cb9848cea4e8c8172", size = 11952833 }, - { url = "https://files.pythonhosted.org/packages/47/42/2f71f5680834688a9c81becbe5c5bb996fd33eaed5c66ae0606c3b1d6a02/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb71a814f66517a65628c9e4a2bb530a6edd2cd5d87ffa0af0f6f773a027d99e", size = 13333903 }, - { url = "https://files.pythonhosted.org/packages/c8/f1/aabfdf91d013320aa2fc46cf43c88ca0182860ff15df872b4552254a9680/onnxruntime-1.20.1-cp312-cp312-win32.whl", hash = "sha256:bd386cc9ee5f686ee8a75ba74037750aca55183085bf1941da8efcfe12d5b120", size = 9814562 }, - { url = "https://files.pythonhosted.org/packages/dd/80/76979e0b744307d488c79e41051117634b956612cc731f1028eb17ee7294/onnxruntime-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:19c2d843eb074f385e8bbb753a40df780511061a63f9def1b216bf53860223fb", size = 11331482 }, - { url = "https://files.pythonhosted.org/packages/f7/71/c5d980ac4189589267a06f758bd6c5667d07e55656bed6c6c0580733ad07/onnxruntime-1.20.1-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:cc01437a32d0042b606f462245c8bbae269e5442797f6213e36ce61d5abdd8cc", size = 31007574 }, - { url = "https://files.pythonhosted.org/packages/81/0d/13bbd9489be2a6944f4a940084bfe388f1100472f38c07080a46fbd4ab96/onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb44b08e017a648924dbe91b82d89b0c105b1adcfe31e90d1dc06b8677ad37be", size = 11951459 }, - { url = "https://files.pythonhosted.org/packages/c0/ea/4454ae122874fd52bbb8a961262de81c5f932edeb1b72217f594c700d6ef/onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda6aebdf7917c1d811f21d41633df00c58aff2bef2f598f69289c1f1dabc4b3", size = 13331620 }, - { url = "https://files.pythonhosted.org/packages/d8/e0/50db43188ca1c945decaa8fc2a024c33446d31afed40149897d4f9de505f/onnxruntime-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:d30367df7e70f1d9fc5a6a68106f5961686d39b54d3221f760085524e8d38e16", size = 11331758 }, - { url = "https://files.pythonhosted.org/packages/d8/55/3821c5fd60b52a6c82a00bba18531793c93c4addfe64fbf061e235c5617a/onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9158465745423b2b5d97ed25aa7740c7d38d2993ee2e5c3bfacb0c4145c49d8", size = 11950342 }, - { url = "https://files.pythonhosted.org/packages/14/56/fd990ca222cef4f9f4a9400567b9a15b220dee2eafffb16b2adbc55c8281/onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0df6f2df83d61f46e842dbcde610ede27218947c33e994545a22333491e72a3b", size = 13337040 }, + { url = "https://files.pythonhosted.org/packages/4e/28/99f903b0eb1cd6f3faa0e343217d9fb9f47b84bca98bd9859884631336ee/onnxruntime-1.20.1-cp310-cp310-macosx_13_0_universal2.whl", hash = "sha256:e50ba5ff7fed4f7d9253a6baf801ca2883cc08491f9d32d78a80da57256a5439", size = 30996314, upload_time = "2024-11-21T00:48:31.43Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c6/c4c0860bee2fde6037bdd9dcd12d323f6e38cf00fcc9a5065b394337fc55/onnxruntime-1.20.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b2908b50101a19e99c4d4e97ebb9905561daf61829403061c1adc1b588bc0de", size = 11954010, upload_time = "2024-11-21T00:48:35.254Z" }, + { url = "https://files.pythonhosted.org/packages/63/47/3dc0b075ab539f16b3d8b09df6b504f51836086ee709690a6278d791737d/onnxruntime-1.20.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d82daaec24045a2e87598b8ac2b417b1cce623244e80e663882e9fe1aae86410", size = 13330452, upload_time = "2024-11-21T00:48:40.02Z" }, + { url = "https://files.pythonhosted.org/packages/27/ef/80fab86289ecc01a734b7ddf115dfb93d8b2e004bd1e1977e12881c72b12/onnxruntime-1.20.1-cp310-cp310-win32.whl", hash = "sha256:4c4b251a725a3b8cf2aab284f7d940c26094ecd9d442f07dd81ab5470e99b83f", size = 9813849, upload_time = "2024-11-21T00:48:43.569Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e6/33ab10066c9875a29d55e66ae97c3bf91b9b9b987179455d67c32261a49c/onnxruntime-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:d3b616bb53a77a9463707bb313637223380fc327f5064c9a782e8ec69c22e6a2", size = 11329702, upload_time = "2024-11-21T00:48:46.599Z" }, + { url = "https://files.pythonhosted.org/packages/95/8d/2634e2959b34aa8a0037989f4229e9abcfa484e9c228f99633b3241768a6/onnxruntime-1.20.1-cp311-cp311-macosx_13_0_universal2.whl", hash = "sha256:06bfbf02ca9ab5f28946e0f912a562a5f005301d0c419283dc57b3ed7969bb7b", size = 30998725, upload_time = "2024-11-21T00:48:51.013Z" }, + { url = "https://files.pythonhosted.org/packages/a5/da/c44bf9bd66cd6d9018a921f053f28d819445c4d84b4dd4777271b0fe52a2/onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6243e34d74423bdd1edf0ae9596dd61023b260f546ee17d701723915f06a9f7", size = 11955227, upload_time = "2024-11-21T00:48:54.556Z" }, + { url = "https://files.pythonhosted.org/packages/11/ac/4120dfb74c8e45cce1c664fc7f7ce010edd587ba67ac41489f7432eb9381/onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5eec64c0269dcdb8d9a9a53dc4d64f87b9e0c19801d9321246a53b7eb5a7d1bc", size = 13331703, upload_time = "2024-11-21T00:48:57.97Z" }, + { url = "https://files.pythonhosted.org/packages/12/f1/cefacac137f7bb7bfba57c50c478150fcd3c54aca72762ac2c05ce0532c1/onnxruntime-1.20.1-cp311-cp311-win32.whl", hash = "sha256:a19bc6e8c70e2485a1725b3d517a2319603acc14c1f1a017dda0afe6d4665b41", size = 9813977, upload_time = "2024-11-21T00:49:00.519Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2d/2d4d202c0bcfb3a4cc2b171abb9328672d7f91d7af9ea52572722c6d8d96/onnxruntime-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:8508887eb1c5f9537a4071768723ec7c30c28eb2518a00d0adcd32c89dea3221", size = 11329895, upload_time = "2024-11-21T00:49:03.845Z" }, + { url = "https://files.pythonhosted.org/packages/e5/39/9335e0874f68f7d27103cbffc0e235e32e26759202df6085716375c078bb/onnxruntime-1.20.1-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:22b0655e2bf4f2161d52706e31f517a0e54939dc393e92577df51808a7edc8c9", size = 31007580, upload_time = "2024-11-21T00:49:07.029Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9d/a42a84e10f1744dd27c6f2f9280cc3fb98f869dd19b7cd042e391ee2ab61/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f56e898815963d6dc4ee1c35fc6c36506466eff6d16f3cb9848cea4e8c8172", size = 11952833, upload_time = "2024-11-21T00:49:10.563Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/2f71f5680834688a9c81becbe5c5bb996fd33eaed5c66ae0606c3b1d6a02/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb71a814f66517a65628c9e4a2bb530a6edd2cd5d87ffa0af0f6f773a027d99e", size = 13333903, upload_time = "2024-11-21T00:49:12.984Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f1/aabfdf91d013320aa2fc46cf43c88ca0182860ff15df872b4552254a9680/onnxruntime-1.20.1-cp312-cp312-win32.whl", hash = "sha256:bd386cc9ee5f686ee8a75ba74037750aca55183085bf1941da8efcfe12d5b120", size = 9814562, upload_time = "2024-11-21T00:49:15.453Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/76979e0b744307d488c79e41051117634b956612cc731f1028eb17ee7294/onnxruntime-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:19c2d843eb074f385e8bbb753a40df780511061a63f9def1b216bf53860223fb", size = 11331482, upload_time = "2024-11-21T00:49:19.412Z" }, + { url = "https://files.pythonhosted.org/packages/f7/71/c5d980ac4189589267a06f758bd6c5667d07e55656bed6c6c0580733ad07/onnxruntime-1.20.1-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:cc01437a32d0042b606f462245c8bbae269e5442797f6213e36ce61d5abdd8cc", size = 31007574, upload_time = "2024-11-21T00:49:23.225Z" }, + { url = "https://files.pythonhosted.org/packages/81/0d/13bbd9489be2a6944f4a940084bfe388f1100472f38c07080a46fbd4ab96/onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb44b08e017a648924dbe91b82d89b0c105b1adcfe31e90d1dc06b8677ad37be", size = 11951459, upload_time = "2024-11-21T00:49:26.269Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/4454ae122874fd52bbb8a961262de81c5f932edeb1b72217f594c700d6ef/onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda6aebdf7917c1d811f21d41633df00c58aff2bef2f598f69289c1f1dabc4b3", size = 13331620, upload_time = "2024-11-21T00:49:28.875Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e0/50db43188ca1c945decaa8fc2a024c33446d31afed40149897d4f9de505f/onnxruntime-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:d30367df7e70f1d9fc5a6a68106f5961686d39b54d3221f760085524e8d38e16", size = 11331758, upload_time = "2024-11-21T00:49:31.417Z" }, + { url = "https://files.pythonhosted.org/packages/d8/55/3821c5fd60b52a6c82a00bba18531793c93c4addfe64fbf061e235c5617a/onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9158465745423b2b5d97ed25aa7740c7d38d2993ee2e5c3bfacb0c4145c49d8", size = 11950342, upload_time = "2024-11-21T00:49:34.164Z" }, + { url = "https://files.pythonhosted.org/packages/14/56/fd990ca222cef4f9f4a9400567b9a15b220dee2eafffb16b2adbc55c8281/onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0df6f2df83d61f46e842dbcde610ede27218947c33e994545a22333491e72a3b", size = 13337040, upload_time = "2024-11-21T00:49:37.271Z" }, ] [[package]] @@ -1558,10 +1558,10 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/57/e9a080f2477b2a4c16925f766e4615fc545098b0f4e20cf8ad803e7a9672/onnxruntime_openvino-1.18.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:565b874d21bcd48126da7d62f57db019f5ec0e1f82ae9b0740afa2ad91f8d331", size = 41971800 }, - { url = "https://files.pythonhosted.org/packages/34/7d/b75913bce58f4ee9bf6a02d1b513b9fc82303a496ec698e6fb1f9d597cb4/onnxruntime_openvino-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:7f1931060f710a6c8e32121bb73044c4772ef5925802fc8776d3fe1e87ab3f75", size = 5963263 }, - { url = "https://files.pythonhosted.org/packages/7e/d3/8299b7285dc8fa7bd986b6f0d7c50b7f0fd13db50dd3b88b93ec269b1e08/onnxruntime_openvino-1.18.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb1723d386f70a8e26398d983ebe35d2c25ba56e9cdb382670ebbf1f5139f8ba", size = 41971927 }, - { url = "https://files.pythonhosted.org/packages/88/d9/ca0bfd7ed37153d9664ccdcfb4d0e5b1963563553b05cb4338b46968feb2/onnxruntime_openvino-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:874a1e263dd86674593e5a879257650b06a8609c4d5768c3d8ed8dc4ae874b9c", size = 5963464 }, + { url = "https://files.pythonhosted.org/packages/b3/57/e9a080f2477b2a4c16925f766e4615fc545098b0f4e20cf8ad803e7a9672/onnxruntime_openvino-1.18.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:565b874d21bcd48126da7d62f57db019f5ec0e1f82ae9b0740afa2ad91f8d331", size = 41971800, upload_time = "2024-06-25T06:30:37.042Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/b75913bce58f4ee9bf6a02d1b513b9fc82303a496ec698e6fb1f9d597cb4/onnxruntime_openvino-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:7f1931060f710a6c8e32121bb73044c4772ef5925802fc8776d3fe1e87ab3f75", size = 5963263, upload_time = "2024-06-24T13:38:15.906Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d3/8299b7285dc8fa7bd986b6f0d7c50b7f0fd13db50dd3b88b93ec269b1e08/onnxruntime_openvino-1.18.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb1723d386f70a8e26398d983ebe35d2c25ba56e9cdb382670ebbf1f5139f8ba", size = 41971927, upload_time = "2024-06-25T06:30:43.765Z" }, + { url = "https://files.pythonhosted.org/packages/88/d9/ca0bfd7ed37153d9664ccdcfb4d0e5b1963563553b05cb4338b46968feb2/onnxruntime_openvino-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:874a1e263dd86674593e5a879257650b06a8609c4d5768c3d8ed8dc4ae874b9c", size = 5963464, upload_time = "2024-06-24T13:38:18.437Z" }, ] [[package]] @@ -1571,171 +1571,171 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929 } +sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929, upload_time = "2025-01-16T13:53:40.22Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/53/2c50afa0b1e05ecdb4603818e85f7d174e683d874ef63a6abe3ac92220c8/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca", size = 37326460 }, - { url = "https://files.pythonhosted.org/packages/3b/43/68555327df94bb9b59a1fd645f63fafb0762515344d2046698762fc19d58/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81", size = 56723330 }, - { url = "https://files.pythonhosted.org/packages/45/be/1438ce43ebe65317344a87e4b150865c5585f4c0db880a34cdae5ac46881/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb", size = 29487060 }, - { url = "https://files.pythonhosted.org/packages/dd/5c/c139a7876099916879609372bfa513b7f1257f7f1a908b0bdc1c2328241b/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b", size = 49969856 }, - { url = "https://files.pythonhosted.org/packages/95/dd/ed1191c9dc91abcc9f752b499b7928aacabf10567bb2c2535944d848af18/opencv_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b", size = 29324425 }, - { url = "https://files.pythonhosted.org/packages/86/8a/69176a64335aed183529207ba8bc3d329c2999d852b4f3818027203f50e6/opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca", size = 39402386 }, + { url = "https://files.pythonhosted.org/packages/dc/53/2c50afa0b1e05ecdb4603818e85f7d174e683d874ef63a6abe3ac92220c8/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca", size = 37326460, upload_time = "2025-01-16T13:52:57.015Z" }, + { url = "https://files.pythonhosted.org/packages/3b/43/68555327df94bb9b59a1fd645f63fafb0762515344d2046698762fc19d58/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81", size = 56723330, upload_time = "2025-01-16T13:55:45.731Z" }, + { url = "https://files.pythonhosted.org/packages/45/be/1438ce43ebe65317344a87e4b150865c5585f4c0db880a34cdae5ac46881/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb", size = 29487060, upload_time = "2025-01-16T13:51:59.625Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5c/c139a7876099916879609372bfa513b7f1257f7f1a908b0bdc1c2328241b/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b", size = 49969856, upload_time = "2025-01-16T13:53:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/95/dd/ed1191c9dc91abcc9f752b499b7928aacabf10567bb2c2535944d848af18/opencv_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b", size = 29324425, upload_time = "2025-01-16T13:52:49.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/8a/69176a64335aed183529207ba8bc3d329c2999d852b4f3818027203f50e6/opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca", size = 39402386, upload_time = "2025-01-16T13:52:56.418Z" }, ] [[package]] name = "orjson" version = "3.10.16" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/c7/03913cc4332174071950acf5b0735463e3f63760c80585ef369270c2b372/orjson-3.10.16.tar.gz", hash = "sha256:d2aaa5c495e11d17b9b93205f5fa196737ee3202f000aaebf028dc9a73750f10", size = 5410415 } +sdist = { url = "https://files.pythonhosted.org/packages/98/c7/03913cc4332174071950acf5b0735463e3f63760c80585ef369270c2b372/orjson-3.10.16.tar.gz", hash = "sha256:d2aaa5c495e11d17b9b93205f5fa196737ee3202f000aaebf028dc9a73750f10", size = 5410415, upload_time = "2025-03-24T17:00:23.312Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/a6/22cb9b03baf167bc2d659c9e74d7580147f36e6a155e633801badfd5a74d/orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8", size = 249179 }, - { url = "https://files.pythonhosted.org/packages/d7/ce/3e68cc33020a6ebd8f359b8628b69d2132cd84fea68155c33057e502ee51/orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00", size = 138510 }, - { url = "https://files.pythonhosted.org/packages/dc/12/63bee7764ce12052f7c1a1393ce7f26dc392c93081eb8754dd3dce9b7c6b/orjson-3.10.16-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c682d852d0ce77613993dc967e90e151899fe2d8e71c20e9be164080f468e370", size = 132373 }, - { url = "https://files.pythonhosted.org/packages/b3/d5/2998c2f319adcd572f2b03ba2083e8176863d1055d8d713683ddcf927b71/orjson-3.10.16-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c520ae736acd2e32df193bcff73491e64c936f3e44a2916b548da048a48b46b", size = 136774 }, - { url = "https://files.pythonhosted.org/packages/00/03/88c236ae307bd0604623204d4a835e15fbf9c75b8535c8f13ef45abd413f/orjson-3.10.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:134f87c76bfae00f2094d85cfab261b289b76d78c6da8a7a3b3c09d362fd1e06", size = 138030 }, - { url = "https://files.pythonhosted.org/packages/66/ba/3e256ddfeb364f98fd6ac65774844090d356158b2d1de8998db2bf984503/orjson-3.10.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b59afde79563e2cf37cfe62ee3b71c063fd5546c8e662d7fcfc2a3d5031a5c4c", size = 142677 }, - { url = "https://files.pythonhosted.org/packages/2c/71/73a1214bd27baa2ea5184fff4aa6193a114dfb0aa5663dad48fe63e8cd29/orjson-3.10.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113602f8241daaff05d6fad25bd481d54c42d8d72ef4c831bb3ab682a54d9e15", size = 132798 }, - { url = "https://files.pythonhosted.org/packages/53/ac/0b2f41c0a1e8c095439d0fab3b33103cf41a39be8e6aa2c56298a6034259/orjson-3.10.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4fc0077d101f8fab4031e6554fc17b4c2ad8fdbc56ee64a727f3c95b379e31da", size = 135450 }, - { url = "https://files.pythonhosted.org/packages/d9/ca/7524c7b0bc815d426ca134dab54cad519802287b808a3846b047a5b2b7a3/orjson-3.10.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9c6bf6ff180cd69e93f3f50380224218cfab79953a868ea3908430bcfaf9cb5e", size = 412356 }, - { url = "https://files.pythonhosted.org/packages/05/1d/3ae2367c255276bf16ff7e1b210dd0af18bc8da20c4e4295755fc7de1268/orjson-3.10.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5673eadfa952f95a7cd76418ff189df11b0a9c34b1995dff43a6fdbce5d63bf4", size = 152769 }, - { url = "https://files.pythonhosted.org/packages/d3/2d/8eb10b6b1d30bb69c35feb15e5ba5ac82466cf743d562e3e8047540efd2f/orjson-3.10.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5fe638a423d852b0ae1e1a79895851696cb0d9fa0946fdbfd5da5072d9bb9551", size = 137223 }, - { url = "https://files.pythonhosted.org/packages/47/42/f043717930cb2de5fbebe47f308f101bed9ec2b3580b1f99c8284b2f5fe8/orjson-3.10.16-cp310-cp310-win32.whl", hash = "sha256:33af58f479b3c6435ab8f8b57999874b4b40c804c7a36b5cc6b54d8f28e1d3dd", size = 141734 }, - { url = "https://files.pythonhosted.org/packages/67/99/795ad7282b425b9fddcfb8a31bded5dcf84dba78ecb1e7ae716e84e794da/orjson-3.10.16-cp310-cp310-win_amd64.whl", hash = "sha256:0338356b3f56d71293c583350af26f053017071836b07e064e92819ecf1aa055", size = 133779 }, - { url = "https://files.pythonhosted.org/packages/97/29/43f91a5512b5d2535594438eb41c5357865fd5e64dec745d90a588820c75/orjson-3.10.16-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44fcbe1a1884f8bc9e2e863168b0f84230c3d634afe41c678637d2728ea8e739", size = 249180 }, - { url = "https://files.pythonhosted.org/packages/0c/36/2a72d55e266473c19a86d97b7363bb8bf558ab450f75205689a287d5ce61/orjson-3.10.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78177bf0a9d0192e0b34c3d78bcff7fe21d1b5d84aeb5ebdfe0dbe637b885225", size = 138510 }, - { url = "https://files.pythonhosted.org/packages/bb/ad/f86d6f55c1a68b57ff6ea7966bce5f4e5163f2e526ddb7db9fc3c2c8d1c4/orjson-3.10.16-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12824073a010a754bb27330cad21d6e9b98374f497f391b8707752b96f72e741", size = 132373 }, - { url = "https://files.pythonhosted.org/packages/5e/8b/d18f2711493a809f3082a88fda89342bc8e16767743b909cd3c34989fba3/orjson-3.10.16-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddd41007e56284e9867864aa2f29f3136bb1dd19a49ca43c0b4eda22a579cf53", size = 136773 }, - { url = "https://files.pythonhosted.org/packages/a1/dc/ce025f002f8e0749e3f057c4d773a4d4de32b7b4c1fc5a50b429e7532586/orjson-3.10.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0877c4d35de639645de83666458ca1f12560d9fa7aa9b25d8bb8f52f61627d14", size = 138029 }, - { url = "https://files.pythonhosted.org/packages/0e/1b/cf9df85852b91160029d9f26014230366a2b4deb8cc51fabe68e250a8c1a/orjson-3.10.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a09a539e9cc3beead3e7107093b4ac176d015bec64f811afb5965fce077a03c", size = 142677 }, - { url = "https://files.pythonhosted.org/packages/92/18/5b1e1e995bffad49dc4311a0bdfd874bc6f135fd20f0e1f671adc2c9910e/orjson-3.10.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31b98bc9b40610fec971d9a4d67bb2ed02eec0a8ae35f8ccd2086320c28526ca", size = 132800 }, - { url = "https://files.pythonhosted.org/packages/d6/eb/467f25b580e942fcca1344adef40633b7f05ac44a65a63fc913f9a805d58/orjson-3.10.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0ce243f5a8739f3a18830bc62dc2e05b69a7545bafd3e3249f86668b2bcd8e50", size = 135451 }, - { url = "https://files.pythonhosted.org/packages/8d/4b/9d10888038975cb375982e9339d9495bac382d5c976c500b8d6f2c8e2e4e/orjson-3.10.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64792c0025bae049b3074c6abe0cf06f23c8e9f5a445f4bab31dc5ca23dbf9e1", size = 412358 }, - { url = "https://files.pythonhosted.org/packages/3b/e2/cfbcfcc4fbe619e0ca9bdbbfccb2d62b540bbfe41e0ee77d44a628594f59/orjson-3.10.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea53f7e68eec718b8e17e942f7ca56c6bd43562eb19db3f22d90d75e13f0431d", size = 152772 }, - { url = "https://files.pythonhosted.org/packages/b9/d6/627a1b00569be46173007c11dde3da4618c9bfe18409325b0e3e2a82fe29/orjson-3.10.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a741ba1a9488c92227711bde8c8c2b63d7d3816883268c808fbeada00400c164", size = 137225 }, - { url = "https://files.pythonhosted.org/packages/0a/7b/a73c67b505021af845b9f05c7c848793258ea141fa2058b52dd9b067c2b4/orjson-3.10.16-cp311-cp311-win32.whl", hash = "sha256:c7ed2c61bb8226384c3fdf1fb01c51b47b03e3f4536c985078cccc2fd19f1619", size = 141733 }, - { url = "https://files.pythonhosted.org/packages/f4/22/5e8217c48d68c0adbfb181e749d6a733761074e598b083c69a1383d18147/orjson-3.10.16-cp311-cp311-win_amd64.whl", hash = "sha256:cd67d8b3e0e56222a2e7b7f7da9031e30ecd1fe251c023340b9f12caca85ab60", size = 133784 }, - { url = "https://files.pythonhosted.org/packages/5d/15/67ce9d4c959c83f112542222ea3b9209c1d424231d71d74c4890ea0acd2b/orjson-3.10.16-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6d3444abbfa71ba21bb042caa4b062535b122248259fdb9deea567969140abca", size = 249325 }, - { url = "https://files.pythonhosted.org/packages/da/2c/1426b06f30a1b9ada74b6f512c1ddf9d2760f53f61cdb59efeb9ad342133/orjson-3.10.16-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:30245c08d818fdcaa48b7d5b81499b8cae09acabb216fe61ca619876b128e184", size = 133621 }, - { url = "https://files.pythonhosted.org/packages/9e/88/18d26130954bc73bee3be10f95371ea1dfb8679e0e2c46b0f6d8c6289402/orjson-3.10.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0ba1d0baa71bf7579a4ccdcf503e6f3098ef9542106a0eca82395898c8a500a", size = 138270 }, - { url = "https://files.pythonhosted.org/packages/4f/f9/6d8b64fcd58fae072e80ee7981be8ba0d7c26ace954e5cd1d027fc80518f/orjson-3.10.16-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb0beefa5ef3af8845f3a69ff2a4aa62529b5acec1cfe5f8a6b4141033fd46ef", size = 132346 }, - { url = "https://files.pythonhosted.org/packages/16/3f/2513fd5bc786f40cd12af569c23cae6381aeddbefeed2a98f0a666eb5d0d/orjson-3.10.16-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6daa0e1c9bf2e030e93c98394de94506f2a4d12e1e9dadd7c53d5e44d0f9628e", size = 136845 }, - { url = "https://files.pythonhosted.org/packages/6d/42/b0e7b36720f5ab722b48e8ccf06514d4f769358dd73c51abd8728ef58d0b/orjson-3.10.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da9019afb21e02410ef600e56666652b73eb3e4d213a0ec919ff391a7dd52aa", size = 138078 }, - { url = "https://files.pythonhosted.org/packages/a3/a8/d220afb8a439604be74fc755dbc740bded5ed14745ca536b304ed32eb18a/orjson-3.10.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:daeb3a1ee17b69981d3aae30c3b4e786b0f8c9e6c71f2b48f1aef934f63f38f4", size = 142712 }, - { url = "https://files.pythonhosted.org/packages/8c/88/7e41e9883c00f84f92fe357a8371edae816d9d7ef39c67b5106960c20389/orjson-3.10.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fed80eaf0e20a31942ae5d0728849862446512769692474be5e6b73123a23b", size = 133136 }, - { url = "https://files.pythonhosted.org/packages/e9/ca/61116095307ad0be828ea26093febaf59e38596d84a9c8d765c3c5e4934f/orjson-3.10.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73390ed838f03764540a7bdc4071fe0123914c2cc02fb6abf35182d5fd1b7a42", size = 135258 }, - { url = "https://files.pythonhosted.org/packages/dc/1b/09493cf7d801505f094c9295f79c98c1e0af2ac01c7ed8d25b30fcb19ada/orjson-3.10.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a22bba012a0c94ec02a7768953020ab0d3e2b884760f859176343a36c01adf87", size = 412326 }, - { url = "https://files.pythonhosted.org/packages/ea/02/125d7bbd7f7a500190ddc8ae5d2d3c39d87ed3ed28f5b37cfe76962c678d/orjson-3.10.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5385bbfdbc90ff5b2635b7e6bebf259652db00a92b5e3c45b616df75b9058e88", size = 152800 }, - { url = "https://files.pythonhosted.org/packages/f9/09/7658a9e3e793d5b3b00598023e0fb6935d0e7bbb8ff72311c5415a8ce677/orjson-3.10.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02c6279016346e774dd92625d46c6c40db687b8a0d685aadb91e26e46cc33e1e", size = 137516 }, - { url = "https://files.pythonhosted.org/packages/29/87/32b7a4831e909d347278101a48d4cf9f3f25901b2295e7709df1651f65a1/orjson-3.10.16-cp312-cp312-win32.whl", hash = "sha256:7ca55097a11426db80f79378e873a8c51f4dde9ffc22de44850f9696b7eb0e8c", size = 141759 }, - { url = "https://files.pythonhosted.org/packages/35/ce/81a27e7b439b807bd393585271364cdddf50dc281fc57c4feef7ccb186a6/orjson-3.10.16-cp312-cp312-win_amd64.whl", hash = "sha256:86d127efdd3f9bf5f04809b70faca1e6836556ea3cc46e662b44dab3fe71f3d6", size = 133944 }, - { url = "https://files.pythonhosted.org/packages/87/b9/ff6aa28b8c86af9526160905593a2fe8d004ac7a5e592ee0b0ff71017511/orjson-3.10.16-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:148a97f7de811ba14bc6dbc4a433e0341ffd2cc285065199fb5f6a98013744bd", size = 249289 }, - { url = "https://files.pythonhosted.org/packages/6c/81/6d92a586149b52684ab8fd70f3623c91d0e6a692f30fd8c728916ab2263c/orjson-3.10.16-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1d960c1bf0e734ea36d0adc880076de3846aaec45ffad29b78c7f1b7962516b8", size = 133640 }, - { url = "https://files.pythonhosted.org/packages/c2/88/b72443f4793d2e16039ab85d0026677932b15ab968595fb7149750d74134/orjson-3.10.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a318cd184d1269f68634464b12871386808dc8b7c27de8565234d25975a7a137", size = 138286 }, - { url = "https://files.pythonhosted.org/packages/c3/3c/72a22d4b28c076c4016d5a52bd644a8e4d849d3bb0373d9e377f9e3b2250/orjson-3.10.16-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df23f8df3ef9223d1d6748bea63fca55aae7da30a875700809c500a05975522b", size = 132307 }, - { url = "https://files.pythonhosted.org/packages/8a/a2/f1259561bdb6ad7061ff1b95dab082fe32758c4bc143ba8d3d70831f0a06/orjson-3.10.16-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b94dda8dd6d1378f1037d7f3f6b21db769ef911c4567cbaa962bb6dc5021cf90", size = 136739 }, - { url = "https://files.pythonhosted.org/packages/3d/af/c7583c4b34f33d8b8b90cfaab010ff18dd64e7074cc1e117a5f1eff20dcf/orjson-3.10.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12970a26666a8775346003fd94347d03ccb98ab8aa063036818381acf5f523e", size = 138076 }, - { url = "https://files.pythonhosted.org/packages/d7/59/d7fc7fbdd3d4a64c2eae4fc7341a5aa39cf9549bd5e2d7f6d3c07f8b715b/orjson-3.10.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a1431a245d856bd56e4d29ea0023eb4d2c8f71efe914beb3dee8ab3f0cd7fb", size = 142643 }, - { url = "https://files.pythonhosted.org/packages/92/0e/3bd8f2197d27601f16b4464ae948826da2bcf128af31230a9dbbad7ceb57/orjson-3.10.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83655cfc247f399a222567d146524674a7b217af7ef8289c0ff53cfe8db09f0", size = 133168 }, - { url = "https://files.pythonhosted.org/packages/af/a8/351fd87b664b02f899f9144d2c3dc848b33ac04a5df05234cbfb9e2a7540/orjson-3.10.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fa59ae64cb6ddde8f09bdbf7baf933c4cd05734ad84dcf4e43b887eb24e37652", size = 135271 }, - { url = "https://files.pythonhosted.org/packages/ba/b0/a6d42a7d412d867c60c0337d95123517dd5a9370deea705ea1be0f89389e/orjson-3.10.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ca5426e5aacc2e9507d341bc169d8af9c3cbe88f4cd4c1cf2f87e8564730eb56", size = 412444 }, - { url = "https://files.pythonhosted.org/packages/79/ec/7572cd4e20863f60996f3f10bc0a6da64a6fd9c35954189a914cec0b7377/orjson-3.10.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6fd5da4edf98a400946cd3a195680de56f1e7575109b9acb9493331047157430", size = 152737 }, - { url = "https://files.pythonhosted.org/packages/a9/19/ceb9e8fed5403b2e76a8ac15f581b9d25780a3be3c9b3aa54b7777a210d5/orjson-3.10.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:980ecc7a53e567169282a5e0ff078393bac78320d44238da4e246d71a4e0e8f5", size = 137482 }, - { url = "https://files.pythonhosted.org/packages/1b/78/a78bb810f3786579dbbbd94768284cbe8f2fd65167cd7020260679665c17/orjson-3.10.16-cp313-cp313-win32.whl", hash = "sha256:28f79944dd006ac540a6465ebd5f8f45dfdf0948ff998eac7a908275b4c1add6", size = 141714 }, - { url = "https://files.pythonhosted.org/packages/81/9c/b66ce9245ff319df2c3278acd351a3f6145ef34b4a2d7f4b0f739368370f/orjson-3.10.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7", size = 133954 }, + { url = "https://files.pythonhosted.org/packages/9d/a6/22cb9b03baf167bc2d659c9e74d7580147f36e6a155e633801badfd5a74d/orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8", size = 249179, upload_time = "2025-03-24T16:58:41.294Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/3e68cc33020a6ebd8f359b8628b69d2132cd84fea68155c33057e502ee51/orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00", size = 138510, upload_time = "2025-03-24T16:58:43.732Z" }, + { url = "https://files.pythonhosted.org/packages/dc/12/63bee7764ce12052f7c1a1393ce7f26dc392c93081eb8754dd3dce9b7c6b/orjson-3.10.16-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c682d852d0ce77613993dc967e90e151899fe2d8e71c20e9be164080f468e370", size = 132373, upload_time = "2025-03-24T16:58:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d5/2998c2f319adcd572f2b03ba2083e8176863d1055d8d713683ddcf927b71/orjson-3.10.16-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c520ae736acd2e32df193bcff73491e64c936f3e44a2916b548da048a48b46b", size = 136774, upload_time = "2025-03-24T16:58:46.273Z" }, + { url = "https://files.pythonhosted.org/packages/00/03/88c236ae307bd0604623204d4a835e15fbf9c75b8535c8f13ef45abd413f/orjson-3.10.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:134f87c76bfae00f2094d85cfab261b289b76d78c6da8a7a3b3c09d362fd1e06", size = 138030, upload_time = "2025-03-24T16:58:47.921Z" }, + { url = "https://files.pythonhosted.org/packages/66/ba/3e256ddfeb364f98fd6ac65774844090d356158b2d1de8998db2bf984503/orjson-3.10.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b59afde79563e2cf37cfe62ee3b71c063fd5546c8e662d7fcfc2a3d5031a5c4c", size = 142677, upload_time = "2025-03-24T16:58:49.191Z" }, + { url = "https://files.pythonhosted.org/packages/2c/71/73a1214bd27baa2ea5184fff4aa6193a114dfb0aa5663dad48fe63e8cd29/orjson-3.10.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113602f8241daaff05d6fad25bd481d54c42d8d72ef4c831bb3ab682a54d9e15", size = 132798, upload_time = "2025-03-24T16:58:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/53/ac/0b2f41c0a1e8c095439d0fab3b33103cf41a39be8e6aa2c56298a6034259/orjson-3.10.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4fc0077d101f8fab4031e6554fc17b4c2ad8fdbc56ee64a727f3c95b379e31da", size = 135450, upload_time = "2025-03-24T16:58:52.481Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ca/7524c7b0bc815d426ca134dab54cad519802287b808a3846b047a5b2b7a3/orjson-3.10.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9c6bf6ff180cd69e93f3f50380224218cfab79953a868ea3908430bcfaf9cb5e", size = 412356, upload_time = "2025-03-24T16:58:54.17Z" }, + { url = "https://files.pythonhosted.org/packages/05/1d/3ae2367c255276bf16ff7e1b210dd0af18bc8da20c4e4295755fc7de1268/orjson-3.10.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5673eadfa952f95a7cd76418ff189df11b0a9c34b1995dff43a6fdbce5d63bf4", size = 152769, upload_time = "2025-03-24T16:58:55.821Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2d/8eb10b6b1d30bb69c35feb15e5ba5ac82466cf743d562e3e8047540efd2f/orjson-3.10.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5fe638a423d852b0ae1e1a79895851696cb0d9fa0946fdbfd5da5072d9bb9551", size = 137223, upload_time = "2025-03-24T16:58:57.136Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/f043717930cb2de5fbebe47f308f101bed9ec2b3580b1f99c8284b2f5fe8/orjson-3.10.16-cp310-cp310-win32.whl", hash = "sha256:33af58f479b3c6435ab8f8b57999874b4b40c804c7a36b5cc6b54d8f28e1d3dd", size = 141734, upload_time = "2025-03-24T16:58:58.516Z" }, + { url = "https://files.pythonhosted.org/packages/67/99/795ad7282b425b9fddcfb8a31bded5dcf84dba78ecb1e7ae716e84e794da/orjson-3.10.16-cp310-cp310-win_amd64.whl", hash = "sha256:0338356b3f56d71293c583350af26f053017071836b07e064e92819ecf1aa055", size = 133779, upload_time = "2025-03-24T16:59:00.254Z" }, + { url = "https://files.pythonhosted.org/packages/97/29/43f91a5512b5d2535594438eb41c5357865fd5e64dec745d90a588820c75/orjson-3.10.16-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44fcbe1a1884f8bc9e2e863168b0f84230c3d634afe41c678637d2728ea8e739", size = 249180, upload_time = "2025-03-24T16:59:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/0c/36/2a72d55e266473c19a86d97b7363bb8bf558ab450f75205689a287d5ce61/orjson-3.10.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78177bf0a9d0192e0b34c3d78bcff7fe21d1b5d84aeb5ebdfe0dbe637b885225", size = 138510, upload_time = "2025-03-24T16:59:02.876Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/f86d6f55c1a68b57ff6ea7966bce5f4e5163f2e526ddb7db9fc3c2c8d1c4/orjson-3.10.16-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12824073a010a754bb27330cad21d6e9b98374f497f391b8707752b96f72e741", size = 132373, upload_time = "2025-03-24T16:59:04.103Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/d18f2711493a809f3082a88fda89342bc8e16767743b909cd3c34989fba3/orjson-3.10.16-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddd41007e56284e9867864aa2f29f3136bb1dd19a49ca43c0b4eda22a579cf53", size = 136773, upload_time = "2025-03-24T16:59:05.636Z" }, + { url = "https://files.pythonhosted.org/packages/a1/dc/ce025f002f8e0749e3f057c4d773a4d4de32b7b4c1fc5a50b429e7532586/orjson-3.10.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0877c4d35de639645de83666458ca1f12560d9fa7aa9b25d8bb8f52f61627d14", size = 138029, upload_time = "2025-03-24T16:59:06.99Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1b/cf9df85852b91160029d9f26014230366a2b4deb8cc51fabe68e250a8c1a/orjson-3.10.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a09a539e9cc3beead3e7107093b4ac176d015bec64f811afb5965fce077a03c", size = 142677, upload_time = "2025-03-24T16:59:08.22Z" }, + { url = "https://files.pythonhosted.org/packages/92/18/5b1e1e995bffad49dc4311a0bdfd874bc6f135fd20f0e1f671adc2c9910e/orjson-3.10.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31b98bc9b40610fec971d9a4d67bb2ed02eec0a8ae35f8ccd2086320c28526ca", size = 132800, upload_time = "2025-03-24T16:59:09.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/eb/467f25b580e942fcca1344adef40633b7f05ac44a65a63fc913f9a805d58/orjson-3.10.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0ce243f5a8739f3a18830bc62dc2e05b69a7545bafd3e3249f86668b2bcd8e50", size = 135451, upload_time = "2025-03-24T16:59:10.823Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4b/9d10888038975cb375982e9339d9495bac382d5c976c500b8d6f2c8e2e4e/orjson-3.10.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64792c0025bae049b3074c6abe0cf06f23c8e9f5a445f4bab31dc5ca23dbf9e1", size = 412358, upload_time = "2025-03-24T16:59:12.113Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e2/cfbcfcc4fbe619e0ca9bdbbfccb2d62b540bbfe41e0ee77d44a628594f59/orjson-3.10.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea53f7e68eec718b8e17e942f7ca56c6bd43562eb19db3f22d90d75e13f0431d", size = 152772, upload_time = "2025-03-24T16:59:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d6/627a1b00569be46173007c11dde3da4618c9bfe18409325b0e3e2a82fe29/orjson-3.10.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a741ba1a9488c92227711bde8c8c2b63d7d3816883268c808fbeada00400c164", size = 137225, upload_time = "2025-03-24T16:59:15.355Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7b/a73c67b505021af845b9f05c7c848793258ea141fa2058b52dd9b067c2b4/orjson-3.10.16-cp311-cp311-win32.whl", hash = "sha256:c7ed2c61bb8226384c3fdf1fb01c51b47b03e3f4536c985078cccc2fd19f1619", size = 141733, upload_time = "2025-03-24T16:59:16.791Z" }, + { url = "https://files.pythonhosted.org/packages/f4/22/5e8217c48d68c0adbfb181e749d6a733761074e598b083c69a1383d18147/orjson-3.10.16-cp311-cp311-win_amd64.whl", hash = "sha256:cd67d8b3e0e56222a2e7b7f7da9031e30ecd1fe251c023340b9f12caca85ab60", size = 133784, upload_time = "2025-03-24T16:59:18.106Z" }, + { url = "https://files.pythonhosted.org/packages/5d/15/67ce9d4c959c83f112542222ea3b9209c1d424231d71d74c4890ea0acd2b/orjson-3.10.16-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6d3444abbfa71ba21bb042caa4b062535b122248259fdb9deea567969140abca", size = 249325, upload_time = "2025-03-24T16:59:19.784Z" }, + { url = "https://files.pythonhosted.org/packages/da/2c/1426b06f30a1b9ada74b6f512c1ddf9d2760f53f61cdb59efeb9ad342133/orjson-3.10.16-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:30245c08d818fdcaa48b7d5b81499b8cae09acabb216fe61ca619876b128e184", size = 133621, upload_time = "2025-03-24T16:59:21.207Z" }, + { url = "https://files.pythonhosted.org/packages/9e/88/18d26130954bc73bee3be10f95371ea1dfb8679e0e2c46b0f6d8c6289402/orjson-3.10.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0ba1d0baa71bf7579a4ccdcf503e6f3098ef9542106a0eca82395898c8a500a", size = 138270, upload_time = "2025-03-24T16:59:22.514Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f9/6d8b64fcd58fae072e80ee7981be8ba0d7c26ace954e5cd1d027fc80518f/orjson-3.10.16-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb0beefa5ef3af8845f3a69ff2a4aa62529b5acec1cfe5f8a6b4141033fd46ef", size = 132346, upload_time = "2025-03-24T16:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/2513fd5bc786f40cd12af569c23cae6381aeddbefeed2a98f0a666eb5d0d/orjson-3.10.16-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6daa0e1c9bf2e030e93c98394de94506f2a4d12e1e9dadd7c53d5e44d0f9628e", size = 136845, upload_time = "2025-03-24T16:59:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/b0e7b36720f5ab722b48e8ccf06514d4f769358dd73c51abd8728ef58d0b/orjson-3.10.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da9019afb21e02410ef600e56666652b73eb3e4d213a0ec919ff391a7dd52aa", size = 138078, upload_time = "2025-03-24T16:59:27.288Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a8/d220afb8a439604be74fc755dbc740bded5ed14745ca536b304ed32eb18a/orjson-3.10.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:daeb3a1ee17b69981d3aae30c3b4e786b0f8c9e6c71f2b48f1aef934f63f38f4", size = 142712, upload_time = "2025-03-24T16:59:28.613Z" }, + { url = "https://files.pythonhosted.org/packages/8c/88/7e41e9883c00f84f92fe357a8371edae816d9d7ef39c67b5106960c20389/orjson-3.10.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fed80eaf0e20a31942ae5d0728849862446512769692474be5e6b73123a23b", size = 133136, upload_time = "2025-03-24T16:59:29.987Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ca/61116095307ad0be828ea26093febaf59e38596d84a9c8d765c3c5e4934f/orjson-3.10.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73390ed838f03764540a7bdc4071fe0123914c2cc02fb6abf35182d5fd1b7a42", size = 135258, upload_time = "2025-03-24T16:59:31.339Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1b/09493cf7d801505f094c9295f79c98c1e0af2ac01c7ed8d25b30fcb19ada/orjson-3.10.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a22bba012a0c94ec02a7768953020ab0d3e2b884760f859176343a36c01adf87", size = 412326, upload_time = "2025-03-24T16:59:32.709Z" }, + { url = "https://files.pythonhosted.org/packages/ea/02/125d7bbd7f7a500190ddc8ae5d2d3c39d87ed3ed28f5b37cfe76962c678d/orjson-3.10.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5385bbfdbc90ff5b2635b7e6bebf259652db00a92b5e3c45b616df75b9058e88", size = 152800, upload_time = "2025-03-24T16:59:34.134Z" }, + { url = "https://files.pythonhosted.org/packages/f9/09/7658a9e3e793d5b3b00598023e0fb6935d0e7bbb8ff72311c5415a8ce677/orjson-3.10.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02c6279016346e774dd92625d46c6c40db687b8a0d685aadb91e26e46cc33e1e", size = 137516, upload_time = "2025-03-24T16:59:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/29/87/32b7a4831e909d347278101a48d4cf9f3f25901b2295e7709df1651f65a1/orjson-3.10.16-cp312-cp312-win32.whl", hash = "sha256:7ca55097a11426db80f79378e873a8c51f4dde9ffc22de44850f9696b7eb0e8c", size = 141759, upload_time = "2025-03-24T16:59:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/35/ce/81a27e7b439b807bd393585271364cdddf50dc281fc57c4feef7ccb186a6/orjson-3.10.16-cp312-cp312-win_amd64.whl", hash = "sha256:86d127efdd3f9bf5f04809b70faca1e6836556ea3cc46e662b44dab3fe71f3d6", size = 133944, upload_time = "2025-03-24T16:59:38.814Z" }, + { url = "https://files.pythonhosted.org/packages/87/b9/ff6aa28b8c86af9526160905593a2fe8d004ac7a5e592ee0b0ff71017511/orjson-3.10.16-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:148a97f7de811ba14bc6dbc4a433e0341ffd2cc285065199fb5f6a98013744bd", size = 249289, upload_time = "2025-03-24T16:59:40.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/81/6d92a586149b52684ab8fd70f3623c91d0e6a692f30fd8c728916ab2263c/orjson-3.10.16-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1d960c1bf0e734ea36d0adc880076de3846aaec45ffad29b78c7f1b7962516b8", size = 133640, upload_time = "2025-03-24T16:59:41.469Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/b72443f4793d2e16039ab85d0026677932b15ab968595fb7149750d74134/orjson-3.10.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a318cd184d1269f68634464b12871386808dc8b7c27de8565234d25975a7a137", size = 138286, upload_time = "2025-03-24T16:59:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3c/72a22d4b28c076c4016d5a52bd644a8e4d849d3bb0373d9e377f9e3b2250/orjson-3.10.16-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df23f8df3ef9223d1d6748bea63fca55aae7da30a875700809c500a05975522b", size = 132307, upload_time = "2025-03-24T16:59:44.143Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/f1259561bdb6ad7061ff1b95dab082fe32758c4bc143ba8d3d70831f0a06/orjson-3.10.16-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b94dda8dd6d1378f1037d7f3f6b21db769ef911c4567cbaa962bb6dc5021cf90", size = 136739, upload_time = "2025-03-24T16:59:45.995Z" }, + { url = "https://files.pythonhosted.org/packages/3d/af/c7583c4b34f33d8b8b90cfaab010ff18dd64e7074cc1e117a5f1eff20dcf/orjson-3.10.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12970a26666a8775346003fd94347d03ccb98ab8aa063036818381acf5f523e", size = 138076, upload_time = "2025-03-24T16:59:47.776Z" }, + { url = "https://files.pythonhosted.org/packages/d7/59/d7fc7fbdd3d4a64c2eae4fc7341a5aa39cf9549bd5e2d7f6d3c07f8b715b/orjson-3.10.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a1431a245d856bd56e4d29ea0023eb4d2c8f71efe914beb3dee8ab3f0cd7fb", size = 142643, upload_time = "2025-03-24T16:59:49.258Z" }, + { url = "https://files.pythonhosted.org/packages/92/0e/3bd8f2197d27601f16b4464ae948826da2bcf128af31230a9dbbad7ceb57/orjson-3.10.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83655cfc247f399a222567d146524674a7b217af7ef8289c0ff53cfe8db09f0", size = 133168, upload_time = "2025-03-24T16:59:51.027Z" }, + { url = "https://files.pythonhosted.org/packages/af/a8/351fd87b664b02f899f9144d2c3dc848b33ac04a5df05234cbfb9e2a7540/orjson-3.10.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fa59ae64cb6ddde8f09bdbf7baf933c4cd05734ad84dcf4e43b887eb24e37652", size = 135271, upload_time = "2025-03-24T16:59:52.449Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/a6d42a7d412d867c60c0337d95123517dd5a9370deea705ea1be0f89389e/orjson-3.10.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ca5426e5aacc2e9507d341bc169d8af9c3cbe88f4cd4c1cf2f87e8564730eb56", size = 412444, upload_time = "2025-03-24T16:59:53.825Z" }, + { url = "https://files.pythonhosted.org/packages/79/ec/7572cd4e20863f60996f3f10bc0a6da64a6fd9c35954189a914cec0b7377/orjson-3.10.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6fd5da4edf98a400946cd3a195680de56f1e7575109b9acb9493331047157430", size = 152737, upload_time = "2025-03-24T16:59:55.599Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/ceb9e8fed5403b2e76a8ac15f581b9d25780a3be3c9b3aa54b7777a210d5/orjson-3.10.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:980ecc7a53e567169282a5e0ff078393bac78320d44238da4e246d71a4e0e8f5", size = 137482, upload_time = "2025-03-24T16:59:57.045Z" }, + { url = "https://files.pythonhosted.org/packages/1b/78/a78bb810f3786579dbbbd94768284cbe8f2fd65167cd7020260679665c17/orjson-3.10.16-cp313-cp313-win32.whl", hash = "sha256:28f79944dd006ac540a6465ebd5f8f45dfdf0948ff998eac7a908275b4c1add6", size = 141714, upload_time = "2025-03-24T16:59:58.666Z" }, + { url = "https://files.pythonhosted.org/packages/81/9c/b66ce9245ff319df2c3278acd351a3f6145ef34b4a2d7f4b0f739368370f/orjson-3.10.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7", size = 133954, upload_time = "2025-03-24T17:00:00.101Z" }, ] [[package]] name = "packaging" version = "23.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/2b/9b9c33ffed44ee921d0967086d653047286054117d584f1b1a7c22ceaf7b/packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", size = 146714 } +sdist = { url = "https://files.pythonhosted.org/packages/fb/2b/9b9c33ffed44ee921d0967086d653047286054117d584f1b1a7c22ceaf7b/packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", size = 146714, upload_time = "2023-10-01T13:50:05.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/1a/610693ac4ee14fcdf2d9bf3c493370e4f2ef7ae2e19217d7a237ff42367d/packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7", size = 53011 }, + { url = "https://files.pythonhosted.org/packages/ec/1a/610693ac4ee14fcdf2d9bf3c493370e4f2ef7ae2e19217d7a237ff42367d/packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7", size = 53011, upload_time = "2023-10-01T13:50:03.745Z" }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload_time = "2023-12-10T22:30:45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload_time = "2023-12-10T22:30:43.14Z" }, ] [[package]] name = "pillow" version = "10.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059 } +sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059, upload_time = "2024-07-01T09:48:43.583Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", size = 3509271 }, - { url = "https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", size = 3375658 }, - { url = "https://files.pythonhosted.org/packages/8a/25/1fc45761955f9359b1169aa75e241551e74ac01a09f487adaaf4c3472d11/pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", size = 4332075 }, - { url = "https://files.pythonhosted.org/packages/5e/dd/425b95d0151e1d6c951f45051112394f130df3da67363b6bc75dc4c27aba/pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", size = 4444808 }, - { url = "https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", size = 4356290 }, - { url = "https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", size = 4525163 }, - { url = "https://files.pythonhosted.org/packages/07/8b/34854bf11a83c248505c8cb0fcf8d3d0b459a2246c8809b967963b6b12ae/pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", size = 4463100 }, - { url = "https://files.pythonhosted.org/packages/78/63/0632aee4e82476d9cbe5200c0cdf9ba41ee04ed77887432845264d81116d/pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", size = 4592880 }, - { url = "https://files.pythonhosted.org/packages/df/56/b8663d7520671b4398b9d97e1ed9f583d4afcbefbda3c6188325e8c297bd/pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", size = 2235218 }, - { url = "https://files.pythonhosted.org/packages/f4/72/0203e94a91ddb4a9d5238434ae6c1ca10e610e8487036132ea9bf806ca2a/pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", size = 2554487 }, - { url = "https://files.pythonhosted.org/packages/bd/52/7e7e93d7a6e4290543f17dc6f7d3af4bd0b3dd9926e2e8a35ac2282bc5f4/pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1", size = 2243219 }, - { url = "https://files.pythonhosted.org/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", size = 3509265 }, - { url = "https://files.pythonhosted.org/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", size = 3375655 }, - { url = "https://files.pythonhosted.org/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", size = 4340304 }, - { url = "https://files.pythonhosted.org/packages/ac/10/c67e20445a707f7a610699bba4fe050583b688d8cd2d202572b257f46600/pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", size = 4452804 }, - { url = "https://files.pythonhosted.org/packages/a9/83/6523837906d1da2b269dee787e31df3b0acb12e3d08f024965a3e7f64665/pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", size = 4365126 }, - { url = "https://files.pythonhosted.org/packages/ba/e5/8c68ff608a4203085158cff5cc2a3c534ec384536d9438c405ed6370d080/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", size = 4533541 }, - { url = "https://files.pythonhosted.org/packages/f4/7c/01b8dbdca5bc6785573f4cee96e2358b0918b7b2c7b60d8b6f3abf87a070/pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", size = 4471616 }, - { url = "https://files.pythonhosted.org/packages/c8/57/2899b82394a35a0fbfd352e290945440e3b3785655a03365c0ca8279f351/pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", size = 4600802 }, - { url = "https://files.pythonhosted.org/packages/4d/d7/a44f193d4c26e58ee5d2d9db3d4854b2cfb5b5e08d360a5e03fe987c0086/pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", size = 2235213 }, - { url = "https://files.pythonhosted.org/packages/c1/d0/5866318eec2b801cdb8c82abf190c8343d8a1cd8bf5a0c17444a6f268291/pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", size = 2554498 }, - { url = "https://files.pythonhosted.org/packages/d4/c8/310ac16ac2b97e902d9eb438688de0d961660a87703ad1561fd3dfbd2aa0/pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", size = 2243219 }, - { url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350 }, - { url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980 }, - { url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799 }, - { url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973 }, - { url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054 }, - { url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484 }, - { url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375 }, - { url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773 }, - { url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690 }, - { url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951 }, - { url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427 }, - { url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685 }, - { url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883 }, - { url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837 }, - { url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562 }, - { url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761 }, - { url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767 }, - { url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989 }, - { url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255 }, - { url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603 }, - { url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972 }, - { url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375 }, - { url = "https://files.pythonhosted.org/packages/38/30/095d4f55f3a053392f75e2eae45eba3228452783bab3d9a920b951ac495c/pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", size = 3493889 }, - { url = "https://files.pythonhosted.org/packages/f3/e8/4ff79788803a5fcd5dc35efdc9386af153569853767bff74540725b45863/pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", size = 3346160 }, - { url = "https://files.pythonhosted.org/packages/d7/ac/4184edd511b14f760c73f5bb8a5d6fd85c591c8aff7c2229677a355c4179/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", size = 3435020 }, - { url = "https://files.pythonhosted.org/packages/da/21/1749cd09160149c0a246a81d646e05f35041619ce76f6493d6a96e8d1103/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", size = 3490539 }, - { url = "https://files.pythonhosted.org/packages/b6/f5/f71fe1888b96083b3f6dfa0709101f61fc9e972c0c8d04e9d93ccef2a045/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", size = 3476125 }, - { url = "https://files.pythonhosted.org/packages/96/b9/c0362c54290a31866c3526848583a2f45a535aa9d725fd31e25d318c805f/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", size = 3579373 }, - { url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661 }, + { url = "https://files.pythonhosted.org/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", size = 3509271, upload_time = "2024-07-01T09:45:22.07Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", size = 3375658, upload_time = "2024-07-01T09:45:25.292Z" }, + { url = "https://files.pythonhosted.org/packages/8a/25/1fc45761955f9359b1169aa75e241551e74ac01a09f487adaaf4c3472d11/pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", size = 4332075, upload_time = "2024-07-01T09:45:27.94Z" }, + { url = "https://files.pythonhosted.org/packages/5e/dd/425b95d0151e1d6c951f45051112394f130df3da67363b6bc75dc4c27aba/pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", size = 4444808, upload_time = "2024-07-01T09:45:30.305Z" }, + { url = "https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", size = 4356290, upload_time = "2024-07-01T09:45:32.868Z" }, + { url = "https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", size = 4525163, upload_time = "2024-07-01T09:45:35.279Z" }, + { url = "https://files.pythonhosted.org/packages/07/8b/34854bf11a83c248505c8cb0fcf8d3d0b459a2246c8809b967963b6b12ae/pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", size = 4463100, upload_time = "2024-07-01T09:45:37.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/63/0632aee4e82476d9cbe5200c0cdf9ba41ee04ed77887432845264d81116d/pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", size = 4592880, upload_time = "2024-07-01T09:45:39.89Z" }, + { url = "https://files.pythonhosted.org/packages/df/56/b8663d7520671b4398b9d97e1ed9f583d4afcbefbda3c6188325e8c297bd/pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", size = 2235218, upload_time = "2024-07-01T09:45:42.771Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/0203e94a91ddb4a9d5238434ae6c1ca10e610e8487036132ea9bf806ca2a/pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", size = 2554487, upload_time = "2024-07-01T09:45:45.176Z" }, + { url = "https://files.pythonhosted.org/packages/bd/52/7e7e93d7a6e4290543f17dc6f7d3af4bd0b3dd9926e2e8a35ac2282bc5f4/pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1", size = 2243219, upload_time = "2024-07-01T09:45:47.274Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", size = 3509265, upload_time = "2024-07-01T09:45:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", size = 3375655, upload_time = "2024-07-01T09:45:52.462Z" }, + { url = "https://files.pythonhosted.org/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", size = 4340304, upload_time = "2024-07-01T09:45:55.006Z" }, + { url = "https://files.pythonhosted.org/packages/ac/10/c67e20445a707f7a610699bba4fe050583b688d8cd2d202572b257f46600/pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", size = 4452804, upload_time = "2024-07-01T09:45:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/a9/83/6523837906d1da2b269dee787e31df3b0acb12e3d08f024965a3e7f64665/pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", size = 4365126, upload_time = "2024-07-01T09:46:00.713Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e5/8c68ff608a4203085158cff5cc2a3c534ec384536d9438c405ed6370d080/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", size = 4533541, upload_time = "2024-07-01T09:46:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7c/01b8dbdca5bc6785573f4cee96e2358b0918b7b2c7b60d8b6f3abf87a070/pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", size = 4471616, upload_time = "2024-07-01T09:46:05.356Z" }, + { url = "https://files.pythonhosted.org/packages/c8/57/2899b82394a35a0fbfd352e290945440e3b3785655a03365c0ca8279f351/pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", size = 4600802, upload_time = "2024-07-01T09:46:08.145Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d7/a44f193d4c26e58ee5d2d9db3d4854b2cfb5b5e08d360a5e03fe987c0086/pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", size = 2235213, upload_time = "2024-07-01T09:46:10.211Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d0/5866318eec2b801cdb8c82abf190c8343d8a1cd8bf5a0c17444a6f268291/pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", size = 2554498, upload_time = "2024-07-01T09:46:12.685Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c8/310ac16ac2b97e902d9eb438688de0d961660a87703ad1561fd3dfbd2aa0/pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", size = 2243219, upload_time = "2024-07-01T09:46:14.83Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350, upload_time = "2024-07-01T09:46:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980, upload_time = "2024-07-01T09:46:19.169Z" }, + { url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799, upload_time = "2024-07-01T09:46:21.883Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973, upload_time = "2024-07-01T09:46:24.321Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054, upload_time = "2024-07-01T09:46:26.825Z" }, + { url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484, upload_time = "2024-07-01T09:46:29.355Z" }, + { url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375, upload_time = "2024-07-01T09:46:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773, upload_time = "2024-07-01T09:46:33.73Z" }, + { url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690, upload_time = "2024-07-01T09:46:36.587Z" }, + { url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951, upload_time = "2024-07-01T09:46:38.777Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427, upload_time = "2024-07-01T09:46:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685, upload_time = "2024-07-01T09:46:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883, upload_time = "2024-07-01T09:46:47.331Z" }, + { url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837, upload_time = "2024-07-01T09:46:49.647Z" }, + { url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562, upload_time = "2024-07-01T09:46:51.811Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761, upload_time = "2024-07-01T09:46:53.961Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767, upload_time = "2024-07-01T09:46:56.664Z" }, + { url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989, upload_time = "2024-07-01T09:46:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255, upload_time = "2024-07-01T09:47:01.189Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603, upload_time = "2024-07-01T09:47:03.918Z" }, + { url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972, upload_time = "2024-07-01T09:47:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375, upload_time = "2024-07-01T09:47:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/38/30/095d4f55f3a053392f75e2eae45eba3228452783bab3d9a920b951ac495c/pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", size = 3493889, upload_time = "2024-07-01T09:48:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e8/4ff79788803a5fcd5dc35efdc9386af153569853767bff74540725b45863/pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", size = 3346160, upload_time = "2024-07-01T09:48:07.206Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ac/4184edd511b14f760c73f5bb8a5d6fd85c591c8aff7c2229677a355c4179/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", size = 3435020, upload_time = "2024-07-01T09:48:09.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/21/1749cd09160149c0a246a81d646e05f35041619ce76f6493d6a96e8d1103/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", size = 3490539, upload_time = "2024-07-01T09:48:12.529Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f5/f71fe1888b96083b3f6dfa0709101f61fc9e972c0c8d04e9d93ccef2a045/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", size = 3476125, upload_time = "2024-07-01T09:48:14.891Z" }, + { url = "https://files.pythonhosted.org/packages/96/b9/c0362c54290a31866c3526848583a2f45a535aa9d725fd31e25d318c805f/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", size = 3579373, upload_time = "2024-07-01T09:48:17.601Z" }, + { url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661, upload_time = "2024-07-01T09:48:20.293Z" }, ] [[package]] name = "platformdirs" version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/62/d1/7feaaacb1a3faeba96c06e6c5091f90695cc0f94b7e8e1a3a3fe2b33ff9a/platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420", size = 19760 } +sdist = { url = "https://files.pythonhosted.org/packages/62/d1/7feaaacb1a3faeba96c06e6c5091f90695cc0f94b7e8e1a3a3fe2b33ff9a/platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420", size = 19760, upload_time = "2023-12-04T15:32:15.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/53/42fe5eab4a09d251a76d0043e018172db324a23fcdac70f77a551c11f618/platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", size = 17420 }, + { url = "https://files.pythonhosted.org/packages/be/53/42fe5eab4a09d251a76d0043e018172db324a23fcdac70f77a551c11f618/platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", size = 17420, upload_time = "2023-12-04T15:32:13.795Z" }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload_time = "2024-04-20T21:34:42.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload_time = "2024-04-20T21:34:40.434Z" }, ] [[package]] @@ -1745,46 +1745,46 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/c0/5e9c4d2a643a00a6f67578ef35485173de273a4567279e4f0c200c01386b/prettytable-3.9.0.tar.gz", hash = "sha256:f4ed94803c23073a90620b201965e5dc0bccf1760b7a7eaf3158cab8aaffdf34", size = 47874 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/c0/5e9c4d2a643a00a6f67578ef35485173de273a4567279e4f0c200c01386b/prettytable-3.9.0.tar.gz", hash = "sha256:f4ed94803c23073a90620b201965e5dc0bccf1760b7a7eaf3158cab8aaffdf34", size = 47874, upload_time = "2023-09-11T14:04:14.548Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/81/316b6a55a0d1f327d04cc7b0ba9d04058cb62de6c3a4d4b0df280cbe3b0b/prettytable-3.9.0-py3-none-any.whl", hash = "sha256:a71292ab7769a5de274b146b276ce938786f56c31cf7cea88b6f3775d82fe8c8", size = 27772 }, + { url = "https://files.pythonhosted.org/packages/4d/81/316b6a55a0d1f327d04cc7b0ba9d04058cb62de6c3a4d4b0df280cbe3b0b/prettytable-3.9.0-py3-none-any.whl", hash = "sha256:a71292ab7769a5de274b146b276ce938786f56c31cf7cea88b6f3775d82fe8c8", size = 27772, upload_time = "2023-09-11T14:03:45.582Z" }, ] [[package]] name = "protobuf" version = "4.25.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/a5/05ea470f4e793c9408bc975ce1c6957447e3134ce7f7a58c13be8b2c216f/protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e", size = 380282 } +sdist = { url = "https://files.pythonhosted.org/packages/db/a5/05ea470f4e793c9408bc975ce1c6957447e3134ce7f7a58c13be8b2c216f/protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e", size = 380282, upload_time = "2024-01-10T19:37:42.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/2f/01f63896ddf22cbb0173ab51f54fde70b0208ca6c2f5e8416950977930e1/protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6", size = 392408 }, - { url = "https://files.pythonhosted.org/packages/c1/00/c3ae19cabb36cfabc94ff0b102aac21b471c9f91a1357f8aafffb9efe8e0/protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9", size = 413397 }, - { url = "https://files.pythonhosted.org/packages/b3/81/0017aefacf23273d4efd1154ef958a27eed9c177c4cc09d2d4ba398fb47f/protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d", size = 394159 }, - { url = "https://files.pythonhosted.org/packages/23/17/405ba44f60a693dfe96c7a18e843707cffa0fcfad80bd8fc4f227f499ea5/protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62", size = 293698 }, - { url = "https://files.pythonhosted.org/packages/81/9e/63501b8d5b4e40c7260049836bd15ec3270c936e83bc57b85e4603cc212c/protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020", size = 294609 }, - { url = "https://files.pythonhosted.org/packages/ff/52/5d23df1fe3b368133ec3e2436fb3dd4ccedf44c8d5ac7f4a88087c75180b/protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830", size = 156463 }, + { url = "https://files.pythonhosted.org/packages/36/2f/01f63896ddf22cbb0173ab51f54fde70b0208ca6c2f5e8416950977930e1/protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6", size = 392408, upload_time = "2024-01-10T19:37:23.466Z" }, + { url = "https://files.pythonhosted.org/packages/c1/00/c3ae19cabb36cfabc94ff0b102aac21b471c9f91a1357f8aafffb9efe8e0/protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9", size = 413397, upload_time = "2024-01-10T19:37:26.321Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/0017aefacf23273d4efd1154ef958a27eed9c177c4cc09d2d4ba398fb47f/protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d", size = 394159, upload_time = "2024-01-10T19:37:28.932Z" }, + { url = "https://files.pythonhosted.org/packages/23/17/405ba44f60a693dfe96c7a18e843707cffa0fcfad80bd8fc4f227f499ea5/protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62", size = 293698, upload_time = "2024-01-10T19:37:30.666Z" }, + { url = "https://files.pythonhosted.org/packages/81/9e/63501b8d5b4e40c7260049836bd15ec3270c936e83bc57b85e4603cc212c/protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020", size = 294609, upload_time = "2024-01-10T19:37:32.777Z" }, + { url = "https://files.pythonhosted.org/packages/ff/52/5d23df1fe3b368133ec3e2436fb3dd4ccedf44c8d5ac7f4a88087c75180b/protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830", size = 156463, upload_time = "2024-01-10T19:37:41.24Z" }, ] [[package]] name = "psutil" version = "5.9.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a0/d0/c9ae661a302931735237791f04cb7086ac244377f78692ba3b3eae3a9619/psutil-5.9.7.tar.gz", hash = "sha256:3f02134e82cfb5d089fddf20bb2e03fd5cd52395321d1c8458a9e58500ff417c", size = 498429 } +sdist = { url = "https://files.pythonhosted.org/packages/a0/d0/c9ae661a302931735237791f04cb7086ac244377f78692ba3b3eae3a9619/psutil-5.9.7.tar.gz", hash = "sha256:3f02134e82cfb5d089fddf20bb2e03fd5cd52395321d1c8458a9e58500ff417c", size = 498429, upload_time = "2023-12-17T11:25:21.22Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/63/86a4ccc640b4ee1193800f57bbd20b766853c0cdbdbb248a27cdfafe6cbf/psutil-5.9.7-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ea36cc62e69a13ec52b2f625c27527f6e4479bca2b340b7a452af55b34fcbe2e", size = 245972 }, - { url = "https://files.pythonhosted.org/packages/58/80/cc6666b3968646f2d94de66bbc63d701d501f4aa04de43dd7d1f5dc477dd/psutil-5.9.7-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1132704b876e58d277168cd729d64750633d5ff0183acf5b3c986b8466cd0284", size = 282514 }, - { url = "https://files.pythonhosted.org/packages/be/fa/f1f626620e3b47e6237dcc64cb8cc1472f139e99422e5b9fa5bbcf457f48/psutil-5.9.7-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8b7f07948f1304497ce4f4684881250cd859b16d06a1dc4d7941eeb6233bfe", size = 285469 }, - { url = "https://files.pythonhosted.org/packages/7c/b8/dc6ebfc030b47cccc5f5229eeb15e64142b4782796c3ce169ccd60b4d511/psutil-5.9.7-cp37-abi3-win32.whl", hash = "sha256:c727ca5a9b2dd5193b8644b9f0c883d54f1248310023b5ad3e92036c5e2ada68", size = 248406 }, - { url = "https://files.pythonhosted.org/packages/50/28/92b74d95dd991c837813ffac0c79a581a3d129eb0fa7c1dd616d9901e0f3/psutil-5.9.7-cp37-abi3-win_amd64.whl", hash = "sha256:f37f87e4d73b79e6c5e749440c3113b81d1ee7d26f21c19c47371ddea834f414", size = 252245 }, - { url = "https://files.pythonhosted.org/packages/ba/8a/000d0e80156f0b96c55bda6c60f5ed6543d7b5e893ccab83117e50de1400/psutil-5.9.7-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:032f4f2c909818c86cea4fe2cc407f1c0f0cde8e6c6d702b28b8ce0c0d143340", size = 246739 }, + { url = "https://files.pythonhosted.org/packages/6c/63/86a4ccc640b4ee1193800f57bbd20b766853c0cdbdbb248a27cdfafe6cbf/psutil-5.9.7-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ea36cc62e69a13ec52b2f625c27527f6e4479bca2b340b7a452af55b34fcbe2e", size = 245972, upload_time = "2023-12-17T11:25:48.202Z" }, + { url = "https://files.pythonhosted.org/packages/58/80/cc6666b3968646f2d94de66bbc63d701d501f4aa04de43dd7d1f5dc477dd/psutil-5.9.7-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1132704b876e58d277168cd729d64750633d5ff0183acf5b3c986b8466cd0284", size = 282514, upload_time = "2023-12-17T11:25:51.371Z" }, + { url = "https://files.pythonhosted.org/packages/be/fa/f1f626620e3b47e6237dcc64cb8cc1472f139e99422e5b9fa5bbcf457f48/psutil-5.9.7-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8b7f07948f1304497ce4f4684881250cd859b16d06a1dc4d7941eeb6233bfe", size = 285469, upload_time = "2023-12-17T11:25:54.25Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b8/dc6ebfc030b47cccc5f5229eeb15e64142b4782796c3ce169ccd60b4d511/psutil-5.9.7-cp37-abi3-win32.whl", hash = "sha256:c727ca5a9b2dd5193b8644b9f0c883d54f1248310023b5ad3e92036c5e2ada68", size = 248406, upload_time = "2023-12-17T12:38:50.326Z" }, + { url = "https://files.pythonhosted.org/packages/50/28/92b74d95dd991c837813ffac0c79a581a3d129eb0fa7c1dd616d9901e0f3/psutil-5.9.7-cp37-abi3-win_amd64.whl", hash = "sha256:f37f87e4d73b79e6c5e749440c3113b81d1ee7d26f21c19c47371ddea834f414", size = 252245, upload_time = "2023-12-17T12:39:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8a/000d0e80156f0b96c55bda6c60f5ed6543d7b5e893ccab83117e50de1400/psutil-5.9.7-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:032f4f2c909818c86cea4fe2cc407f1c0f0cde8e6c6d702b28b8ce0c0d143340", size = 246739, upload_time = "2023-12-17T11:25:57.305Z" }, ] [[package]] name = "pycparser" version = "2.21" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/0b/95d387f5f4433cb0f53ff7ad859bd2c6051051cebbb564f139a999ab46de/pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206", size = 170877 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/0b/95d387f5f4433cb0f53ff7ad859bd2c6051051cebbb564f139a999ab46de/pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206", size = 170877, upload_time = "2021-11-06T12:48:46.095Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/d5/5f610ebe421e85889f2e55e33b7f9a6795bd982198517d912eb1c76e1a53/pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", size = 118697 }, + { url = "https://files.pythonhosted.org/packages/62/d5/5f610ebe421e85889f2e55e33b7f9a6795bd982198517d912eb1c76e1a53/pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", size = 118697, upload_time = "2021-11-06T12:50:13.61Z" }, ] [[package]] @@ -1797,9 +1797,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 } +sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513, upload_time = "2025-04-08T13:27:06.399Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 }, + { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591, upload_time = "2025-04-08T13:27:03.789Z" }, ] [[package]] @@ -1809,124 +1809,125 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 } +sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395, upload_time = "2025-04-02T09:49:41.8Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021 }, - { url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742 }, - { url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414 }, - { url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848 }, - { url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055 }, - { url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806 }, - { url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777 }, - { url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803 }, - { url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755 }, - { url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358 }, - { url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916 }, - { url = "https://files.pythonhosted.org/packages/7d/ba/91eea2047e681a6853c81c20aeca9dcdaa5402ccb7404a2097c2adf9d038/pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383", size = 1923823 }, - { url = "https://files.pythonhosted.org/packages/94/c0/fcdf739bf60d836a38811476f6ecd50374880b01e3014318b6e809ddfd52/pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504", size = 1952494 }, - { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224 }, - { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845 }, - { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029 }, - { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784 }, - { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075 }, - { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849 }, - { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794 }, - { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237 }, - { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351 }, - { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914 }, - { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385 }, - { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765 }, - { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688 }, - { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185 }, - { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 }, - { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 }, - { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 }, - { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 }, - { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 }, - { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 }, - { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 }, - { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 }, - { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 }, - { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 }, - { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 }, - { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 }, - { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 }, - { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 }, - { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 }, - { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 }, - { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 }, - { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 }, - { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 }, - { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 }, - { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 }, - { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 }, - { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 }, - { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 }, - { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 }, - { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 }, - { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 }, - { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 }, - { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 }, - { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 }, - { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 }, - { url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659 }, - { url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294 }, - { url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771 }, - { url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558 }, - { url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038 }, - { url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315 }, - { url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063 }, - { url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631 }, - { url = "https://files.pythonhosted.org/packages/13/e0/bafa46476d328e4553b85ab9b2f7409e7aaef0ce4c937c894821c542d347/pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c", size = 2080877 }, - { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858 }, - { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745 }, - { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188 }, - { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479 }, - { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415 }, - { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623 }, - { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175 }, - { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674 }, - { url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951 }, + { url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021, upload_time = "2025-04-02T09:46:45.065Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742, upload_time = "2025-04-02T09:46:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414, upload_time = "2025-04-02T09:46:48.263Z" }, + { url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848, upload_time = "2025-04-02T09:46:49.441Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055, upload_time = "2025-04-02T09:46:50.602Z" }, + { url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806, upload_time = "2025-04-02T09:46:52.116Z" }, + { url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777, upload_time = "2025-04-02T09:46:53.675Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803, upload_time = "2025-04-02T09:46:55.789Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755, upload_time = "2025-04-02T09:46:56.956Z" }, + { url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358, upload_time = "2025-04-02T09:46:58.445Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916, upload_time = "2025-04-02T09:46:59.726Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ba/91eea2047e681a6853c81c20aeca9dcdaa5402ccb7404a2097c2adf9d038/pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383", size = 1923823, upload_time = "2025-04-02T09:47:01.278Z" }, + { url = "https://files.pythonhosted.org/packages/94/c0/fcdf739bf60d836a38811476f6ecd50374880b01e3014318b6e809ddfd52/pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504", size = 1952494, upload_time = "2025-04-02T09:47:02.976Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224, upload_time = "2025-04-02T09:47:04.199Z" }, + { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845, upload_time = "2025-04-02T09:47:05.686Z" }, + { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029, upload_time = "2025-04-02T09:47:07.042Z" }, + { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784, upload_time = "2025-04-02T09:47:08.63Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075, upload_time = "2025-04-02T09:47:10.267Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849, upload_time = "2025-04-02T09:47:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794, upload_time = "2025-04-02T09:47:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237, upload_time = "2025-04-02T09:47:14.355Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351, upload_time = "2025-04-02T09:47:15.676Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914, upload_time = "2025-04-02T09:47:17Z" }, + { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385, upload_time = "2025-04-02T09:47:18.631Z" }, + { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765, upload_time = "2025-04-02T09:47:20.34Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688, upload_time = "2025-04-02T09:47:22.029Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185, upload_time = "2025-04-02T09:47:23.385Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640, upload_time = "2025-04-02T09:47:25.394Z" }, + { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649, upload_time = "2025-04-02T09:47:27.417Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472, upload_time = "2025-04-02T09:47:29.006Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509, upload_time = "2025-04-02T09:47:33.464Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702, upload_time = "2025-04-02T09:47:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428, upload_time = "2025-04-02T09:47:37.315Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753, upload_time = "2025-04-02T09:47:39.013Z" }, + { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849, upload_time = "2025-04-02T09:47:40.427Z" }, + { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541, upload_time = "2025-04-02T09:47:42.01Z" }, + { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225, upload_time = "2025-04-02T09:47:43.425Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373, upload_time = "2025-04-02T09:47:44.979Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034, upload_time = "2025-04-02T09:47:46.843Z" }, + { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848, upload_time = "2025-04-02T09:47:48.404Z" }, + { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986, upload_time = "2025-04-02T09:47:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551, upload_time = "2025-04-02T09:47:51.648Z" }, + { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785, upload_time = "2025-04-02T09:47:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758, upload_time = "2025-04-02T09:47:55.006Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109, upload_time = "2025-04-02T09:47:56.532Z" }, + { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159, upload_time = "2025-04-02T09:47:58.088Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222, upload_time = "2025-04-02T09:47:59.591Z" }, + { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980, upload_time = "2025-04-02T09:48:01.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840, upload_time = "2025-04-02T09:48:03.056Z" }, + { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518, upload_time = "2025-04-02T09:48:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025, upload_time = "2025-04-02T09:48:06.226Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991, upload_time = "2025-04-02T09:48:08.114Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262, upload_time = "2025-04-02T09:48:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626, upload_time = "2025-04-02T09:48:11.288Z" }, + { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590, upload_time = "2025-04-02T09:48:12.861Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963, upload_time = "2025-04-02T09:48:14.553Z" }, + { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896, upload_time = "2025-04-02T09:48:16.222Z" }, + { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810, upload_time = "2025-04-02T09:48:17.97Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659, upload_time = "2025-04-02T09:48:45.342Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294, upload_time = "2025-04-02T09:48:47.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771, upload_time = "2025-04-02T09:48:49.468Z" }, + { url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558, upload_time = "2025-04-02T09:48:51.409Z" }, + { url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038, upload_time = "2025-04-02T09:48:53.702Z" }, + { url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315, upload_time = "2025-04-02T09:48:55.555Z" }, + { url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063, upload_time = "2025-04-02T09:48:57.479Z" }, + { url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631, upload_time = "2025-04-02T09:48:59.581Z" }, + { url = "https://files.pythonhosted.org/packages/13/e0/bafa46476d328e4553b85ab9b2f7409e7aaef0ce4c937c894821c542d347/pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c", size = 2080877, upload_time = "2025-04-02T09:49:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858, upload_time = "2025-04-02T09:49:03.419Z" }, + { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745, upload_time = "2025-04-02T09:49:05.391Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188, upload_time = "2025-04-02T09:49:07.352Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479, upload_time = "2025-04-02T09:49:09.304Z" }, + { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415, upload_time = "2025-04-02T09:49:11.25Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623, upload_time = "2025-04-02T09:49:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175, upload_time = "2025-04-02T09:49:15.597Z" }, + { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674, upload_time = "2025-04-02T09:49:17.61Z" }, + { url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951, upload_time = "2025-04-02T09:49:19.559Z" }, ] [[package]] name = "pydantic-settings" -version = "2.8.1" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload_time = "2025-04-18T16:44:48.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload_time = "2025-04-18T16:44:46.617Z" }, ] [[package]] name = "pygments" version = "2.17.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/55/59/8bccf4157baf25e4aa5a0bb7fa3ba8600907de105ebc22b0c78cfbf6f565/pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367", size = 4827772 } +sdist = { url = "https://files.pythonhosted.org/packages/55/59/8bccf4157baf25e4aa5a0bb7fa3ba8600907de105ebc22b0c78cfbf6f565/pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367", size = 4827772, upload_time = "2023-11-21T20:43:53.875Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", size = 1179756 }, + { url = "https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", size = 1179756, upload_time = "2023-11-21T20:43:49.423Z" }, ] [[package]] name = "pyparsing" version = "3.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/fe/65c989f70bd630b589adfbbcd6ed238af22319e90f059946c26b4835e44b/pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db", size = 884814 } +sdist = { url = "https://files.pythonhosted.org/packages/37/fe/65c989f70bd630b589adfbbcd6ed238af22319e90f059946c26b4835e44b/pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db", size = 884814, upload_time = "2023-07-30T15:07:02.617Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/92/8486ede85fcc088f1b3dba4ce92dd29d126fd96b0008ea213167940a2475/pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb", size = 103139 }, + { url = "https://files.pythonhosted.org/packages/39/92/8486ede85fcc088f1b3dba4ce92dd29d126fd96b0008ea213167940a2475/pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb", size = 103139, upload_time = "2023-07-30T15:06:59.829Z" }, ] [[package]] name = "pyreadline3" version = "3.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/86/3d61a61f36a0067874a00cb4dceb9028d34b6060e47828f7fc86fb9f7ee9/pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae", size = 86465 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/86/3d61a61f36a0067874a00cb4dceb9028d34b6060e47828f7fc86fb9f7ee9/pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae", size = 86465, upload_time = "2022-01-24T20:05:11.66Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/fc/a3c13ded7b3057680c8ae95a9b6cc83e63657c38e0005c400a5d018a33a7/pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb", size = 95203 }, + { url = "https://files.pythonhosted.org/packages/56/fc/a3c13ded7b3057680c8ae95a9b6cc83e63657c38e0005c400a5d018a33a7/pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb", size = 95203, upload_time = "2022-01-24T20:05:10.442Z" }, ] [[package]] @@ -1941,9 +1942,9 @@ dependencies = [ { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload_time = "2025-03-02T12:54:54.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload_time = "2025-03-02T12:54:52.069Z" }, ] [[package]] @@ -1953,9 +1954,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156, upload_time = "2025-03-25T06:22:28.883Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694 }, + { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload_time = "2025-03-25T06:22:27.807Z" }, ] [[package]] @@ -1966,9 +1967,9 @@ dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload_time = "2025-04-05T14:07:51.592Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload_time = "2025-04-05T14:07:49.641Z" }, ] [[package]] @@ -1978,9 +1979,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814, upload_time = "2024-03-21T22:14:04.964Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, + { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863, upload_time = "2024-03-21T22:14:02.694Z" }, ] [[package]] @@ -1990,27 +1991,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/c4/13b4776ea2d76c115c1d1b84579f3764ee6d57204f6be27119f13a61d0a9/python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", size = 357324 } +sdist = { url = "https://files.pythonhosted.org/packages/4c/c4/13b4776ea2d76c115c1d1b84579f3764ee6d57204f6be27119f13a61d0a9/python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", size = 357324, upload_time = "2021-07-14T08:19:19.783Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/7a/87837f39d0296e723bb9b62bbb257d0355c7f6128853c78955f57342a56d/python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9", size = 247702 }, + { url = "https://files.pythonhosted.org/packages/36/7a/87837f39d0296e723bb9b62bbb257d0355c7f6128853c78955f57342a56d/python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9", size = 247702, upload_time = "2021-07-14T08:19:18.161Z" }, ] [[package]] name = "python-dotenv" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/06/1ef763af20d0572c032fa22882cfbfb005fba6e7300715a37840858c919e/python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", size = 37399 } +sdist = { url = "https://files.pythonhosted.org/packages/31/06/1ef763af20d0572c032fa22882cfbfb005fba6e7300715a37840858c919e/python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", size = 37399, upload_time = "2023-02-24T06:46:37.282Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/2f/62ea1c8b593f4e093cc1a7768f0d46112107e790c3e478532329e434f00b/python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a", size = 19482 }, + { url = "https://files.pythonhosted.org/packages/44/2f/62ea1c8b593f4e093cc1a7768f0d46112107e790c3e478532329e434f00b/python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a", size = 19482, upload_time = "2023-02-24T06:46:36.009Z" }, ] [[package]] name = "python-multipart" version = "0.0.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload_time = "2024-12-16T19:45:46.972Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload_time = "2024-12-16T19:45:44.423Z" }, ] [[package]] @@ -2018,45 +2019,45 @@ name = "pywin32" version = "306" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/dc/28c668097edfaf4eac4617ef7adf081b9cf50d254672fcf399a70f5efc41/pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d", size = 8506422 }, - { url = "https://files.pythonhosted.org/packages/d3/d6/891894edec688e72c2e308b3243fad98b4066e1839fd2fe78f04129a9d31/pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8", size = 9226392 }, - { url = "https://files.pythonhosted.org/packages/8b/1e/fc18ad83ca553e01b97aa8393ff10e33c1fb57801db05488b83282ee9913/pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407", size = 8507689 }, - { url = "https://files.pythonhosted.org/packages/7e/9e/ad6b1ae2a5ad1066dc509350e0fbf74d8d50251a51e420a2a8feaa0cecbd/pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e", size = 9227547 }, - { url = "https://files.pythonhosted.org/packages/91/20/f744bff1da8f43388498503634378dbbefbe493e65675f2cc52f7185c2c2/pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a", size = 10388324 }, - { url = "https://files.pythonhosted.org/packages/14/91/17e016d5923e178346aabda3dfec6629d1a26efe587d19667542105cf0a6/pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b", size = 8507705 }, - { url = "https://files.pythonhosted.org/packages/83/1c/25b79fc3ec99b19b0a0730cc47356f7e2959863bf9f3cd314332bddb4f68/pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e", size = 9227429 }, - { url = "https://files.pythonhosted.org/packages/1c/43/e3444dc9a12f8365d9603c2145d16bf0a2f8180f343cf87be47f5579e547/pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040", size = 10388145 }, + { url = "https://files.pythonhosted.org/packages/08/dc/28c668097edfaf4eac4617ef7adf081b9cf50d254672fcf399a70f5efc41/pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d", size = 8506422, upload_time = "2023-03-26T03:27:46.303Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d6/891894edec688e72c2e308b3243fad98b4066e1839fd2fe78f04129a9d31/pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8", size = 9226392, upload_time = "2023-03-26T03:27:53.591Z" }, + { url = "https://files.pythonhosted.org/packages/8b/1e/fc18ad83ca553e01b97aa8393ff10e33c1fb57801db05488b83282ee9913/pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407", size = 8507689, upload_time = "2023-03-25T23:50:08.499Z" }, + { url = "https://files.pythonhosted.org/packages/7e/9e/ad6b1ae2a5ad1066dc509350e0fbf74d8d50251a51e420a2a8feaa0cecbd/pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e", size = 9227547, upload_time = "2023-03-25T23:50:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/91/20/f744bff1da8f43388498503634378dbbefbe493e65675f2cc52f7185c2c2/pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a", size = 10388324, upload_time = "2023-03-25T23:50:30.904Z" }, + { url = "https://files.pythonhosted.org/packages/14/91/17e016d5923e178346aabda3dfec6629d1a26efe587d19667542105cf0a6/pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b", size = 8507705, upload_time = "2023-03-25T23:50:40.279Z" }, + { url = "https://files.pythonhosted.org/packages/83/1c/25b79fc3ec99b19b0a0730cc47356f7e2959863bf9f3cd314332bddb4f68/pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e", size = 9227429, upload_time = "2023-03-25T23:50:50.222Z" }, + { url = "https://files.pythonhosted.org/packages/1c/43/e3444dc9a12f8365d9603c2145d16bf0a2f8180f343cf87be47f5579e547/pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040", size = 10388145, upload_time = "2023-03-25T23:51:01.401Z" }, ] [[package]] name = "pyyaml" version = "6.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", size = 125201 } +sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", size = 125201, upload_time = "2023-07-18T00:00:23.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/06/4beb652c0fe16834032e54f0956443d4cc797fe645527acee59e7deaa0a2/PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", size = 189447 }, - { url = "https://files.pythonhosted.org/packages/5b/07/10033a403b23405a8fc48975444463d3d10a5c2736b7eb2550b07b367429/PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f", size = 169264 }, - { url = "https://files.pythonhosted.org/packages/f1/26/55e4f21db1f72eaef092015d9017c11510e7e6301c62a6cfee91295d13c6/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", size = 677003 }, - { url = "https://files.pythonhosted.org/packages/ba/91/090818dfa62e85181f3ae23dd1e8b7ea7f09684864a900cab72d29c57346/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", size = 699070 }, - { url = "https://files.pythonhosted.org/packages/29/61/bf33c6c85c55bc45a29eee3195848ff2d518d84735eb0e2d8cb42e0d285e/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", size = 705525 }, - { url = "https://files.pythonhosted.org/packages/07/91/45dfd0ef821a7f41d9d0136ea3608bb5b1653e42fd56a7970532cb5c003f/PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", size = 707514 }, - { url = "https://files.pythonhosted.org/packages/b6/a0/b6700da5d49e9fed49dc3243d3771b598dad07abb37cc32e524607f96adc/PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", size = 130488 }, - { url = "https://files.pythonhosted.org/packages/24/97/9b59b43431f98d01806b288532da38099cc6f2fea0f3d712e21e269c0279/PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", size = 145338 }, - { url = "https://files.pythonhosted.org/packages/ec/0d/26fb23e8863e0aeaac0c64e03fd27367ad2ae3f3cccf3798ee98ce160368/PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", size = 187867 }, - { url = "https://files.pythonhosted.org/packages/28/09/55f715ddbf95a054b764b547f617e22f1d5e45d83905660e9a088078fe67/PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", size = 167530 }, - { url = "https://files.pythonhosted.org/packages/5e/94/7d5ee059dfb92ca9e62f4057dcdec9ac08a9e42679644854dc01177f8145/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", size = 732244 }, - { url = "https://files.pythonhosted.org/packages/06/92/e0224aa6ebf9dc54a06a4609da37da40bb08d126f5535d81bff6b417b2ae/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", size = 752871 }, - { url = "https://files.pythonhosted.org/packages/7b/5e/efd033ab7199a0b2044dab3b9f7a4f6670e6a52c089de572e928d2873b06/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", size = 757729 }, - { url = "https://files.pythonhosted.org/packages/03/5c/c4671451b2f1d76ebe352c0945d4cd13500adb5d05f5a51ee296d80152f7/PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", size = 748528 }, - { url = "https://files.pythonhosted.org/packages/73/9c/766e78d1efc0d1fca637a6b62cea1b4510a7fb93617eb805223294fef681/PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", size = 130286 }, - { url = "https://files.pythonhosted.org/packages/b3/34/65bb4b2d7908044963ebf614fe0fdb080773fc7030d7e39c8d3eddcd4257/PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", size = 144699 }, - { url = "https://files.pythonhosted.org/packages/bc/06/1b305bf6aa704343be85444c9d011f626c763abb40c0edc1cad13bfd7f86/PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", size = 178692 }, - { url = "https://files.pythonhosted.org/packages/84/02/404de95ced348b73dd84f70e15a41843d817ff8c1744516bf78358f2ffd2/PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", size = 165622 }, - { url = "https://files.pythonhosted.org/packages/c7/4c/4a2908632fc980da6d918b9de9c1d9d7d7e70b2672b1ad5166ed27841ef7/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", size = 696937 }, - { url = "https://files.pythonhosted.org/packages/b4/33/720548182ffa8344418126017aa1d4ab4aeec9a2275f04ce3f3573d8ace8/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", size = 724969 }, - { url = "https://files.pythonhosted.org/packages/4f/78/77b40157b6cb5f2d3d31a3d9b2efd1ba3505371f76730d267e8b32cf4b7f/PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", size = 712604 }, - { url = "https://files.pythonhosted.org/packages/2e/97/3e0e089ee85e840f4b15bfa00e4e63d84a3691ababbfea92d6f820ea6f21/PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", size = 126098 }, - { url = "https://files.pythonhosted.org/packages/2b/9f/fbade56564ad486809c27b322d0f7e6a89c01f6b4fe208402e90d4443a99/PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", size = 138675 }, + { url = "https://files.pythonhosted.org/packages/96/06/4beb652c0fe16834032e54f0956443d4cc797fe645527acee59e7deaa0a2/PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", size = 189447, upload_time = "2023-07-17T23:57:04.325Z" }, + { url = "https://files.pythonhosted.org/packages/5b/07/10033a403b23405a8fc48975444463d3d10a5c2736b7eb2550b07b367429/PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f", size = 169264, upload_time = "2023-07-17T23:57:07.787Z" }, + { url = "https://files.pythonhosted.org/packages/f1/26/55e4f21db1f72eaef092015d9017c11510e7e6301c62a6cfee91295d13c6/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", size = 677003, upload_time = "2023-07-17T23:57:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/090818dfa62e85181f3ae23dd1e8b7ea7f09684864a900cab72d29c57346/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", size = 699070, upload_time = "2023-07-17T23:57:19.402Z" }, + { url = "https://files.pythonhosted.org/packages/29/61/bf33c6c85c55bc45a29eee3195848ff2d518d84735eb0e2d8cb42e0d285e/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", size = 705525, upload_time = "2023-07-17T23:57:25.272Z" }, + { url = "https://files.pythonhosted.org/packages/07/91/45dfd0ef821a7f41d9d0136ea3608bb5b1653e42fd56a7970532cb5c003f/PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", size = 707514, upload_time = "2023-08-28T18:43:20.945Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a0/b6700da5d49e9fed49dc3243d3771b598dad07abb37cc32e524607f96adc/PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", size = 130488, upload_time = "2023-07-17T23:57:28.144Z" }, + { url = "https://files.pythonhosted.org/packages/24/97/9b59b43431f98d01806b288532da38099cc6f2fea0f3d712e21e269c0279/PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", size = 145338, upload_time = "2023-07-17T23:57:31.118Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0d/26fb23e8863e0aeaac0c64e03fd27367ad2ae3f3cccf3798ee98ce160368/PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", size = 187867, upload_time = "2023-07-17T23:57:34.35Z" }, + { url = "https://files.pythonhosted.org/packages/28/09/55f715ddbf95a054b764b547f617e22f1d5e45d83905660e9a088078fe67/PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", size = 167530, upload_time = "2023-07-17T23:57:36.975Z" }, + { url = "https://files.pythonhosted.org/packages/5e/94/7d5ee059dfb92ca9e62f4057dcdec9ac08a9e42679644854dc01177f8145/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", size = 732244, upload_time = "2023-07-17T23:57:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/06/92/e0224aa6ebf9dc54a06a4609da37da40bb08d126f5535d81bff6b417b2ae/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", size = 752871, upload_time = "2023-07-17T23:57:51.921Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/efd033ab7199a0b2044dab3b9f7a4f6670e6a52c089de572e928d2873b06/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", size = 757729, upload_time = "2023-07-17T23:57:59.865Z" }, + { url = "https://files.pythonhosted.org/packages/03/5c/c4671451b2f1d76ebe352c0945d4cd13500adb5d05f5a51ee296d80152f7/PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", size = 748528, upload_time = "2023-08-28T18:43:23.207Z" }, + { url = "https://files.pythonhosted.org/packages/73/9c/766e78d1efc0d1fca637a6b62cea1b4510a7fb93617eb805223294fef681/PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", size = 130286, upload_time = "2023-07-17T23:58:02.964Z" }, + { url = "https://files.pythonhosted.org/packages/b3/34/65bb4b2d7908044963ebf614fe0fdb080773fc7030d7e39c8d3eddcd4257/PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", size = 144699, upload_time = "2023-07-17T23:58:05.586Z" }, + { url = "https://files.pythonhosted.org/packages/bc/06/1b305bf6aa704343be85444c9d011f626c763abb40c0edc1cad13bfd7f86/PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", size = 178692, upload_time = "2023-08-28T18:43:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/84/02/404de95ced348b73dd84f70e15a41843d817ff8c1744516bf78358f2ffd2/PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", size = 165622, upload_time = "2023-08-28T18:43:26.54Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4c/4a2908632fc980da6d918b9de9c1d9d7d7e70b2672b1ad5166ed27841ef7/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", size = 696937, upload_time = "2024-01-18T20:40:22.92Z" }, + { url = "https://files.pythonhosted.org/packages/b4/33/720548182ffa8344418126017aa1d4ab4aeec9a2275f04ce3f3573d8ace8/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", size = 724969, upload_time = "2023-08-28T18:43:28.56Z" }, + { url = "https://files.pythonhosted.org/packages/4f/78/77b40157b6cb5f2d3d31a3d9b2efd1ba3505371f76730d267e8b32cf4b7f/PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", size = 712604, upload_time = "2023-08-28T18:43:30.206Z" }, + { url = "https://files.pythonhosted.org/packages/2e/97/3e0e089ee85e840f4b15bfa00e4e63d84a3691ababbfea92d6f820ea6f21/PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", size = 126098, upload_time = "2023-08-28T18:43:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/fbade56564ad486809c27b322d0f7e6a89c01f6b4fe208402e90d4443a99/PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", size = 138675, upload_time = "2023-08-28T18:43:33.613Z" }, ] [[package]] @@ -2066,46 +2067,46 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/33/1a3683fc9a4bd64d8ccc0290da75c8f042184a1a49c146d28398414d3341/pyzmq-25.1.2.tar.gz", hash = "sha256:93f1aa311e8bb912e34f004cf186407a4e90eec4f0ecc0efd26056bf7eda0226", size = 1402339 } +sdist = { url = "https://files.pythonhosted.org/packages/3a/33/1a3683fc9a4bd64d8ccc0290da75c8f042184a1a49c146d28398414d3341/pyzmq-25.1.2.tar.gz", hash = "sha256:93f1aa311e8bb912e34f004cf186407a4e90eec4f0ecc0efd26056bf7eda0226", size = 1402339, upload_time = "2023-12-05T07:34:47.976Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f4/901edb48b2b2c00ad73de0db2ee76e24ce5903ef815ad0ad10e14555d989/pyzmq-25.1.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:e624c789359f1a16f83f35e2c705d07663ff2b4d4479bad35621178d8f0f6ea4", size = 1872310 }, - { url = "https://files.pythonhosted.org/packages/5e/46/2de69c7c79fd78bf4c22a9e8165fa6312f5d49410f1be6ddab51a6fe7236/pyzmq-25.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49151b0efece79f6a79d41a461d78535356136ee70084a1c22532fc6383f4ad0", size = 1249619 }, - { url = "https://files.pythonhosted.org/packages/d1/f5/d6b9755713843bf9701ae86bf6fd97ec294a52cf2af719cd14fdf9392f65/pyzmq-25.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9a5f194cf730f2b24d6af1f833c14c10f41023da46a7f736f48b6d35061e76e", size = 897360 }, - { url = "https://files.pythonhosted.org/packages/7c/88/c1aef8820f12e710d136024d231e70e24684a01314aa1814f0758960ba01/pyzmq-25.1.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:faf79a302f834d9e8304fafdc11d0d042266667ac45209afa57e5efc998e3872", size = 1156959 }, - { url = "https://files.pythonhosted.org/packages/82/1b/b25d2c4ac3b4dae238c98e63395dbb88daf11968b168948d3c6289c3e95c/pyzmq-25.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f51a7b4ead28d3fca8dda53216314a553b0f7a91ee8fc46a72b402a78c3e43d", size = 1100585 }, - { url = "https://files.pythonhosted.org/packages/67/bf/6bc0977acd934b66eacab79cec303ecf08ae4a6150d57c628aa919615488/pyzmq-25.1.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0ddd6d71d4ef17ba5a87becf7ddf01b371eaba553c603477679ae817a8d84d75", size = 1109267 }, - { url = "https://files.pythonhosted.org/packages/64/fb/4f07424e56c6a5fb47306d9ba744c3c250250c2e7272f9c81efbf8daaccf/pyzmq-25.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:246747b88917e4867e2367b005fc8eefbb4a54b7db363d6c92f89d69abfff4b6", size = 1431853 }, - { url = "https://files.pythonhosted.org/packages/a2/10/2b88c1d4beb59a1d45c13983c4b7c5dcd6ef7988db3c03d23b0cabc5adca/pyzmq-25.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:00c48ae2fd81e2a50c3485de1b9d5c7c57cd85dc8ec55683eac16846e57ac979", size = 1766212 }, - { url = "https://files.pythonhosted.org/packages/bc/ab/c9a22eacfd5bd82620501ae426a3dd6ffa374ac335b21e54209d7a93d3fb/pyzmq-25.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a68d491fc20762b630e5db2191dd07ff89834086740f70e978bb2ef2668be08", size = 1653737 }, - { url = "https://files.pythonhosted.org/packages/d6/e5/71bd89e47eedb7ebec31ef9a49dcdb0517dbbb063bd5de363980a6911eb1/pyzmq-25.1.2-cp310-cp310-win32.whl", hash = "sha256:09dfe949e83087da88c4a76767df04b22304a682d6154de2c572625c62ad6886", size = 906288 }, - { url = "https://files.pythonhosted.org/packages/9d/5f/2defc8a579e8b5679d92720ab3a4cb93e3a77d923070bf4c1a103d3ae478/pyzmq-25.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:fa99973d2ed20417744fca0073390ad65ce225b546febb0580358e36aa90dba6", size = 1170923 }, - { url = "https://files.pythonhosted.org/packages/35/de/7579518bc58cebf92568b48e354a702fb52525d0fab166dc544f2a0615dc/pyzmq-25.1.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:82544e0e2d0c1811482d37eef297020a040c32e0687c1f6fc23a75b75db8062c", size = 1870360 }, - { url = "https://files.pythonhosted.org/packages/ce/f9/58b6cc9a110b1832f666fa6b5a67dc4d520fabfc680ca87a8167b2061d5d/pyzmq-25.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01171fc48542348cd1a360a4b6c3e7d8f46cdcf53a8d40f84db6707a6768acc1", size = 1249008 }, - { url = "https://files.pythonhosted.org/packages/bc/4a/ac6469c01813cb3652ab4e30ec4a37815cc9949afc18af33f64e2ec704aa/pyzmq-25.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc69c96735ab501419c432110016329bf0dea8898ce16fab97c6d9106dc0b348", size = 904394 }, - { url = "https://files.pythonhosted.org/packages/77/b7/8cee519b11bdd3f76c1a6eb537ab13c1bfef2964d725717705c86f524e4c/pyzmq-25.1.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e124e6b1dd3dfbeb695435dff0e383256655bb18082e094a8dd1f6293114642", size = 1161453 }, - { url = "https://files.pythonhosted.org/packages/b6/1d/c35a956a44b333b064ae1b1c588c2dfa0e01b7ec90884c1972bfcef119c3/pyzmq-25.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7598d2ba821caa37a0f9d54c25164a4fa351ce019d64d0b44b45540950458840", size = 1105501 }, - { url = "https://files.pythonhosted.org/packages/18/d1/b3d1e985318ed7287737ea9e6b6e21748cc7c89accc2443347cd2c8d5f0f/pyzmq-25.1.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d1299d7e964c13607efd148ca1f07dcbf27c3ab9e125d1d0ae1d580a1682399d", size = 1109513 }, - { url = "https://files.pythonhosted.org/packages/14/9b/341cdfb47440069010101403298dc24d449150370c6cb322e73bfa1949bd/pyzmq-25.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4e6f689880d5ad87918430957297c975203a082d9a036cc426648fcbedae769b", size = 1433541 }, - { url = "https://files.pythonhosted.org/packages/fa/52/c6d4e76e020c554e965459d41a98201b4d45277a288648f53a4e5a2429cc/pyzmq-25.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cc69949484171cc961e6ecd4a8911b9ce7a0d1f738fcae717177c231bf77437b", size = 1766133 }, - { url = "https://files.pythonhosted.org/packages/1d/6d/0cbd8dd5b8979fd6b9cf1852ed067b9d2cd6fa0c09c3bafe6874d2d2e03c/pyzmq-25.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9880078f683466b7f567b8624bfc16cad65077be046b6e8abb53bed4eeb82dd3", size = 1653636 }, - { url = "https://files.pythonhosted.org/packages/f5/af/d90eed9cf3840685d54d4a35d5f9e242a8a48b5410d41146f14c1e098302/pyzmq-25.1.2-cp311-cp311-win32.whl", hash = "sha256:4e5837af3e5aaa99a091302df5ee001149baff06ad22b722d34e30df5f0d9097", size = 904865 }, - { url = "https://files.pythonhosted.org/packages/20/d2/09443dc73053ad01c846d7fb77e09fe9d93c09d4e900215f3c8b7b56bfec/pyzmq-25.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:25c2dbb97d38b5ac9fd15586e048ec5eb1e38f3d47fe7d92167b0c77bb3584e9", size = 1171332 }, - { url = "https://files.pythonhosted.org/packages/6e/f0/d71cf69dc039c9adc8b625efc3bad3684f3660a570e47f0f0c64df787b41/pyzmq-25.1.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:11e70516688190e9c2db14fcf93c04192b02d457b582a1f6190b154691b4c93a", size = 1871111 }, - { url = "https://files.pythonhosted.org/packages/68/62/d365773edf56ad71993579ee574105f02f83530caf600ebf28bea15d88d0/pyzmq-25.1.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:313c3794d650d1fccaaab2df942af9f2c01d6217c846177cfcbc693c7410839e", size = 1248844 }, - { url = "https://files.pythonhosted.org/packages/72/55/cc3163e20f40615a49245fa7041badec6103e8ee7e482dbb0feea00a7b84/pyzmq-25.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b3cbba2f47062b85fe0ef9de5b987612140a9ba3a9c6d2543c6dec9f7c2ab27", size = 899373 }, - { url = "https://files.pythonhosted.org/packages/40/aa/ae292bd85deda637230970bbc53c1dc53696a99e82fc7cd6d373ec173853/pyzmq-25.1.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc31baa0c32a2ca660784d5af3b9487e13b61b3032cb01a115fce6588e1bed30", size = 1160901 }, - { url = "https://files.pythonhosted.org/packages/93/b7/6e291eafbbbc66d0e87658dd21383ec2b4ab35edcfb283902c580a6db76f/pyzmq-25.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c9087b109070c5ab0b383079fa1b5f797f8d43e9a66c07a4b8b8bdecfd88ee", size = 1101147 }, - { url = "https://files.pythonhosted.org/packages/3a/f1/e296d5a507eac519d1fe1382851b1a4575f690bc2b2d2c8eca2ed7e4bd1f/pyzmq-25.1.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f8429b17cbb746c3e043cb986328da023657e79d5ed258b711c06a70c2ea7537", size = 1105315 }, - { url = "https://files.pythonhosted.org/packages/56/63/5c2abb556ab4cf013d98e01782d5bd642238a0ed9b019e965a7d7e957f56/pyzmq-25.1.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5074adeacede5f810b7ef39607ee59d94e948b4fd954495bdb072f8c54558181", size = 1427747 }, - { url = "https://files.pythonhosted.org/packages/b1/71/5dba5f6b12ef54fb977c9b9279075e151c04fc0dd6851e9663d9e66b593f/pyzmq-25.1.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7ae8f354b895cbd85212da245f1a5ad8159e7840e37d78b476bb4f4c3f32a9fe", size = 1762221 }, - { url = "https://files.pythonhosted.org/packages/cf/49/54d7e8bb3df82a3509325b11491d33450dc91580d4826b62fa5e554bb9cf/pyzmq-25.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b264bf2cc96b5bc43ce0e852be995e400376bd87ceb363822e2cb1964fcdc737", size = 1649505 }, - { url = "https://files.pythonhosted.org/packages/34/14/58e5037229bc37963e2ce804c2c075a3a541e3f84bf1c231e7c9779d36f1/pyzmq-25.1.2-cp312-cp312-win32.whl", hash = "sha256:02bbc1a87b76e04fd780b45e7f695471ae6de747769e540da909173d50ff8e2d", size = 954891 }, - { url = "https://files.pythonhosted.org/packages/2c/2d/04fab685ef3a8e6e955220fd2a54dc99efaee960a88675bf5c92cd277164/pyzmq-25.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:ced111c2e81506abd1dc142e6cd7b68dd53747b3b7ae5edbea4578c5eeff96b7", size = 1252773 }, - { url = "https://files.pythonhosted.org/packages/6b/fe/ed38fe12c540bafc1cae32c3ff638e9df32528f5cf91b5e400e6a8f5b3ec/pyzmq-25.1.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a8c1d566344aee826b74e472e16edae0a02e2a044f14f7c24e123002dcff1c05", size = 963654 }, - { url = "https://files.pythonhosted.org/packages/44/97/a760a2dff0672c408f22f726f2ea10a7a516ffa5001ca5a3641e355a45f9/pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759cfd391a0996345ba94b6a5110fca9c557ad4166d86a6e81ea526c376a01e8", size = 609436 }, - { url = "https://files.pythonhosted.org/packages/41/81/ace39daa19c78b2f4fc12ef217d9d5f1ac658d5828d692bbbb68240cd55b/pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c61e346ac34b74028ede1c6b4bcecf649d69b707b3ff9dc0fab453821b04d1e", size = 843396 }, - { url = "https://files.pythonhosted.org/packages/4c/43/150b0b203f5461a9aeadaa925c55167e2b4215c9322b6911a64360d2243e/pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cb8fc1f8d69b411b8ec0b5f1ffbcaf14c1db95b6bccea21d83610987435f1a4", size = 800856 }, - { url = "https://files.pythonhosted.org/packages/5f/91/a618b56aaabe40dddcd25db85624d7408768fd32f5bfcf81bc0af5b1ce75/pyzmq-25.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3c00c9b7d1ca8165c610437ca0c92e7b5607b2f9076f4eb4b095c85d6e680a1d", size = 413836 }, + { url = "https://files.pythonhosted.org/packages/5e/f4/901edb48b2b2c00ad73de0db2ee76e24ce5903ef815ad0ad10e14555d989/pyzmq-25.1.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:e624c789359f1a16f83f35e2c705d07663ff2b4d4479bad35621178d8f0f6ea4", size = 1872310, upload_time = "2023-12-05T07:48:13.713Z" }, + { url = "https://files.pythonhosted.org/packages/5e/46/2de69c7c79fd78bf4c22a9e8165fa6312f5d49410f1be6ddab51a6fe7236/pyzmq-25.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49151b0efece79f6a79d41a461d78535356136ee70084a1c22532fc6383f4ad0", size = 1249619, upload_time = "2023-12-05T07:50:38.691Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f5/d6b9755713843bf9701ae86bf6fd97ec294a52cf2af719cd14fdf9392f65/pyzmq-25.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9a5f194cf730f2b24d6af1f833c14c10f41023da46a7f736f48b6d35061e76e", size = 897360, upload_time = "2023-12-05T07:42:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/7c/88/c1aef8820f12e710d136024d231e70e24684a01314aa1814f0758960ba01/pyzmq-25.1.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:faf79a302f834d9e8304fafdc11d0d042266667ac45209afa57e5efc998e3872", size = 1156959, upload_time = "2023-12-05T07:44:29.904Z" }, + { url = "https://files.pythonhosted.org/packages/82/1b/b25d2c4ac3b4dae238c98e63395dbb88daf11968b168948d3c6289c3e95c/pyzmq-25.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f51a7b4ead28d3fca8dda53216314a553b0f7a91ee8fc46a72b402a78c3e43d", size = 1100585, upload_time = "2023-12-05T07:45:05.518Z" }, + { url = "https://files.pythonhosted.org/packages/67/bf/6bc0977acd934b66eacab79cec303ecf08ae4a6150d57c628aa919615488/pyzmq-25.1.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0ddd6d71d4ef17ba5a87becf7ddf01b371eaba553c603477679ae817a8d84d75", size = 1109267, upload_time = "2023-12-05T07:39:51.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/fb/4f07424e56c6a5fb47306d9ba744c3c250250c2e7272f9c81efbf8daaccf/pyzmq-25.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:246747b88917e4867e2367b005fc8eefbb4a54b7db363d6c92f89d69abfff4b6", size = 1431853, upload_time = "2023-12-05T07:41:09.261Z" }, + { url = "https://files.pythonhosted.org/packages/a2/10/2b88c1d4beb59a1d45c13983c4b7c5dcd6ef7988db3c03d23b0cabc5adca/pyzmq-25.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:00c48ae2fd81e2a50c3485de1b9d5c7c57cd85dc8ec55683eac16846e57ac979", size = 1766212, upload_time = "2023-12-05T07:49:05.926Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ab/c9a22eacfd5bd82620501ae426a3dd6ffa374ac335b21e54209d7a93d3fb/pyzmq-25.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a68d491fc20762b630e5db2191dd07ff89834086740f70e978bb2ef2668be08", size = 1653737, upload_time = "2023-12-05T07:49:09.096Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e5/71bd89e47eedb7ebec31ef9a49dcdb0517dbbb063bd5de363980a6911eb1/pyzmq-25.1.2-cp310-cp310-win32.whl", hash = "sha256:09dfe949e83087da88c4a76767df04b22304a682d6154de2c572625c62ad6886", size = 906288, upload_time = "2023-12-05T07:42:05.509Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5f/2defc8a579e8b5679d92720ab3a4cb93e3a77d923070bf4c1a103d3ae478/pyzmq-25.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:fa99973d2ed20417744fca0073390ad65ce225b546febb0580358e36aa90dba6", size = 1170923, upload_time = "2023-12-05T07:44:54.296Z" }, + { url = "https://files.pythonhosted.org/packages/35/de/7579518bc58cebf92568b48e354a702fb52525d0fab166dc544f2a0615dc/pyzmq-25.1.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:82544e0e2d0c1811482d37eef297020a040c32e0687c1f6fc23a75b75db8062c", size = 1870360, upload_time = "2023-12-05T07:48:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f9/58b6cc9a110b1832f666fa6b5a67dc4d520fabfc680ca87a8167b2061d5d/pyzmq-25.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01171fc48542348cd1a360a4b6c3e7d8f46cdcf53a8d40f84db6707a6768acc1", size = 1249008, upload_time = "2023-12-05T07:50:40.442Z" }, + { url = "https://files.pythonhosted.org/packages/bc/4a/ac6469c01813cb3652ab4e30ec4a37815cc9949afc18af33f64e2ec704aa/pyzmq-25.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc69c96735ab501419c432110016329bf0dea8898ce16fab97c6d9106dc0b348", size = 904394, upload_time = "2023-12-05T07:42:27.815Z" }, + { url = "https://files.pythonhosted.org/packages/77/b7/8cee519b11bdd3f76c1a6eb537ab13c1bfef2964d725717705c86f524e4c/pyzmq-25.1.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e124e6b1dd3dfbeb695435dff0e383256655bb18082e094a8dd1f6293114642", size = 1161453, upload_time = "2023-12-05T07:44:32.003Z" }, + { url = "https://files.pythonhosted.org/packages/b6/1d/c35a956a44b333b064ae1b1c588c2dfa0e01b7ec90884c1972bfcef119c3/pyzmq-25.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7598d2ba821caa37a0f9d54c25164a4fa351ce019d64d0b44b45540950458840", size = 1105501, upload_time = "2023-12-05T07:45:07.18Z" }, + { url = "https://files.pythonhosted.org/packages/18/d1/b3d1e985318ed7287737ea9e6b6e21748cc7c89accc2443347cd2c8d5f0f/pyzmq-25.1.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d1299d7e964c13607efd148ca1f07dcbf27c3ab9e125d1d0ae1d580a1682399d", size = 1109513, upload_time = "2023-12-05T07:39:53.338Z" }, + { url = "https://files.pythonhosted.org/packages/14/9b/341cdfb47440069010101403298dc24d449150370c6cb322e73bfa1949bd/pyzmq-25.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4e6f689880d5ad87918430957297c975203a082d9a036cc426648fcbedae769b", size = 1433541, upload_time = "2023-12-05T07:41:10.786Z" }, + { url = "https://files.pythonhosted.org/packages/fa/52/c6d4e76e020c554e965459d41a98201b4d45277a288648f53a4e5a2429cc/pyzmq-25.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cc69949484171cc961e6ecd4a8911b9ce7a0d1f738fcae717177c231bf77437b", size = 1766133, upload_time = "2023-12-05T07:49:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6d/0cbd8dd5b8979fd6b9cf1852ed067b9d2cd6fa0c09c3bafe6874d2d2e03c/pyzmq-25.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9880078f683466b7f567b8624bfc16cad65077be046b6e8abb53bed4eeb82dd3", size = 1653636, upload_time = "2023-12-05T07:49:13.787Z" }, + { url = "https://files.pythonhosted.org/packages/f5/af/d90eed9cf3840685d54d4a35d5f9e242a8a48b5410d41146f14c1e098302/pyzmq-25.1.2-cp311-cp311-win32.whl", hash = "sha256:4e5837af3e5aaa99a091302df5ee001149baff06ad22b722d34e30df5f0d9097", size = 904865, upload_time = "2023-12-05T07:42:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/20/d2/09443dc73053ad01c846d7fb77e09fe9d93c09d4e900215f3c8b7b56bfec/pyzmq-25.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:25c2dbb97d38b5ac9fd15586e048ec5eb1e38f3d47fe7d92167b0c77bb3584e9", size = 1171332, upload_time = "2023-12-05T07:44:56.111Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/d71cf69dc039c9adc8b625efc3bad3684f3660a570e47f0f0c64df787b41/pyzmq-25.1.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:11e70516688190e9c2db14fcf93c04192b02d457b582a1f6190b154691b4c93a", size = 1871111, upload_time = "2023-12-05T07:48:17.868Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/d365773edf56ad71993579ee574105f02f83530caf600ebf28bea15d88d0/pyzmq-25.1.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:313c3794d650d1fccaaab2df942af9f2c01d6217c846177cfcbc693c7410839e", size = 1248844, upload_time = "2023-12-05T07:50:42.922Z" }, + { url = "https://files.pythonhosted.org/packages/72/55/cc3163e20f40615a49245fa7041badec6103e8ee7e482dbb0feea00a7b84/pyzmq-25.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b3cbba2f47062b85fe0ef9de5b987612140a9ba3a9c6d2543c6dec9f7c2ab27", size = 899373, upload_time = "2023-12-05T07:42:29.595Z" }, + { url = "https://files.pythonhosted.org/packages/40/aa/ae292bd85deda637230970bbc53c1dc53696a99e82fc7cd6d373ec173853/pyzmq-25.1.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc31baa0c32a2ca660784d5af3b9487e13b61b3032cb01a115fce6588e1bed30", size = 1160901, upload_time = "2023-12-05T07:44:33.819Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/6e291eafbbbc66d0e87658dd21383ec2b4ab35edcfb283902c580a6db76f/pyzmq-25.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c9087b109070c5ab0b383079fa1b5f797f8d43e9a66c07a4b8b8bdecfd88ee", size = 1101147, upload_time = "2023-12-05T07:45:10.058Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f1/e296d5a507eac519d1fe1382851b1a4575f690bc2b2d2c8eca2ed7e4bd1f/pyzmq-25.1.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f8429b17cbb746c3e043cb986328da023657e79d5ed258b711c06a70c2ea7537", size = 1105315, upload_time = "2023-12-05T07:39:55.851Z" }, + { url = "https://files.pythonhosted.org/packages/56/63/5c2abb556ab4cf013d98e01782d5bd642238a0ed9b019e965a7d7e957f56/pyzmq-25.1.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5074adeacede5f810b7ef39607ee59d94e948b4fd954495bdb072f8c54558181", size = 1427747, upload_time = "2023-12-05T07:41:13.219Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/5dba5f6b12ef54fb977c9b9279075e151c04fc0dd6851e9663d9e66b593f/pyzmq-25.1.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7ae8f354b895cbd85212da245f1a5ad8159e7840e37d78b476bb4f4c3f32a9fe", size = 1762221, upload_time = "2023-12-05T07:49:16.352Z" }, + { url = "https://files.pythonhosted.org/packages/cf/49/54d7e8bb3df82a3509325b11491d33450dc91580d4826b62fa5e554bb9cf/pyzmq-25.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b264bf2cc96b5bc43ce0e852be995e400376bd87ceb363822e2cb1964fcdc737", size = 1649505, upload_time = "2023-12-05T07:49:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/34/14/58e5037229bc37963e2ce804c2c075a3a541e3f84bf1c231e7c9779d36f1/pyzmq-25.1.2-cp312-cp312-win32.whl", hash = "sha256:02bbc1a87b76e04fd780b45e7f695471ae6de747769e540da909173d50ff8e2d", size = 954891, upload_time = "2023-12-05T07:42:09.208Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2d/04fab685ef3a8e6e955220fd2a54dc99efaee960a88675bf5c92cd277164/pyzmq-25.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:ced111c2e81506abd1dc142e6cd7b68dd53747b3b7ae5edbea4578c5eeff96b7", size = 1252773, upload_time = "2023-12-05T07:44:58.16Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fe/ed38fe12c540bafc1cae32c3ff638e9df32528f5cf91b5e400e6a8f5b3ec/pyzmq-25.1.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a8c1d566344aee826b74e472e16edae0a02e2a044f14f7c24e123002dcff1c05", size = 963654, upload_time = "2023-12-05T07:47:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/44/97/a760a2dff0672c408f22f726f2ea10a7a516ffa5001ca5a3641e355a45f9/pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759cfd391a0996345ba94b6a5110fca9c557ad4166d86a6e81ea526c376a01e8", size = 609436, upload_time = "2023-12-05T07:42:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/41/81/ace39daa19c78b2f4fc12ef217d9d5f1ac658d5828d692bbbb68240cd55b/pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c61e346ac34b74028ede1c6b4bcecf649d69b707b3ff9dc0fab453821b04d1e", size = 843396, upload_time = "2023-12-05T07:44:43.727Z" }, + { url = "https://files.pythonhosted.org/packages/4c/43/150b0b203f5461a9aeadaa925c55167e2b4215c9322b6911a64360d2243e/pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cb8fc1f8d69b411b8ec0b5f1ffbcaf14c1db95b6bccea21d83610987435f1a4", size = 800856, upload_time = "2023-12-05T07:45:21.117Z" }, + { url = "https://files.pythonhosted.org/packages/5f/91/a618b56aaabe40dddcd25db85624d7408768fd32f5bfcf81bc0af5b1ce75/pyzmq-25.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3c00c9b7d1ca8165c610437ca0c92e7b5607b2f9076f4eb4b095c85d6e680a1d", size = 413836, upload_time = "2023-12-05T07:53:22.583Z" }, ] [[package]] @@ -2118,9 +2119,9 @@ dependencies = [ { name = "scikit-learn" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/2d/bab8babd9dc9a9e4df6eb115540cee4322c1a74078fb6f3b3ebc452a22b3/qudida-0.0.4.tar.gz", hash = "sha256:db198e2887ab0c9aa0023e565afbff41dfb76b361f85fd5e13f780d75ba18cc8", size = 3100 } +sdist = { url = "https://files.pythonhosted.org/packages/3e/2d/bab8babd9dc9a9e4df6eb115540cee4322c1a74078fb6f3b3ebc452a22b3/qudida-0.0.4.tar.gz", hash = "sha256:db198e2887ab0c9aa0023e565afbff41dfb76b361f85fd5e13f780d75ba18cc8", size = 3100, upload_time = "2021-08-09T16:47:55.807Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/a1/a5f4bebaa31d109003909809d88aeb0d4b201463a9ea29308d9e4f9e7655/qudida-0.0.4-py3-none-any.whl", hash = "sha256:4519714c40cd0f2e6c51e1735edae8f8b19f4efe1f33be13e9d644ca5f736dd6", size = 3478 }, + { url = "https://files.pythonhosted.org/packages/f0/a1/a5f4bebaa31d109003909809d88aeb0d4b201463a9ea29308d9e4f9e7655/qudida-0.0.4-py3-none-any.whl", hash = "sha256:4519714c40cd0f2e6c51e1735edae8f8b19f4efe1f33be13e9d644ca5f736dd6", size = 3478, upload_time = "2021-08-09T16:47:54.637Z" }, ] [[package]] @@ -2133,9 +2134,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload_time = "2024-05-29T15:37:49.536Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload_time = "2024-05-29T15:37:47.027Z" }, ] [[package]] @@ -2147,9 +2148,9 @@ dependencies = [ { name = "pygments" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload_time = "2024-11-01T16:43:57.873Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload_time = "2024-11-01T16:43:55.817Z" }, ] [[package]] @@ -2162,9 +2163,9 @@ dependencies = [ { name = "ruamel-yaml" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/77/6af374a4a8cd2aee762a1fb8a3050dcf3f129134bbdc4bb6bed755c4325b/rknn_toolkit_lite2-2.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b6733689bd09a262bcb6ba4744e690dd4b37ebeac4ed427cf45242c4b4ce9a4", size = 559372 }, - { url = "https://files.pythonhosted.org/packages/9b/0c/76ff1eb09d09ce4394a6959d2343a321d28dd9e604348ffdafceafdc344c/rknn_toolkit_lite2-2.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3e4fefe355dc34a155680e4bcb9e4abb37ebc271f045ec9e0a4a3a018bc5beb", size = 569149 }, - { url = "https://files.pythonhosted.org/packages/0d/6e/8679562028051b02312212defc6e8c07248953f10dd7ad506e941b575bf3/rknn_toolkit_lite2-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37394371d1561f470c553f39869d7c35ff93405dffe3d0d72babf297a2b0aee9", size = 527457 }, + { url = "https://files.pythonhosted.org/packages/ed/77/6af374a4a8cd2aee762a1fb8a3050dcf3f129134bbdc4bb6bed755c4325b/rknn_toolkit_lite2-2.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b6733689bd09a262bcb6ba4744e690dd4b37ebeac4ed427cf45242c4b4ce9a4", size = 559372, upload_time = "2024-11-11T03:51:20.599Z" }, + { url = "https://files.pythonhosted.org/packages/9b/0c/76ff1eb09d09ce4394a6959d2343a321d28dd9e604348ffdafceafdc344c/rknn_toolkit_lite2-2.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3e4fefe355dc34a155680e4bcb9e4abb37ebc271f045ec9e0a4a3a018bc5beb", size = 569149, upload_time = "2024-11-11T03:51:22.838Z" }, + { url = "https://files.pythonhosted.org/packages/0d/6e/8679562028051b02312212defc6e8c07248953f10dd7ad506e941b575bf3/rknn_toolkit_lite2-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37394371d1561f470c553f39869d7c35ff93405dffe3d0d72babf297a2b0aee9", size = 527457, upload_time = "2024-11-11T03:51:25.456Z" }, ] [[package]] @@ -2174,78 +2175,78 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ruamel-yaml-clib", marker = "python_full_version < '3.13' and platform_python_implementation == 'CPython'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/46/f44d8be06b85bc7c4d8c95d658be2b68f27711f279bf9dd0612a5e4794f5/ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58", size = 143447 } +sdist = { url = "https://files.pythonhosted.org/packages/ea/46/f44d8be06b85bc7c4d8c95d658be2b68f27711f279bf9dd0612a5e4794f5/ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58", size = 143447, upload_time = "2025-01-06T14:08:51.334Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/36/dfc1ebc0081e6d39924a2cc53654497f967a084a436bb64402dfce4254d9/ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1", size = 117729 }, + { url = "https://files.pythonhosted.org/packages/c2/36/dfc1ebc0081e6d39924a2cc53654497f967a084a436bb64402dfce4254d9/ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1", size = 117729, upload_time = "2025-01-06T14:08:47.471Z" }, ] [[package]] name = "ruamel-yaml-clib" version = "0.2.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315 } +sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315, upload_time = "2024-10-20T10:10:56.22Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/57/40a958e863e299f0c74ef32a3bde9f2d1ea8d69669368c0c502a0997f57f/ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5", size = 131301 }, - { url = "https://files.pythonhosted.org/packages/98/a8/29a3eb437b12b95f50a6bcc3d7d7214301c6c529d8fdc227247fa84162b5/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969", size = 633728 }, - { url = "https://files.pythonhosted.org/packages/35/6d/ae05a87a3ad540259c3ad88d71275cbd1c0f2d30ae04c65dcbfb6dcd4b9f/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df", size = 722230 }, - { url = "https://files.pythonhosted.org/packages/7f/b7/20c6f3c0b656fe609675d69bc135c03aac9e3865912444be6339207b6648/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76", size = 686712 }, - { url = "https://files.pythonhosted.org/packages/cd/11/d12dbf683471f888d354dac59593873c2b45feb193c5e3e0f2ebf85e68b9/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6", size = 663936 }, - { url = "https://files.pythonhosted.org/packages/72/14/4c268f5077db5c83f743ee1daeb236269fa8577133a5cfa49f8b382baf13/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd", size = 696580 }, - { url = "https://files.pythonhosted.org/packages/30/fc/8cd12f189c6405a4c1cf37bd633aa740a9538c8e40497c231072d0fef5cf/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a", size = 663393 }, - { url = "https://files.pythonhosted.org/packages/80/29/c0a017b704aaf3cbf704989785cd9c5d5b8ccec2dae6ac0c53833c84e677/ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da", size = 100326 }, - { url = "https://files.pythonhosted.org/packages/3a/65/fa39d74db4e2d0cd252355732d966a460a41cd01c6353b820a0952432839/ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28", size = 118079 }, - { url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224 }, - { url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480 }, - { url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068 }, - { url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012 }, - { url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352 }, - { url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344 }, - { url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498 }, - { url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205 }, - { url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185 }, - { url = "https://files.pythonhosted.org/packages/48/41/e7a405afbdc26af961678474a55373e1b323605a4f5e2ddd4a80ea80f628/ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632", size = 133433 }, - { url = "https://files.pythonhosted.org/packages/ec/b0/b850385604334c2ce90e3ee1013bd911aedf058a934905863a6ea95e9eb4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d", size = 647362 }, - { url = "https://files.pythonhosted.org/packages/44/d0/3f68a86e006448fb6c005aee66565b9eb89014a70c491d70c08de597f8e4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c", size = 754118 }, - { url = "https://files.pythonhosted.org/packages/52/a9/d39f3c5ada0a3bb2870d7db41901125dbe2434fa4f12ca8c5b83a42d7c53/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd", size = 706497 }, - { url = "https://files.pythonhosted.org/packages/b0/fa/097e38135dadd9ac25aecf2a54be17ddf6e4c23e43d538492a90ab3d71c6/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31", size = 698042 }, - { url = "https://files.pythonhosted.org/packages/ec/d5/a659ca6f503b9379b930f13bc6b130c9f176469b73b9834296822a83a132/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680", size = 745831 }, - { url = "https://files.pythonhosted.org/packages/db/5d/36619b61ffa2429eeaefaab4f3374666adf36ad8ac6330d855848d7d36fd/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d", size = 715692 }, - { url = "https://files.pythonhosted.org/packages/b1/82/85cb92f15a4231c89b95dfe08b09eb6adca929ef7df7e17ab59902b6f589/ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5", size = 98777 }, - { url = "https://files.pythonhosted.org/packages/d7/8f/c3654f6f1ddb75daf3922c3d8fc6005b1ab56671ad56ffb874d908bfa668/ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4", size = 115523 }, - { url = "https://files.pythonhosted.org/packages/29/00/4864119668d71a5fa45678f380b5923ff410701565821925c69780356ffa/ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a", size = 132011 }, - { url = "https://files.pythonhosted.org/packages/7f/5e/212f473a93ae78c669ffa0cb051e3fee1139cb2d385d2ae1653d64281507/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475", size = 642488 }, - { url = "https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef", size = 745066 }, - { url = "https://files.pythonhosted.org/packages/e2/a9/28f60726d29dfc01b8decdb385de4ced2ced9faeb37a847bd5cf26836815/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6", size = 701785 }, - { url = "https://files.pythonhosted.org/packages/84/7e/8e7ec45920daa7f76046578e4f677a3215fe8f18ee30a9cb7627a19d9b4c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf", size = 693017 }, - { url = "https://files.pythonhosted.org/packages/c5/b3/d650eaade4ca225f02a648321e1ab835b9d361c60d51150bac49063b83fa/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1", size = 741270 }, - { url = "https://files.pythonhosted.org/packages/87/b8/01c29b924dcbbed75cc45b30c30d565d763b9c4d540545a0eeecffb8f09c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01", size = 709059 }, - { url = "https://files.pythonhosted.org/packages/30/8c/ed73f047a73638257aa9377ad356bea4d96125b305c34a28766f4445cc0f/ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6", size = 98583 }, - { url = "https://files.pythonhosted.org/packages/b0/85/e8e751d8791564dd333d5d9a4eab0a7a115f7e349595417fd50ecae3395c/ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", size = 115190 }, + { url = "https://files.pythonhosted.org/packages/70/57/40a958e863e299f0c74ef32a3bde9f2d1ea8d69669368c0c502a0997f57f/ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5", size = 131301, upload_time = "2024-10-20T10:12:35.876Z" }, + { url = "https://files.pythonhosted.org/packages/98/a8/29a3eb437b12b95f50a6bcc3d7d7214301c6c529d8fdc227247fa84162b5/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969", size = 633728, upload_time = "2024-10-20T10:12:37.858Z" }, + { url = "https://files.pythonhosted.org/packages/35/6d/ae05a87a3ad540259c3ad88d71275cbd1c0f2d30ae04c65dcbfb6dcd4b9f/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df", size = 722230, upload_time = "2024-10-20T10:12:39.457Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b7/20c6f3c0b656fe609675d69bc135c03aac9e3865912444be6339207b6648/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76", size = 686712, upload_time = "2024-10-20T10:12:41.119Z" }, + { url = "https://files.pythonhosted.org/packages/cd/11/d12dbf683471f888d354dac59593873c2b45feb193c5e3e0f2ebf85e68b9/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6", size = 663936, upload_time = "2024-10-21T11:26:37.419Z" }, + { url = "https://files.pythonhosted.org/packages/72/14/4c268f5077db5c83f743ee1daeb236269fa8577133a5cfa49f8b382baf13/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd", size = 696580, upload_time = "2024-10-21T11:26:39.503Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/8cd12f189c6405a4c1cf37bd633aa740a9538c8e40497c231072d0fef5cf/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a", size = 663393, upload_time = "2024-12-11T19:58:13.873Z" }, + { url = "https://files.pythonhosted.org/packages/80/29/c0a017b704aaf3cbf704989785cd9c5d5b8ccec2dae6ac0c53833c84e677/ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da", size = 100326, upload_time = "2024-10-20T10:12:42.967Z" }, + { url = "https://files.pythonhosted.org/packages/3a/65/fa39d74db4e2d0cd252355732d966a460a41cd01c6353b820a0952432839/ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28", size = 118079, upload_time = "2024-10-20T10:12:44.117Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224, upload_time = "2024-10-20T10:12:45.162Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480, upload_time = "2024-10-20T10:12:46.758Z" }, + { url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068, upload_time = "2024-10-20T10:12:48.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012, upload_time = "2024-10-20T10:12:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352, upload_time = "2024-10-21T11:26:41.438Z" }, + { url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344, upload_time = "2024-10-21T11:26:43.62Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498, upload_time = "2024-12-11T19:58:15.592Z" }, + { url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205, upload_time = "2024-10-20T10:12:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185, upload_time = "2024-10-20T10:12:54.652Z" }, + { url = "https://files.pythonhosted.org/packages/48/41/e7a405afbdc26af961678474a55373e1b323605a4f5e2ddd4a80ea80f628/ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632", size = 133433, upload_time = "2024-10-20T10:12:55.657Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b0/b850385604334c2ce90e3ee1013bd911aedf058a934905863a6ea95e9eb4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d", size = 647362, upload_time = "2024-10-20T10:12:57.155Z" }, + { url = "https://files.pythonhosted.org/packages/44/d0/3f68a86e006448fb6c005aee66565b9eb89014a70c491d70c08de597f8e4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c", size = 754118, upload_time = "2024-10-20T10:12:58.501Z" }, + { url = "https://files.pythonhosted.org/packages/52/a9/d39f3c5ada0a3bb2870d7db41901125dbe2434fa4f12ca8c5b83a42d7c53/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd", size = 706497, upload_time = "2024-10-20T10:13:00.211Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fa/097e38135dadd9ac25aecf2a54be17ddf6e4c23e43d538492a90ab3d71c6/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31", size = 698042, upload_time = "2024-10-21T11:26:46.038Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d5/a659ca6f503b9379b930f13bc6b130c9f176469b73b9834296822a83a132/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680", size = 745831, upload_time = "2024-10-21T11:26:47.487Z" }, + { url = "https://files.pythonhosted.org/packages/db/5d/36619b61ffa2429eeaefaab4f3374666adf36ad8ac6330d855848d7d36fd/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d", size = 715692, upload_time = "2024-12-11T19:58:17.252Z" }, + { url = "https://files.pythonhosted.org/packages/b1/82/85cb92f15a4231c89b95dfe08b09eb6adca929ef7df7e17ab59902b6f589/ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5", size = 98777, upload_time = "2024-10-20T10:13:01.395Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8f/c3654f6f1ddb75daf3922c3d8fc6005b1ab56671ad56ffb874d908bfa668/ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4", size = 115523, upload_time = "2024-10-20T10:13:02.768Z" }, + { url = "https://files.pythonhosted.org/packages/29/00/4864119668d71a5fa45678f380b5923ff410701565821925c69780356ffa/ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a", size = 132011, upload_time = "2024-10-20T10:13:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/212f473a93ae78c669ffa0cb051e3fee1139cb2d385d2ae1653d64281507/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475", size = 642488, upload_time = "2024-10-20T10:13:05.906Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef", size = 745066, upload_time = "2024-10-20T10:13:07.26Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/28f60726d29dfc01b8decdb385de4ced2ced9faeb37a847bd5cf26836815/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6", size = 701785, upload_time = "2024-10-20T10:13:08.504Z" }, + { url = "https://files.pythonhosted.org/packages/84/7e/8e7ec45920daa7f76046578e4f677a3215fe8f18ee30a9cb7627a19d9b4c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf", size = 693017, upload_time = "2024-10-21T11:26:48.866Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b3/d650eaade4ca225f02a648321e1ab835b9d361c60d51150bac49063b83fa/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1", size = 741270, upload_time = "2024-10-21T11:26:50.213Z" }, + { url = "https://files.pythonhosted.org/packages/87/b8/01c29b924dcbbed75cc45b30c30d565d763b9c4d540545a0eeecffb8f09c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01", size = 709059, upload_time = "2024-12-11T19:58:18.846Z" }, + { url = "https://files.pythonhosted.org/packages/30/8c/ed73f047a73638257aa9377ad356bea4d96125b305c34a28766f4445cc0f/ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6", size = 98583, upload_time = "2024-10-20T10:13:09.658Z" }, + { url = "https://files.pythonhosted.org/packages/b0/85/e8e751d8791564dd333d5d9a4eab0a7a115f7e349595417fd50ecae3395c/ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", size = 115190, upload_time = "2024-10-20T10:13:10.66Z" }, ] [[package]] name = "ruff" -version = "0.11.5" +version = "0.11.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/71/5759b2a6b2279bb77fe15b1435b89473631c2cd6374d45ccdb6b785810be/ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef", size = 3976488 } +sdist = { url = "https://files.pythonhosted.org/packages/d9/11/bcef6784c7e5d200b8a1f5c2ddf53e5da0efec37e6e5a44d163fb97e04ba/ruff-0.11.6.tar.gz", hash = "sha256:bec8bcc3ac228a45ccc811e45f7eb61b950dbf4cf31a67fa89352574b01c7d79", size = 4010053, upload_time = "2025-04-17T13:35:53.905Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/db/6efda6381778eec7f35875b5cbefd194904832a1153d68d36d6b269d81a8/ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b", size = 10103150 }, - { url = "https://files.pythonhosted.org/packages/44/f2/06cd9006077a8db61956768bc200a8e52515bf33a8f9b671ee527bb10d77/ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077", size = 10898637 }, - { url = "https://files.pythonhosted.org/packages/18/f5/af390a013c56022fe6f72b95c86eb7b2585c89cc25d63882d3bfe411ecf1/ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779", size = 10236012 }, - { url = "https://files.pythonhosted.org/packages/b8/ca/b9bf954cfed165e1a0c24b86305d5c8ea75def256707f2448439ac5e0d8b/ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794", size = 10415338 }, - { url = "https://files.pythonhosted.org/packages/d9/4d/2522dde4e790f1b59885283f8786ab0046958dfd39959c81acc75d347467/ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038", size = 9965277 }, - { url = "https://files.pythonhosted.org/packages/e5/7a/749f56f150eef71ce2f626a2f6988446c620af2f9ba2a7804295ca450397/ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f", size = 11541614 }, - { url = "https://files.pythonhosted.org/packages/89/b2/7d9b8435222485b6aac627d9c29793ba89be40b5de11584ca604b829e960/ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82", size = 12198873 }, - { url = "https://files.pythonhosted.org/packages/00/e0/a1a69ef5ffb5c5f9c31554b27e030a9c468fc6f57055886d27d316dfbabd/ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304", size = 11670190 }, - { url = "https://files.pythonhosted.org/packages/05/61/c1c16df6e92975072c07f8b20dad35cd858e8462b8865bc856fe5d6ccb63/ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470", size = 13902301 }, - { url = "https://files.pythonhosted.org/packages/79/89/0af10c8af4363304fd8cb833bd407a2850c760b71edf742c18d5a87bb3ad/ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a", size = 11350132 }, - { url = "https://files.pythonhosted.org/packages/b9/e1/ecb4c687cbf15164dd00e38cf62cbab238cad05dd8b6b0fc68b0c2785e15/ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b", size = 10312937 }, - { url = "https://files.pythonhosted.org/packages/cf/4f/0e53fe5e500b65934500949361e3cd290c5ba60f0324ed59d15f46479c06/ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a", size = 9936683 }, - { url = "https://files.pythonhosted.org/packages/04/a8/8183c4da6d35794ae7f76f96261ef5960853cd3f899c2671961f97a27d8e/ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159", size = 10950217 }, - { url = "https://files.pythonhosted.org/packages/26/88/9b85a5a8af21e46a0639b107fcf9bfc31da4f1d263f2fc7fbe7199b47f0a/ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783", size = 11404521 }, - { url = "https://files.pythonhosted.org/packages/fc/52/047f35d3b20fd1ae9ccfe28791ef0f3ca0ef0b3e6c1a58badd97d450131b/ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe", size = 10320697 }, - { url = "https://files.pythonhosted.org/packages/b9/fe/00c78010e3332a6e92762424cf4c1919065707e962232797d0b57fd8267e/ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800", size = 11378665 }, - { url = "https://files.pythonhosted.org/packages/43/7c/c83fe5cbb70ff017612ff36654edfebec4b1ef79b558b8e5fd933bab836b/ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e", size = 10460287 }, + { url = "https://files.pythonhosted.org/packages/6e/1f/8848b625100ebcc8740c8bac5b5dd8ba97dd4ee210970e98832092c1635b/ruff-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:d84dcbe74cf9356d1bdb4a78cf74fd47c740bf7bdeb7529068f69b08272239a1", size = 10248105, upload_time = "2025-04-17T13:35:14.758Z" }, + { url = "https://files.pythonhosted.org/packages/e0/47/c44036e70c6cc11e6ee24399c2a1e1f1e99be5152bd7dff0190e4b325b76/ruff-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9bc583628e1096148011a5d51ff3c836f51899e61112e03e5f2b1573a9b726de", size = 11001494, upload_time = "2025-04-17T13:35:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5b/170444061650202d84d316e8f112de02d092bff71fafe060d3542f5bc5df/ruff-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2959049faeb5ba5e3b378709e9d1bf0cab06528b306b9dd6ebd2a312127964a", size = 10352151, upload_time = "2025-04-17T13:35:20.563Z" }, + { url = "https://files.pythonhosted.org/packages/ff/91/f02839fb3787c678e112c8865f2c3e87cfe1744dcc96ff9fc56cfb97dda2/ruff-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c5d4e30d9d0de7fedbfb3e9e20d134b73a30c1e74b596f40f0629d5c28a193", size = 10541951, upload_time = "2025-04-17T13:35:22.522Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f3/c09933306096ff7a08abede3cc2534d6fcf5529ccd26504c16bf363989b5/ruff-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4b9a4e1439f7d0a091c6763a100cef8fbdc10d68593df6f3cfa5abdd9246e", size = 10079195, upload_time = "2025-04-17T13:35:24.485Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/a87f8933fccbc0d8c653cfbf44bedda69c9582ba09210a309c066794e2ee/ruff-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5edf270223dd622218256569636dc3e708c2cb989242262fe378609eccf1308", size = 11698918, upload_time = "2025-04-17T13:35:26.504Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/8eac0bd083ea8a0b55b7e4628428203441ca68cd55e0b67c135a4bc6e309/ruff-0.11.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f55844e818206a9dd31ff27f91385afb538067e2dc0beb05f82c293ab84f7d55", size = 12319426, upload_time = "2025-04-17T13:35:28.452Z" }, + { url = "https://files.pythonhosted.org/packages/c2/dc/d0c17d875662d0c86fadcf4ca014ab2001f867621b793d5d7eef01b9dcce/ruff-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d8f782286c5ff562e4e00344f954b9320026d8e3fae2ba9e6948443fafd9ffc", size = 11791012, upload_time = "2025-04-17T13:35:30.455Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f3/81a1aea17f1065449a72509fc7ccc3659cf93148b136ff2a8291c4bc3ef1/ruff-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01c63ba219514271cee955cd0adc26a4083df1956d57847978383b0e50ffd7d2", size = 13949947, upload_time = "2025-04-17T13:35:33.133Z" }, + { url = "https://files.pythonhosted.org/packages/61/9f/a3e34de425a668284e7024ee6fd41f452f6fa9d817f1f3495b46e5e3a407/ruff-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15adac20ef2ca296dd3d8e2bedc6202ea6de81c091a74661c3666e5c4c223ff6", size = 11471753, upload_time = "2025-04-17T13:35:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/4a57a86d12542c0f6e2744f262257b2aa5a3783098ec14e40f3e4b3a354a/ruff-0.11.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4dd6b09e98144ad7aec026f5588e493c65057d1b387dd937d7787baa531d9bc2", size = 10417121, upload_time = "2025-04-17T13:35:38.224Z" }, + { url = "https://files.pythonhosted.org/packages/58/3f/a3b4346dff07ef5b862e2ba06d98fcbf71f66f04cf01d375e871382b5e4b/ruff-0.11.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:45b2e1d6c0eed89c248d024ea95074d0e09988d8e7b1dad8d3ab9a67017a5b03", size = 10073829, upload_time = "2025-04-17T13:35:40.255Z" }, + { url = "https://files.pythonhosted.org/packages/93/cc/7ed02e0b86a649216b845b3ac66ed55d8aa86f5898c5f1691797f408fcb9/ruff-0.11.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd40de4115b2ec4850302f1a1d8067f42e70b4990b68838ccb9ccd9f110c5e8b", size = 11076108, upload_time = "2025-04-17T13:35:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/39/5e/5b09840fef0eff1a6fa1dea6296c07d09c17cb6fb94ed5593aa591b50460/ruff-0.11.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:77cda2dfbac1ab73aef5e514c4cbfc4ec1fbef4b84a44c736cc26f61b3814cd9", size = 11512366, upload_time = "2025-04-17T13:35:45.702Z" }, + { url = "https://files.pythonhosted.org/packages/6f/4c/1cd5a84a412d3626335ae69f5f9de2bb554eea0faf46deb1f0cb48534042/ruff-0.11.6-py3-none-win32.whl", hash = "sha256:5151a871554be3036cd6e51d0ec6eef56334d74dfe1702de717a995ee3d5b287", size = 10485900, upload_time = "2025-04-17T13:35:47.695Z" }, + { url = "https://files.pythonhosted.org/packages/42/46/8997872bc44d43df986491c18d4418f1caff03bc47b7f381261d62c23442/ruff-0.11.6-py3-none-win_amd64.whl", hash = "sha256:cce85721d09c51f3b782c331b0abd07e9d7d5f775840379c640606d3159cae0e", size = 11558592, upload_time = "2025-04-17T13:35:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6a/65fecd51a9ca19e1477c3879a7fda24f8904174d1275b419422ac00f6eee/ruff-0.11.6-py3-none-win_arm64.whl", hash = "sha256:3567ba0d07fb170b1b48d944715e3294b77f5b7679e8ba258199a250383ccb79", size = 10682766, upload_time = "2025-04-17T13:35:52.014Z" }, ] [[package]] @@ -2262,23 +2263,23 @@ dependencies = [ { name = "scipy" }, { name = "tifffile" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/c1/a49da20845f0f0e1afbb1c2586d406dc0acb84c26ae293bad6d7e7f718bc/scikit_image-0.22.0.tar.gz", hash = "sha256:018d734df1d2da2719087d15f679d19285fce97cd37695103deadfaef2873236", size = 22685018 } +sdist = { url = "https://files.pythonhosted.org/packages/65/c1/a49da20845f0f0e1afbb1c2586d406dc0acb84c26ae293bad6d7e7f718bc/scikit_image-0.22.0.tar.gz", hash = "sha256:018d734df1d2da2719087d15f679d19285fce97cd37695103deadfaef2873236", size = 22685018, upload_time = "2023-10-03T21:36:34.274Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/8c/381ae42b37cf3e9e99a1deb3ffe76ca5ff5dd18ffa368293476164507fad/scikit_image-0.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74ec5c1d4693506842cc7c9487c89d8fc32aed064e9363def7af08b8f8cbb31d", size = 13905039 }, - { url = "https://files.pythonhosted.org/packages/16/06/4bfba08f5cce26d5070bb2cf4e3f9f479480978806355d1c5bea6f26a17c/scikit_image-0.22.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:a05ae4fe03d802587ed8974e900b943275548cde6a6807b785039d63e9a7a5ff", size = 13279212 }, - { url = "https://files.pythonhosted.org/packages/74/57/dbf744ca00eea2a09b1848c9dec28a43978c16dc049b1fba949cb050bedf/scikit_image-0.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a92dca3d95b1301442af055e196a54b5a5128c6768b79fc0a4098f1d662dee6", size = 14091779 }, - { url = "https://files.pythonhosted.org/packages/f1/6c/49f5a0ce8ddcdbdac5ac69c129654938cc6de0a936303caa6cad495ceb2a/scikit_image-0.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3663d063d8bf2fb9bdfb0ca967b9ee3b6593139c860c7abc2d2351a8a8863938", size = 14682042 }, - { url = "https://files.pythonhosted.org/packages/86/f0/18895318109f9b508f2310f136922e455a453550826a8240b412063c2528/scikit_image-0.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:ebdbdc901bae14dab637f8d5c99f6d5cc7aaf4a3b6f4003194e003e9f688a6fc", size = 24492345 }, - { url = "https://files.pythonhosted.org/packages/9f/d9/dc99e527d1a0050f0353d2fff3548273b4df6151884806e324f26572fd6b/scikit_image-0.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95d6da2d8a44a36ae04437c76d32deb4e3c993ffc846b394b9949fd8ded73cb2", size = 13883619 }, - { url = "https://files.pythonhosted.org/packages/80/37/7670020b112ff9a47e49b1e36f438d000db5b632aab8a8fd7e6be545d065/scikit_image-0.22.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:2c6ef454a85f569659b813ac2a93948022b0298516b757c9c6c904132be327e2", size = 13264761 }, - { url = "https://files.pythonhosted.org/packages/ad/85/dadf1194793ac1c895370f3ed048bb91dda083775b42e11d9672a50494d5/scikit_image-0.22.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e87872f067444ee90a00dd49ca897208308645382e8a24bd3e76f301af2352cd", size = 14070710 }, - { url = "https://files.pythonhosted.org/packages/d4/34/e27bf2bfe7b52b884b49bd71ea91ff81e4737246735ee5ea383314c31876/scikit_image-0.22.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5c378db54e61b491b9edeefff87e49fcf7fdf729bb93c777d7a5f15d36f743e", size = 14664172 }, - { url = "https://files.pythonhosted.org/packages/ce/d0/a3f60c9f57ed295b3076e4acdb29a37bbd8823452562ab2ad51b03d6f377/scikit_image-0.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:2bcb74adb0634258a67f66c2bb29978c9a3e222463e003b67ba12056c003971b", size = 24491321 }, - { url = "https://files.pythonhosted.org/packages/da/a4/b0b69bde4d6360e801d647691591dc9967a25a18a4c63ecf7f87d94e3fac/scikit_image-0.22.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:003ca2274ac0fac252280e7179ff986ff783407001459ddea443fe7916e38cff", size = 13968808 }, - { url = "https://files.pythonhosted.org/packages/e4/65/3c0f77e7a9bae100a8f7f5cebde410fca1a3cf64e1ecdd343666e27b11d4/scikit_image-0.22.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:cf3c0c15b60ae3e557a0c7575fbd352f0c3ce0afca562febfe3ab80efbeec0e9", size = 13323763 }, - { url = "https://files.pythonhosted.org/packages/4a/ed/7faf9f7a55d5b3095d33990a85603b66866cce2a608b27f0e1487d70a451/scikit_image-0.22.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b23908dd4d120e6aecb1ed0277563e8cbc8d6c0565bdc4c4c6475d53608452", size = 13877233 }, - { url = "https://files.pythonhosted.org/packages/ae/9d/09d06f36ce71fa276e1d9453fb4b04250a7038292b13b8c273a5a1a8f7c0/scikit_image-0.22.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be79d7493f320a964f8fcf603121595ba82f84720de999db0fcca002266a549a", size = 14954814 }, - { url = "https://files.pythonhosted.org/packages/dc/35/e6327ae498c6f557cb0a7c3fc284effe7958d2d1c43fb61cd77804fc2c4f/scikit_image-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:722b970aa5da725dca55252c373b18bbea7858c1cdb406e19f9b01a4a73b30b2", size = 25004857 }, + { url = "https://files.pythonhosted.org/packages/9c/8c/381ae42b37cf3e9e99a1deb3ffe76ca5ff5dd18ffa368293476164507fad/scikit_image-0.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74ec5c1d4693506842cc7c9487c89d8fc32aed064e9363def7af08b8f8cbb31d", size = 13905039, upload_time = "2023-10-03T21:35:27.279Z" }, + { url = "https://files.pythonhosted.org/packages/16/06/4bfba08f5cce26d5070bb2cf4e3f9f479480978806355d1c5bea6f26a17c/scikit_image-0.22.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:a05ae4fe03d802587ed8974e900b943275548cde6a6807b785039d63e9a7a5ff", size = 13279212, upload_time = "2023-10-03T21:35:30.864Z" }, + { url = "https://files.pythonhosted.org/packages/74/57/dbf744ca00eea2a09b1848c9dec28a43978c16dc049b1fba949cb050bedf/scikit_image-0.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a92dca3d95b1301442af055e196a54b5a5128c6768b79fc0a4098f1d662dee6", size = 14091779, upload_time = "2023-10-03T21:35:34.273Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6c/49f5a0ce8ddcdbdac5ac69c129654938cc6de0a936303caa6cad495ceb2a/scikit_image-0.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3663d063d8bf2fb9bdfb0ca967b9ee3b6593139c860c7abc2d2351a8a8863938", size = 14682042, upload_time = "2023-10-03T21:35:37.787Z" }, + { url = "https://files.pythonhosted.org/packages/86/f0/18895318109f9b508f2310f136922e455a453550826a8240b412063c2528/scikit_image-0.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:ebdbdc901bae14dab637f8d5c99f6d5cc7aaf4a3b6f4003194e003e9f688a6fc", size = 24492345, upload_time = "2023-10-03T21:35:41.122Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d9/dc99e527d1a0050f0353d2fff3548273b4df6151884806e324f26572fd6b/scikit_image-0.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95d6da2d8a44a36ae04437c76d32deb4e3c993ffc846b394b9949fd8ded73cb2", size = 13883619, upload_time = "2023-10-03T21:35:44.88Z" }, + { url = "https://files.pythonhosted.org/packages/80/37/7670020b112ff9a47e49b1e36f438d000db5b632aab8a8fd7e6be545d065/scikit_image-0.22.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:2c6ef454a85f569659b813ac2a93948022b0298516b757c9c6c904132be327e2", size = 13264761, upload_time = "2023-10-03T21:35:48.865Z" }, + { url = "https://files.pythonhosted.org/packages/ad/85/dadf1194793ac1c895370f3ed048bb91dda083775b42e11d9672a50494d5/scikit_image-0.22.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e87872f067444ee90a00dd49ca897208308645382e8a24bd3e76f301af2352cd", size = 14070710, upload_time = "2023-10-03T21:35:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/d4/34/e27bf2bfe7b52b884b49bd71ea91ff81e4737246735ee5ea383314c31876/scikit_image-0.22.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5c378db54e61b491b9edeefff87e49fcf7fdf729bb93c777d7a5f15d36f743e", size = 14664172, upload_time = "2023-10-03T21:35:55.752Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d0/a3f60c9f57ed295b3076e4acdb29a37bbd8823452562ab2ad51b03d6f377/scikit_image-0.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:2bcb74adb0634258a67f66c2bb29978c9a3e222463e003b67ba12056c003971b", size = 24491321, upload_time = "2023-10-03T21:35:58.847Z" }, + { url = "https://files.pythonhosted.org/packages/da/a4/b0b69bde4d6360e801d647691591dc9967a25a18a4c63ecf7f87d94e3fac/scikit_image-0.22.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:003ca2274ac0fac252280e7179ff986ff783407001459ddea443fe7916e38cff", size = 13968808, upload_time = "2023-10-03T21:36:02.526Z" }, + { url = "https://files.pythonhosted.org/packages/e4/65/3c0f77e7a9bae100a8f7f5cebde410fca1a3cf64e1ecdd343666e27b11d4/scikit_image-0.22.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:cf3c0c15b60ae3e557a0c7575fbd352f0c3ce0afca562febfe3ab80efbeec0e9", size = 13323763, upload_time = "2023-10-03T21:36:05.504Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ed/7faf9f7a55d5b3095d33990a85603b66866cce2a608b27f0e1487d70a451/scikit_image-0.22.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b23908dd4d120e6aecb1ed0277563e8cbc8d6c0565bdc4c4c6475d53608452", size = 13877233, upload_time = "2023-10-03T21:36:08.352Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/09d06f36ce71fa276e1d9453fb4b04250a7038292b13b8c273a5a1a8f7c0/scikit_image-0.22.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be79d7493f320a964f8fcf603121595ba82f84720de999db0fcca002266a549a", size = 14954814, upload_time = "2023-10-03T21:36:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/dc/35/e6327ae498c6f557cb0a7c3fc284effe7958d2d1c43fb61cd77804fc2c4f/scikit_image-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:722b970aa5da725dca55252c373b18bbea7858c1cdb406e19f9b01a4a73b30b2", size = 25004857, upload_time = "2023-10-03T21:36:15.457Z" }, ] [[package]] @@ -2291,23 +2292,23 @@ dependencies = [ { name = "scipy" }, { name = "threadpoolctl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/00/835e3d280fdd7784e76bdef91dd9487582d7951a7254f59fc8004fc8b213/scikit-learn-1.3.2.tar.gz", hash = "sha256:a2f54c76accc15a34bfb9066e6c7a56c1e7235dda5762b990792330b52ccfb05", size = 7510251 } +sdist = { url = "https://files.pythonhosted.org/packages/88/00/835e3d280fdd7784e76bdef91dd9487582d7951a7254f59fc8004fc8b213/scikit-learn-1.3.2.tar.gz", hash = "sha256:a2f54c76accc15a34bfb9066e6c7a56c1e7235dda5762b990792330b52ccfb05", size = 7510251, upload_time = "2023-10-23T13:47:55.287Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/53/570b55a6e10b8694ac1e3024d2df5cd443f1b4ff6d28430845da8b9019b3/scikit_learn-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e326c0eb5cf4d6ba40f93776a20e9a7a69524c4db0757e7ce24ba222471ee8a1", size = 10209999 }, - { url = "https://files.pythonhosted.org/packages/70/d0/50ace22129f79830e3cf682d0a2bd4843ef91573299d43112d52790163a8/scikit_learn-1.3.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:535805c2a01ccb40ca4ab7d081d771aea67e535153e35a1fd99418fcedd1648a", size = 9479353 }, - { url = "https://files.pythonhosted.org/packages/8f/46/fcc35ed7606c50d3072eae5a107a45cfa5b7f5fa8cc48610edd8cc8e8550/scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1215e5e58e9880b554b01187b8c9390bf4dc4692eedeaf542d3273f4785e342c", size = 10304705 }, - { url = "https://files.pythonhosted.org/packages/d0/0b/26ad95cf0b747be967b15fb71a06f5ac67aba0fd2f9cd174de6edefc4674/scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ee107923a623b9f517754ea2f69ea3b62fc898a3641766cb7deb2f2ce450161", size = 10827807 }, - { url = "https://files.pythonhosted.org/packages/69/8a/cf17d6443f5f537e099be81535a56ab68a473f9393fbffda38cd19899fc8/scikit_learn-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:35a22e8015048c628ad099da9df5ab3004cdbf81edc75b396fd0cff8699ac58c", size = 9255427 }, - { url = "https://files.pythonhosted.org/packages/08/5d/e5acecd6e99a6b656e42e7a7b18284e2f9c9f512e8ed6979e1e75d25f05f/scikit_learn-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6fb6bc98f234fda43163ddbe36df8bcde1d13ee176c6dc9b92bb7d3fc842eb66", size = 10116376 }, - { url = "https://files.pythonhosted.org/packages/40/c6/2e91eefb757822e70d351e02cc38d07c137212ae7c41ac12746415b4860a/scikit_learn-1.3.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:18424efee518a1cde7b0b53a422cde2f6625197de6af36da0b57ec502f126157", size = 9383415 }, - { url = "https://files.pythonhosted.org/packages/fa/fd/b3637639e73bb72b12803c5245f2a7299e09b2acd85a0f23937c53369a1c/scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3271552a5eb16f208a6f7f617b8cc6d1f137b52c8a1ef8edf547db0259b2c9fb", size = 10279163 }, - { url = "https://files.pythonhosted.org/packages/0c/2a/d3ff6091406bc2207e0adb832ebd15e40ac685811c7e2e3b432bfd969b71/scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4144a5004a676d5022b798d9e573b05139e77f271253a4703eed295bde0433", size = 10884422 }, - { url = "https://files.pythonhosted.org/packages/4e/ba/ce9bd1cd4953336a0e213b29cb80bb11816f2a93de8c99f88ef0b446ad0c/scikit_learn-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:67f37d708f042a9b8d59551cf94d30431e01374e00dc2645fa186059c6c5d78b", size = 9207060 }, - { url = "https://files.pythonhosted.org/packages/26/7e/2c3b82c8c29aa384c8bf859740419278627d2cdd0050db503c8840e72477/scikit_learn-1.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8db94cd8a2e038b37a80a04df8783e09caac77cbe052146432e67800e430c028", size = 9979322 }, - { url = "https://files.pythonhosted.org/packages/cf/fc/6c52ffeb587259b6b893b7cac268f1eb1b5426bcce1aa20e53523bfe6944/scikit_learn-1.3.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:61a6efd384258789aa89415a410dcdb39a50e19d3d8410bd29be365bcdd512d5", size = 9270688 }, - { url = "https://files.pythonhosted.org/packages/e5/a7/6f4ae76f72ae9de162b97acbf1f53acbe404c555f968d13da21e4112a002/scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb06f8dce3f5ddc5dee1715a9b9f19f20d295bed8e3cd4fa51e1d050347de525", size = 10280398 }, - { url = "https://files.pythonhosted.org/packages/5d/b7/ee35904c07a0666784349529412fbb9814a56382b650d30fd9d6be5e5054/scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b2de18d86f630d68fe1f87af690d451388bb186480afc719e5f770590c2ef6c", size = 10796478 }, - { url = "https://files.pythonhosted.org/packages/fe/6b/db949ed5ac367987b1f250f070f340b7715d22f0c9c965bdf07de6ca75a3/scikit_learn-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:0402638c9a7c219ee52c94cbebc8fcb5eb9fe9c773717965c1f4185588ad3107", size = 9133979 }, + { url = "https://files.pythonhosted.org/packages/0d/53/570b55a6e10b8694ac1e3024d2df5cd443f1b4ff6d28430845da8b9019b3/scikit_learn-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e326c0eb5cf4d6ba40f93776a20e9a7a69524c4db0757e7ce24ba222471ee8a1", size = 10209999, upload_time = "2023-10-23T13:46:30.373Z" }, + { url = "https://files.pythonhosted.org/packages/70/d0/50ace22129f79830e3cf682d0a2bd4843ef91573299d43112d52790163a8/scikit_learn-1.3.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:535805c2a01ccb40ca4ab7d081d771aea67e535153e35a1fd99418fcedd1648a", size = 9479353, upload_time = "2023-10-23T13:46:34.368Z" }, + { url = "https://files.pythonhosted.org/packages/8f/46/fcc35ed7606c50d3072eae5a107a45cfa5b7f5fa8cc48610edd8cc8e8550/scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1215e5e58e9880b554b01187b8c9390bf4dc4692eedeaf542d3273f4785e342c", size = 10304705, upload_time = "2023-10-23T13:46:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0b/26ad95cf0b747be967b15fb71a06f5ac67aba0fd2f9cd174de6edefc4674/scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ee107923a623b9f517754ea2f69ea3b62fc898a3641766cb7deb2f2ce450161", size = 10827807, upload_time = "2023-10-23T13:46:41.59Z" }, + { url = "https://files.pythonhosted.org/packages/69/8a/cf17d6443f5f537e099be81535a56ab68a473f9393fbffda38cd19899fc8/scikit_learn-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:35a22e8015048c628ad099da9df5ab3004cdbf81edc75b396fd0cff8699ac58c", size = 9255427, upload_time = "2023-10-23T13:46:44.826Z" }, + { url = "https://files.pythonhosted.org/packages/08/5d/e5acecd6e99a6b656e42e7a7b18284e2f9c9f512e8ed6979e1e75d25f05f/scikit_learn-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6fb6bc98f234fda43163ddbe36df8bcde1d13ee176c6dc9b92bb7d3fc842eb66", size = 10116376, upload_time = "2023-10-23T13:46:48.147Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/2e91eefb757822e70d351e02cc38d07c137212ae7c41ac12746415b4860a/scikit_learn-1.3.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:18424efee518a1cde7b0b53a422cde2f6625197de6af36da0b57ec502f126157", size = 9383415, upload_time = "2023-10-23T13:46:51.324Z" }, + { url = "https://files.pythonhosted.org/packages/fa/fd/b3637639e73bb72b12803c5245f2a7299e09b2acd85a0f23937c53369a1c/scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3271552a5eb16f208a6f7f617b8cc6d1f137b52c8a1ef8edf547db0259b2c9fb", size = 10279163, upload_time = "2023-10-23T13:46:54.642Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/d3ff6091406bc2207e0adb832ebd15e40ac685811c7e2e3b432bfd969b71/scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4144a5004a676d5022b798d9e573b05139e77f271253a4703eed295bde0433", size = 10884422, upload_time = "2023-10-23T13:46:58.087Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ba/ce9bd1cd4953336a0e213b29cb80bb11816f2a93de8c99f88ef0b446ad0c/scikit_learn-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:67f37d708f042a9b8d59551cf94d30431e01374e00dc2645fa186059c6c5d78b", size = 9207060, upload_time = "2023-10-23T13:47:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/26/7e/2c3b82c8c29aa384c8bf859740419278627d2cdd0050db503c8840e72477/scikit_learn-1.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8db94cd8a2e038b37a80a04df8783e09caac77cbe052146432e67800e430c028", size = 9979322, upload_time = "2023-10-23T13:47:03.977Z" }, + { url = "https://files.pythonhosted.org/packages/cf/fc/6c52ffeb587259b6b893b7cac268f1eb1b5426bcce1aa20e53523bfe6944/scikit_learn-1.3.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:61a6efd384258789aa89415a410dcdb39a50e19d3d8410bd29be365bcdd512d5", size = 9270688, upload_time = "2023-10-23T13:47:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a7/6f4ae76f72ae9de162b97acbf1f53acbe404c555f968d13da21e4112a002/scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb06f8dce3f5ddc5dee1715a9b9f19f20d295bed8e3cd4fa51e1d050347de525", size = 10280398, upload_time = "2023-10-23T13:47:10.796Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b7/ee35904c07a0666784349529412fbb9814a56382b650d30fd9d6be5e5054/scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b2de18d86f630d68fe1f87af690d451388bb186480afc719e5f770590c2ef6c", size = 10796478, upload_time = "2023-10-23T13:47:14.077Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6b/db949ed5ac367987b1f250f070f340b7715d22f0c9c965bdf07de6ca75a3/scikit_learn-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:0402638c9a7c219ee52c94cbebc8fcb5eb9fe9c773717965c1f4185588ad3107", size = 9133979, upload_time = "2023-10-23T13:47:17.389Z" }, ] [[package]] @@ -2317,53 +2318,53 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/1f/91144ba78dccea567a6466262922786ffc97be1e9b06ed9574ef0edc11e1/scipy-1.11.4.tar.gz", hash = "sha256:90a2b78e7f5733b9de748f589f09225013685f9b218275257f8a8168ededaeaa", size = 56336202 } +sdist = { url = "https://files.pythonhosted.org/packages/6e/1f/91144ba78dccea567a6466262922786ffc97be1e9b06ed9574ef0edc11e1/scipy-1.11.4.tar.gz", hash = "sha256:90a2b78e7f5733b9de748f589f09225013685f9b218275257f8a8168ededaeaa", size = 56336202, upload_time = "2023-11-18T21:06:08.277Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/c6/a32add319475d21f89733c034b99c81b3a7c6c7c19f96f80c7ca3ff1bbd4/scipy-1.11.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc9a714581f561af0848e6b69947fda0614915f072dfd14142ed1bfe1b806710", size = 37293259 }, - { url = "https://files.pythonhosted.org/packages/de/0d/4fa68303568c70fd56fbf40668b6c6807cfee4cad975f07d80bdd26d013e/scipy-1.11.4-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:cf00bd2b1b0211888d4dc75656c0412213a8b25e80d73898083f402b50f47e41", size = 29760656 }, - { url = "https://files.pythonhosted.org/packages/13/e5/8012be7857db6cbbbdbeea8a154dbacdfae845e95e1e19c028e82236d4a0/scipy-1.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9999c008ccf00e8fbcce1236f85ade5c569d13144f77a1946bef8863e8f6eb4", size = 32922489 }, - { url = "https://files.pythonhosted.org/packages/e0/9e/80e2205d138960a49caea391f3710600895dd8292b6868dc9aff7aa593f9/scipy-1.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:933baf588daa8dc9a92c20a0be32f56d43faf3d1a60ab11b3f08c356430f6e56", size = 36442040 }, - { url = "https://files.pythonhosted.org/packages/69/60/30a9c3fbe5066a3a93eefe3e2d44553df13587e6f792e1bff20dfed3d17e/scipy-1.11.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8fce70f39076a5aa62e92e69a7f62349f9574d8405c0a5de6ed3ef72de07f446", size = 36643257 }, - { url = "https://files.pythonhosted.org/packages/f8/ec/b46756f80e3f4c5f0989f6e4492c2851f156d9c239d554754a3c8cffd4e2/scipy-1.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:6550466fbeec7453d7465e74d4f4b19f905642c89a7525571ee91dd7adabb5a3", size = 44149285 }, - { url = "https://files.pythonhosted.org/packages/b8/f2/1aefbd5e54ebd8c6163ccf7f73e5d17bc8cb38738d312befc524fce84bb4/scipy-1.11.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f313b39a7e94f296025e3cffc2c567618174c0b1dde173960cf23808f9fae4be", size = 37159197 }, - { url = "https://files.pythonhosted.org/packages/4b/48/20e77ddb1f473d4717a7d4d3fc8d15557f406f7708496054c59f635b7734/scipy-1.11.4-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1b7c3dca977f30a739e0409fb001056484661cb2541a01aba0bb0029f7b68db8", size = 29675057 }, - { url = "https://files.pythonhosted.org/packages/75/2e/a781862190d0e7e76afa74752ef363488a9a9d6ea86e46d5e5506cee8df6/scipy-1.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00150c5eae7b610c32589dda259eacc7c4f1665aedf25d921907f4d08a951b1c", size = 32882747 }, - { url = "https://files.pythonhosted.org/packages/6b/d4/d62ce38ba00dc67d7ec4ec5cc19d36958d8ed70e63778715ad626bcbc796/scipy-1.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:530f9ad26440e85766509dbf78edcfe13ffd0ab7fec2560ee5c36ff74d6269ff", size = 36402732 }, - { url = "https://files.pythonhosted.org/packages/88/86/827b56aea1ed04adbb044a675672a73c84d81076a350092bbfcfc1ae723b/scipy-1.11.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5e347b14fe01003d3b78e196e84bd3f48ffe4c8a7b8a1afbcb8f5505cb710993", size = 36622138 }, - { url = "https://files.pythonhosted.org/packages/43/d0/f3cd75b62e1b90f48dbf091261b2fc7ceec14a700e308c50f6a69c83d337/scipy-1.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:acf8ed278cc03f5aff035e69cb511741e0418681d25fbbb86ca65429c4f4d9cd", size = 44095631 }, - { url = "https://files.pythonhosted.org/packages/df/64/8a690570485b636da614acff35fd725fcbc487f8b1fa9bdb12871b77412f/scipy-1.11.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:028eccd22e654b3ea01ee63705681ee79933652b2d8f873e7949898dda6d11b6", size = 37053653 }, - { url = "https://files.pythonhosted.org/packages/5e/43/abf331745a7e5f4af51f13d40e2a72f516048db41ecbcf3ac6f86ada54a3/scipy-1.11.4-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c6ff6ef9cc27f9b3db93a6f8b38f97387e6e0591600369a297a50a8e96e835d", size = 29641601 }, - { url = "https://files.pythonhosted.org/packages/47/9b/62d0ec086dd2871009da8769c504bec6e39b80f4c182c6ead0fcebd8b323/scipy-1.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b030c6674b9230d37c5c60ab456e2cf12f6784596d15ce8da9365e70896effc4", size = 32272137 }, - { url = "https://files.pythonhosted.org/packages/08/77/f90f7306d755ac68bd159c50bb86fffe38400e533e8c609dd8484bd0f172/scipy-1.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad669df80528aeca5f557712102538f4f37e503f0c5b9541655016dd0932ca79", size = 35777534 }, - { url = "https://files.pythonhosted.org/packages/00/de/b9f6938090c37b5092969ba1c67118e9114e8e6ef9d197251671444e839c/scipy-1.11.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce7fff2e23ab2cc81ff452a9444c215c28e6305f396b2ba88343a567feec9660", size = 35963721 }, - { url = "https://files.pythonhosted.org/packages/c6/a1/357e4cd43af2748e1e0407ae0e9a5ea8aaaa6b702833c81be11670dcbad8/scipy-1.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:36750b7733d960d7994888f0d148d31ea3017ac15eef664194b4ef68d36a4a97", size = 43730653 }, + { url = "https://files.pythonhosted.org/packages/34/c6/a32add319475d21f89733c034b99c81b3a7c6c7c19f96f80c7ca3ff1bbd4/scipy-1.11.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc9a714581f561af0848e6b69947fda0614915f072dfd14142ed1bfe1b806710", size = 37293259, upload_time = "2023-11-18T21:01:18.805Z" }, + { url = "https://files.pythonhosted.org/packages/de/0d/4fa68303568c70fd56fbf40668b6c6807cfee4cad975f07d80bdd26d013e/scipy-1.11.4-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:cf00bd2b1b0211888d4dc75656c0412213a8b25e80d73898083f402b50f47e41", size = 29760656, upload_time = "2023-11-18T21:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/13/e5/8012be7857db6cbbbdbeea8a154dbacdfae845e95e1e19c028e82236d4a0/scipy-1.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9999c008ccf00e8fbcce1236f85ade5c569d13144f77a1946bef8863e8f6eb4", size = 32922489, upload_time = "2023-11-18T21:01:50.637Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9e/80e2205d138960a49caea391f3710600895dd8292b6868dc9aff7aa593f9/scipy-1.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:933baf588daa8dc9a92c20a0be32f56d43faf3d1a60ab11b3f08c356430f6e56", size = 36442040, upload_time = "2023-11-18T21:02:00.119Z" }, + { url = "https://files.pythonhosted.org/packages/69/60/30a9c3fbe5066a3a93eefe3e2d44553df13587e6f792e1bff20dfed3d17e/scipy-1.11.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8fce70f39076a5aa62e92e69a7f62349f9574d8405c0a5de6ed3ef72de07f446", size = 36643257, upload_time = "2023-11-18T21:02:06.798Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ec/b46756f80e3f4c5f0989f6e4492c2851f156d9c239d554754a3c8cffd4e2/scipy-1.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:6550466fbeec7453d7465e74d4f4b19f905642c89a7525571ee91dd7adabb5a3", size = 44149285, upload_time = "2023-11-18T21:02:15.592Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f2/1aefbd5e54ebd8c6163ccf7f73e5d17bc8cb38738d312befc524fce84bb4/scipy-1.11.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f313b39a7e94f296025e3cffc2c567618174c0b1dde173960cf23808f9fae4be", size = 37159197, upload_time = "2023-11-18T21:02:21.959Z" }, + { url = "https://files.pythonhosted.org/packages/4b/48/20e77ddb1f473d4717a7d4d3fc8d15557f406f7708496054c59f635b7734/scipy-1.11.4-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1b7c3dca977f30a739e0409fb001056484661cb2541a01aba0bb0029f7b68db8", size = 29675057, upload_time = "2023-11-18T21:02:28.169Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/a781862190d0e7e76afa74752ef363488a9a9d6ea86e46d5e5506cee8df6/scipy-1.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00150c5eae7b610c32589dda259eacc7c4f1665aedf25d921907f4d08a951b1c", size = 32882747, upload_time = "2023-11-18T21:02:33.683Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d4/d62ce38ba00dc67d7ec4ec5cc19d36958d8ed70e63778715ad626bcbc796/scipy-1.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:530f9ad26440e85766509dbf78edcfe13ffd0ab7fec2560ee5c36ff74d6269ff", size = 36402732, upload_time = "2023-11-18T21:02:39.762Z" }, + { url = "https://files.pythonhosted.org/packages/88/86/827b56aea1ed04adbb044a675672a73c84d81076a350092bbfcfc1ae723b/scipy-1.11.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5e347b14fe01003d3b78e196e84bd3f48ffe4c8a7b8a1afbcb8f5505cb710993", size = 36622138, upload_time = "2023-11-18T21:02:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/43/d0/f3cd75b62e1b90f48dbf091261b2fc7ceec14a700e308c50f6a69c83d337/scipy-1.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:acf8ed278cc03f5aff035e69cb511741e0418681d25fbbb86ca65429c4f4d9cd", size = 44095631, upload_time = "2023-11-18T21:02:52.859Z" }, + { url = "https://files.pythonhosted.org/packages/df/64/8a690570485b636da614acff35fd725fcbc487f8b1fa9bdb12871b77412f/scipy-1.11.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:028eccd22e654b3ea01ee63705681ee79933652b2d8f873e7949898dda6d11b6", size = 37053653, upload_time = "2023-11-18T21:03:00.107Z" }, + { url = "https://files.pythonhosted.org/packages/5e/43/abf331745a7e5f4af51f13d40e2a72f516048db41ecbcf3ac6f86ada54a3/scipy-1.11.4-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c6ff6ef9cc27f9b3db93a6f8b38f97387e6e0591600369a297a50a8e96e835d", size = 29641601, upload_time = "2023-11-18T21:03:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/47/9b/62d0ec086dd2871009da8769c504bec6e39b80f4c182c6ead0fcebd8b323/scipy-1.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b030c6674b9230d37c5c60ab456e2cf12f6784596d15ce8da9365e70896effc4", size = 32272137, upload_time = "2023-11-18T21:03:14.877Z" }, + { url = "https://files.pythonhosted.org/packages/08/77/f90f7306d755ac68bd159c50bb86fffe38400e533e8c609dd8484bd0f172/scipy-1.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad669df80528aeca5f557712102538f4f37e503f0c5b9541655016dd0932ca79", size = 35777534, upload_time = "2023-11-18T21:03:21.451Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/b9f6938090c37b5092969ba1c67118e9114e8e6ef9d197251671444e839c/scipy-1.11.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce7fff2e23ab2cc81ff452a9444c215c28e6305f396b2ba88343a567feec9660", size = 35963721, upload_time = "2023-11-18T21:03:27.85Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a1/357e4cd43af2748e1e0407ae0e9a5ea8aaaa6b702833c81be11670dcbad8/scipy-1.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:36750b7733d960d7994888f0d148d31ea3017ac15eef664194b4ef68d36a4a97", size = 43730653, upload_time = "2023-11-18T21:03:34.758Z" }, ] [[package]] name = "setuptools" version = "70.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/d8/10a70e86f6c28ae59f101a9de6d77bf70f147180fbf40c3af0f64080adc3/setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5", size = 2333112 } +sdist = { url = "https://files.pythonhosted.org/packages/65/d8/10a70e86f6c28ae59f101a9de6d77bf70f147180fbf40c3af0f64080adc3/setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5", size = 2333112, upload_time = "2024-07-09T16:08:06.251Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/15/88e46eb9387e905704b69849618e699dc2f54407d8953cc4ec4b8b46528d/setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc", size = 931070 }, + { url = "https://files.pythonhosted.org/packages/ef/15/88e46eb9387e905704b69849618e699dc2f54407d8953cc4ec4b8b46528d/setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc", size = 931070, upload_time = "2024-07-09T16:07:58.829Z" }, ] [[package]] name = "six" version = "1.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041, upload_time = "2021-05-05T14:18:18.379Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053, upload_time = "2021-05-05T14:18:17.237Z" }, ] [[package]] name = "sniffio" version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/50/d49c388cae4ec10e8109b1b833fd265511840706808576df3ada99ecb0ac/sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", size = 17103 } +sdist = { url = "https://files.pythonhosted.org/packages/cd/50/d49c388cae4ec10e8109b1b833fd265511840706808576df3ada99ecb0ac/sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", size = 17103, upload_time = "2022-09-01T12:31:36.968Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/a0/5dba8ed157b0136607c7f2151db695885606968d1fae123dc3391e0cfdbf/sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384", size = 10165 }, + { url = "https://files.pythonhosted.org/packages/c3/a0/5dba8ed157b0136607c7f2151db695885606968d1fae123dc3391e0cfdbf/sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384", size = 10165, upload_time = "2022-09-01T12:31:34.186Z" }, ] [[package]] @@ -2373,9 +2374,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/da/1fb4bdb72ae12b834becd7e1e7e47001d32f91ec0ce8d7bc1b618d9f0bd9/starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62", size = 2573867 } +sdist = { url = "https://files.pythonhosted.org/packages/3e/da/1fb4bdb72ae12b834becd7e1e7e47001d32f91ec0ce8d7bc1b618d9f0bd9/starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62", size = 2573867, upload_time = "2024-10-27T08:20:02.818Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/43/f185bfd0ca1d213beb4293bed51d92254df23d8ceaf6c0e17146d508a776/starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d", size = 73259 }, + { url = "https://files.pythonhosted.org/packages/54/43/f185bfd0ca1d213beb4293bed51d92254df23d8ceaf6c0e17146d508a776/starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d", size = 73259, upload_time = "2024-10-27T08:20:00.052Z" }, ] [[package]] @@ -2385,18 +2386,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mpmath" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/57/3485a1a3dff51bfd691962768b14310dae452431754bfc091250be50dd29/sympy-1.12.tar.gz", hash = "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8", size = 6722203 } +sdist = { url = "https://files.pythonhosted.org/packages/e5/57/3485a1a3dff51bfd691962768b14310dae452431754bfc091250be50dd29/sympy-1.12.tar.gz", hash = "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8", size = 6722203, upload_time = "2023-05-10T18:23:00.378Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/05/e6600db80270777c4a64238a98d442f0fd07cc8915be2a1c16da7f2b9e74/sympy-1.12-py3-none-any.whl", hash = "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5", size = 5742435 }, + { url = "https://files.pythonhosted.org/packages/d2/05/e6600db80270777c4a64238a98d442f0fd07cc8915be2a1c16da7f2b9e74/sympy-1.12-py3-none-any.whl", hash = "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5", size = 5742435, upload_time = "2023-05-10T18:22:14.76Z" }, ] [[package]] name = "threadpoolctl" version = "3.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/8a/c05f7831beb32aff70f808766224f11c650f7edfd49b27a8fc6666107006/threadpoolctl-3.2.0.tar.gz", hash = "sha256:c96a0ba3bdddeaca37dc4cc7344aafad41cdb8c313f74fdfe387a867bba93355", size = 36266 } +sdist = { url = "https://files.pythonhosted.org/packages/47/8a/c05f7831beb32aff70f808766224f11c650f7edfd49b27a8fc6666107006/threadpoolctl-3.2.0.tar.gz", hash = "sha256:c96a0ba3bdddeaca37dc4cc7344aafad41cdb8c313f74fdfe387a867bba93355", size = 36266, upload_time = "2023-07-13T14:53:53.299Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/12/fd4dea011af9d69e1cad05c75f3f7202cdcbeac9b712eea58ca779a72865/threadpoolctl-3.2.0-py3-none-any.whl", hash = "sha256:2b7818516e423bdaebb97c723f86a7c6b0a83d3f3b0970328d66f4d9104dc032", size = 15539 }, + { url = "https://files.pythonhosted.org/packages/81/12/fd4dea011af9d69e1cad05c75f3f7202cdcbeac9b712eea58ca779a72865/threadpoolctl-3.2.0-py3-none-any.whl", hash = "sha256:2b7818516e423bdaebb97c723f86a7c6b0a83d3f3b0970328d66f4d9104dc032", size = 15539, upload_time = "2023-07-13T14:53:39.336Z" }, ] [[package]] @@ -2406,9 +2407,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/6c0eadea1ccfcda27e6cce400c366098b5b082138a073f4252fe399f4148/tifffile-2023.12.9.tar.gz", hash = "sha256:9dd1da91180a6453018a241ff219e1905f169384355cd89c9ef4034c1b46cdb8", size = 353467 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/6c0eadea1ccfcda27e6cce400c366098b5b082138a073f4252fe399f4148/tifffile-2023.12.9.tar.gz", hash = "sha256:9dd1da91180a6453018a241ff219e1905f169384355cd89c9ef4034c1b46cdb8", size = 353467, upload_time = "2023-12-09T20:46:29.203Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/a4/569fc717831969cf48bced350bdaf070cdeab06918d179429899e144358d/tifffile-2023.12.9-py3-none-any.whl", hash = "sha256:9b066e4b1a900891ea42ffd33dab8ba34c537935618b9893ddef42d7d422692f", size = 223627 }, + { url = "https://files.pythonhosted.org/packages/54/a4/569fc717831969cf48bced350bdaf070cdeab06918d179429899e144358d/tifffile-2023.12.9-py3-none-any.whl", hash = "sha256:9b066e4b1a900891ea42ffd33dab8ba34c537935618b9893ddef42d7d422692f", size = 223627, upload_time = "2023-12-09T20:46:26.569Z" }, ] [[package]] @@ -2418,31 +2419,31 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256 } +sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256, upload_time = "2025-03-13T10:51:18.189Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767 }, - { url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555 }, - { url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541 }, - { url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058 }, - { url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278 }, - { url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253 }, - { url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225 }, - { url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874 }, - { url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448 }, - { url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877 }, - { url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645 }, - { url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380 }, - { url = "https://files.pythonhosted.org/packages/e8/5b/a5d98c89f747455e8b7a9504910c865d5e51da55e825a7ae641fb5ff0a58/tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3", size = 2239506 }, - { url = "https://files.pythonhosted.org/packages/e6/b6/072a8e053ae600dcc2ac0da81a23548e3b523301a442a6ca900e92ac35be/tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382", size = 2435481 }, + { url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767, upload_time = "2025-03-13T10:51:09.459Z" }, + { url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555, upload_time = "2025-03-13T10:51:07.692Z" }, + { url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541, upload_time = "2025-03-13T10:50:56.679Z" }, + { url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058, upload_time = "2025-03-13T10:50:59.525Z" }, + { url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278, upload_time = "2025-03-13T10:51:04.678Z" }, + { url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253, upload_time = "2025-03-13T10:51:01.261Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225, upload_time = "2025-03-13T10:51:03.243Z" }, + { url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874, upload_time = "2025-03-13T10:51:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448, upload_time = "2025-03-13T10:51:10.927Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877, upload_time = "2025-03-13T10:51:12.688Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645, upload_time = "2025-03-13T10:51:14.723Z" }, + { url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380, upload_time = "2025-03-13T10:51:16.526Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5b/a5d98c89f747455e8b7a9504910c865d5e51da55e825a7ae641fb5ff0a58/tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3", size = 2239506, upload_time = "2025-03-13T10:51:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b6/072a8e053ae600dcc2ac0da81a23548e3b523301a442a6ca900e92ac35be/tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382", size = 2435481, upload_time = "2025-03-13T10:51:19.243Z" }, ] [[package]] name = "tomli" version = "2.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164, upload_time = "2022-02-08T10:54:04.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757, upload_time = "2022-02-08T10:54:02.017Z" }, ] [[package]] @@ -2452,18 +2453,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/00/6a9b3aedb0b60a80995ade30f718f1a9902612f22a1aaf531c85a02987f7/tqdm-4.66.3.tar.gz", hash = "sha256:23097a41eba115ba99ecae40d06444c15d1c0c698d527a01c6c8bd1c5d0647e5", size = 169551 } +sdist = { url = "https://files.pythonhosted.org/packages/03/00/6a9b3aedb0b60a80995ade30f718f1a9902612f22a1aaf531c85a02987f7/tqdm-4.66.3.tar.gz", hash = "sha256:23097a41eba115ba99ecae40d06444c15d1c0c698d527a01c6c8bd1c5d0647e5", size = 169551, upload_time = "2024-05-02T21:44:05.084Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/ad/7d47bbf2cae78ff79f29db0bed5016ec9c56b212a93fca624bb88b551a7c/tqdm-4.66.3-py3-none-any.whl", hash = "sha256:4f41d54107ff9a223dca80b53efe4fb654c67efaba7f47bada3ee9d50e05bd53", size = 78374 }, + { url = "https://files.pythonhosted.org/packages/d1/ad/7d47bbf2cae78ff79f29db0bed5016ec9c56b212a93fca624bb88b551a7c/tqdm-4.66.3-py3-none-any.whl", hash = "sha256:4f41d54107ff9a223dca80b53efe4fb654c67efaba7f47bada3ee9d50e05bd53", size = 78374, upload_time = "2024-05-02T21:44:01.541Z" }, ] [[package]] name = "types-pyyaml" version = "6.0.12.20250402" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/68/609eed7402f87c9874af39d35942744e39646d1ea9011765ec87b01b2a3c/types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075", size = 17282 } +sdist = { url = "https://files.pythonhosted.org/packages/2d/68/609eed7402f87c9874af39d35942744e39646d1ea9011765ec87b01b2a3c/types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075", size = 17282, upload_time = "2025-04-02T02:56:00.235Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/56/1fe61db05685fbb512c07ea9323f06ea727125951f1eb4dff110b3311da3/types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681", size = 20329 }, + { url = "https://files.pythonhosted.org/packages/ed/56/1fe61db05685fbb512c07ea9323f06ea727125951f1eb4dff110b3311da3/types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681", size = 20329, upload_time = "2025-04-02T02:55:59.382Z" }, ] [[package]] @@ -2473,9 +2474,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/7d/eb174f74e3f5634eaacb38031bbe467dfe2e545bc255e5c90096ec46bc46/types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32", size = 22995 } +sdist = { url = "https://files.pythonhosted.org/packages/00/7d/eb174f74e3f5634eaacb38031bbe467dfe2e545bc255e5c90096ec46bc46/types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32", size = 22995, upload_time = "2025-03-28T02:55:13.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/15/3700282a9d4ea3b37044264d3e4d1b1f0095a4ebf860a99914fd544e3be3/types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2", size = 20663 }, + { url = "https://files.pythonhosted.org/packages/cc/15/3700282a9d4ea3b37044264d3e4d1b1f0095a4ebf860a99914fd544e3be3/types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2", size = 20663, upload_time = "2025-03-28T02:55:11.946Z" }, ] [[package]] @@ -2485,36 +2486,36 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b8/0f/2d1d000c2be3919bcdea15e5da48456bf1e55c18d02c5509ea59dade1408/types_setuptools-76.0.0.20250313.tar.gz", hash = "sha256:b2be66f550f95f3cad2a7d46177b273c7e9c80df7d257fa57addbbcfc8126a9e", size = 43627 } +sdist = { url = "https://files.pythonhosted.org/packages/b8/0f/2d1d000c2be3919bcdea15e5da48456bf1e55c18d02c5509ea59dade1408/types_setuptools-76.0.0.20250313.tar.gz", hash = "sha256:b2be66f550f95f3cad2a7d46177b273c7e9c80df7d257fa57addbbcfc8126a9e", size = 43627, upload_time = "2025-03-13T02:51:28.3Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/89/ea9669a0a76b160ffb312d0b02b15bad053c1bc81d2a54e42e3a402ca754/types_setuptools-76.0.0.20250313-py3-none-any.whl", hash = "sha256:bf454b2a49b8cfd7ebcf5844d4dd5fe4c8666782df1e3663c5866fd51a47460e", size = 65845 }, + { url = "https://files.pythonhosted.org/packages/ca/89/ea9669a0a76b160ffb312d0b02b15bad053c1bc81d2a54e42e3a402ca754/types_setuptools-76.0.0.20250313-py3-none-any.whl", hash = "sha256:bf454b2a49b8cfd7ebcf5844d4dd5fe4c8666782df1e3663c5866fd51a47460e", size = 65845, upload_time = "2025-03-13T02:51:27.055Z" }, ] [[package]] name = "types-simplejson" version = "3.20.0.20250326" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/14/e26fc55e1ea56f9ea470917d3e2f8240e6d043ca914181021d04115ae0f7/types_simplejson-3.20.0.20250326.tar.gz", hash = "sha256:b2689bc91e0e672d7a5a947b4cb546b76ae7ddc2899c6678e72a10bf96cd97d2", size = 10489 } +sdist = { url = "https://files.pythonhosted.org/packages/af/14/e26fc55e1ea56f9ea470917d3e2f8240e6d043ca914181021d04115ae0f7/types_simplejson-3.20.0.20250326.tar.gz", hash = "sha256:b2689bc91e0e672d7a5a947b4cb546b76ae7ddc2899c6678e72a10bf96cd97d2", size = 10489, upload_time = "2025-03-26T02:53:35.825Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/bf/d3f3a5ba47fd18115e8446d39f025b85905d2008677c29ee4d03b4cddd57/types_simplejson-3.20.0.20250326-py3-none-any.whl", hash = "sha256:db1ddea7b8f7623b27a137578f22fc6c618db8c83ccfb1828ca0d2f0ec11efa7", size = 10462 }, + { url = "https://files.pythonhosted.org/packages/76/bf/d3f3a5ba47fd18115e8446d39f025b85905d2008677c29ee4d03b4cddd57/types_simplejson-3.20.0.20250326-py3-none-any.whl", hash = "sha256:db1ddea7b8f7623b27a137578f22fc6c618db8c83ccfb1828ca0d2f0ec11efa7", size = 10462, upload_time = "2025-03-26T02:53:35.036Z" }, ] [[package]] name = "types-ujson" version = "5.10.0.20250326" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/5c/c974451c4babdb4ae3588925487edde492d59a8403010b4642a554d09954/types_ujson-5.10.0.20250326.tar.gz", hash = "sha256:5469e05f2c31ecb3c4c0267cc8fe41bcd116826fbb4ded69801a645c687dd014", size = 8340 } +sdist = { url = "https://files.pythonhosted.org/packages/cc/5c/c974451c4babdb4ae3588925487edde492d59a8403010b4642a554d09954/types_ujson-5.10.0.20250326.tar.gz", hash = "sha256:5469e05f2c31ecb3c4c0267cc8fe41bcd116826fbb4ded69801a645c687dd014", size = 8340, upload_time = "2025-03-26T02:53:39.197Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/c9/8a73a5f8fa6e70fc02eed506d5ac0ae9ceafbd2b8c9ad34a7de0f29900d6/types_ujson-5.10.0.20250326-py3-none-any.whl", hash = "sha256:acc0913f569def62ef6a892c8a47703f65d05669a3252391a97765cf207dca5b", size = 7644 }, + { url = "https://files.pythonhosted.org/packages/3e/c9/8a73a5f8fa6e70fc02eed506d5ac0ae9ceafbd2b8c9ad34a7de0f29900d6/types_ujson-5.10.0.20250326-py3-none-any.whl", hash = "sha256:acc0913f569def62ef6a892c8a47703f65d05669a3252391a97765cf207dca5b", size = 7644, upload_time = "2025-03-26T02:53:38.2Z" }, ] [[package]] name = "typing-extensions" version = "4.12.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321, upload_time = "2024-06-07T18:52:15.995Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438, upload_time = "2024-06-07T18:52:13.582Z" }, ] [[package]] @@ -2524,32 +2525,32 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload_time = "2025-02-25T17:27:59.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload_time = "2025-02-25T17:27:57.754Z" }, ] [[package]] name = "urllib3" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/dd/a6b232f449e1bc71802a5b7950dc3675d32c6dbc2a1bd6d71f065551adb6/urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54", size = 263900 } +sdist = { url = "https://files.pythonhosted.org/packages/36/dd/a6b232f449e1bc71802a5b7950dc3675d32c6dbc2a1bd6d71f065551adb6/urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54", size = 263900, upload_time = "2023-11-13T12:29:45.049Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/94/c31f58c7a7f470d5665935262ebd7455c7e4c7782eb525658d3dbf4b9403/urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", size = 104579 }, + { url = "https://files.pythonhosted.org/packages/96/94/c31f58c7a7f470d5665935262ebd7455c7e4c7782eb525658d3dbf4b9403/urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", size = 104579, upload_time = "2023-11-13T12:29:42.719Z" }, ] [[package]] name = "uvicorn" -version = "0.34.0" +version = "0.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } +sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload_time = "2025-04-19T06:02:50.101Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, + { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload_time = "2025-04-19T06:02:48.42Z" }, ] [package.optional-dependencies] @@ -2567,26 +2568,26 @@ standard = [ name = "uvloop" version = "0.19.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/16/728cc5dde368e6eddb299c5aec4d10eaf25335a5af04e8c0abd68e2e9d32/uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd", size = 2318492 } +sdist = { url = "https://files.pythonhosted.org/packages/9c/16/728cc5dde368e6eddb299c5aec4d10eaf25335a5af04e8c0abd68e2e9d32/uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd", size = 2318492, upload_time = "2023-10-22T22:03:57.665Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/c2/27bf858a576b1fa35b5c2c2029c8cec424a8789e87545ed2f25466d1f21d/uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e", size = 1443484 }, - { url = "https://files.pythonhosted.org/packages/4e/35/05b6064b93f4113412d1fd92bdcb6018607e78ae94d1712e63e533f9b2fa/uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428", size = 793850 }, - { url = "https://files.pythonhosted.org/packages/aa/56/b62ab4e10458ce96bb30c98d327c127f989d3bb4ef899e4c410c739f7ef6/uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8", size = 3418601 }, - { url = "https://files.pythonhosted.org/packages/ab/ed/12729fba5e3b7e02ee70b3ea230b88e60a50375cf63300db22607694d2f0/uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849", size = 3416731 }, - { url = "https://files.pythonhosted.org/packages/a2/23/80381a2d728d2a0c36e2eef202f5b77428990004d8fbdd3865558ff49fa5/uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957", size = 4128572 }, - { url = "https://files.pythonhosted.org/packages/6b/23/1ee41a15e1ad15182e2bd12cbfd37bcb6802f01d6bbcaddf6ca136cbb308/uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd", size = 4129235 }, - { url = "https://files.pythonhosted.org/packages/41/2a/608ad69f27f51280098abee440c33e921d3ad203e2c86f7262e241e49c99/uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef", size = 1357681 }, - { url = "https://files.pythonhosted.org/packages/13/00/d0923d66d80c8717983493a4d7af747ce47f1c2147d82df057a846ba6bff/uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2", size = 746421 }, - { url = "https://files.pythonhosted.org/packages/1f/c7/e494c367b0c6e6453f9bed5a78548f5b2ff49add36302cd915a91d347d88/uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1", size = 3481000 }, - { url = "https://files.pythonhosted.org/packages/86/cc/1829b3f740e4cb1baefff8240a1c6fc8db9e3caac7b93169aec7d4386069/uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24", size = 3476361 }, - { url = "https://files.pythonhosted.org/packages/7a/4c/ca87e8f5a30629ffa2038c20907c8ab455c5859ff10e810227b76e60d927/uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533", size = 4169571 }, - { url = "https://files.pythonhosted.org/packages/d2/a9/f947a00c47b1c87c937cac2423243a41ba08f0fb76d04eb0d1d170606e0a/uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12", size = 4170459 }, - { url = "https://files.pythonhosted.org/packages/85/57/6736733bb0e86a4b5380d04082463b289c0baecaa205934ba81e8a1d5ea4/uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650", size = 1355376 }, - { url = "https://files.pythonhosted.org/packages/eb/0c/51339463da912ed34b48d470538d98a91660749b2db56902f23db9b42fdd/uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec", size = 745031 }, - { url = "https://files.pythonhosted.org/packages/e6/fc/f0daaf19f5b2116a2d26eb9f98c4a45084aea87bf03c33bcca7aa1ff36e5/uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc", size = 4077630 }, - { url = "https://files.pythonhosted.org/packages/fd/96/fdc318ffe82ae567592b213ec2fcd8ecedd927b5da068cf84d56b28c51a4/uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6", size = 4159957 }, - { url = "https://files.pythonhosted.org/packages/71/bc/092068ae7fc16dcf20f3e389126ba7800cee75ffba83f78bf1d167aee3cd/uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593", size = 4014951 }, - { url = "https://files.pythonhosted.org/packages/a6/f2/6ce1e73933eb038c89f929e26042e64b2cb8d4453410153eed14918ca9a8/uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3", size = 4100911 }, + { url = "https://files.pythonhosted.org/packages/36/c2/27bf858a576b1fa35b5c2c2029c8cec424a8789e87545ed2f25466d1f21d/uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e", size = 1443484, upload_time = "2023-10-22T22:02:54.169Z" }, + { url = "https://files.pythonhosted.org/packages/4e/35/05b6064b93f4113412d1fd92bdcb6018607e78ae94d1712e63e533f9b2fa/uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428", size = 793850, upload_time = "2023-10-22T22:02:56.311Z" }, + { url = "https://files.pythonhosted.org/packages/aa/56/b62ab4e10458ce96bb30c98d327c127f989d3bb4ef899e4c410c739f7ef6/uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8", size = 3418601, upload_time = "2023-10-22T22:02:58.717Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ed/12729fba5e3b7e02ee70b3ea230b88e60a50375cf63300db22607694d2f0/uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849", size = 3416731, upload_time = "2023-10-22T22:03:01.043Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/80381a2d728d2a0c36e2eef202f5b77428990004d8fbdd3865558ff49fa5/uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957", size = 4128572, upload_time = "2023-10-22T22:03:02.874Z" }, + { url = "https://files.pythonhosted.org/packages/6b/23/1ee41a15e1ad15182e2bd12cbfd37bcb6802f01d6bbcaddf6ca136cbb308/uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd", size = 4129235, upload_time = "2023-10-22T22:03:05.361Z" }, + { url = "https://files.pythonhosted.org/packages/41/2a/608ad69f27f51280098abee440c33e921d3ad203e2c86f7262e241e49c99/uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef", size = 1357681, upload_time = "2023-10-22T22:03:07.158Z" }, + { url = "https://files.pythonhosted.org/packages/13/00/d0923d66d80c8717983493a4d7af747ce47f1c2147d82df057a846ba6bff/uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2", size = 746421, upload_time = "2023-10-22T22:03:09.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c7/e494c367b0c6e6453f9bed5a78548f5b2ff49add36302cd915a91d347d88/uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1", size = 3481000, upload_time = "2023-10-22T22:03:11.755Z" }, + { url = "https://files.pythonhosted.org/packages/86/cc/1829b3f740e4cb1baefff8240a1c6fc8db9e3caac7b93169aec7d4386069/uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24", size = 3476361, upload_time = "2023-10-22T22:03:13.841Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4c/ca87e8f5a30629ffa2038c20907c8ab455c5859ff10e810227b76e60d927/uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533", size = 4169571, upload_time = "2023-10-22T22:03:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a9/f947a00c47b1c87c937cac2423243a41ba08f0fb76d04eb0d1d170606e0a/uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12", size = 4170459, upload_time = "2023-10-22T22:03:17.988Z" }, + { url = "https://files.pythonhosted.org/packages/85/57/6736733bb0e86a4b5380d04082463b289c0baecaa205934ba81e8a1d5ea4/uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650", size = 1355376, upload_time = "2023-10-22T22:03:20.075Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0c/51339463da912ed34b48d470538d98a91660749b2db56902f23db9b42fdd/uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec", size = 745031, upload_time = "2023-10-22T22:03:21.404Z" }, + { url = "https://files.pythonhosted.org/packages/e6/fc/f0daaf19f5b2116a2d26eb9f98c4a45084aea87bf03c33bcca7aa1ff36e5/uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc", size = 4077630, upload_time = "2023-10-22T22:03:23.568Z" }, + { url = "https://files.pythonhosted.org/packages/fd/96/fdc318ffe82ae567592b213ec2fcd8ecedd927b5da068cf84d56b28c51a4/uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6", size = 4159957, upload_time = "2023-10-22T22:03:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/71/bc/092068ae7fc16dcf20f3e389126ba7800cee75ffba83f78bf1d167aee3cd/uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593", size = 4014951, upload_time = "2023-10-22T22:03:27.055Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f2/6ce1e73933eb038c89f929e26042e64b2cb8d4453410153eed14918ca9a8/uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3", size = 4100911, upload_time = "2023-10-22T22:03:29.39Z" }, ] [[package]] @@ -2596,106 +2597,106 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/79/0ee412e1228aaf6f9568aa180b43cb482472de52560fbd7c283c786534af/watchfiles-0.21.0.tar.gz", hash = "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3", size = 37098 } +sdist = { url = "https://files.pythonhosted.org/packages/66/79/0ee412e1228aaf6f9568aa180b43cb482472de52560fbd7c283c786534af/watchfiles-0.21.0.tar.gz", hash = "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3", size = 37098, upload_time = "2023-10-13T13:06:39.809Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/85/ea2a035b7d86bf0a29ee1c32bc2df8ad4da77e6602806e679d9735ff28cb/watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa", size = 428182 }, - { url = "https://files.pythonhosted.org/packages/b5/e5/240e5eb3ff0ee3da3b028ac5be2019c407bdd0f3fdb02bd75fdf3bd10aff/watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e", size = 418275 }, - { url = "https://files.pythonhosted.org/packages/5b/79/ecd0dfb04443a1900cd3952d7ea6493bf655c2db9a0d3736a5d98a15da39/watchfiles-0.21.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03", size = 1379785 }, - { url = "https://files.pythonhosted.org/packages/41/0e/3333b986b1889bb71f0e44b3fac0591824a679619b8b8ddd70ff8858edc4/watchfiles-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124", size = 1349374 }, - { url = "https://files.pythonhosted.org/packages/18/c4/ad5ad16cad900a29aaa792e0ed121ff70d76f74062b051661090d88c6dfd/watchfiles-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab", size = 1348033 }, - { url = "https://files.pythonhosted.org/packages/4e/d2/769254ff04ba88ceb179a6e892606ac4da17338eb010e85ca7a9c3339234/watchfiles-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303", size = 1464393 }, - { url = "https://files.pythonhosted.org/packages/14/d0/662800e778ca20e7664dd5df57751aa79ef18b6abb92224b03c8c2e852a6/watchfiles-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d", size = 1542953 }, - { url = "https://files.pythonhosted.org/packages/f7/4b/b90dcdc3bbaf3bb2db733e1beea2d01566b601c15fcf8e71dfcc8686c097/watchfiles-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c", size = 1346961 }, - { url = "https://files.pythonhosted.org/packages/92/ff/75cc1b30c5abcad13a2a72e75625ec619c7a393028a111d7d24dba578d5e/watchfiles-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9", size = 1464393 }, - { url = "https://files.pythonhosted.org/packages/9a/65/12cbeb363bf220482a559c48107edfd87f09248f55e1ac315a36c2098a0f/watchfiles-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9", size = 1463409 }, - { url = "https://files.pythonhosted.org/packages/f2/08/92e28867c66f0d9638bb131feca739057efc48dbcd391fd7f0a55507e470/watchfiles-0.21.0-cp310-none-win32.whl", hash = "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293", size = 268101 }, - { url = "https://files.pythonhosted.org/packages/4b/ea/80527adf1ad51488a96fc201715730af5879f4dfeccb5e2069ff82d890d4/watchfiles-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235", size = 279675 }, - { url = "https://files.pythonhosted.org/packages/57/b9/2667286003dd305b81d3a3aa824d3dfc63dacbf2a96faae09e72d953c430/watchfiles-0.21.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7", size = 428210 }, - { url = "https://files.pythonhosted.org/packages/a3/87/6793ac60d2e20c9c1883aec7431c2e7b501ee44a839f6da1b747c13baa23/watchfiles-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef", size = 418196 }, - { url = "https://files.pythonhosted.org/packages/5d/12/e1d1d220c5b99196eea38c9a878964f30a2b55ec9d72fd713191725b35e8/watchfiles-0.21.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586", size = 1380287 }, - { url = "https://files.pythonhosted.org/packages/0e/cf/126f0a8683f326d190c3539a769e45e747a80a5fcbf797de82e738c946ae/watchfiles-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317", size = 1349653 }, - { url = "https://files.pythonhosted.org/packages/20/6e/6cffd795ec65dbc82f15d95b73d3042c1ddaffc4dd25f6c8240bfcf0640f/watchfiles-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b", size = 1348844 }, - { url = "https://files.pythonhosted.org/packages/d5/2a/f9633279d8937ad84c532997405dd106fa6100e8d2b83e364f1c87561f96/watchfiles-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1", size = 1464343 }, - { url = "https://files.pythonhosted.org/packages/d7/49/9b2199bbf3c89e7c8dd795fced9dac29f201be8a28a5df0c8ff625737df6/watchfiles-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d", size = 1542858 }, - { url = "https://files.pythonhosted.org/packages/35/e0/e8a9c1fe30e98c5b3507ad381abc4d9ee2c3b9c0ae62ffe9c164a5838186/watchfiles-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7", size = 1347464 }, - { url = "https://files.pythonhosted.org/packages/ba/66/873739dd7defdfaee4b880114de9463fae18ba13ae2ddd784806b0ee33b6/watchfiles-0.21.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0", size = 1464343 }, - { url = "https://files.pythonhosted.org/packages/bd/51/d7539aa258d8f0e5d7b870af8b9b8964b4f88a1e4517eeb8a2efb838e9b3/watchfiles-0.21.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365", size = 1463338 }, - { url = "https://files.pythonhosted.org/packages/ee/92/219c539a2a93b6870fa7b84eace946983126b20a7e15c6c034d8d0472682/watchfiles-0.21.0-cp311-none-win32.whl", hash = "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400", size = 267658 }, - { url = "https://files.pythonhosted.org/packages/f3/dc/2a8a447b783f5059c4bf7a6bad8fe59375a5a9ce872774763b25c21c2860/watchfiles-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe", size = 280113 }, - { url = "https://files.pythonhosted.org/packages/22/15/e4085181cf0210a6ec6eb29fee0c6088de867ee33d81555076a4a2726e8b/watchfiles-0.21.0-cp311-none-win_arm64.whl", hash = "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078", size = 268688 }, - { url = "https://files.pythonhosted.org/packages/a1/fd/2f009eb17809afd32a143b442856628585c9ce3a9c6d5c1841e44e35a16c/watchfiles-0.21.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a", size = 426902 }, - { url = "https://files.pythonhosted.org/packages/e0/62/a2605f212a136e06f2d056ee7491ede9935ba0f1d5ceafd1f7da2a0c8625/watchfiles-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1", size = 417300 }, - { url = "https://files.pythonhosted.org/packages/69/0e/29f158fa22eb2cc1f188b5ec20fb5c0a64eb801e3901ad5b7ad546cbaed0/watchfiles-0.21.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a", size = 1378126 }, - { url = "https://files.pythonhosted.org/packages/e8/f3/c67865cb5a174201c52d34e870cc7956b8408ee83ce9a02909d6a2a93a14/watchfiles-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915", size = 1348275 }, - { url = "https://files.pythonhosted.org/packages/d7/eb/b6f1184d1c7b9670f5bd1e184e4c221ecf25fd817cf2fcac6adc387882b5/watchfiles-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360", size = 1347255 }, - { url = "https://files.pythonhosted.org/packages/c8/27/e534e4b3fe739f4bf8bd5dc4c26cbc5d3baa427125d8ef78a6556acd6ff4/watchfiles-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6", size = 1462845 }, - { url = "https://files.pythonhosted.org/packages/b0/ba/a0d1c1c55f75e7e47c8f79f2314f7ec670b5177596f6d27764aecc7048cd/watchfiles-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7", size = 1528957 }, - { url = "https://files.pythonhosted.org/packages/1c/3a/4e38518c4dff58090c01fc8cc051fa08ac9ae00b361c855075809b0058ce/watchfiles-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c", size = 1345542 }, - { url = "https://files.pythonhosted.org/packages/9f/b7/783097f8137a710d5cd9ccbfcd92e4b453d38dab05cfcb5dbd2c587752e5/watchfiles-0.21.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235", size = 1462238 }, - { url = "https://files.pythonhosted.org/packages/6b/4c/b741eb38f2c408ae9c5a25235f6506b1dda43486ae0fdb4c462ef75bce11/watchfiles-0.21.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7", size = 1462406 }, - { url = "https://files.pythonhosted.org/packages/77/e4/8d2b3c67364671b0e1c0ce383895a5415f45ecb3e8586982deff4a8e85c9/watchfiles-0.21.0-cp312-none-win32.whl", hash = "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3", size = 266789 }, - { url = "https://files.pythonhosted.org/packages/da/f2/6b1de38aeb21eb9dac1ae6a1ee4521566e79690117032036c737cfab52fa/watchfiles-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094", size = 280292 }, - { url = "https://files.pythonhosted.org/packages/5a/a5/7aba9435beb863c2490bae3173a45f42044ac7a48155d3dd42ab49cfae45/watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6", size = 268026 }, - { url = "https://files.pythonhosted.org/packages/62/66/7463ceb43eabc6deaa795c7969ff4d4fd938de54e655035483dfd1e97c84/watchfiles-0.21.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994", size = 429092 }, - { url = "https://files.pythonhosted.org/packages/fe/a3/42686af3a089f34aba35c39abac852869661938dae7025c1a0580dfe0fbf/watchfiles-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f", size = 419188 }, - { url = "https://files.pythonhosted.org/packages/37/17/4825999346f15d650f4c69093efa64fb040fbff4f706a20e8c4745f64070/watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c", size = 1350366 }, - { url = "https://files.pythonhosted.org/packages/70/76/8d124e14cf51af4d6bba926c7473f253c6efd1539ba62577f079a2d71537/watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc", size = 1346270 }, + { url = "https://files.pythonhosted.org/packages/6e/85/ea2a035b7d86bf0a29ee1c32bc2df8ad4da77e6602806e679d9735ff28cb/watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa", size = 428182, upload_time = "2023-10-13T13:04:34.803Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/240e5eb3ff0ee3da3b028ac5be2019c407bdd0f3fdb02bd75fdf3bd10aff/watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e", size = 418275, upload_time = "2023-10-13T13:04:36.632Z" }, + { url = "https://files.pythonhosted.org/packages/5b/79/ecd0dfb04443a1900cd3952d7ea6493bf655c2db9a0d3736a5d98a15da39/watchfiles-0.21.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03", size = 1379785, upload_time = "2023-10-13T13:04:38.641Z" }, + { url = "https://files.pythonhosted.org/packages/41/0e/3333b986b1889bb71f0e44b3fac0591824a679619b8b8ddd70ff8858edc4/watchfiles-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124", size = 1349374, upload_time = "2023-10-13T13:04:41.711Z" }, + { url = "https://files.pythonhosted.org/packages/18/c4/ad5ad16cad900a29aaa792e0ed121ff70d76f74062b051661090d88c6dfd/watchfiles-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab", size = 1348033, upload_time = "2023-10-13T13:04:43.324Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d2/769254ff04ba88ceb179a6e892606ac4da17338eb010e85ca7a9c3339234/watchfiles-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303", size = 1464393, upload_time = "2023-10-13T13:04:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/662800e778ca20e7664dd5df57751aa79ef18b6abb92224b03c8c2e852a6/watchfiles-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d", size = 1542953, upload_time = "2023-10-13T13:04:46.714Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4b/b90dcdc3bbaf3bb2db733e1beea2d01566b601c15fcf8e71dfcc8686c097/watchfiles-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c", size = 1346961, upload_time = "2023-10-13T13:04:48.072Z" }, + { url = "https://files.pythonhosted.org/packages/92/ff/75cc1b30c5abcad13a2a72e75625ec619c7a393028a111d7d24dba578d5e/watchfiles-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9", size = 1464393, upload_time = "2023-10-13T13:04:49.638Z" }, + { url = "https://files.pythonhosted.org/packages/9a/65/12cbeb363bf220482a559c48107edfd87f09248f55e1ac315a36c2098a0f/watchfiles-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9", size = 1463409, upload_time = "2023-10-13T13:04:51.762Z" }, + { url = "https://files.pythonhosted.org/packages/f2/08/92e28867c66f0d9638bb131feca739057efc48dbcd391fd7f0a55507e470/watchfiles-0.21.0-cp310-none-win32.whl", hash = "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293", size = 268101, upload_time = "2023-10-13T13:04:53.78Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ea/80527adf1ad51488a96fc201715730af5879f4dfeccb5e2069ff82d890d4/watchfiles-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235", size = 279675, upload_time = "2023-10-13T13:04:55.113Z" }, + { url = "https://files.pythonhosted.org/packages/57/b9/2667286003dd305b81d3a3aa824d3dfc63dacbf2a96faae09e72d953c430/watchfiles-0.21.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7", size = 428210, upload_time = "2023-10-13T13:04:56.894Z" }, + { url = "https://files.pythonhosted.org/packages/a3/87/6793ac60d2e20c9c1883aec7431c2e7b501ee44a839f6da1b747c13baa23/watchfiles-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef", size = 418196, upload_time = "2023-10-13T13:04:58.19Z" }, + { url = "https://files.pythonhosted.org/packages/5d/12/e1d1d220c5b99196eea38c9a878964f30a2b55ec9d72fd713191725b35e8/watchfiles-0.21.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586", size = 1380287, upload_time = "2023-10-13T13:04:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cf/126f0a8683f326d190c3539a769e45e747a80a5fcbf797de82e738c946ae/watchfiles-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317", size = 1349653, upload_time = "2023-10-13T13:05:01.622Z" }, + { url = "https://files.pythonhosted.org/packages/20/6e/6cffd795ec65dbc82f15d95b73d3042c1ddaffc4dd25f6c8240bfcf0640f/watchfiles-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b", size = 1348844, upload_time = "2023-10-13T13:05:03.805Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2a/f9633279d8937ad84c532997405dd106fa6100e8d2b83e364f1c87561f96/watchfiles-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1", size = 1464343, upload_time = "2023-10-13T13:05:05.248Z" }, + { url = "https://files.pythonhosted.org/packages/d7/49/9b2199bbf3c89e7c8dd795fced9dac29f201be8a28a5df0c8ff625737df6/watchfiles-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d", size = 1542858, upload_time = "2023-10-13T13:05:06.791Z" }, + { url = "https://files.pythonhosted.org/packages/35/e0/e8a9c1fe30e98c5b3507ad381abc4d9ee2c3b9c0ae62ffe9c164a5838186/watchfiles-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7", size = 1347464, upload_time = "2023-10-13T13:05:08.622Z" }, + { url = "https://files.pythonhosted.org/packages/ba/66/873739dd7defdfaee4b880114de9463fae18ba13ae2ddd784806b0ee33b6/watchfiles-0.21.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0", size = 1464343, upload_time = "2023-10-13T13:05:10.584Z" }, + { url = "https://files.pythonhosted.org/packages/bd/51/d7539aa258d8f0e5d7b870af8b9b8964b4f88a1e4517eeb8a2efb838e9b3/watchfiles-0.21.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365", size = 1463338, upload_time = "2023-10-13T13:05:12.671Z" }, + { url = "https://files.pythonhosted.org/packages/ee/92/219c539a2a93b6870fa7b84eace946983126b20a7e15c6c034d8d0472682/watchfiles-0.21.0-cp311-none-win32.whl", hash = "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400", size = 267658, upload_time = "2023-10-13T13:05:13.972Z" }, + { url = "https://files.pythonhosted.org/packages/f3/dc/2a8a447b783f5059c4bf7a6bad8fe59375a5a9ce872774763b25c21c2860/watchfiles-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe", size = 280113, upload_time = "2023-10-13T13:05:15.289Z" }, + { url = "https://files.pythonhosted.org/packages/22/15/e4085181cf0210a6ec6eb29fee0c6088de867ee33d81555076a4a2726e8b/watchfiles-0.21.0-cp311-none-win_arm64.whl", hash = "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078", size = 268688, upload_time = "2023-10-13T13:05:17.144Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fd/2f009eb17809afd32a143b442856628585c9ce3a9c6d5c1841e44e35a16c/watchfiles-0.21.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a", size = 426902, upload_time = "2023-10-13T13:05:18.828Z" }, + { url = "https://files.pythonhosted.org/packages/e0/62/a2605f212a136e06f2d056ee7491ede9935ba0f1d5ceafd1f7da2a0c8625/watchfiles-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1", size = 417300, upload_time = "2023-10-13T13:05:20.116Z" }, + { url = "https://files.pythonhosted.org/packages/69/0e/29f158fa22eb2cc1f188b5ec20fb5c0a64eb801e3901ad5b7ad546cbaed0/watchfiles-0.21.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a", size = 1378126, upload_time = "2023-10-13T13:05:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f3/c67865cb5a174201c52d34e870cc7956b8408ee83ce9a02909d6a2a93a14/watchfiles-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915", size = 1348275, upload_time = "2023-10-13T13:05:22.995Z" }, + { url = "https://files.pythonhosted.org/packages/d7/eb/b6f1184d1c7b9670f5bd1e184e4c221ecf25fd817cf2fcac6adc387882b5/watchfiles-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360", size = 1347255, upload_time = "2023-10-13T13:05:24.618Z" }, + { url = "https://files.pythonhosted.org/packages/c8/27/e534e4b3fe739f4bf8bd5dc4c26cbc5d3baa427125d8ef78a6556acd6ff4/watchfiles-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6", size = 1462845, upload_time = "2023-10-13T13:05:26.531Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/a0d1c1c55f75e7e47c8f79f2314f7ec670b5177596f6d27764aecc7048cd/watchfiles-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7", size = 1528957, upload_time = "2023-10-13T13:05:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3a/4e38518c4dff58090c01fc8cc051fa08ac9ae00b361c855075809b0058ce/watchfiles-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c", size = 1345542, upload_time = "2023-10-13T13:05:29.862Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b7/783097f8137a710d5cd9ccbfcd92e4b453d38dab05cfcb5dbd2c587752e5/watchfiles-0.21.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235", size = 1462238, upload_time = "2023-10-13T13:05:32.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4c/b741eb38f2c408ae9c5a25235f6506b1dda43486ae0fdb4c462ef75bce11/watchfiles-0.21.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7", size = 1462406, upload_time = "2023-10-13T13:05:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/77/e4/8d2b3c67364671b0e1c0ce383895a5415f45ecb3e8586982deff4a8e85c9/watchfiles-0.21.0-cp312-none-win32.whl", hash = "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3", size = 266789, upload_time = "2023-10-13T13:05:35.606Z" }, + { url = "https://files.pythonhosted.org/packages/da/f2/6b1de38aeb21eb9dac1ae6a1ee4521566e79690117032036c737cfab52fa/watchfiles-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094", size = 280292, upload_time = "2023-10-13T13:05:37.357Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a5/7aba9435beb863c2490bae3173a45f42044ac7a48155d3dd42ab49cfae45/watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6", size = 268026, upload_time = "2023-10-13T13:05:38.591Z" }, + { url = "https://files.pythonhosted.org/packages/62/66/7463ceb43eabc6deaa795c7969ff4d4fd938de54e655035483dfd1e97c84/watchfiles-0.21.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994", size = 429092, upload_time = "2023-10-13T13:06:21.419Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a3/42686af3a089f34aba35c39abac852869661938dae7025c1a0580dfe0fbf/watchfiles-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f", size = 419188, upload_time = "2023-10-13T13:06:22.934Z" }, + { url = "https://files.pythonhosted.org/packages/37/17/4825999346f15d650f4c69093efa64fb040fbff4f706a20e8c4745f64070/watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c", size = 1350366, upload_time = "2023-10-13T13:06:24.254Z" }, + { url = "https://files.pythonhosted.org/packages/70/76/8d124e14cf51af4d6bba926c7473f253c6efd1539ba62577f079a2d71537/watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc", size = 1346270, upload_time = "2023-10-13T13:06:25.742Z" }, ] [[package]] name = "wcwidth" version = "0.2.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload_time = "2024-01-06T02:10:57.829Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload_time = "2024-01-06T02:10:55.763Z" }, ] [[package]] name = "websockets" version = "12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/62/7a7874b7285413c954a4cca3c11fd851f11b2fe5b4ae2d9bee4f6d9bdb10/websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b", size = 104994 } +sdist = { url = "https://files.pythonhosted.org/packages/2e/62/7a7874b7285413c954a4cca3c11fd851f11b2fe5b4ae2d9bee4f6d9bdb10/websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b", size = 104994, upload_time = "2023-10-21T14:21:11.88Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/b9/360b86ded0920a93bff0db4e4b0aa31370b0208ca240b2e98d62aad8d082/websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374", size = 124025 }, - { url = "https://files.pythonhosted.org/packages/bb/d3/1eca0d8fb6f0665c96f0dc7c0d0ec8aa1a425e8c003e0c18e1451f65d177/websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be", size = 121261 }, - { url = "https://files.pythonhosted.org/packages/4e/e1/f6c3ecf7f1bfd9209e13949db027d7fdea2faf090c69b5f2d17d1d796d96/websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547", size = 121328 }, - { url = "https://files.pythonhosted.org/packages/74/4d/f88eeceb23cb587c4aeca779e3f356cf54817af2368cb7f2bd41f93c8360/websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2", size = 130925 }, - { url = "https://files.pythonhosted.org/packages/16/17/f63d9ee6ffd9afbeea021d5950d6e8db84cd4aead306c6c2ca523805699e/websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558", size = 129930 }, - { url = "https://files.pythonhosted.org/packages/9a/12/c7a7504f5bf74d6ee0533f6fc7d30d8f4b79420ab179d1df2484b07602eb/websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480", size = 130245 }, - { url = "https://files.pythonhosted.org/packages/e4/6a/3600c7771eb31116d2e77383d7345618b37bb93709d041e328c08e2a8eb3/websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c", size = 134966 }, - { url = "https://files.pythonhosted.org/packages/22/26/df77c4b7538caebb78c9b97f43169ef742a4f445e032a5ea1aaef88f8f46/websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8", size = 134196 }, - { url = "https://files.pythonhosted.org/packages/e5/18/18ce9a4a08203c8d0d3d561e3ea4f453daf32f099601fc831e60c8a9b0f2/websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603", size = 134822 }, - { url = "https://files.pythonhosted.org/packages/45/51/1f823a341fc20a880e67ae62f6c38c4880a24a4b60fbe544a38f516f39a1/websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f", size = 124454 }, - { url = "https://files.pythonhosted.org/packages/41/b0/5ec054cfcf23adfc88d39359b85e81d043af8a141e3ac8ce40f45a5ce5f4/websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf", size = 124974 }, - { url = "https://files.pythonhosted.org/packages/02/73/9c1e168a2e7fdf26841dc98f5f5502e91dea47428da7690a08101f616169/websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4", size = 124047 }, - { url = "https://files.pythonhosted.org/packages/e4/2d/9a683359ad2ed11b2303a7a94800db19c61d33fa3bde271df09e99936022/websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f", size = 121282 }, - { url = "https://files.pythonhosted.org/packages/95/aa/75fa3b893142d6d98a48cb461169bd268141f2da8bfca97392d6462a02eb/websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3", size = 121325 }, - { url = "https://files.pythonhosted.org/packages/6e/a4/51a25e591d645df71ee0dc3a2c880b28e5514c00ce752f98a40a87abcd1e/websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c", size = 131502 }, - { url = "https://files.pythonhosted.org/packages/cd/ea/0ceeea4f5b87398fe2d9f5bcecfa00a1bcd542e2bfcac2f2e5dd612c4e9e/websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45", size = 130491 }, - { url = "https://files.pythonhosted.org/packages/e3/05/f52a60b66d9faf07a4f7d71dc056bffafe36a7e98c4eb5b78f04fe6e4e85/websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04", size = 130872 }, - { url = "https://files.pythonhosted.org/packages/ac/4e/c7361b2d7b964c40fea924d64881145164961fcd6c90b88b7e3ab2c4f431/websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447", size = 136318 }, - { url = "https://files.pythonhosted.org/packages/0a/31/337bf35ae5faeaf364c9cddec66681cdf51dc4414ee7a20f92a18e57880f/websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca", size = 135594 }, - { url = "https://files.pythonhosted.org/packages/95/aa/1ac767825c96f9d7e43c4c95683757d4ef28cf11fa47a69aca42428d3e3a/websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53", size = 136191 }, - { url = "https://files.pythonhosted.org/packages/28/4b/344ec5cfeb6bc417da097f8253607c3aed11d9a305fb58346f506bf556d8/websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402", size = 124453 }, - { url = "https://files.pythonhosted.org/packages/d1/40/6b169cd1957476374f51f4486a3e85003149e62a14e6b78a958c2222337a/websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b", size = 124971 }, - { url = "https://files.pythonhosted.org/packages/a9/6d/23cc898647c8a614a0d9ca703695dd04322fb5135096a20c2684b7c852b6/websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df", size = 124061 }, - { url = "https://files.pythonhosted.org/packages/39/34/364f30fdf1a375e4002a26ee3061138d1571dfda6421126127d379d13930/websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc", size = 121296 }, - { url = "https://files.pythonhosted.org/packages/2e/00/96ae1c9dcb3bc316ef683f2febd8c97dde9f254dc36c3afc65c7645f734c/websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b", size = 121326 }, - { url = "https://files.pythonhosted.org/packages/af/f1/bba1e64430685dd456c1a1fd6b0c791ae33104967b928aefeff261761e8d/websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb", size = 131807 }, - { url = "https://files.pythonhosted.org/packages/62/3b/98ee269712f37d892b93852ce07b3e6d7653160ca4c0d4f8c8663f8021f8/websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92", size = 130751 }, - { url = "https://files.pythonhosted.org/packages/f1/00/d6f01ca2b191f8b0808e4132ccd2e7691f0453cbd7d0f72330eb97453c3a/websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed", size = 131176 }, - { url = "https://files.pythonhosted.org/packages/af/9c/703ff3cd8109dcdee6152bae055d852ebaa7750117760ded697ab836cbcf/websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5", size = 136246 }, - { url = "https://files.pythonhosted.org/packages/0b/a5/1a38fb85a456b9dc874ec984f3ff34f6550eafd17a3da28753cd3c1628e8/websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2", size = 135466 }, - { url = "https://files.pythonhosted.org/packages/3c/98/1261f289dff7e65a38d59d2f591de6ed0a2580b729aebddec033c4d10881/websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113", size = 136083 }, - { url = "https://files.pythonhosted.org/packages/a9/1c/f68769fba63ccb9c13fe0a25b616bd5aebeef1c7ddebc2ccc32462fb784d/websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d", size = 124460 }, - { url = "https://files.pythonhosted.org/packages/20/52/8915f51f9aaef4e4361c89dd6cf69f72a0159f14e0d25026c81b6ad22525/websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f", size = 124985 }, - { url = "https://files.pythonhosted.org/packages/43/8b/554a8a8bb6da9dd1ce04c44125e2192af7b7beebf6e3dbfa5d0e285cc20f/websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd", size = 121110 }, - { url = "https://files.pythonhosted.org/packages/b0/8e/58b8812940d746ad74d395fb069497255cb5ef50748dfab1e8b386b1f339/websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870", size = 123216 }, - { url = "https://files.pythonhosted.org/packages/81/ee/272cb67ace1786ce6d9f39d47b3c55b335e8b75dd1972a7967aad39178b6/websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077", size = 122821 }, - { url = "https://files.pythonhosted.org/packages/a8/03/387fc902b397729df166763e336f4e5cec09fe7b9d60f442542c94a21be1/websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b", size = 122768 }, - { url = "https://files.pythonhosted.org/packages/50/f0/5939fbc9bc1979d79a774ce5b7c4b33c0cefe99af22fb70f7462d0919640/websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30", size = 125009 }, - { url = "https://files.pythonhosted.org/packages/79/4d/9cc401e7b07e80532ebc8c8e993f42541534da9e9249c59ee0139dcb0352/websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e", size = 118370 }, + { url = "https://files.pythonhosted.org/packages/b1/b9/360b86ded0920a93bff0db4e4b0aa31370b0208ca240b2e98d62aad8d082/websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374", size = 124025, upload_time = "2023-10-21T14:19:28.387Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d3/1eca0d8fb6f0665c96f0dc7c0d0ec8aa1a425e8c003e0c18e1451f65d177/websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be", size = 121261, upload_time = "2023-10-21T14:19:30.203Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/f6c3ecf7f1bfd9209e13949db027d7fdea2faf090c69b5f2d17d1d796d96/websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547", size = 121328, upload_time = "2023-10-21T14:19:31.765Z" }, + { url = "https://files.pythonhosted.org/packages/74/4d/f88eeceb23cb587c4aeca779e3f356cf54817af2368cb7f2bd41f93c8360/websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2", size = 130925, upload_time = "2023-10-21T14:19:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/16/17/f63d9ee6ffd9afbeea021d5950d6e8db84cd4aead306c6c2ca523805699e/websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558", size = 129930, upload_time = "2023-10-21T14:19:35.109Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/c7a7504f5bf74d6ee0533f6fc7d30d8f4b79420ab179d1df2484b07602eb/websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480", size = 130245, upload_time = "2023-10-21T14:19:36.761Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6a/3600c7771eb31116d2e77383d7345618b37bb93709d041e328c08e2a8eb3/websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c", size = 134966, upload_time = "2023-10-21T14:19:38.481Z" }, + { url = "https://files.pythonhosted.org/packages/22/26/df77c4b7538caebb78c9b97f43169ef742a4f445e032a5ea1aaef88f8f46/websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8", size = 134196, upload_time = "2023-10-21T14:19:40.264Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/18ce9a4a08203c8d0d3d561e3ea4f453daf32f099601fc831e60c8a9b0f2/websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603", size = 134822, upload_time = "2023-10-21T14:19:41.836Z" }, + { url = "https://files.pythonhosted.org/packages/45/51/1f823a341fc20a880e67ae62f6c38c4880a24a4b60fbe544a38f516f39a1/websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f", size = 124454, upload_time = "2023-10-21T14:19:43.639Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/5ec054cfcf23adfc88d39359b85e81d043af8a141e3ac8ce40f45a5ce5f4/websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf", size = 124974, upload_time = "2023-10-21T14:19:44.934Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/9c1e168a2e7fdf26841dc98f5f5502e91dea47428da7690a08101f616169/websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4", size = 124047, upload_time = "2023-10-21T14:19:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/e4/2d/9a683359ad2ed11b2303a7a94800db19c61d33fa3bde271df09e99936022/websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f", size = 121282, upload_time = "2023-10-21T14:19:47.739Z" }, + { url = "https://files.pythonhosted.org/packages/95/aa/75fa3b893142d6d98a48cb461169bd268141f2da8bfca97392d6462a02eb/websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3", size = 121325, upload_time = "2023-10-21T14:19:49.4Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/51a25e591d645df71ee0dc3a2c880b28e5514c00ce752f98a40a87abcd1e/websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c", size = 131502, upload_time = "2023-10-21T14:19:50.683Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ea/0ceeea4f5b87398fe2d9f5bcecfa00a1bcd542e2bfcac2f2e5dd612c4e9e/websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45", size = 130491, upload_time = "2023-10-21T14:19:51.835Z" }, + { url = "https://files.pythonhosted.org/packages/e3/05/f52a60b66d9faf07a4f7d71dc056bffafe36a7e98c4eb5b78f04fe6e4e85/websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04", size = 130872, upload_time = "2023-10-21T14:19:53.071Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4e/c7361b2d7b964c40fea924d64881145164961fcd6c90b88b7e3ab2c4f431/websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447", size = 136318, upload_time = "2023-10-21T14:19:54.41Z" }, + { url = "https://files.pythonhosted.org/packages/0a/31/337bf35ae5faeaf364c9cddec66681cdf51dc4414ee7a20f92a18e57880f/websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca", size = 135594, upload_time = "2023-10-21T14:19:55.982Z" }, + { url = "https://files.pythonhosted.org/packages/95/aa/1ac767825c96f9d7e43c4c95683757d4ef28cf11fa47a69aca42428d3e3a/websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53", size = 136191, upload_time = "2023-10-21T14:19:57.349Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/344ec5cfeb6bc417da097f8253607c3aed11d9a305fb58346f506bf556d8/websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402", size = 124453, upload_time = "2023-10-21T14:19:59.11Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/6b169cd1957476374f51f4486a3e85003149e62a14e6b78a958c2222337a/websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b", size = 124971, upload_time = "2023-10-21T14:20:00.243Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6d/23cc898647c8a614a0d9ca703695dd04322fb5135096a20c2684b7c852b6/websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df", size = 124061, upload_time = "2023-10-21T14:20:02.221Z" }, + { url = "https://files.pythonhosted.org/packages/39/34/364f30fdf1a375e4002a26ee3061138d1571dfda6421126127d379d13930/websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc", size = 121296, upload_time = "2023-10-21T14:20:03.591Z" }, + { url = "https://files.pythonhosted.org/packages/2e/00/96ae1c9dcb3bc316ef683f2febd8c97dde9f254dc36c3afc65c7645f734c/websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b", size = 121326, upload_time = "2023-10-21T14:20:04.956Z" }, + { url = "https://files.pythonhosted.org/packages/af/f1/bba1e64430685dd456c1a1fd6b0c791ae33104967b928aefeff261761e8d/websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb", size = 131807, upload_time = "2023-10-21T14:20:06.153Z" }, + { url = "https://files.pythonhosted.org/packages/62/3b/98ee269712f37d892b93852ce07b3e6d7653160ca4c0d4f8c8663f8021f8/websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92", size = 130751, upload_time = "2023-10-21T14:20:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/f1/00/d6f01ca2b191f8b0808e4132ccd2e7691f0453cbd7d0f72330eb97453c3a/websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed", size = 131176, upload_time = "2023-10-21T14:20:09.212Z" }, + { url = "https://files.pythonhosted.org/packages/af/9c/703ff3cd8109dcdee6152bae055d852ebaa7750117760ded697ab836cbcf/websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5", size = 136246, upload_time = "2023-10-21T14:20:10.423Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a5/1a38fb85a456b9dc874ec984f3ff34f6550eafd17a3da28753cd3c1628e8/websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2", size = 135466, upload_time = "2023-10-21T14:20:11.826Z" }, + { url = "https://files.pythonhosted.org/packages/3c/98/1261f289dff7e65a38d59d2f591de6ed0a2580b729aebddec033c4d10881/websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113", size = 136083, upload_time = "2023-10-21T14:20:13.451Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1c/f68769fba63ccb9c13fe0a25b616bd5aebeef1c7ddebc2ccc32462fb784d/websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d", size = 124460, upload_time = "2023-10-21T14:20:14.719Z" }, + { url = "https://files.pythonhosted.org/packages/20/52/8915f51f9aaef4e4361c89dd6cf69f72a0159f14e0d25026c81b6ad22525/websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f", size = 124985, upload_time = "2023-10-21T14:20:15.817Z" }, + { url = "https://files.pythonhosted.org/packages/43/8b/554a8a8bb6da9dd1ce04c44125e2192af7b7beebf6e3dbfa5d0e285cc20f/websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd", size = 121110, upload_time = "2023-10-21T14:20:48.335Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8e/58b8812940d746ad74d395fb069497255cb5ef50748dfab1e8b386b1f339/websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870", size = 123216, upload_time = "2023-10-21T14:20:50.083Z" }, + { url = "https://files.pythonhosted.org/packages/81/ee/272cb67ace1786ce6d9f39d47b3c55b335e8b75dd1972a7967aad39178b6/websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077", size = 122821, upload_time = "2023-10-21T14:20:51.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/03/387fc902b397729df166763e336f4e5cec09fe7b9d60f442542c94a21be1/websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b", size = 122768, upload_time = "2023-10-21T14:20:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/50/f0/5939fbc9bc1979d79a774ce5b7c4b33c0cefe99af22fb70f7462d0919640/websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30", size = 125009, upload_time = "2023-10-21T14:20:54.419Z" }, + { url = "https://files.pythonhosted.org/packages/79/4d/9cc401e7b07e80532ebc8c8e993f42541534da9e9249c59ee0139dcb0352/websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e", size = 118370, upload_time = "2023-10-21T14:21:10.075Z" }, ] [[package]] @@ -2705,9 +2706,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/51/2e0fc149e7a810d300422ab543f87f2bcf64d985eb6f1228c4efd6e4f8d4/werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18", size = 803342 } +sdist = { url = "https://files.pythonhosted.org/packages/02/51/2e0fc149e7a810d300422ab543f87f2bcf64d985eb6f1228c4efd6e4f8d4/werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18", size = 803342, upload_time = "2024-05-05T23:10:31.999Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/6e/e792999e816d19d7fcbfa94c730936750036d65656a76a5a688b57a656c4/werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8", size = 227274 }, + { url = "https://files.pythonhosted.org/packages/9d/6e/e792999e816d19d7fcbfa94c730936750036d65656a76a5a688b57a656c4/werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8", size = 227274, upload_time = "2024-05-05T23:10:29.567Z" }, ] [[package]] @@ -2717,9 +2718,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/c2/427f1867bb96555d1d34342f1dd97f8c420966ab564d58d18469a1db8736/zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd", size = 17350 } +sdist = { url = "https://files.pythonhosted.org/packages/46/c2/427f1867bb96555d1d34342f1dd97f8c420966ab564d58d18469a1db8736/zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd", size = 17350, upload_time = "2023-06-23T06:28:35.709Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/42/f8dbc2b9ad59e927940325a22d6d3931d630c3644dae7e2369ef5d9ba230/zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", size = 6824 }, + { url = "https://files.pythonhosted.org/packages/fe/42/f8dbc2b9ad59e927940325a22d6d3931d630c3644dae7e2369ef5d9ba230/zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", size = 6824, upload_time = "2023-06-23T06:28:32.652Z" }, ] [[package]] @@ -2729,24 +2730,24 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/03/6b85c1df2dca1b9acca38b423d1e226d8ffdf30ebd78bcb398c511de8b54/zope.interface-6.1.tar.gz", hash = "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309", size = 293914 } +sdist = { url = "https://files.pythonhosted.org/packages/87/03/6b85c1df2dca1b9acca38b423d1e226d8ffdf30ebd78bcb398c511de8b54/zope.interface-6.1.tar.gz", hash = "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309", size = 293914, upload_time = "2023-10-05T11:24:38.943Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/ec/c1e7ce928dc10bfe02c6da7e964342d941aaf168f96f8084636167ea50d2/zope.interface-6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb", size = 202417 }, - { url = "https://files.pythonhosted.org/packages/f7/0b/12f269ad049fc40a7a3ab85445d7855b6bc6f1e774c5ca9dd6f5c32becb3/zope.interface-6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92", size = 202528 }, - { url = "https://files.pythonhosted.org/packages/7f/85/3a35144509eb4a5a2208b48ae8d116a969d67de62cc6513d85602144d9cd/zope.interface-6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3", size = 247532 }, - { url = "https://files.pythonhosted.org/packages/50/d6/6176aaa1f6588378f5a5a4a9c6ad50a36824e902b2f844ca8de7f1b0c4a7/zope.interface-6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd", size = 241703 }, - { url = "https://files.pythonhosted.org/packages/4f/20/94d4f221989b4bbdd09004b2afb329958e776b7015b7ea8bc915327e195a/zope.interface-6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41", size = 247078 }, - { url = "https://files.pythonhosted.org/packages/97/7e/b790b4ab9605010816a91df26a715f163e228d60eb36c947c3118fb65190/zope.interface-6.1-cp310-cp310-win_amd64.whl", hash = "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f", size = 204155 }, - { url = "https://files.pythonhosted.org/packages/4a/0b/1d8817b8a3631384a26ff7faa4c1f3e6726f7e4950c3442721cfef2c95eb/zope.interface-6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1", size = 202441 }, - { url = "https://files.pythonhosted.org/packages/3e/1f/43557bb2b6e8537002a5a26af9b899171e26ddfcdf17a00ff729b00c036b/zope.interface-6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736", size = 202530 }, - { url = "https://files.pythonhosted.org/packages/37/a1/5d2b265f4b7371630cad5873d0873965e35ca3de993d11b9336c720f7259/zope.interface-6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605", size = 249584 }, - { url = "https://files.pythonhosted.org/packages/8b/6d/547bfa7465e5b296adba0aff5c7ace1150f2a9e429fbf6c33d6618275162/zope.interface-6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8", size = 243737 }, - { url = "https://files.pythonhosted.org/packages/db/5f/46946b588c43eb28efe0e46f4cf455b1ed8b2d1ea62a21b0001c6610662f/zope.interface-6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de", size = 249104 }, - { url = "https://files.pythonhosted.org/packages/6c/9c/9d3c0e7e5362ea59da3c42b3b2b9fc073db433a0fe3bc6cae0809ccec395/zope.interface-6.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1", size = 204155 }, - { url = "https://files.pythonhosted.org/packages/3c/91/68a0bbc97c2554f87d39572091954e94d043bcd83897cd6a779ca85cb5cc/zope.interface-6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a", size = 202757 }, - { url = "https://files.pythonhosted.org/packages/e1/84/850092a8ab7e87a3ea615daf3f822f7196c52592e3e92f264621b4cfe5a2/zope.interface-6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7", size = 202654 }, - { url = "https://files.pythonhosted.org/packages/57/23/508f7f79619ae4e025f5b264a9283efc3c805ed4c0ad75cb28c091179ced/zope.interface-6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d", size = 254400 }, - { url = "https://files.pythonhosted.org/packages/7c/0d/db0ccf0d12767015f23b302aebe98d5eca218aaadc70c2e3908b85fecd2a/zope.interface-6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff", size = 248853 }, - { url = "https://files.pythonhosted.org/packages/fd/4f/8e80173ebcdefe0ff4164444c22b171cf8bd72533026befc2adf079f3ac8/zope.interface-6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0", size = 255127 }, - { url = "https://files.pythonhosted.org/packages/0f/d5/81f9789311d9773a02ed048af7452fc6cedce059748dba956c1dc040340a/zope.interface-6.1-cp312-cp312-win_amd64.whl", hash = "sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b", size = 204268 }, + { url = "https://files.pythonhosted.org/packages/3c/ec/c1e7ce928dc10bfe02c6da7e964342d941aaf168f96f8084636167ea50d2/zope.interface-6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb", size = 202417, upload_time = "2023-10-05T11:24:25.141Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/12f269ad049fc40a7a3ab85445d7855b6bc6f1e774c5ca9dd6f5c32becb3/zope.interface-6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92", size = 202528, upload_time = "2023-10-05T11:24:27.336Z" }, + { url = "https://files.pythonhosted.org/packages/7f/85/3a35144509eb4a5a2208b48ae8d116a969d67de62cc6513d85602144d9cd/zope.interface-6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3", size = 247532, upload_time = "2023-10-05T11:49:20.587Z" }, + { url = "https://files.pythonhosted.org/packages/50/d6/6176aaa1f6588378f5a5a4a9c6ad50a36824e902b2f844ca8de7f1b0c4a7/zope.interface-6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd", size = 241703, upload_time = "2023-10-05T11:25:33.542Z" }, + { url = "https://files.pythonhosted.org/packages/4f/20/94d4f221989b4bbdd09004b2afb329958e776b7015b7ea8bc915327e195a/zope.interface-6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41", size = 247078, upload_time = "2023-10-05T11:25:48.235Z" }, + { url = "https://files.pythonhosted.org/packages/97/7e/b790b4ab9605010816a91df26a715f163e228d60eb36c947c3118fb65190/zope.interface-6.1-cp310-cp310-win_amd64.whl", hash = "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f", size = 204155, upload_time = "2023-10-05T11:37:56.715Z" }, + { url = "https://files.pythonhosted.org/packages/4a/0b/1d8817b8a3631384a26ff7faa4c1f3e6726f7e4950c3442721cfef2c95eb/zope.interface-6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1", size = 202441, upload_time = "2023-10-05T11:24:20.414Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1f/43557bb2b6e8537002a5a26af9b899171e26ddfcdf17a00ff729b00c036b/zope.interface-6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736", size = 202530, upload_time = "2023-10-05T11:24:22.975Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/5d2b265f4b7371630cad5873d0873965e35ca3de993d11b9336c720f7259/zope.interface-6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605", size = 249584, upload_time = "2023-10-05T11:49:22.978Z" }, + { url = "https://files.pythonhosted.org/packages/8b/6d/547bfa7465e5b296adba0aff5c7ace1150f2a9e429fbf6c33d6618275162/zope.interface-6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8", size = 243737, upload_time = "2023-10-05T11:25:35.439Z" }, + { url = "https://files.pythonhosted.org/packages/db/5f/46946b588c43eb28efe0e46f4cf455b1ed8b2d1ea62a21b0001c6610662f/zope.interface-6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de", size = 249104, upload_time = "2023-10-05T11:25:51.355Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9c/9d3c0e7e5362ea59da3c42b3b2b9fc073db433a0fe3bc6cae0809ccec395/zope.interface-6.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1", size = 204155, upload_time = "2023-10-05T11:39:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/3c/91/68a0bbc97c2554f87d39572091954e94d043bcd83897cd6a779ca85cb5cc/zope.interface-6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a", size = 202757, upload_time = "2023-10-05T11:25:05.865Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/850092a8ab7e87a3ea615daf3f822f7196c52592e3e92f264621b4cfe5a2/zope.interface-6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7", size = 202654, upload_time = "2023-10-05T11:25:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/57/23/508f7f79619ae4e025f5b264a9283efc3c805ed4c0ad75cb28c091179ced/zope.interface-6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d", size = 254400, upload_time = "2023-10-05T11:49:25.326Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/db0ccf0d12767015f23b302aebe98d5eca218aaadc70c2e3908b85fecd2a/zope.interface-6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff", size = 248853, upload_time = "2023-10-05T11:25:37.37Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4f/8e80173ebcdefe0ff4164444c22b171cf8bd72533026befc2adf079f3ac8/zope.interface-6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0", size = 255127, upload_time = "2023-10-05T11:25:53.819Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d5/81f9789311d9773a02ed048af7452fc6cedce059748dba956c1dc040340a/zope.interface-6.1-cp312-cp312-win_amd64.whl", hash = "sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b", size = 204268, upload_time = "2023-10-05T11:41:22.778Z" }, ] From 0426b574fee6cbc735005aa08f0386002614d181 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:45:38 +0000 Subject: [PATCH 036/356] fix(deps): update typescript-projects (#17625) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Zack Pollard --- cli/package-lock.json | 84 +++++++++++- e2e/package-lock.json | 22 +-- server/package-lock.json | 228 ++++++++++++++++++++++++------ server/package.json | 10 +- web/package-lock.json | 290 +++++++++++++++++++++------------------ 5 files changed, 435 insertions(+), 199 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index daac54a552..17d8cbab8e 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -4028,6 +4028,51 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinypool": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", @@ -4247,15 +4292,18 @@ } }, "node_modules/vite": { - "version": "6.2.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", - "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz", + "integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.3", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.12" }, "bin": { "vite": "bin/vite.js" @@ -4361,6 +4409,34 @@ } } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vitest": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.1.tgz", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 07234a770f..cfaff74545 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1082,13 +1082,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.1.tgz", - "integrity": "sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.51.1" + "playwright": "1.52.0" }, "bin": { "playwright": "cli.js" @@ -5424,13 +5424,13 @@ } }, "node_modules/playwright": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", - "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.51.1" + "playwright-core": "1.52.0" }, "bin": { "playwright": "cli.js" @@ -5443,9 +5443,9 @@ } }, "node_modules/playwright-core": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", - "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/server/package-lock.json b/server/package-lock.json index ca0fc8b1fd..297370187d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -19,7 +19,7 @@ "@nestjs/schedule": "^5.0.0", "@nestjs/swagger": "^11.0.2", "@nestjs/websockets": "^11.0.4", - "@opentelemetry/auto-instrumentations-node": "^0.57.0", + "@opentelemetry/auto-instrumentations-node": "^0.58.0", "@opentelemetry/context-async-hooks": "^2.0.0", "@opentelemetry/exporter-prometheus": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0", @@ -63,7 +63,7 @@ "sanitize-filename": "^1.6.3", "sanitize-html": "^2.14.0", "semver": "^7.6.2", - "sharp": "^0.33.0", + "sharp": "^0.33.5", "sirv": "^3.0.0", "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", @@ -858,9 +858,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.2.tgz", - "integrity": "sha512-+b+3BJl18a0LKeHvy5eLOwPkiaz10C2MUUYKQ25itZS50TlP5FuDh2Q5EiFlB++vAuCS6HnrihqVlbdcRYyp9w==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", "license": "MIT", "optional": true, "dependencies": { @@ -2741,12 +2741,14 @@ } }, "node_modules/@nestjs/common": { - "version": "11.0.17", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.0.17.tgz", - "integrity": "sha512-FwKylI/hVxaNvzBJdWMMG1LH0cLKz4Oh4jKOHet2JUVMM9j6CuodRbrSnL++KL6PJY/b2E6AY58UDPLNeCqJWw==", + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.0.20.tgz", + "integrity": "sha512-/GH8NDCczjn6+6RNEtSNAts/nq/wQE8L1qZ9TRjqjNqEsZNE1vpFuRIhmcO2isQZ0xY5rySnpaRdrOAul3gQ3A==", "license": "MIT", "dependencies": { + "file-type": "20.4.1", "iterare": "1.2.1", + "load-esm": "1.0.2", "tslib": "2.8.1", "uid": "2.0.2" }, @@ -2757,7 +2759,6 @@ "peerDependencies": { "class-transformer": "*", "class-validator": "*", - "file-type": "^20.4.1", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, @@ -2767,16 +2768,13 @@ }, "class-validator": { "optional": true - }, - "file-type": { - "optional": true } } }, "node_modules/@nestjs/core": { - "version": "11.0.17", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.0.17.tgz", - "integrity": "sha512-ImK6qNxtegKqK7EJLGTBpP5Ild/DTpcduEtAOS+WLLjZOMjK1k214G9roXvlrNQwlVt9ALAY2jcqnsasdEd7Ow==", + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.0.20.tgz", + "integrity": "sha512-yUkEzBGiRNSEThVl6vMCXgoA9sDGWoRbJsTLdYdCC7lg7PE1iXBnna1FiBfQjT995pm0fjyM1e3WsXmyWeJXbw==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -2848,9 +2846,9 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.0.17", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.0.17.tgz", - "integrity": "sha512-et6Ydd6dR0FlcE/WR/9VRnQoTqEpDdzBgGK+aWadA0dFJ65wlN+snJRg/9JGP4ngj90S6xwe0VKD/BbfUGj9cw==", + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.0.20.tgz", + "integrity": "sha512-h/Xq2x0Qi2cr9T64w9DfLejZws1M1hYu7n7XWuC4vxX00FlfOz1jSWGgaTo/Gjq6vtULfq34Gp5Fzf0w34XDyQ==", "license": "MIT", "dependencies": { "cors": "2.8.5", @@ -2869,9 +2867,9 @@ } }, "node_modules/@nestjs/platform-socket.io": { - "version": "11.0.17", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.0.17.tgz", - "integrity": "sha512-l9b8VNb7N7rB9IUwKeln2bMQDltsR9mpenzHOaYYqDkz5BtuQSiyT8NpLR2vWhxDjppxMY3DkW8fQAvXh54pMg==", + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.0.20.tgz", + "integrity": "sha512-fUyDjLt0wJ4WK+rXrd5/oSWw5xWpfDOknpP7YNgaFfvYW726KuS5gWysV7JPD2mgH85S6i+qiO3qZvHIs5DvxQ==", "license": "MIT", "dependencies": { "socket.io": "4.8.1", @@ -3024,9 +3022,9 @@ } }, "node_modules/@nestjs/swagger": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.1.3.tgz", - "integrity": "sha512-vhbW/Xu05Diti/EwYQp3Ea7Hj2M++wiakCcxqUUDA2n7NvCZC8LKsrcGynw6/x/lugdXyklYS+s2FhdAfeAikg==", + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.1.4.tgz", + "integrity": "sha512-px+ue6YeNyExL7Vg39HMJb3iPgFJD5oiOzUFS+4I0PhKNznMjSxMMZyDh1M8cuhqt4s3YvE7b0e/v6BgWdx/bQ==", "license": "MIT", "dependencies": { "@microsoft/tsdoc": "0.15.1", @@ -3057,9 +3055,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "11.0.17", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.0.17.tgz", - "integrity": "sha512-ryEx6fCYZFCsjEBZo8jOVikQluEHMESocVqHdXWOkkG7UqMPMHimf9gT2qij0GpNnYeDAGw+i7FhSJN3Cajoug==", + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.0.20.tgz", + "integrity": "sha512-3o+HWsVfA46tt81ctKuNj5ufL9srfmp3dQBCAIx9fzvjooEKwWl5L69AcvDh6JhdB79jhhM1lkSSU+1fBGbxgw==", "dev": true, "license": "MIT", "dependencies": { @@ -3085,9 +3083,9 @@ } }, "node_modules/@nestjs/websockets": { - "version": "11.0.17", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.0.17.tgz", - "integrity": "sha512-2LSjxA/lUKs5hv/g5lPk555CoRNTCt/XywHFteKMSrxo09Cq3yfOQOAPwEWG929EnqAjAAsQaDVbfUHUFisFCg==", + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.0.20.tgz", + "integrity": "sha512-qcybahXdrPJFMILhAwJML9D/bExBEBFsfwFiePCeI4f//tiP0rXiLspLVOHClSeUPBaCNrx+Ae/HVe9UP+wtOg==", "license": "MIT", "dependencies": { "iterare": "1.2.1", @@ -3314,9 +3312,9 @@ } }, "node_modules/@opentelemetry/auto-instrumentations-node": { - "version": "0.57.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.57.1.tgz", - "integrity": "sha512-yy+K3vYybqJ6Z4XZCXYYxEC1DtEpPrnJdwxkhI0sTtVlrVnzx49iRLqpMmdvQ4b09+PrvXSN9t0jODMCGNrs8w==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.58.0.tgz", + "integrity": "sha512-gtqPqkXp8TG6vrmbzAJUKjJm3nrCiVGgImlV1tj8lsVqpnKDCB1Kl7bCcXod36+Tq/O4rCeTDmW90dCHeuv9jQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/instrumentation": "^0.200.0", @@ -3329,7 +3327,7 @@ "@opentelemetry/instrumentation-cucumber": "^0.15.0", "@opentelemetry/instrumentation-dataloader": "^0.17.0", "@opentelemetry/instrumentation-dns": "^0.44.0", - "@opentelemetry/instrumentation-express": "^0.48.0", + "@opentelemetry/instrumentation-express": "^0.48.1", "@opentelemetry/instrumentation-fastify": "^0.45.0", "@opentelemetry/instrumentation-fs": "^0.20.0", "@opentelemetry/instrumentation-generic-pool": "^0.44.0", @@ -3338,7 +3336,7 @@ "@opentelemetry/instrumentation-hapi": "^0.46.0", "@opentelemetry/instrumentation-http": "^0.200.0", "@opentelemetry/instrumentation-ioredis": "^0.48.0", - "@opentelemetry/instrumentation-kafkajs": "^0.9.0", + "@opentelemetry/instrumentation-kafkajs": "^0.9.1", "@opentelemetry/instrumentation-knex": "^0.45.0", "@opentelemetry/instrumentation-koa": "^0.48.0", "@opentelemetry/instrumentation-lru-memoizer": "^0.45.0", @@ -3355,6 +3353,7 @@ "@opentelemetry/instrumentation-redis-4": "^0.47.0", "@opentelemetry/instrumentation-restify": "^0.46.0", "@opentelemetry/instrumentation-router": "^0.45.0", + "@opentelemetry/instrumentation-runtime-node": "^0.14.0", "@opentelemetry/instrumentation-socket.io": "^0.47.0", "@opentelemetry/instrumentation-tedious": "^0.19.0", "@opentelemetry/instrumentation-undici": "^0.11.0", @@ -3801,9 +3800,9 @@ } }, "node_modules/@opentelemetry/instrumentation-express": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.48.0.tgz", - "integrity": "sha512-x9L6YD7AfE+7hysSv8k0d0sFmq3Vo3zoa/5eeJBYkGWHnD92CvekKouPyqUt71oX0htmZRdIawrhrwrAi2sonQ==", + "version": "0.48.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.48.1.tgz", + "integrity": "sha512-j8NYOf9DRWtchbWor/zA0poI42TpZG9tViIKA0e1lC+6MshTqSJYtgNv8Fn1sx1Wn/TRyp+5OgSXiE4LDfvpEg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", @@ -3949,9 +3948,9 @@ } }, "node_modules/@opentelemetry/instrumentation-kafkajs": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.9.0.tgz", - "integrity": "sha512-Uxt/LTSmrzTYtnPpPn/L2W7+tjn38+v8tSnJ7hvaE3/aRXmZA5e72n+pHv0mlCI0pVNTihiQCUE62XYWPZ4jjA==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.9.1.tgz", + "integrity": "sha512-eGl5WKBqd0unOKm7PJKjEa1G+ac9nvpDjyv870nUYuSnUkyDc/Fag5keddIjHixTJwRp3FmyP7n+AadAjh52Vw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/instrumentation": "^0.200.0", @@ -4232,6 +4231,21 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-runtime-node": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.14.0.tgz", + "integrity": "sha512-y78dGoFMKwHSz0SD113Gt1dFTcfunpPZXIJh2SzJN27Lyb9FIzuMfjc3Iu3+s/N6qNOLuS9mKnPe3/qVGG4Waw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.200.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-socket.io": { "version": "0.47.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.47.0.tgz", @@ -5658,6 +5672,30 @@ "testcontainers": "^10.24.2" } }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, "node_modules/@turf/boolean-point-in-polygon": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.2.0.tgz", @@ -9756,6 +9794,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -9802,6 +9846,24 @@ "stream-source": "0.3" } }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -11702,6 +11764,25 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/load-esm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.2.tgz", + "integrity": "sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, "node_modules/load-tsconfig": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", @@ -13218,6 +13299,19 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/peek-readable": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-7.0.0.tgz", + "integrity": "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/pg": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.1.tgz", @@ -14636,9 +14730,9 @@ } }, "node_modules/sanitize-html": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.15.0.tgz", - "integrity": "sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.16.0.tgz", + "integrity": "sha512-0s4caLuHHaZFVxFTG74oW91+j6vW7gKbGD6CD2+miP73CE6z6YtOBN0ArtLd2UGyi4IC7K47v3ENUbQX4jV3Mg==", "license": "MIT", "dependencies": { "deepmerge": "^4.2.2", @@ -15525,6 +15619,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.2.tgz", + "integrity": "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -16397,6 +16508,23 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", + "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -16921,6 +17049,18 @@ "node": ">= 4.0.0" } }, + "node_modules/uint8array-extras": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undici": { "version": "5.29.0", "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", diff --git a/server/package.json b/server/package.json index 4373e4abb7..441f04abeb 100644 --- a/server/package.json +++ b/server/package.json @@ -44,7 +44,7 @@ "@nestjs/schedule": "^5.0.0", "@nestjs/swagger": "^11.0.2", "@nestjs/websockets": "^11.0.4", - "@opentelemetry/auto-instrumentations-node": "^0.57.0", + "@opentelemetry/auto-instrumentations-node": "^0.58.0", "@opentelemetry/context-async-hooks": "^2.0.0", "@opentelemetry/exporter-prometheus": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0", @@ -88,7 +88,7 @@ "sanitize-filename": "^1.6.3", "sanitize-html": "^2.14.0", "semver": "^7.6.2", - "sharp": "^0.33.0", + "sharp": "^0.33.5", "sirv": "^3.0.0", "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", @@ -130,6 +130,7 @@ "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^57.0.0", "globals": "^16.0.0", + "jsdom": "^26.1.0", "mock-fs": "^5.2.0", "node-addon-api": "^8.3.0", "patch-package": "^8.0.0", @@ -147,10 +148,9 @@ "unplugin-swc": "^1.4.5", "utimes": "^5.2.1", "vite-tsconfig-paths": "^5.0.0", - "vitest": "^3.0.0", - "jsdom": "^26.1.0" + "vitest": "^3.0.0" }, "volta": { "node": "22.14.0" } -} \ No newline at end of file +} diff --git a/web/package-lock.json b/web/package-lock.json index 57af7fa56d..fb6a059197 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -87,7 +87,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.14.0", + "@types/node": "^22.14.1", "typescript": "^5.3.3" } }, @@ -495,9 +495,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", + "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", "cpu": [ "arm64" ], @@ -528,9 +528,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", + "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", "cpu": [ "arm64" ], @@ -1705,50 +1705,50 @@ } }, "node_modules/@photo-sphere-viewer/core": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.13.1.tgz", - "integrity": "sha512-f5fkoGPCBUt5BD9S9U37h+UDmo2slMZThesoH82iyjKR6uRyYnJvJXwopwo+iMfc6x1ZAWmustBBuES4qKzx+g==", + "version": "5.13.2", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.13.2.tgz", + "integrity": "sha512-rL4Ey39Prx4Iyxt1f2tAqlXvqu4/ovXfUvIpLt540OpZJiFjWccs6qLywof9vuhBJ7PXHudHWCjRPce0W8kx8w==", "license": "MIT", "dependencies": { "three": "^0.175.0" } }, "node_modules/@photo-sphere-viewer/equirectangular-video-adapter": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.13.1.tgz", - "integrity": "sha512-5LCMMc1bnKMFvR//TKguSwyBEBF+fTYLOGnnCzS7HHHNr8jc+bmaxBsPhOENZf8VcoupXYxo4KRNfYwIB0nTEA==", + "version": "5.13.2", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.13.2.tgz", + "integrity": "sha512-Ln9VyZSGAEjqtJ5dYluiSYkUF87FsOwzZvQoEgAt4odQR/q7ktSaVDdRfuuTMcbBKq6kTdsavNzdg+g877WyhA==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.13.1", - "@photo-sphere-viewer/video-plugin": "5.13.1" + "@photo-sphere-viewer/core": "5.13.2", + "@photo-sphere-viewer/video-plugin": "5.13.2" } }, "node_modules/@photo-sphere-viewer/resolution-plugin": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/resolution-plugin/-/resolution-plugin-5.13.1.tgz", - "integrity": "sha512-XVxR5rAtGYbcy0PQfgGgMAuRAg5gb/tRjgMiB9zzQ6sESLviWCqvk247z4Q6J4TxNYeGSQzKbyous1eS+nUqTg==", + "version": "5.13.2", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/resolution-plugin/-/resolution-plugin-5.13.2.tgz", + "integrity": "sha512-T2bUvtKqhPk7FVqRJfynWhnglMpar5FNxCgf3EsnFjV9g+Xnc0LmOLlCeNmsCWXv0lRmNbohDMRN1WpY1O3ojA==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.13.1", - "@photo-sphere-viewer/settings-plugin": "5.13.1" + "@photo-sphere-viewer/core": "5.13.2", + "@photo-sphere-viewer/settings-plugin": "5.13.2" } }, "node_modules/@photo-sphere-viewer/settings-plugin": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/settings-plugin/-/settings-plugin-5.13.1.tgz", - "integrity": "sha512-W2naZCP9huhN6cmFcGfgJEvxqrBB481/an8o/qice5iIH9xw50qXiDq6czLCtUo8GD4P9ULtsoWo9DUT2EsWzw==", + "version": "5.13.2", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/settings-plugin/-/settings-plugin-5.13.2.tgz", + "integrity": "sha512-z1539qy4XC9UextvgxFBBZqiNKQ1DzaI4EZRbrRbfG6LnSsjKwGgX8gIZ8ZpBHoZdU+b2d8PRPmNKiSjhYOvGA==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.13.1" + "@photo-sphere-viewer/core": "5.13.2" } }, "node_modules/@photo-sphere-viewer/video-plugin": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.13.1.tgz", - "integrity": "sha512-GmjI4weoKRCOACNEIloL3XSAbYVyrO6s8esJfmpjdGKdDr3kQjKK+oiplRSD28ALIhSxyqemNOGodfgzWH7xLA==", + "version": "5.13.2", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.13.2.tgz", + "integrity": "sha512-6/tajOJaPUDP7mwtdQZul+KNfjL2sUPUt7EcAHZ9KcSq1WcwqZfaUYSCdKaW2uxdpn4BLESSD1h0mJdWNw7vJA==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.13.1" + "@photo-sphere-viewer/core": "5.13.2" } }, "node_modules/@pkgjs/parseargs": { @@ -4601,9 +4601,9 @@ } }, "node_modules/fdir": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", - "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -8174,9 +8174,9 @@ } }, "node_modules/svelte": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.27.0.tgz", - "integrity": "sha512-Uai13Ydt1ZE+bUHme6b9U38PCYVNCqBRoBMkUKbFbKiD7kHWjdUUrklYAQZJxyKK81qII4mrBwe/YmvEMSlC9w==", + "version": "5.27.3", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.27.3.tgz", + "integrity": "sha512-MK16NUEFwAunCkdJpIIJ6hvKElx0zFlKMqQd7NAIugMfrL0YeOH8VEn5pg9g2Q6RLj2JrGJL6c0zaAwmXx/nHQ==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -8741,6 +8741,23 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tinypool": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", @@ -9036,15 +9053,18 @@ } }, "node_modules/vite": { - "version": "6.2.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", - "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz", + "integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.3", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.12" }, "bin": { "vite": "bin/vite.js" @@ -9146,9 +9166,9 @@ } }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", + "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", "cpu": [ "ppc64" ], @@ -9163,9 +9183,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", + "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", "cpu": [ "arm" ], @@ -9180,9 +9200,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", + "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", "cpu": [ "arm64" ], @@ -9197,9 +9217,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", + "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", "cpu": [ "x64" ], @@ -9214,9 +9234,9 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", + "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", "cpu": [ "arm64" ], @@ -9231,9 +9251,9 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", + "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", "cpu": [ "x64" ], @@ -9248,9 +9268,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", + "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", "cpu": [ "arm64" ], @@ -9265,9 +9285,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", + "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", "cpu": [ "x64" ], @@ -9282,9 +9302,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", + "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", "cpu": [ "arm" ], @@ -9299,9 +9319,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", + "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", "cpu": [ "arm64" ], @@ -9316,9 +9336,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", + "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", "cpu": [ "ia32" ], @@ -9333,9 +9353,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", + "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", "cpu": [ "loong64" ], @@ -9350,9 +9370,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", + "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", "cpu": [ "mips64el" ], @@ -9367,9 +9387,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", + "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", "cpu": [ "ppc64" ], @@ -9384,9 +9404,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", + "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", "cpu": [ "riscv64" ], @@ -9401,9 +9421,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", + "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", "cpu": [ "s390x" ], @@ -9418,9 +9438,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", + "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", "cpu": [ "x64" ], @@ -9435,9 +9455,9 @@ } }, "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", + "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", "cpu": [ "x64" ], @@ -9452,9 +9472,9 @@ } }, "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", + "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", "cpu": [ "x64" ], @@ -9469,9 +9489,9 @@ } }, "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", + "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", "cpu": [ "x64" ], @@ -9486,9 +9506,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", + "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", "cpu": [ "arm64" ], @@ -9503,9 +9523,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", + "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", "cpu": [ "ia32" ], @@ -9520,9 +9540,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", + "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", "cpu": [ "x64" ], @@ -9537,9 +9557,9 @@ } }, "node_modules/vite/node_modules/esbuild": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", + "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -9550,31 +9570,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.2", - "@esbuild/android-arm": "0.25.2", - "@esbuild/android-arm64": "0.25.2", - "@esbuild/android-x64": "0.25.2", - "@esbuild/darwin-arm64": "0.25.2", - "@esbuild/darwin-x64": "0.25.2", - "@esbuild/freebsd-arm64": "0.25.2", - "@esbuild/freebsd-x64": "0.25.2", - "@esbuild/linux-arm": "0.25.2", - "@esbuild/linux-arm64": "0.25.2", - "@esbuild/linux-ia32": "0.25.2", - "@esbuild/linux-loong64": "0.25.2", - "@esbuild/linux-mips64el": "0.25.2", - "@esbuild/linux-ppc64": "0.25.2", - "@esbuild/linux-riscv64": "0.25.2", - "@esbuild/linux-s390x": "0.25.2", - "@esbuild/linux-x64": "0.25.2", - "@esbuild/netbsd-arm64": "0.25.2", - "@esbuild/netbsd-x64": "0.25.2", - "@esbuild/openbsd-arm64": "0.25.2", - "@esbuild/openbsd-x64": "0.25.2", - "@esbuild/sunos-x64": "0.25.2", - "@esbuild/win32-arm64": "0.25.2", - "@esbuild/win32-ia32": "0.25.2", - "@esbuild/win32-x64": "0.25.2" + "@esbuild/aix-ppc64": "0.25.3", + "@esbuild/android-arm": "0.25.3", + "@esbuild/android-arm64": "0.25.3", + "@esbuild/android-x64": "0.25.3", + "@esbuild/darwin-arm64": "0.25.3", + "@esbuild/darwin-x64": "0.25.3", + "@esbuild/freebsd-arm64": "0.25.3", + "@esbuild/freebsd-x64": "0.25.3", + "@esbuild/linux-arm": "0.25.3", + "@esbuild/linux-arm64": "0.25.3", + "@esbuild/linux-ia32": "0.25.3", + "@esbuild/linux-loong64": "0.25.3", + "@esbuild/linux-mips64el": "0.25.3", + "@esbuild/linux-ppc64": "0.25.3", + "@esbuild/linux-riscv64": "0.25.3", + "@esbuild/linux-s390x": "0.25.3", + "@esbuild/linux-x64": "0.25.3", + "@esbuild/netbsd-arm64": "0.25.3", + "@esbuild/netbsd-x64": "0.25.3", + "@esbuild/openbsd-arm64": "0.25.3", + "@esbuild/openbsd-x64": "0.25.3", + "@esbuild/sunos-x64": "0.25.3", + "@esbuild/win32-arm64": "0.25.3", + "@esbuild/win32-ia32": "0.25.3", + "@esbuild/win32-x64": "0.25.3" } }, "node_modules/vitefu": { From bc5875ba8dfd6905731ad13e70ab0f79a998554a Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Wed, 23 Apr 2025 13:05:31 +0100 Subject: [PATCH 037/356] chore: multithreaded web linting (#17809) --- .github/workflows/test.yml | 55 ++++++++++++++++++++++++++++--------- web/package-lock.json | 56 ++++++++++++++++++++------------------ web/package.json | 2 ++ 3 files changed, 74 insertions(+), 39 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d7e07c383..2840a631eb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -184,8 +184,49 @@ jobs: run: npm run test:cov if: ${{ !cancelled() }} + web-lint: + name: Lint Web + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_web == 'true' }} + runs-on: mich + permissions: + contents: read + defaults: + run: + working-directory: ./web + + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version-file: './web/.nvmrc' + + - name: Run setup typescript-sdk + run: npm ci && npm run build + working-directory: ./open-api/typescript-sdk + + - name: Run npm install + run: npm ci + + - name: Run linter + run: npm run lint:p + if: ${{ !cancelled() }} + + - name: Run formatter + run: npm run format + if: ${{ !cancelled() }} + + - name: Run svelte checks + run: npm run check:svelte + if: ${{ !cancelled() }} + web-unit-tests: - name: Test & Lint Web + name: Test Web needs: pre-job if: ${{ needs.pre-job.outputs.should_run_web == 'true' }} runs-on: ubuntu-latest @@ -213,18 +254,6 @@ jobs: - name: Run npm install run: npm ci - - name: Run linter - run: npm run lint - if: ${{ !cancelled() }} - - - name: Run formatter - run: npm run format - if: ${{ !cancelled() }} - - - name: Run svelte checks - run: npm run check:svelte - if: ${{ !cancelled() }} - - name: Run tsc run: npm run check:typescript if: ${{ !cancelled() }} diff --git a/web/package-lock.json b/web/package-lock.json index fb6a059197..af87772105 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -59,6 +59,7 @@ "dotenv": "^16.4.7", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.0", + "eslint-p": "^0.21.0", "eslint-plugin-svelte": "^3.0.0", "eslint-plugin-unicorn": "^57.0.0", "factory.ts": "^1.4.1", @@ -692,9 +693,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -742,9 +743,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", - "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", + "version": "9.25.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", + "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", "dev": true, "license": "MIT", "engines": { @@ -775,19 +776,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@faker-js/faker": { "version": "9.7.0", "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.7.0.tgz", @@ -4189,20 +4177,20 @@ } }, "node_modules/eslint": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", - "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", + "version": "9.25.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz", + "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.0", - "@eslint/core": "^0.12.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.24.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/js": "9.25.1", + "@eslint/plugin-kit": "^0.2.8", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -4262,6 +4250,22 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-p": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/eslint-p/-/eslint-p-0.21.0.tgz", + "integrity": "sha512-w6krTooDpMF5Rezfc14FOQwX87QyH2ouZoTDySixbL8auiBRsxA4JPBrzOS5IfuLxcJHJPHjf/FnGfjlvI/kDw==", + "dev": true, + "license": "ISC", + "dependencies": { + "eslint": "9.25.1" + }, + "bin": { + "eslint-p": "lib/eslint-p.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/eslint-plugin-svelte": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.5.1.tgz", diff --git a/web/package.json b/web/package.json index 4ee7c015c8..80610e661e 100644 --- a/web/package.json +++ b/web/package.json @@ -15,6 +15,7 @@ "check:code": "npm run format && npm run lint && npm run check:svelte && npm run check:typescript", "check:all": "npm run check:code && npm run test:cov", "lint": "eslint . --max-warnings 0", + "lint:p": "eslint-p . --max-warnings 0 --concurrency=4", "lint:fix": "npm run lint -- --fix", "format": "prettier --check .", "format:fix": "prettier --write .", @@ -74,6 +75,7 @@ "dotenv": "^16.4.7", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.0", + "eslint-p": "^0.21.0", "eslint-plugin-svelte": "^3.0.0", "eslint-plugin-unicorn": "^57.0.0", "factory.ts": "^1.4.1", From 1de2eae12d6aa0007497628ee09e2bbab5752ec6 Mon Sep 17 00:00:00 2001 From: Toni <51962051+EinToni@users.noreply.github.com> Date: Wed, 23 Apr 2025 15:55:51 +0200 Subject: [PATCH 038/356] perf(mobile): remove load of thumbnails in the image provider (#17773) Remove loading of thumbnail in the image provider * Removed the load of the thumbnail from the local and remote image provider as they shall provide the image, not the thumbnail. The thumbnail gets provided by the thumbnail provider. * The thumbnail provider is used as the loadingBuilder and the image provider as the imageProvider. Therefore loading the thumbnail in the image provider loads it a second time which is completely redundant, uses precious time and yields no results. Co-authored-by: Alex --- .../image/immich_local_image_provider.dart | 21 +++---------------- .../image/immich_remote_image_provider.dart | 21 ------------------- 2 files changed, 3 insertions(+), 39 deletions(-) diff --git a/mobile/lib/providers/image/immich_local_image_provider.dart b/mobile/lib/providers/image/immich_local_image_provider.dart index c152934333..4c77ee4b56 100644 --- a/mobile/lib/providers/image/immich_local_image_provider.dart +++ b/mobile/lib/providers/image/immich_local_image_provider.dart @@ -53,50 +53,35 @@ class ImmichLocalImageProvider extends ImageProvider { ImageDecoderCallback decode, StreamController chunkEvents, ) async* { - ui.ImmutableBuffer? buffer; try { final local = asset.local; if (local == null) { throw StateError('Asset ${asset.fileName} has no local data'); } - var thumbBytes = await local - .thumbnailDataWithSize(const ThumbnailSize.square(256), quality: 80); - if (thumbBytes == null) { - throw StateError("Loading thumbnail for ${asset.fileName} failed"); - } - buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); - thumbBytes = null; - yield await decode(buffer); - buffer = null; - switch (asset.type) { case AssetType.image: final File? file = await local.originFile; if (file == null) { throw StateError("Opening file for asset ${asset.fileName} failed"); } - buffer = await ui.ImmutableBuffer.fromFilePath(file.path); + final buffer = await ui.ImmutableBuffer.fromFilePath(file.path); yield await decode(buffer); - buffer = null; break; case AssetType.video: final size = ThumbnailSize(width.ceil(), height.ceil()); - thumbBytes = await local.thumbnailDataWithSize(size); + final thumbBytes = await local.thumbnailDataWithSize(size); if (thumbBytes == null) { throw StateError("Failed to load preview for ${asset.fileName}"); } - buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); - thumbBytes = null; + final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); yield await decode(buffer); - buffer = null; break; default: throw StateError('Unsupported asset type ${asset.type}'); } } catch (error, stack) { log.severe('Error loading local image ${asset.fileName}', error, stack); - buffer?.dispose(); } finally { chunkEvents.close(); } diff --git a/mobile/lib/providers/image/immich_remote_image_provider.dart b/mobile/lib/providers/image/immich_remote_image_provider.dart index 9e1d8aa120..d5189fa4fc 100644 --- a/mobile/lib/providers/image/immich_remote_image_provider.dart +++ b/mobile/lib/providers/image/immich_remote_image_provider.dart @@ -57,12 +57,6 @@ class ImmichRemoteImageProvider AppSettingsEnum.loadOriginal.defaultValue, ); - /// Whether to load the preview thumbnail first or not - bool get _loadPreview => Store.get( - AppSettingsEnum.loadPreview.storeKey, - AppSettingsEnum.loadPreview.defaultValue, - ); - // Streams in each stage of the image as we ask for it Stream _codec( ImmichRemoteImageProvider key, @@ -70,21 +64,6 @@ class ImmichRemoteImageProvider ImageDecoderCallback decode, StreamController chunkEvents, ) async* { - // Load a preview to the chunk events - if (_loadPreview) { - final preview = getThumbnailUrlForRemoteId( - key.assetId, - type: api.AssetMediaSize.thumbnail, - ); - - yield await ImageLoader.loadImageFromCache( - preview, - cache: cache, - decode: decode, - chunkEvents: chunkEvents, - ); - } - // Load the higher resolution version of the image final url = getThumbnailUrlForRemoteId( key.assetId, From 13d6bd67b163b9fd58b8cc0129eb14ee397e5cdc Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 23 Apr 2025 09:02:51 -0500 Subject: [PATCH 039/356] feat: no small local thumbnail (#17787) * feat: no small local thumbnail * pr feedback --- .../immich_local_thumbnail_provider.dart | 35 +++++++------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart index 54dfd97983..1e2f5d312e 100644 --- a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart +++ b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart @@ -42,42 +42,33 @@ class ImmichLocalThumbnailProvider scale: 1.0, chunkEvents: chunkEvents.stream, informationCollector: () sync* { - yield ErrorDescription(asset.fileName); + yield ErrorDescription(key.asset.fileName); }, ); } // Streams in each stage of the image as we ask for it Stream _codec( - Asset key, + Asset assetData, ImageDecoderCallback decode, StreamController chunkEvents, ) async* { - // Load a small thumbnail - final thumbBytes = await asset.local?.thumbnailDataWithSize( - const ThumbnailSize.square(32), - quality: 75, - ); - if (thumbBytes != null) { - final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); - final codec = await decode(buffer); - yield codec; - } else { - debugPrint("Loading thumb for ${asset.fileName} failed"); - } - - final normalThumbBytes = - await asset.local?.thumbnailDataWithSize(ThumbnailSize(width, height)); - if (normalThumbBytes == null) { + final thumbBytes = await assetData.local + ?.thumbnailDataWithSize(ThumbnailSize(width, height)); + if (thumbBytes == null) { + chunkEvents.close(); throw StateError( "Loading thumb for local photo ${asset.fileName} failed", ); } - final buffer = await ui.ImmutableBuffer.fromUint8List(normalThumbBytes); - final codec = await decode(buffer); - yield codec; - chunkEvents.close(); + try { + final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); + final codec = await decode(buffer); + yield codec; + } finally { + chunkEvents.close(); + } } @override From b7a0cf2470cdb4ed1008bd2c13b6e24ceca7d876 Mon Sep 17 00:00:00 2001 From: Tin Pecirep Date: Wed, 23 Apr 2025 16:05:00 +0200 Subject: [PATCH 040/356] feat: add oauth2 code verifier * fix: ensure oauth state param matches before finishing oauth flow Signed-off-by: Tin Pecirep * chore: upgrade openid-client to v6 Signed-off-by: Tin Pecirep * feat: use PKCE for oauth2 on supported clients Signed-off-by: Tin Pecirep * feat: use state and PKCE in mobile app Signed-off-by: Tin Pecirep * fix: remove obsolete oauth repository init Signed-off-by: Tin Pecirep * fix: rewrite callback url if mobile redirect url is enabled Signed-off-by: Tin Pecirep * fix: propagate oidc client error cause when oauth callback fails Signed-off-by: Tin Pecirep * fix: adapt auth service tests to required state and PKCE params Signed-off-by: Tin Pecirep * fix: update sdk types Signed-off-by: Tin Pecirep * fix: adapt oauth e2e test to work with PKCE Signed-off-by: Tin Pecirep * fix: allow insecure (http) oauth clients Signed-off-by: Tin Pecirep --------- Signed-off-by: Tin Pecirep Co-authored-by: Jason Rasmussen --- e2e/src/api/specs/oauth.e2e-spec.ts | 118 +++++++++++---- mobile/lib/services/oauth.service.dart | 16 +- .../lib/widgets/forms/login/login_form.dart | 33 ++++- .../lib/model/o_auth_callback_dto.dart | 43 ++++-- .../openapi/lib/model/o_auth_config_dto.dart | 43 ++++-- mobile/pubspec.lock | 2 +- mobile/pubspec.yaml | 1 + open-api/immich-openapi-specs.json | 12 ++ open-api/typescript-sdk/src/fetch-client.ts | 4 + server/package-lock.json | 49 +++--- server/package.json | 2 +- server/src/controllers/oauth.controller.ts | 32 +++- server/src/dtos/auth.dto.ts | 20 ++- server/src/enum.ts | 2 + server/src/repositories/oauth.repository.ts | 74 ++++++--- server/src/services/auth.service.spec.ts | 140 +++++++++++------- server/src/services/auth.service.ts | 68 ++++++--- server/src/utils/response.ts | 2 + 18 files changed, 469 insertions(+), 192 deletions(-) diff --git a/e2e/src/api/specs/oauth.e2e-spec.ts b/e2e/src/api/specs/oauth.e2e-spec.ts index 9cd5f0252a..3b1e75d3e5 100644 --- a/e2e/src/api/specs/oauth.e2e-spec.ts +++ b/e2e/src/api/specs/oauth.e2e-spec.ts @@ -6,6 +6,7 @@ import { startOAuth, updateConfig, } from '@immich/sdk'; +import { createHash, randomBytes } from 'node:crypto'; import { errorDto } from 'src/responses'; import { OAuthClient, OAuthUser } from 'src/setup/auth-server'; import { app, asBearerAuth, baseUrl, utils } from 'src/utils'; @@ -21,18 +22,30 @@ const mobileOverrideRedirectUri = 'https://photos.immich.app/oauth/mobile-redire const redirect = async (url: string, cookies?: string[]) => { const { headers } = await request(url) - .get('/') + .get('') .set('Cookie', cookies || []); return { cookies: (headers['set-cookie'] as unknown as string[]) || [], location: headers.location }; }; +// Function to generate a code challenge from the verifier +const generateCodeChallenge = async (codeVerifier: string): Promise => { + const hashed = createHash('sha256').update(codeVerifier).digest(); + return hashed.toString('base64url'); +}; + const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) => { - const { url } = await startOAuth({ oAuthConfigDto: { redirectUri: redirectUri ?? `${baseUrl}/auth/login` } }); + const state = randomBytes(16).toString('base64url'); + const codeVerifier = randomBytes(64).toString('base64url'); + const codeChallenge = await generateCodeChallenge(codeVerifier); + + const { url } = await startOAuth({ + oAuthConfigDto: { redirectUri: redirectUri ?? `${baseUrl}/auth/login`, state, codeChallenge }, + }); // login const response1 = await redirect(url.replace(authServer.internal, authServer.external)); const response2 = await request(authServer.external + response1.location) - .post('/') + .post('') .set('Cookie', response1.cookies) .type('form') .send({ prompt: 'login', login: sub, password: 'password' }); @@ -40,7 +53,7 @@ const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) => // approve const response3 = await redirect(response2.header.location, response1.cookies); const response4 = await request(authServer.external + response3.location) - .post('/') + .post('') .type('form') .set('Cookie', response3.cookies) .send({ prompt: 'consent' }); @@ -51,9 +64,9 @@ const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) => expect(redirectUrl).toBeDefined(); const params = new URL(redirectUrl).searchParams; expect(params.get('code')).toBeDefined(); - expect(params.get('state')).toBeDefined(); + expect(params.get('state')).toBe(state); - return redirectUrl; + return { url: redirectUrl, state, codeVerifier }; }; const setupOAuth = async (token: string, dto: Partial) => { @@ -119,9 +132,42 @@ describe(`/oauth`, () => { expect(body).toEqual(errorDto.badRequest(['url should not be empty'])); }); - it('should auto register the user by default', async () => { - const url = await loginWithOAuth('oauth-auto-register'); + it(`should throw an error if the state is not provided`, async () => { + const { url } = await loginWithOAuth('oauth-auto-register'); const { status, body } = await request(app).post('/oauth/callback').send({ url }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('OAuth state is missing')); + }); + + it(`should throw an error if the state mismatches`, async () => { + const callbackParams = await loginWithOAuth('oauth-auto-register'); + const { state } = await loginWithOAuth('oauth-auto-register'); + const { status, body } = await request(app) + .post('/oauth/callback') + .send({ ...callbackParams, state }); + expect(status).toBeGreaterThanOrEqual(400); + }); + + it(`should throw an error if the codeVerifier is not provided`, async () => { + const { url, state } = await loginWithOAuth('oauth-auto-register'); + const { status, body } = await request(app).post('/oauth/callback').send({ url, state }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('OAuth code verifier is missing')); + }); + + it(`should throw an error if the codeVerifier doesn't match the challenge`, async () => { + const callbackParams = await loginWithOAuth('oauth-auto-register'); + const { codeVerifier } = await loginWithOAuth('oauth-auto-register'); + const { status, body } = await request(app) + .post('/oauth/callback') + .send({ ...callbackParams, codeVerifier }); + console.log(body); + expect(status).toBeGreaterThanOrEqual(400); + }); + + it('should auto register the user by default', async () => { + const callbackParams = await loginWithOAuth('oauth-auto-register'); + const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); expect(status).toBe(201); expect(body).toMatchObject({ accessToken: expect.any(String), @@ -132,16 +178,30 @@ describe(`/oauth`, () => { }); }); + it('should allow passing state and codeVerifier via cookies', async () => { + const { url, state, codeVerifier } = await loginWithOAuth('oauth-auto-register'); + const { status, body } = await request(app) + .post('/oauth/callback') + .set('Cookie', [`immich_oauth_state=${state}`, `immich_oauth_code_verifier=${codeVerifier}`]) + .send({ url }); + expect(status).toBe(201); + expect(body).toMatchObject({ + accessToken: expect.any(String), + userId: expect.any(String), + userEmail: 'oauth-auto-register@immich.app', + }); + }); + it('should handle a user without an email', async () => { - const url = await loginWithOAuth(OAuthUser.NO_EMAIL); - const { status, body } = await request(app).post('/oauth/callback').send({ url }); + const callbackParams = await loginWithOAuth(OAuthUser.NO_EMAIL); + const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); expect(status).toBe(400); expect(body).toEqual(errorDto.badRequest('OAuth profile does not have an email address')); }); it('should set the quota from a claim', async () => { - const url = await loginWithOAuth(OAuthUser.WITH_QUOTA); - const { status, body } = await request(app).post('/oauth/callback').send({ url }); + const callbackParams = await loginWithOAuth(OAuthUser.WITH_QUOTA); + const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); expect(status).toBe(201); expect(body).toMatchObject({ accessToken: expect.any(String), @@ -154,8 +214,8 @@ describe(`/oauth`, () => { }); it('should set the storage label from a claim', async () => { - const url = await loginWithOAuth(OAuthUser.WITH_USERNAME); - const { status, body } = await request(app).post('/oauth/callback').send({ url }); + const callbackParams = await loginWithOAuth(OAuthUser.WITH_USERNAME); + const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); expect(status).toBe(201); expect(body).toMatchObject({ accessToken: expect.any(String), @@ -176,8 +236,8 @@ describe(`/oauth`, () => { buttonText: 'Login with Immich', signingAlgorithm: 'RS256', }); - const url = await loginWithOAuth('oauth-RS256-token'); - const { status, body } = await request(app).post('/oauth/callback').send({ url }); + const callbackParams = await loginWithOAuth('oauth-RS256-token'); + const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); expect(status).toBe(201); expect(body).toMatchObject({ accessToken: expect.any(String), @@ -196,8 +256,8 @@ describe(`/oauth`, () => { buttonText: 'Login with Immich', profileSigningAlgorithm: 'RS256', }); - const url = await loginWithOAuth('oauth-signed-profile'); - const { status, body } = await request(app).post('/oauth/callback').send({ url }); + const callbackParams = await loginWithOAuth('oauth-signed-profile'); + const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); expect(status).toBe(201); expect(body).toMatchObject({ userId: expect.any(String), @@ -213,8 +273,8 @@ describe(`/oauth`, () => { buttonText: 'Login with Immich', signingAlgorithm: 'something-that-does-not-work', }); - const url = await loginWithOAuth('oauth-signed-bad'); - const { status, body } = await request(app).post('/oauth/callback').send({ url }); + const callbackParams = await loginWithOAuth('oauth-signed-bad'); + const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); expect(status).toBe(500); expect(body).toMatchObject({ error: 'Internal Server Error', @@ -235,8 +295,8 @@ describe(`/oauth`, () => { }); it('should not auto register the user', async () => { - const url = await loginWithOAuth('oauth-no-auto-register'); - const { status, body } = await request(app).post('/oauth/callback').send({ url }); + const callbackParams = await loginWithOAuth('oauth-no-auto-register'); + const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); expect(status).toBe(400); expect(body).toEqual(errorDto.badRequest('User does not exist and auto registering is disabled.')); }); @@ -247,8 +307,8 @@ describe(`/oauth`, () => { email: 'oauth-user3@immich.app', password: 'password', }); - const url = await loginWithOAuth('oauth-user3'); - const { status, body } = await request(app).post('/oauth/callback').send({ url }); + const callbackParams = await loginWithOAuth('oauth-user3'); + const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); expect(status).toBe(201); expect(body).toMatchObject({ userId, @@ -286,13 +346,15 @@ describe(`/oauth`, () => { }); it('should auto register the user by default', async () => { - const url = await loginWithOAuth('oauth-mobile-override', 'app.immich:///oauth-callback'); - expect(url).toEqual(expect.stringContaining(mobileOverrideRedirectUri)); + const callbackParams = await loginWithOAuth('oauth-mobile-override', 'app.immich:///oauth-callback'); + expect(callbackParams.url).toEqual(expect.stringContaining(mobileOverrideRedirectUri)); // simulate redirecting back to mobile app - const redirectUri = url.replace(mobileOverrideRedirectUri, 'app.immich:///oauth-callback'); + const url = callbackParams.url.replace(mobileOverrideRedirectUri, 'app.immich:///oauth-callback'); - const { status, body } = await request(app).post('/oauth/callback').send({ url: redirectUri }); + const { status, body } = await request(app) + .post('/oauth/callback') + .send({ ...callbackParams, url }); expect(status).toBe(201); expect(body).toMatchObject({ accessToken: expect.any(String), diff --git a/mobile/lib/services/oauth.service.dart b/mobile/lib/services/oauth.service.dart index ddd97522f8..9a54a8d7c9 100644 --- a/mobile/lib/services/oauth.service.dart +++ b/mobile/lib/services/oauth.service.dart @@ -13,6 +13,8 @@ class OAuthService { Future getOAuthServerUrl( String serverUrl, + String state, + String codeChallenge, ) async { // Resolve API server endpoint from user provided serverUrl await _apiService.resolveAndSetEndpoint(serverUrl); @@ -22,7 +24,11 @@ class OAuthService { ); final dto = await _apiService.oAuthApi.startOAuth( - OAuthConfigDto(redirectUri: redirectUri), + OAuthConfigDto( + redirectUri: redirectUri, + state: state, + codeChallenge: codeChallenge, + ), ); final authUrl = dto?.url; @@ -31,7 +37,11 @@ class OAuthService { return authUrl; } - Future oAuthLogin(String oauthUrl) async { + Future oAuthLogin( + String oauthUrl, + String state, + String codeVerifier, + ) async { String result = await FlutterWebAuth2.authenticate( url: oauthUrl, callbackUrlScheme: callbackUrlScheme, @@ -49,6 +59,8 @@ class OAuthService { return await _apiService.oAuthApi.finishOAuth( OAuthCallbackDto( url: result, + state: state, + codeVerifier: codeVerifier, ), ); } diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 7af52b413d..3433648e9f 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -1,6 +1,9 @@ +import 'dart:convert'; import 'dart:io'; +import 'dart:math'; import 'package:auto_route/auto_route.dart'; +import 'package:crypto/crypto.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -203,13 +206,32 @@ class LoginForm extends HookConsumerWidget { } } + String generateRandomString(int length) { + final random = Random.secure(); + return base64Url + .encode(List.generate(32, (i) => random.nextInt(256))); + } + + Future generatePKCECodeChallenge(String codeVerifier) async { + var bytes = utf8.encode(codeVerifier); + var digest = sha256.convert(bytes); + return base64Url.encode(digest.bytes).replaceAll('=', ''); + } + oAuthLogin() async { var oAuthService = ref.watch(oAuthServiceProvider); String? oAuthServerUrl; + final state = generateRandomString(32); + final codeVerifier = generateRandomString(64); + final codeChallenge = await generatePKCECodeChallenge(codeVerifier); + try { - oAuthServerUrl = await oAuthService - .getOAuthServerUrl(sanitizeUrl(serverEndpointController.text)); + oAuthServerUrl = await oAuthService.getOAuthServerUrl( + sanitizeUrl(serverEndpointController.text), + state, + codeChallenge, + ); isLoading.value = true; @@ -230,8 +252,11 @@ class LoginForm extends HookConsumerWidget { if (oAuthServerUrl != null) { try { - final loginResponseDto = - await oAuthService.oAuthLogin(oAuthServerUrl); + final loginResponseDto = await oAuthService.oAuthLogin( + oAuthServerUrl, + state, + codeVerifier, + ); if (loginResponseDto == null) { return; diff --git a/mobile/openapi/lib/model/o_auth_callback_dto.dart b/mobile/openapi/lib/model/o_auth_callback_dto.dart index d0b98d5c6f..ebe2661c52 100644 --- a/mobile/openapi/lib/model/o_auth_callback_dto.dart +++ b/mobile/openapi/lib/model/o_auth_callback_dto.dart @@ -14,25 +14,36 @@ class OAuthCallbackDto { /// Returns a new [OAuthCallbackDto] instance. OAuthCallbackDto({ required this.url, + required this.state, + required this.codeVerifier, }); String url; + String state; + String codeVerifier; @override - bool operator ==(Object other) => identical(this, other) || other is OAuthCallbackDto && - other.url == url; + bool operator ==(Object other) => + identical(this, other) || + other is OAuthCallbackDto && + other.url == url && + other.state == state && + other.codeVerifier == codeVerifier; @override int get hashCode => - // ignore: unnecessary_parenthesis - (url.hashCode); + // ignore: unnecessary_parenthesis + (url.hashCode) + (state.hashCode) + (codeVerifier.hashCode); @override - String toString() => 'OAuthCallbackDto[url=$url]'; + String toString() => + 'OAuthCallbackDto[url=$url, state=$state, codeVerifier=$codeVerifier]'; Map toJson() { final json = {}; - json[r'url'] = this.url; + json[r'url'] = this.url; + json[r'state'] = this.state; + json[r'codeVerifier'] = this.codeVerifier; return json; } @@ -46,12 +57,17 @@ class OAuthCallbackDto { return OAuthCallbackDto( url: mapValueOfType(json, r'url')!, + state: mapValueOfType(json, r'state')!, + codeVerifier: mapValueOfType(json, r'codeVerifier')!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { + static List listFromJson( + dynamic json, { + bool growable = false, + }) { final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { @@ -79,13 +95,19 @@ class OAuthCallbackDto { } // maps a json object with a list of OAuthCallbackDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + static Map> mapListFromJson( + dynamic json, { + bool growable = false, + }) { final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = OAuthCallbackDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = OAuthCallbackDto.listFromJson( + entry.value, + growable: growable, + ); } } return map; @@ -94,6 +116,7 @@ class OAuthCallbackDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'url', + 'state', + 'codeVerifier', }; } - diff --git a/mobile/openapi/lib/model/o_auth_config_dto.dart b/mobile/openapi/lib/model/o_auth_config_dto.dart index 86c79b4e04..e142c17c06 100644 --- a/mobile/openapi/lib/model/o_auth_config_dto.dart +++ b/mobile/openapi/lib/model/o_auth_config_dto.dart @@ -14,25 +14,36 @@ class OAuthConfigDto { /// Returns a new [OAuthConfigDto] instance. OAuthConfigDto({ required this.redirectUri, + required this.state, + required this.codeChallenge, }); String redirectUri; + String state; + String codeChallenge; @override - bool operator ==(Object other) => identical(this, other) || other is OAuthConfigDto && - other.redirectUri == redirectUri; + bool operator ==(Object other) => + identical(this, other) || + other is OAuthConfigDto && + other.redirectUri == redirectUri && + other.state == state && + other.codeChallenge == codeChallenge; @override int get hashCode => - // ignore: unnecessary_parenthesis - (redirectUri.hashCode); + // ignore: unnecessary_parenthesis + (redirectUri.hashCode) + (state.hashCode) + (codeChallenge.hashCode); @override - String toString() => 'OAuthConfigDto[redirectUri=$redirectUri]'; + String toString() => + 'OAuthConfigDto[redirectUri=$redirectUri, state=$state, codeChallenge=$codeChallenge]'; Map toJson() { final json = {}; - json[r'redirectUri'] = this.redirectUri; + json[r'redirectUri'] = this.redirectUri; + json[r'state'] = this.state; + json[r'codeChallenge'] = this.codeChallenge; return json; } @@ -46,12 +57,17 @@ class OAuthConfigDto { return OAuthConfigDto( redirectUri: mapValueOfType(json, r'redirectUri')!, + state: mapValueOfType(json, r'state')!, + codeChallenge: mapValueOfType(json, r'codeChallenge')!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { + static List listFromJson( + dynamic json, { + bool growable = false, + }) { final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { @@ -79,13 +95,19 @@ class OAuthConfigDto { } // maps a json object with a list of OAuthConfigDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + static Map> mapListFromJson( + dynamic json, { + bool growable = false, + }) { final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = OAuthConfigDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = OAuthConfigDto.listFromJson( + entry.value, + growable: growable, + ); } } return map; @@ -94,6 +116,7 @@ class OAuthConfigDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'redirectUri', + 'state', + 'codeChallenge', }; } - diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 9e8aced11c..7e490edd25 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -303,7 +303,7 @@ packages: source: hosted version: "0.3.4+2" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 44d2e7e5d1..4e57b0fb3b 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: collection: ^1.18.0 connectivity_plus: ^6.1.3 crop_image: ^1.0.16 + crypto: ^3.0.6 device_info_plus: ^11.3.3 dynamic_color: ^1.7.0 easy_image_viewer: ^1.5.1 diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 169c076341..c9ea04ac5f 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -10354,6 +10354,12 @@ }, "OAuthCallbackDto": { "properties": { + "codeVerifier": { + "type": "string" + }, + "state": { + "type": "string" + }, "url": { "type": "string" } @@ -10365,8 +10371,14 @@ }, "OAuthConfigDto": { "properties": { + "codeChallenge": { + "type": "string" + }, "redirectUri": { "type": "string" + }, + "state": { + "type": "string" } }, "required": [ diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index e45449c9cd..3b0b32916d 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -688,12 +688,16 @@ export type TestEmailResponseDto = { }; export type OAuthConfigDto = { redirectUri: string; + state?: string; + codeChallenge?: string; }; export type OAuthAuthorizeResponseDto = { url: string; }; export type OAuthCallbackDto = { url: string; + state?: string; + codeVerifier?: string; }; export type PartnerResponseDto = { avatarColor: UserAvatarColor; diff --git a/server/package-lock.json b/server/package-lock.json index 297370187d..72fd6f451d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -52,7 +52,7 @@ "nestjs-kysely": "^1.1.0", "nestjs-otel": "^6.0.0", "nodemailer": "^6.9.13", - "openid-client": "^5.4.3", + "openid-client": "^6.3.3", "pg": "^8.11.3", "picomatch": "^4.0.2", "react": "^19.0.0", @@ -11370,9 +11370,9 @@ } }, "node_modules/jose": { - "version": "4.15.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.8.tgz", + "integrity": "sha512-EyUPtOKyTYq+iMOszO42eobQllaIjJnwkZ2U93aJzNyPibCy7CEvT9UQnaCVB51IAd49gbNdCew1c0LcLTCB2g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -11879,18 +11879,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/luxon": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", @@ -12750,6 +12738,14 @@ "set-blocking": "^2.0.0" } }, + "node_modules/oauth4webapi": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.3.0.tgz", + "integrity": "sha512-ZlozhPlFfobzh3hB72gnBFLjXpugl/dljz1fJSRdqaV2r3D5dmi5lg2QWI0LmUYuazmE+b5exsloEv6toUtw9g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } "node_modules/nwsapi": { "version": "2.2.20", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", @@ -12869,29 +12865,18 @@ } }, "node_modules/openid-client": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", - "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.3.3.tgz", + "integrity": "sha512-lTK8AV8SjqCM4qznLX0asVESAwzV39XTVdfMAM185ekuaZCnkWdPzcxMTXNlsm9tsUAMa1Q30MBmKAykdT1LWw==", "license": "MIT", "dependencies": { - "jose": "^4.15.9", - "lru-cache": "^6.0.0", - "object-hash": "^2.2.0", - "oidc-token-hash": "^5.0.3" + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0" }, "funding": { "url": "https://github.com/sponsors/panva" } }, - "node_modules/openid-client/node_modules/object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", diff --git a/server/package.json b/server/package.json index 441f04abeb..178f0fb0a0 100644 --- a/server/package.json +++ b/server/package.json @@ -77,7 +77,7 @@ "nestjs-kysely": "^1.1.0", "nestjs-otel": "^6.0.0", "nodemailer": "^6.9.13", - "openid-client": "^5.4.3", + "openid-client": "^6.3.3", "pg": "^8.11.3", "picomatch": "^4.0.2", "react": "^19.0.0", diff --git a/server/src/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts index b5b94030f2..23ddff5ddc 100644 --- a/server/src/controllers/oauth.controller.ts +++ b/server/src/controllers/oauth.controller.ts @@ -29,17 +29,35 @@ export class OAuthController { } @Post('authorize') - startOAuth(@Body() dto: OAuthConfigDto): Promise { - return this.service.authorize(dto); + async startOAuth( + @Body() dto: OAuthConfigDto, + @Res({ passthrough: true }) res: Response, + @GetLoginDetails() loginDetails: LoginDetails, + ): Promise { + const { url, state, codeVerifier } = await this.service.authorize(dto); + return respondWithCookie( + res, + { url }, + { + isSecure: loginDetails.isSecure, + values: [ + { key: ImmichCookie.OAUTH_STATE, value: state }, + { key: ImmichCookie.OAUTH_CODE_VERIFIER, value: codeVerifier }, + ], + }, + ); } @Post('callback') async finishOAuth( + @Req() request: Request, @Res({ passthrough: true }) res: Response, @Body() dto: OAuthCallbackDto, @GetLoginDetails() loginDetails: LoginDetails, ): Promise { - const body = await this.service.callback(dto, loginDetails); + const body = await this.service.callback(dto, request.headers, loginDetails); + res.clearCookie(ImmichCookie.OAUTH_STATE); + res.clearCookie(ImmichCookie.OAUTH_CODE_VERIFIER); return respondWithCookie(res, body, { isSecure: loginDetails.isSecure, values: [ @@ -52,8 +70,12 @@ export class OAuthController { @Post('link') @Authenticated() - linkOAuthAccount(@Auth() auth: AuthDto, @Body() dto: OAuthCallbackDto): Promise { - return this.service.link(auth, dto); + linkOAuthAccount( + @Req() request: Request, + @Auth() auth: AuthDto, + @Body() dto: OAuthCallbackDto, + ): Promise { + return this.service.link(auth, dto, request.headers); } @Post('unlink') diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index 7f2ffa5878..a1978d39dd 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -3,11 +3,11 @@ import { Transform } from 'class-transformer'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database'; import { ImmichCookie } from 'src/enum'; -import { toEmail } from 'src/validation'; +import { Optional, toEmail } from 'src/validation'; export type CookieResponse = { isSecure: boolean; - values: Array<{ key: ImmichCookie; value: string }>; + values: Array<{ key: ImmichCookie; value: string | null }>; }; export class AuthDto { @@ -87,12 +87,28 @@ export class OAuthCallbackDto { @IsString() @ApiProperty() url!: string; + + @Optional() + @IsString() + state?: string; + + @Optional() + @IsString() + codeVerifier?: string; } export class OAuthConfigDto { @IsNotEmpty() @IsString() redirectUri!: string; + + @Optional() + @IsString() + state?: string; + + @Optional() + @IsString() + codeChallenge?: string; } export class OAuthAuthorizeResponseDto { diff --git a/server/src/enum.ts b/server/src/enum.ts index e5c6039be8..baf864aa49 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -8,6 +8,8 @@ export enum ImmichCookie { AUTH_TYPE = 'immich_auth_type', IS_AUTHENTICATED = 'immich_is_authenticated', SHARED_LINK_TOKEN = 'immich_shared_link_token', + OAUTH_STATE = 'immich_oauth_state', + OAUTH_CODE_VERIFIER = 'immich_oauth_code_verifier', } export enum ImmichHeader { diff --git a/server/src/repositories/oauth.repository.ts b/server/src/repositories/oauth.repository.ts index dc19a1fe01..d3e0372089 100644 --- a/server/src/repositories/oauth.repository.ts +++ b/server/src/repositories/oauth.repository.ts @@ -1,5 +1,5 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; -import { custom, generators, Issuer, UserinfoResponse } from 'openid-client'; +import type { UserInfoResponse } from 'openid-client' with { 'resolution-mode': 'import' }; import { LoggingRepository } from 'src/repositories/logging.repository'; export type OAuthConfig = { @@ -12,7 +12,7 @@ export type OAuthConfig = { scope: string; signingAlgorithm: string; }; -export type OAuthProfile = UserinfoResponse; +export type OAuthProfile = UserInfoResponse; @Injectable() export class OAuthRepository { @@ -20,30 +20,47 @@ export class OAuthRepository { this.logger.setContext(OAuthRepository.name); } - init() { - custom.setHttpOptionsDefaults({ timeout: 30_000 }); - } - - async authorize(config: OAuthConfig, redirectUrl: string) { + async authorize(config: OAuthConfig, redirectUrl: string, state?: string, codeChallenge?: string) { + const { buildAuthorizationUrl, randomState, randomPKCECodeVerifier, calculatePKCECodeChallenge } = await import( + 'openid-client' + ); const client = await this.getClient(config); - return client.authorizationUrl({ + state ??= randomState(); + let codeVerifier: string | null; + if (codeChallenge) { + codeVerifier = null; + } else { + codeVerifier = randomPKCECodeVerifier(); + codeChallenge = await calculatePKCECodeChallenge(codeVerifier); + } + const url = buildAuthorizationUrl(client, { redirect_uri: redirectUrl, scope: config.scope, - state: generators.state(), - }); + state, + code_challenge: codeChallenge, + code_challenge_method: 'S256', + }).toString(); + return { url, state, codeVerifier }; } async getLogoutEndpoint(config: OAuthConfig) { const client = await this.getClient(config); - return client.issuer.metadata.end_session_endpoint; + return client.serverMetadata().end_session_endpoint; } - async getProfile(config: OAuthConfig, url: string, redirectUrl: string): Promise { + async getProfile( + config: OAuthConfig, + url: string, + expectedState: string, + codeVerifier: string, + ): Promise { + const { authorizationCodeGrant, fetchUserInfo, ...oidc } = await import('openid-client'); const client = await this.getClient(config); - const params = client.callbackParams(url); + const pkceCodeVerifier = client.serverMetadata().supportsPKCE() ? codeVerifier : undefined; + try { - const tokens = await client.callback(redirectUrl, params, { state: params.state }); - const profile = await client.userinfo(tokens.access_token || ''); + const tokens = await authorizationCodeGrant(client, new URL(url), { expectedState, pkceCodeVerifier }); + const profile = await fetchUserInfo(client, tokens.access_token, oidc.skipSubjectCheck); if (!profile.sub) { throw new Error('Unexpected profile response, no `sub`'); } @@ -59,6 +76,11 @@ export class OAuthRepository { ); } + if (error.code === 'OAUTH_INVALID_RESPONSE') { + this.logger.warn(`Invalid response from authorization server. Cause: ${error.cause?.message}`); + throw error.cause; + } + throw error; } } @@ -83,14 +105,20 @@ export class OAuthRepository { signingAlgorithm, }: OAuthConfig) { try { - const issuer = await Issuer.discover(issuerUrl); - return new issuer.Client({ - client_id: clientId, - client_secret: clientSecret, - response_types: ['code'], - userinfo_signed_response_alg: profileSigningAlgorithm === 'none' ? undefined : profileSigningAlgorithm, - id_token_signed_response_alg: signingAlgorithm, - }); + const { allowInsecureRequests, discovery } = await import('openid-client'); + return await discovery( + new URL(issuerUrl), + clientId, + { + client_secret: clientSecret, + response_types: ['code'], + userinfo_signed_response_alg: profileSigningAlgorithm === 'none' ? undefined : profileSigningAlgorithm, + id_token_signed_response_alg: signingAlgorithm, + timeout: 30_000, + }, + undefined, + { execute: [allowInsecureRequests] }, + ); } catch (error: any | AggregateError) { this.logger.error(`Error in OAuth discovery: ${error}`, error?.stack, error?.errors); throw new InternalServerErrorException(`Error in OAuth discovery: ${error}`, { cause: error }); diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index b1bfe00e85..4624159925 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -55,7 +55,7 @@ describe(AuthService.name, () => { beforeEach(() => { ({ sut, mocks } = newTestService(AuthService)); - mocks.oauth.authorize.mockResolvedValue('access-token'); + mocks.oauth.authorize.mockResolvedValue({ url: 'http://test', state: 'state', codeVerifier: 'codeVerifier' }); mocks.oauth.getProfile.mockResolvedValue({ sub, email }); mocks.oauth.getLogoutEndpoint.mockResolvedValue('http://end-session-endpoint'); }); @@ -64,16 +64,6 @@ describe(AuthService.name, () => { expect(sut).toBeDefined(); }); - describe('onBootstrap', () => { - it('should init the repo', () => { - mocks.oauth.init.mockResolvedValue(); - - sut.onBootstrap(); - - expect(mocks.oauth.init).toHaveBeenCalled(); - }); - }); - describe('login', () => { it('should throw an error if password login is disabled', async () => { mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.disabled); @@ -519,16 +509,22 @@ describe(AuthService.name, () => { describe('callback', () => { it('should throw an error if OAuth is not enabled', async () => { - await expect(sut.callback({ url: '' }, loginDetails)).rejects.toBeInstanceOf(BadRequestException); + await expect( + sut.callback({ url: '', state: 'xyz789', codeVerifier: 'foo' }, {}, loginDetails), + ).rejects.toBeInstanceOf(BadRequestException); }); it('should not allow auto registering', async () => { mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled); mocks.user.getByEmail.mockResolvedValue(void 0); - await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toBeInstanceOf( - BadRequestException, - ); + await expect( + sut.callback( + { url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' }, + {}, + loginDetails, + ), + ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1); }); @@ -541,9 +537,13 @@ describe(AuthService.name, () => { mocks.user.update.mockResolvedValue(user); mocks.session.create.mockResolvedValue(factory.session()); - await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( - oauthResponse(user), - ); + await expect( + sut.callback( + { url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foobar' }, + {}, + loginDetails, + ), + ).resolves.toEqual(oauthResponse(user)); expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1); expect(mocks.user.update).toHaveBeenCalledWith(user.id, { oauthId: sub }); @@ -557,9 +557,13 @@ describe(AuthService.name, () => { mocks.user.getAdmin.mockResolvedValue(user); mocks.user.create.mockResolvedValue(user); - await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toThrow( - BadRequestException, - ); + await expect( + sut.callback( + { url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foobar' }, + {}, + loginDetails, + ), + ).rejects.toThrow(BadRequestException); expect(mocks.user.update).not.toHaveBeenCalled(); expect(mocks.user.create).not.toHaveBeenCalled(); @@ -574,9 +578,13 @@ describe(AuthService.name, () => { mocks.user.create.mockResolvedValue(user); mocks.session.create.mockResolvedValue(factory.session()); - await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( - oauthResponse(user), - ); + await expect( + sut.callback( + { url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foobar' }, + {}, + loginDetails, + ), + ).resolves.toEqual(oauthResponse(user)); expect(mocks.user.getByEmail).toHaveBeenCalledTimes(2); // second call is for domain check before create expect(mocks.user.create).toHaveBeenCalledTimes(1); @@ -592,18 +600,19 @@ describe(AuthService.name, () => { mocks.session.create.mockResolvedValue(factory.session()); mocks.oauth.getProfile.mockResolvedValue({ sub, email: undefined }); - await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toBeInstanceOf( - BadRequestException, - ); + await expect( + sut.callback( + { url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foobar' }, + {}, + loginDetails, + ), + ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.user.getByEmail).not.toHaveBeenCalled(); expect(mocks.user.create).not.toHaveBeenCalled(); }); for (const url of [ - 'app.immich:/', - 'app.immich://', - 'app.immich:///', 'app.immich:/oauth-callback?code=abc123', 'app.immich://oauth-callback?code=abc123', 'app.immich:///oauth-callback?code=abc123', @@ -615,9 +624,14 @@ describe(AuthService.name, () => { mocks.user.getByOAuthId.mockResolvedValue(user); mocks.session.create.mockResolvedValue(factory.session()); - await sut.callback({ url }, loginDetails); + await sut.callback({ url, state: 'xyz789', codeVerifier: 'foo' }, {}, loginDetails); - expect(mocks.oauth.getProfile).toHaveBeenCalledWith(expect.objectContaining({}), url, 'http://mobile-redirect'); + expect(mocks.oauth.getProfile).toHaveBeenCalledWith( + expect.objectContaining({}), + 'http://mobile-redirect?code=abc123', + 'xyz789', + 'foo', + ); }); } @@ -630,9 +644,13 @@ describe(AuthService.name, () => { mocks.user.create.mockResolvedValue(user); mocks.session.create.mockResolvedValue(factory.session()); - await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( - oauthResponse(user), - ); + await expect( + sut.callback( + { url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' }, + {}, + loginDetails, + ), + ).resolves.toEqual(oauthResponse(user)); expect(mocks.user.create).toHaveBeenCalledWith(expect.objectContaining({ quotaSizeInBytes: 1_073_741_824 })); }); @@ -647,9 +665,13 @@ describe(AuthService.name, () => { mocks.user.create.mockResolvedValue(user); mocks.session.create.mockResolvedValue(factory.session()); - await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( - oauthResponse(user), - ); + await expect( + sut.callback( + { url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' }, + {}, + loginDetails, + ), + ).resolves.toEqual(oauthResponse(user)); expect(mocks.user.create).toHaveBeenCalledWith(expect.objectContaining({ quotaSizeInBytes: 1_073_741_824 })); }); @@ -664,9 +686,13 @@ describe(AuthService.name, () => { mocks.user.create.mockResolvedValue(user); mocks.session.create.mockResolvedValue(factory.session()); - await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( - oauthResponse(user), - ); + await expect( + sut.callback( + { url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' }, + {}, + loginDetails, + ), + ).resolves.toEqual(oauthResponse(user)); expect(mocks.user.create).toHaveBeenCalledWith(expect.objectContaining({ quotaSizeInBytes: 1_073_741_824 })); }); @@ -681,9 +707,13 @@ describe(AuthService.name, () => { mocks.user.create.mockResolvedValue(user); mocks.session.create.mockResolvedValue(factory.session()); - await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( - oauthResponse(user), - ); + await expect( + sut.callback( + { url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' }, + {}, + loginDetails, + ), + ).resolves.toEqual(oauthResponse(user)); expect(mocks.user.create).toHaveBeenCalledWith({ email: user.email, @@ -705,9 +735,13 @@ describe(AuthService.name, () => { mocks.user.create.mockResolvedValue(user); mocks.session.create.mockResolvedValue(factory.session()); - await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( - oauthResponse(user), - ); + await expect( + sut.callback( + { url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' }, + {}, + loginDetails, + ), + ).resolves.toEqual(oauthResponse(user)); expect(mocks.user.create).toHaveBeenCalledWith({ email: user.email, @@ -779,7 +813,11 @@ describe(AuthService.name, () => { mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled); mocks.user.update.mockResolvedValue(user); - await sut.link(auth, { url: 'http://immich/user-settings?code=abc123' }); + await sut.link( + auth, + { url: 'http://immich/user-settings?code=abc123', state: 'xyz789', codeVerifier: 'foo' }, + {}, + ); expect(mocks.user.update).toHaveBeenCalledWith(auth.user.id, { oauthId: sub }); }); @@ -792,9 +830,9 @@ describe(AuthService.name, () => { mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled); mocks.user.getByOAuthId.mockResolvedValue({ id: 'other-user' } as UserAdmin); - await expect(sut.link(auth, { url: 'http://immich/user-settings?code=abc123' })).rejects.toBeInstanceOf( - BadRequestException, - ); + await expect( + sut.link(auth, { url: 'http://immich/user-settings?code=abc123', state: 'xyz789', codeVerifier: 'foo' }, {}), + ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.user.update).not.toHaveBeenCalled(); }); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index ee4ca4dc5d..b250b63a5e 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -7,13 +7,11 @@ import { join } from 'node:path'; import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { UserAdmin } from 'src/database'; -import { OnEvent } from 'src/decorators'; import { AuthDto, ChangePasswordDto, LoginCredentialDto, LogoutResponseDto, - OAuthAuthorizeResponseDto, OAuthCallbackDto, OAuthConfigDto, SignUpDto, @@ -52,11 +50,6 @@ export type ValidateRequest = { @Injectable() export class AuthService extends BaseService { - @OnEvent({ name: 'app.bootstrap' }) - onBootstrap() { - this.oauthRepository.init(); - } - async login(dto: LoginCredentialDto, details: LoginDetails) { const config = await this.getConfig({ withCache: false }); if (!config.passwordLogin.enabled) { @@ -176,20 +169,35 @@ export class AuthService extends BaseService { return `${MOBILE_REDIRECT}?${url.split('?')[1] || ''}`; } - async authorize(dto: OAuthConfigDto): Promise { + async authorize(dto: OAuthConfigDto) { const { oauth } = await this.getConfig({ withCache: false }); if (!oauth.enabled) { throw new BadRequestException('OAuth is not enabled'); } - const url = await this.oauthRepository.authorize(oauth, this.resolveRedirectUri(oauth, dto.redirectUri)); - return { url }; + return await this.oauthRepository.authorize( + oauth, + this.resolveRedirectUri(oauth, dto.redirectUri), + dto.state, + dto.codeChallenge, + ); } - async callback(dto: OAuthCallbackDto, loginDetails: LoginDetails) { + async callback(dto: OAuthCallbackDto, headers: IncomingHttpHeaders, loginDetails: LoginDetails) { + const expectedState = dto.state ?? this.getCookieOauthState(headers); + if (!expectedState?.length) { + throw new BadRequestException('OAuth state is missing'); + } + + const codeVerifier = dto.codeVerifier ?? this.getCookieCodeVerifier(headers); + if (!codeVerifier?.length) { + throw new BadRequestException('OAuth code verifier is missing'); + } + const { oauth } = await this.getConfig({ withCache: false }); - const profile = await this.oauthRepository.getProfile(oauth, dto.url, this.resolveRedirectUri(oauth, dto.url)); + const url = this.resolveRedirectUri(oauth, dto.url); + const profile = await this.oauthRepository.getProfile(oauth, url, expectedState, codeVerifier); const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim } = oauth; this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`); let user: UserAdmin | undefined = await this.userRepository.getByOAuthId(profile.sub); @@ -271,13 +279,19 @@ export class AuthService extends BaseService { } } - async link(auth: AuthDto, dto: OAuthCallbackDto): Promise { + async link(auth: AuthDto, dto: OAuthCallbackDto, headers: IncomingHttpHeaders): Promise { + const expectedState = dto.state ?? this.getCookieOauthState(headers); + if (!expectedState?.length) { + throw new BadRequestException('OAuth state is missing'); + } + + const codeVerifier = dto.codeVerifier ?? this.getCookieCodeVerifier(headers); + if (!codeVerifier?.length) { + throw new BadRequestException('OAuth code verifier is missing'); + } + const { oauth } = await this.getConfig({ withCache: false }); - const { sub: oauthId } = await this.oauthRepository.getProfile( - oauth, - dto.url, - this.resolveRedirectUri(oauth, dto.url), - ); + const { sub: oauthId } = await this.oauthRepository.getProfile(oauth, dto.url, expectedState, codeVerifier); const duplicate = await this.userRepository.getByOAuthId(oauthId); if (duplicate && duplicate.id !== auth.user.id) { this.logger.warn(`OAuth link account failed: sub is already linked to another user (${duplicate.email}).`); @@ -320,6 +334,16 @@ export class AuthService extends BaseService { return cookies[ImmichCookie.ACCESS_TOKEN] || null; } + private getCookieOauthState(headers: IncomingHttpHeaders): string | null { + const cookies = parse(headers.cookie || ''); + return cookies[ImmichCookie.OAUTH_STATE] || null; + } + + private getCookieCodeVerifier(headers: IncomingHttpHeaders): string | null { + const cookies = parse(headers.cookie || ''); + return cookies[ImmichCookie.OAUTH_CODE_VERIFIER] || null; + } + async validateSharedLink(key: string | string[]): Promise { key = Array.isArray(key) ? key[0] : key; @@ -399,11 +423,9 @@ export class AuthService extends BaseService { { mobileRedirectUri, mobileOverrideEnabled }: { mobileRedirectUri: string; mobileOverrideEnabled: boolean }, url: string, ) { - const redirectUri = url.split('?')[0]; - const isMobile = redirectUri.startsWith('app.immich:/'); - if (isMobile && mobileOverrideEnabled && mobileRedirectUri) { - return mobileRedirectUri; + if (mobileOverrideEnabled && mobileRedirectUri) { + return url.replace(/app\.immich:\/+oauth-callback/, mobileRedirectUri); } - return redirectUri; + return url; } } diff --git a/server/src/utils/response.ts b/server/src/utils/response.ts index 679d947afb..a50e86a4ff 100644 --- a/server/src/utils/response.ts +++ b/server/src/utils/response.ts @@ -15,6 +15,8 @@ export const respondWithCookie = (res: Response, body: T, { isSecure, values const cookieOptions: Record = { [ImmichCookie.AUTH_TYPE]: defaults, [ImmichCookie.ACCESS_TOKEN]: defaults, + [ImmichCookie.OAUTH_STATE]: defaults, + [ImmichCookie.OAUTH_CODE_VERIFIER]: defaults, // no httpOnly so that the client can know the auth state [ImmichCookie.IS_AUTHENTICATED]: { ...defaults, httpOnly: false }, [ImmichCookie.SHARED_LINK_TOKEN]: { ...defaults, maxAge: Duration.fromObject({ days: 1 }).toMillis() }, From 1b5e981a451d3214f8a774a471a71bc244982f9b Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 23 Apr 2025 10:59:54 -0400 Subject: [PATCH 041/356] fix: failing ci checks (#17810) --- .../lib/model/o_auth_callback_dto.dart | 77 +- .../openapi/lib/model/o_auth_config_dto.dart | 77 +- open-api/typescript-sdk/src/fetch-client.ts | 6 +- server/package-lock.json | 1945 +---------------- server/src/services/auth.service.spec.ts | 20 +- server/test/vitest.config.mjs | 6 - 6 files changed, 176 insertions(+), 1955 deletions(-) diff --git a/mobile/openapi/lib/model/o_auth_callback_dto.dart b/mobile/openapi/lib/model/o_auth_callback_dto.dart index ebe2661c52..ea8cac31a0 100644 --- a/mobile/openapi/lib/model/o_auth_callback_dto.dart +++ b/mobile/openapi/lib/model/o_auth_callback_dto.dart @@ -13,37 +13,58 @@ part of openapi.api; class OAuthCallbackDto { /// Returns a new [OAuthCallbackDto] instance. OAuthCallbackDto({ + this.codeVerifier, + this.state, required this.url, - required this.state, - required this.codeVerifier, }); + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? codeVerifier; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? state; + String url; - String state; - String codeVerifier; @override - bool operator ==(Object other) => - identical(this, other) || - other is OAuthCallbackDto && - other.url == url && - other.state == state && - other.codeVerifier == codeVerifier; + bool operator ==(Object other) => identical(this, other) || other is OAuthCallbackDto && + other.codeVerifier == codeVerifier && + other.state == state && + other.url == url; @override int get hashCode => - // ignore: unnecessary_parenthesis - (url.hashCode) + (state.hashCode) + (codeVerifier.hashCode); + // ignore: unnecessary_parenthesis + (codeVerifier == null ? 0 : codeVerifier!.hashCode) + + (state == null ? 0 : state!.hashCode) + + (url.hashCode); @override - String toString() => - 'OAuthCallbackDto[url=$url, state=$state, codeVerifier=$codeVerifier]'; + String toString() => 'OAuthCallbackDto[codeVerifier=$codeVerifier, state=$state, url=$url]'; Map toJson() { final json = {}; - json[r'url'] = this.url; - json[r'state'] = this.state; - json[r'codeVerifier'] = this.codeVerifier; + if (this.codeVerifier != null) { + json[r'codeVerifier'] = this.codeVerifier; + } else { + // json[r'codeVerifier'] = null; + } + if (this.state != null) { + json[r'state'] = this.state; + } else { + // json[r'state'] = null; + } + json[r'url'] = this.url; return json; } @@ -56,18 +77,15 @@ class OAuthCallbackDto { final json = value.cast(); return OAuthCallbackDto( + codeVerifier: mapValueOfType(json, r'codeVerifier'), + state: mapValueOfType(json, r'state'), url: mapValueOfType(json, r'url')!, - state: mapValueOfType(json, r'state')!, - codeVerifier: mapValueOfType(json, r'codeVerifier')!, ); } return null; } - static List listFromJson( - dynamic json, { - bool growable = false, - }) { + static List listFromJson(dynamic json, {bool growable = false,}) { final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { @@ -95,19 +113,13 @@ class OAuthCallbackDto { } // maps a json object with a list of OAuthCallbackDto-objects as value to a dart map - static Map> mapListFromJson( - dynamic json, { - bool growable = false, - }) { + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = OAuthCallbackDto.listFromJson( - entry.value, - growable: growable, - ); + map[entry.key] = OAuthCallbackDto.listFromJson(entry.value, growable: growable,); } } return map; @@ -116,7 +128,6 @@ class OAuthCallbackDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'url', - 'state', - 'codeVerifier', }; } + diff --git a/mobile/openapi/lib/model/o_auth_config_dto.dart b/mobile/openapi/lib/model/o_auth_config_dto.dart index e142c17c06..bb3e8d448d 100644 --- a/mobile/openapi/lib/model/o_auth_config_dto.dart +++ b/mobile/openapi/lib/model/o_auth_config_dto.dart @@ -13,37 +13,58 @@ part of openapi.api; class OAuthConfigDto { /// Returns a new [OAuthConfigDto] instance. OAuthConfigDto({ + this.codeChallenge, required this.redirectUri, - required this.state, - required this.codeChallenge, + this.state, }); + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? codeChallenge; + String redirectUri; - String state; - String codeChallenge; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? state; @override - bool operator ==(Object other) => - identical(this, other) || - other is OAuthConfigDto && - other.redirectUri == redirectUri && - other.state == state && - other.codeChallenge == codeChallenge; + bool operator ==(Object other) => identical(this, other) || other is OAuthConfigDto && + other.codeChallenge == codeChallenge && + other.redirectUri == redirectUri && + other.state == state; @override int get hashCode => - // ignore: unnecessary_parenthesis - (redirectUri.hashCode) + (state.hashCode) + (codeChallenge.hashCode); + // ignore: unnecessary_parenthesis + (codeChallenge == null ? 0 : codeChallenge!.hashCode) + + (redirectUri.hashCode) + + (state == null ? 0 : state!.hashCode); @override - String toString() => - 'OAuthConfigDto[redirectUri=$redirectUri, state=$state, codeChallenge=$codeChallenge]'; + String toString() => 'OAuthConfigDto[codeChallenge=$codeChallenge, redirectUri=$redirectUri, state=$state]'; Map toJson() { final json = {}; - json[r'redirectUri'] = this.redirectUri; - json[r'state'] = this.state; - json[r'codeChallenge'] = this.codeChallenge; + if (this.codeChallenge != null) { + json[r'codeChallenge'] = this.codeChallenge; + } else { + // json[r'codeChallenge'] = null; + } + json[r'redirectUri'] = this.redirectUri; + if (this.state != null) { + json[r'state'] = this.state; + } else { + // json[r'state'] = null; + } return json; } @@ -56,18 +77,15 @@ class OAuthConfigDto { final json = value.cast(); return OAuthConfigDto( + codeChallenge: mapValueOfType(json, r'codeChallenge'), redirectUri: mapValueOfType(json, r'redirectUri')!, - state: mapValueOfType(json, r'state')!, - codeChallenge: mapValueOfType(json, r'codeChallenge')!, + state: mapValueOfType(json, r'state'), ); } return null; } - static List listFromJson( - dynamic json, { - bool growable = false, - }) { + static List listFromJson(dynamic json, {bool growable = false,}) { final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { @@ -95,19 +113,13 @@ class OAuthConfigDto { } // maps a json object with a list of OAuthConfigDto-objects as value to a dart map - static Map> mapListFromJson( - dynamic json, { - bool growable = false, - }) { + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = OAuthConfigDto.listFromJson( - entry.value, - growable: growable, - ); + map[entry.key] = OAuthConfigDto.listFromJson(entry.value, growable: growable,); } } return map; @@ -116,7 +128,6 @@ class OAuthConfigDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'redirectUri', - 'state', - 'codeChallenge', }; } + diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 3b0b32916d..9fa219a92b 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -687,17 +687,17 @@ export type TestEmailResponseDto = { messageId: string; }; export type OAuthConfigDto = { + codeChallenge?: string; redirectUri: string; state?: string; - codeChallenge?: string; }; export type OAuthAuthorizeResponseDto = { url: string; }; export type OAuthCallbackDto = { - url: string; - state?: string; codeVerifier?: string; + state?: string; + url: string; }; export type PartnerResponseDto = { avatarColor: UserAvatarColor; diff --git a/server/package-lock.json b/server/package-lock.json index 72fd6f451d..402dfc164d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -437,9 +437,9 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.3.tgz", - "integrity": "sha512-u25AyjuNrRFGb1O7KmWEu0ExN6iJMlUmDSlOPW/11JF8khOrIGG6oCoYpC+4mZlthNVhFUahk68lNrNI91f6Yg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.4.tgz", + "integrity": "sha512-SeuBV4rnjpFNjI8HSgKUwteuFdkHwkboq31HWzznuqgySQir+jSTczoWVVL4jvOjKjuH80fMDG0Fvg1Sb+OJsA==", "dev": true, "license": "MIT", "dependencies": { @@ -450,13 +450,6 @@ "lru-cache": "^10.4.3" } }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -857,272 +850,6 @@ "node": ">=18" } }, - "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", - "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", - "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", - "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", - "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", - "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", - "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", - "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", - "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", - "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", - "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", - "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", - "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", - "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", - "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", - "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", - "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", @@ -1139,135 +866,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", - "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", - "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", - "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", - "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", - "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", - "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", - "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.0.tgz", @@ -1567,130 +1165,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@img/sharp-libvips-linux-x64": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", @@ -1707,22 +1181,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", @@ -1739,72 +1197,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" - } - }, "node_modules/@img/sharp-linux-x64": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", @@ -1827,28 +1219,6 @@ "@img/sharp-libvips-linux-x64": "1.0.4" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" - } - }, "node_modules/@img/sharp-linuxmusl-x64": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", @@ -1871,63 +1241,6 @@ "@img/sharp-libvips-linuxmusl-x64": "1.0.4" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@inquirer/checkbox": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.5.tgz", @@ -2472,58 +1785,6 @@ "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", "license": "MIT" }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", @@ -2537,19 +1798,6 @@ "linux" ] }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@nestjs/bull-shared": { "version": "11.0.2", "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-11.0.2.tgz", @@ -2741,14 +1989,12 @@ } }, "node_modules/@nestjs/common": { - "version": "11.0.20", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.0.20.tgz", - "integrity": "sha512-/GH8NDCczjn6+6RNEtSNAts/nq/wQE8L1qZ9TRjqjNqEsZNE1vpFuRIhmcO2isQZ0xY5rySnpaRdrOAul3gQ3A==", + "version": "11.0.17", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.0.17.tgz", + "integrity": "sha512-FwKylI/hVxaNvzBJdWMMG1LH0cLKz4Oh4jKOHet2JUVMM9j6CuodRbrSnL++KL6PJY/b2E6AY58UDPLNeCqJWw==", "license": "MIT", "dependencies": { - "file-type": "20.4.1", "iterare": "1.2.1", - "load-esm": "1.0.2", "tslib": "2.8.1", "uid": "2.0.2" }, @@ -2759,6 +2005,7 @@ "peerDependencies": { "class-transformer": "*", "class-validator": "*", + "file-type": "^20.4.1", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, @@ -2768,13 +2015,16 @@ }, "class-validator": { "optional": true + }, + "file-type": { + "optional": true } } }, "node_modules/@nestjs/core": { - "version": "11.0.20", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.0.20.tgz", - "integrity": "sha512-yUkEzBGiRNSEThVl6vMCXgoA9sDGWoRbJsTLdYdCC7lg7PE1iXBnna1FiBfQjT995pm0fjyM1e3WsXmyWeJXbw==", + "version": "11.0.17", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.0.17.tgz", + "integrity": "sha512-ImK6qNxtegKqK7EJLGTBpP5Ild/DTpcduEtAOS+WLLjZOMjK1k214G9roXvlrNQwlVt9ALAY2jcqnsasdEd7Ow==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -2846,9 +2096,9 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.0.20", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.0.20.tgz", - "integrity": "sha512-h/Xq2x0Qi2cr9T64w9DfLejZws1M1hYu7n7XWuC4vxX00FlfOz1jSWGgaTo/Gjq6vtULfq34Gp5Fzf0w34XDyQ==", + "version": "11.0.17", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.0.17.tgz", + "integrity": "sha512-et6Ydd6dR0FlcE/WR/9VRnQoTqEpDdzBgGK+aWadA0dFJ65wlN+snJRg/9JGP4ngj90S6xwe0VKD/BbfUGj9cw==", "license": "MIT", "dependencies": { "cors": "2.8.5", @@ -2867,9 +2117,9 @@ } }, "node_modules/@nestjs/platform-socket.io": { - "version": "11.0.20", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.0.20.tgz", - "integrity": "sha512-fUyDjLt0wJ4WK+rXrd5/oSWw5xWpfDOknpP7YNgaFfvYW726KuS5gWysV7JPD2mgH85S6i+qiO3qZvHIs5DvxQ==", + "version": "11.0.17", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.0.17.tgz", + "integrity": "sha512-l9b8VNb7N7rB9IUwKeln2bMQDltsR9mpenzHOaYYqDkz5BtuQSiyT8NpLR2vWhxDjppxMY3DkW8fQAvXh54pMg==", "license": "MIT", "dependencies": { "socket.io": "4.8.1", @@ -3022,9 +2272,9 @@ } }, "node_modules/@nestjs/swagger": { - "version": "11.1.4", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.1.4.tgz", - "integrity": "sha512-px+ue6YeNyExL7Vg39HMJb3iPgFJD5oiOzUFS+4I0PhKNznMjSxMMZyDh1M8cuhqt4s3YvE7b0e/v6BgWdx/bQ==", + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.1.3.tgz", + "integrity": "sha512-vhbW/Xu05Diti/EwYQp3Ea7Hj2M++wiakCcxqUUDA2n7NvCZC8LKsrcGynw6/x/lugdXyklYS+s2FhdAfeAikg==", "license": "MIT", "dependencies": { "@microsoft/tsdoc": "0.15.1", @@ -3055,9 +2305,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "11.0.20", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.0.20.tgz", - "integrity": "sha512-3o+HWsVfA46tt81ctKuNj5ufL9srfmp3dQBCAIx9fzvjooEKwWl5L69AcvDh6JhdB79jhhM1lkSSU+1fBGbxgw==", + "version": "11.0.17", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.0.17.tgz", + "integrity": "sha512-ryEx6fCYZFCsjEBZo8jOVikQluEHMESocVqHdXWOkkG7UqMPMHimf9gT2qij0GpNnYeDAGw+i7FhSJN3Cajoug==", "dev": true, "license": "MIT", "dependencies": { @@ -3083,9 +2333,9 @@ } }, "node_modules/@nestjs/websockets": { - "version": "11.0.20", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.0.20.tgz", - "integrity": "sha512-qcybahXdrPJFMILhAwJML9D/bExBEBFsfwFiePCeI4f//tiP0rXiLspLVOHClSeUPBaCNrx+Ae/HVe9UP+wtOg==", + "version": "11.0.17", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.0.17.tgz", + "integrity": "sha512-2LSjxA/lUKs5hv/g5lPk555CoRNTCt/XywHFteKMSrxo09Cq3yfOQOAPwEWG929EnqAjAAsQaDVbfUHUFisFCg==", "license": "MIT", "dependencies": { "iterare": "1.2.1", @@ -3111,70 +2361,6 @@ "integrity": "sha512-Hm3jIGsoUl6RLB1vzY+dZeqb+/kWPZ+h34yiWxW0dV87l8Im/eMOwpOA+a0L78U0HM04syEjXuRlCozqpwuojQ==", "license": "MIT" }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.2.tgz", - "integrity": "sha512-b9TN7q+j5/7+rGLhFAVZiKJGIASuo8tWvInGfAd8wsULjB1uNGRCj1z1WZwwPWzVQbIKWFYqc+9L7W09qwt52w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.2.tgz", - "integrity": "sha512-caR62jNDUCU+qobStO6YJ05p9E+LR0EoXh1EEmyU69cYydsAy7drMcOlUlRtQihM6K6QfvNwJuLhsHcCzNpqtA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.2.tgz", - "integrity": "sha512-fHHXBusURjBmN6VBUtu6/5s7cCeEkuGAb/ZZiGHBLVBXMBy4D5QpM8P33Or8JD1nlOjm/ZT9sEE5HouQ0F+hUA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.2.tgz", - "integrity": "sha512-9CF1Pnivij7+M3G74lxr+e9h6o2YNIe7QtExWq1KUK4hsOLTBv6FJikEwCaC3NeYTflzrm69E5UfwEAbV2U9/g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@next/swc-linux-x64-gnu": { "version": "15.1.2", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.2.tgz", @@ -3207,38 +2393,6 @@ "node": ">= 10" } }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.2.tgz", - "integrity": "sha512-wvg7MlfnaociP7k8lxLX4s2iBJm4BrNiNFhVUY+Yur5yhAJHfkS8qPPeDEUH8rQiY0PX3u/P7Q/wcg6Mv6GSAA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.2.tgz", - "integrity": "sha512-D3cNA8NoT3aWISWmo7HF5Eyko/0OdOO+VagkoJuiTk7pyX3P/b+n8XA/MYvyR+xSVcbKn68B1rY9fgqjNISqzQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5048,216 +4202,6 @@ } } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", - "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", - "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", - "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", - "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", - "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", - "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", - "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", - "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", - "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", - "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", - "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", - "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", - "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", - "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", - "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.40.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", @@ -5286,48 +4230,6 @@ "linux" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", - "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", - "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", - "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -5454,91 +4356,6 @@ } } }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.21.tgz", - "integrity": "sha512-v6gjw9YFWvKulCw3ZA1dY+LGMafYzJksm1mD4UZFZ9b36CyHFowYVYug1ajYRIRqEvvfIhHUNV660zTLoVFR8g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.21.tgz", - "integrity": "sha512-CUiTiqKlzskwswrx9Ve5NhNoab30L1/ScOfQwr1duvNlFvarC8fvQSgdtpw2Zh3MfnfNPpyLZnYg7ah4kbT9JQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.21.tgz", - "integrity": "sha512-YyBTAFM/QPqt1PscD8hDmCLnqPGKmUZpqeE25HXY8OLjl2MUs8+O4KjwPZZ+OGxpdTbwuWFyMoxjcLy80JODvg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.21.tgz", - "integrity": "sha512-DQD+ooJmwpNsh4acrftdkuwl5LNxxg8U4+C/RJNDd7m5FP9Wo4c0URi5U0a9Vk/6sQNh9aSGcYChDpqCDWEcBw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.21.tgz", - "integrity": "sha512-y1L49+snt1a1gLTYPY641slqy55QotPdtRK9Y6jMi4JBQyZwxC8swWYlQWb+MyILwxA614fi62SCNZNznB3XSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/core-linux-x64-gnu": { "version": "1.11.21", "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.21.tgz", @@ -5573,57 +4390,6 @@ "node": ">=10" } }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.21.tgz", - "integrity": "sha512-DJJe9k6gXR/15ZZVLv1SKhXkFst8lYCeZRNHH99SlBodvu4slhh/MKQ6YCixINRhCwliHrpXPym8/5fOq8b7Ig==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.21.tgz", - "integrity": "sha512-TqEXuy6wedId7bMwLIr9byds+mKsaXVHctTN88R1UIBPwJA92Pdk0uxDgip0pEFzHB/ugU27g6d8cwUH3h2eIw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.21.tgz", - "integrity": "sha512-BT9BNNbMxdpUM1PPAkYtviaV0A8QcXttjs2MDtOeSqqvSJaPtyM+Fof2/+xSwQDmDEFzbGCcn75M5+xy3lGqpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -5672,30 +4438,6 @@ "testcontainers": "^10.24.2" } }, - "node_modules/@tokenizer/inflate": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", - "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "fflate": "^0.8.2", - "token-types": "^6.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "license": "MIT" - }, "node_modules/@turf/boolean-point-in-polygon": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.2.0.tgz", @@ -7145,12 +5887,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/archiver-utils/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, "node_modules/archiver-utils/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -9590,16 +8326,6 @@ "exiftool-vendored.pl": "13.0.1" } }, - "node_modules/exiftool-vendored.exe": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-13.0.0.tgz", - "integrity": "sha512-4zAMuFGgxZkOoyQIzZMHv1HlvgyJK3AkNqjAgm8A8V0UmOZO7yv3pH49cDV1OduzFJqgs6yQ6eG4OGydhKtxlg==", - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/exiftool-vendored.pl": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-13.0.1.tgz", @@ -9794,12 +8520,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "license": "MIT" - }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -9846,24 +8566,6 @@ "stream-source": "0.3" } }, - "node_modules/file-type": { - "version": "20.4.1", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", - "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", - "license": "MIT", - "dependencies": { - "@tokenizer/inflate": "^0.2.6", - "strtok3": "^10.2.0", - "token-types": "^6.0.0", - "uint8array-extras": "^1.4.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -10192,20 +8894,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -10670,13 +9358,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -11370,9 +10051,9 @@ } }, "node_modules/jose": { - "version": "6.0.8", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.8.tgz", - "integrity": "sha512-EyUPtOKyTYq+iMOszO42eobQllaIjJnwkZ2U93aJzNyPibCy7CEvT9UQnaCVB51IAd49gbNdCew1c0LcLTCB2g==", + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.10.tgz", + "integrity": "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -11764,25 +10445,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, - "node_modules/load-esm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.2.tgz", - "integrity": "sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - }, - { - "type": "buymeacoffee", - "url": "https://buymeacoffee.com/borewit" - } - ], - "license": "MIT", - "engines": { - "node": ">=13.2.0" - } - }, "node_modules/load-tsconfig": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", @@ -11879,6 +10541,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/luxon": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", @@ -12738,14 +11406,6 @@ "set-blocking": "^2.0.0" } }, - "node_modules/oauth4webapi": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.3.0.tgz", - "integrity": "sha512-ZlozhPlFfobzh3hB72gnBFLjXpugl/dljz1fJSRdqaV2r3D5dmi5lg2QWI0LmUYuazmE+b5exsloEv6toUtw9g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } "node_modules/nwsapi": { "version": "2.2.20", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", @@ -12753,6 +11413,15 @@ "dev": true, "license": "MIT" }, + "node_modules/oauth4webapi": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.5.0.tgz", + "integrity": "sha512-DF3mLWNuxPkxJkHmWxbSFz4aE5CjWOsm465VBfBdWzmzX4Mg3vF8icxK+iKqfdWrIumBJ2TaoNQWx+SQc2bsPQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -12793,15 +11462,6 @@ "node": ">= 0.4" } }, - "node_modules/oidc-token-hash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", - "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==", - "license": "MIT", - "engines": { - "node": "^10.13.0 || >=12.0.0" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -12865,13 +11525,13 @@ } }, "node_modules/openid-client": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.3.3.tgz", - "integrity": "sha512-lTK8AV8SjqCM4qznLX0asVESAwzV39XTVdfMAM185ekuaZCnkWdPzcxMTXNlsm9tsUAMa1Q30MBmKAykdT1LWw==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.4.2.tgz", + "integrity": "sha512-4zBRTsKNRTyKxV5cFzl+LtamsYx/FsWhejjax+qgMkFNGtLj1gMtng2iSoJqqWUT0FHU3IUhO53aeBePg7Sp/g==", "license": "MIT", "dependencies": { - "jose": "^6.0.6", - "oauth4webapi": "^3.3.0" + "jose": "^6.0.10", + "oauth4webapi": "^3.4.1" }, "funding": { "url": "https://github.com/sponsors/panva" @@ -13023,18 +11683,31 @@ "license": "MIT" }, "node_modules/parse5": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", - "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^4.5.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseley": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", @@ -13284,19 +11957,6 @@ "url": "https://ko-fi.com/killymxi" } }, - "node_modules/peek-readable": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-7.0.0.tgz", - "integrity": "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/pg": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.1.tgz", @@ -14071,12 +12731,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/react-email/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, "node_modules/react-email/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -14715,9 +13369,9 @@ } }, "node_modules/sanitize-html": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.16.0.tgz", - "integrity": "sha512-0s4caLuHHaZFVxFTG74oW91+j6vW7gKbGD6CD2+miP73CE6z6YtOBN0ArtLd2UGyi4IC7K47v3ENUbQX4jV3Mg==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.15.0.tgz", + "integrity": "sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA==", "license": "MIT", "dependencies": { "deepmerge": "^4.2.2", @@ -15604,23 +14258,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strtok3": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.2.tgz", - "integrity": "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -15714,13 +14351,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/sucrase/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC", - "peer": true - }, "node_modules/sucrase/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -16271,13 +14901,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/test-exclude/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/test-exclude/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -16493,23 +15116,6 @@ "node": ">=0.6" } }, - "node_modules/token-types": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", - "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -16860,12 +15466,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/typeorm/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, "node_modules/typeorm/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -17034,18 +15634,6 @@ "node": ">= 4.0.0" } }, - "node_modules/uint8array-extras": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", - "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/undici": { "version": "5.29.0", "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", @@ -17357,278 +15945,6 @@ } } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/vite/node_modules/@esbuild/linux-x64": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", @@ -17646,125 +15962,6 @@ "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/vite/node_modules/esbuild": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 4624159925..75f5b8a52d 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -772,9 +772,13 @@ describe(AuthService.name, () => { mocks.user.update.mockResolvedValue(user); mocks.session.create.mockResolvedValue(factory.session()); - await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( - oauthResponse(user), - ); + await expect( + sut.callback( + { url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' }, + {}, + loginDetails, + ), + ).resolves.toEqual(oauthResponse(user)); expect(mocks.user.update).toHaveBeenCalledWith(user.id, { profileImagePath: `upload/profile/${user.id}/${fileId}.jpg`, @@ -796,9 +800,13 @@ describe(AuthService.name, () => { mocks.user.update.mockResolvedValue(user); mocks.session.create.mockResolvedValue(factory.session()); - await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( - oauthResponse(user), - ); + await expect( + sut.callback( + { url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' }, + {}, + loginDetails, + ), + ).resolves.toEqual(oauthResponse(user)); expect(mocks.user.update).not.toHaveBeenCalled(); expect(mocks.oauth.getProfilePicture).not.toHaveBeenCalled(); diff --git a/server/test/vitest.config.mjs b/server/test/vitest.config.mjs index a6929bf806..a22a6751c3 100644 --- a/server/test/vitest.config.mjs +++ b/server/test/vitest.config.mjs @@ -20,12 +20,6 @@ export default defineConfig({ 'src/services/index.ts', 'src/sql-tools/from-database/index.ts', ], - thresholds: { - lines: 85, - statements: 85, - branches: 90, - functions: 85, - }, }, server: { deps: { From 987e5ab76ccb40d3a49325da1ee9306eb54ec38d Mon Sep 17 00:00:00 2001 From: Thomas <9749173+uhthomas@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:07:32 +0100 Subject: [PATCH 042/356] fix(server): start job workers after DB (#17806) Job workers are currently started on app init, which means they are started before the DB is initialised. This can be problematic if jobs which need to use the DB start running before it's ready. It also means that swapping out the queue implementation for something which uses the DB won't work. --- server/src/app.module.ts | 9 +++------ server/src/enum.ts | 2 ++ server/src/repositories/job.repository.ts | 2 +- server/src/services/job.service.ts | 16 ++++++++++++++++ 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 5720f7af0b..05dbc090fc 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -17,12 +17,12 @@ import { LoggingInterceptor } from 'src/middleware/logging.interceptor'; import { repositories } from 'src/repositories'; import { ConfigRepository } from 'src/repositories/config.repository'; import { EventRepository } from 'src/repositories/event.repository'; -import { JobRepository } from 'src/repositories/job.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository'; import { services } from 'src/services'; import { AuthService } from 'src/services/auth.service'; import { CliService } from 'src/services/cli.service'; +import { JobService } from 'src/services/job.service'; import { getKyselyConfig } from 'src/utils/database'; const common = [...repositories, ...services, GlobalExceptionFilter]; @@ -52,7 +52,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy { @Inject(IWorker) private worker: ImmichWorker, logger: LoggingRepository, private eventRepository: EventRepository, - private jobRepository: JobRepository, + private jobService: JobService, private telemetryRepository: TelemetryRepository, private authService: AuthService, ) { @@ -62,10 +62,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy { async onModuleInit() { this.telemetryRepository.setup({ repositories }); - this.jobRepository.setup({ services }); - if (this.worker === ImmichWorker.MICROSERVICES) { - this.jobRepository.startWorkers(); - } + this.jobService.setServices(services); this.eventRepository.setAuthFn(async (client) => this.authService.authenticate({ diff --git a/server/src/enum.ts b/server/src/enum.ts index baf864aa49..b9a914671a 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -407,6 +407,8 @@ export enum DatabaseExtension { export enum BootstrapEventPriority { // Database service should be initialized before anything else, most other services need database access DatabaseService = -200, + // Other services may need to queue jobs on bootstrap. + JobService = -190, // Initialise config after other bootstrap services, stop other services from using config on bootstrap SystemConfig = 100, } diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts index fd9f4c5363..0912759d1c 100644 --- a/server/src/repositories/job.repository.ts +++ b/server/src/repositories/job.repository.ts @@ -33,7 +33,7 @@ export class JobRepository { this.logger.setContext(JobRepository.name); } - setup({ services }: { services: ClassConstructor[] }) { + setup(services: ClassConstructor[]) { const reflector = this.moduleRef.get(Reflector, { strict: false }); // discovery diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index f8298336a8..b81256de81 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -1,10 +1,12 @@ import { BadRequestException, Injectable } from '@nestjs/common'; +import { ClassConstructor } from 'class-transformer'; import { snakeCase } from 'lodash'; import { OnEvent } from 'src/decorators'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto'; import { AssetType, + BootstrapEventPriority, ImmichWorker, JobCommand, JobName, @@ -51,6 +53,8 @@ const asJobItem = (dto: JobCreateDto): JobItem => { @Injectable() export class JobService extends BaseService { + private services: ClassConstructor[] = []; + @OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] }) onConfigInit({ newConfig: config }: ArgOf<'config.init'>) { this.logger.debug(`Updating queue concurrency settings`); @@ -69,6 +73,18 @@ export class JobService extends BaseService { this.onConfigInit({ newConfig: config }); } + @OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.JobService }) + onBootstrap() { + this.jobRepository.setup(this.services); + if (this.worker === ImmichWorker.MICROSERVICES) { + this.jobRepository.startWorkers(); + } + } + + setServices(services: ClassConstructor[]) { + this.services = services; + } + async create(dto: JobCreateDto): Promise { await this.jobRepository.queue(asJobItem(dto)); } From 19746a8685cbf073b6651f94643b7951ef4c1dca Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Wed, 23 Apr 2025 16:31:18 +0100 Subject: [PATCH 043/356] fix: cache build versions (#17811) --- .../repositories/server-info.repository.ts | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts index deb24123d0..cc7e1770fc 100644 --- a/server/src/repositories/server-info.repository.ts +++ b/server/src/repositories/server-info.repository.ts @@ -73,26 +73,32 @@ export class ServerInfoRepository { } } + buildVersions?: ServerBuildVersions; + async getBuildVersions(): Promise { - const { nodeVersion, resourcePaths } = this.configRepository.getEnv(); + if (!this.buildVersions) { + const { nodeVersion, resourcePaths } = this.configRepository.getEnv(); - const [nodejsOutput, ffmpegOutput, magickOutput] = await Promise.all([ - maybeFirstLine('node --version'), - maybeFirstLine('ffmpeg -version'), - maybeFirstLine('convert --version'), - ]); + const [nodejsOutput, ffmpegOutput, magickOutput] = await Promise.all([ + maybeFirstLine('node --version'), + maybeFirstLine('ffmpeg -version'), + maybeFirstLine('convert --version'), + ]); - const lockfile = await readFile(resourcePaths.lockFile) - .then((buffer) => JSON.parse(buffer.toString())) - .catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`)); + const lockfile = await readFile(resourcePaths.lockFile) + .then((buffer) => JSON.parse(buffer.toString())) + .catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`)); - return { - nodejs: nodejsOutput || nodeVersion || '', - exiftool: await exiftool.version(), - ffmpeg: getLockfileVersion('ffmpeg', lockfile) || ffmpegOutput.replaceAll('ffmpeg version', '') || '', - libvips: getLockfileVersion('libvips', lockfile) || sharp.versions.vips, - imagemagick: - getLockfileVersion('imagemagick', lockfile) || magickOutput.replaceAll('Version: ImageMagick ', '') || '', - }; + this.buildVersions = { + nodejs: nodejsOutput || nodeVersion || '', + exiftool: await exiftool.version(), + ffmpeg: getLockfileVersion('ffmpeg', lockfile) || ffmpegOutput.replaceAll('ffmpeg version', '') || '', + libvips: getLockfileVersion('libvips', lockfile) || sharp.versions.vips, + imagemagick: + getLockfileVersion('imagemagick', lockfile) || magickOutput.replaceAll('Version: ImageMagick ', '') || '', + }; + } + + return this.buildVersions; } } From 59fa8fbd0e6f3df35276d70d180771de612fa544 Mon Sep 17 00:00:00 2001 From: Toni <51962051+EinToni@users.noreply.github.com> Date: Wed, 23 Apr 2025 17:31:35 +0200 Subject: [PATCH 044/356] perf(mobile): remove small thumbnail and cache generated thumbnails (#17792) * Remove small thumbnail and cache generated thumbnails * Creating the small thumbnails takes quite some time, which should not be underestimated. * The time needed to generate the small or big thumbnail is not too different from each other. Therefore there is no real benefit of the small thumbnail and it only adds frustration to the end user experience. That is because the image appeared to have loaded (the visual move from blur to something better) but it's still so bad that it is basically a blur. The better solution is therefore to stay at the blur until the actual thumbnail has loaded. * Additionaly to the faster generation of the thumbnail, it now also gets cached similarly to the remote thumbnail which already gets cached. This further speeds up the all over usage of the app and prevents a repeatet thumbnail generation when opening the app. * Decreased the quality from the default 95 to 80 to provide similar quality with much reduces thumbnail size. * Use try catch around the read of the cache file. * Use the key provided in the loadImage method instead of the asset of the constructor. * Use userId instead of ownerId * Remove import * Add checksum to thumbnail cache key --- .../immich_local_thumbnail_provider.dart | 52 +++++++++++++------ .../lib/widgets/common/immich_thumbnail.dart | 12 +++-- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart index 1e2f5d312e..edcf8a9458 100644 --- a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart +++ b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart @@ -2,11 +2,14 @@ import 'dart:async'; import 'dart:ui' as ui; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:photo_manager/photo_manager.dart' show ThumbnailSize; +import 'package:logging/logging.dart'; /// The local image provider for an asset /// Only viable @@ -15,11 +18,16 @@ class ImmichLocalThumbnailProvider final Asset asset; final int height; final int width; + final CacheManager? cacheManager; + final Logger log = Logger("ImmichLocalThumbnailProvider"); + final String? userId; ImmichLocalThumbnailProvider({ required this.asset, this.height = 256, this.width = 256, + this.cacheManager, + this.userId, }) : assert(asset.local != null, 'Only usable when asset.local is set'); /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key @@ -36,11 +44,10 @@ class ImmichLocalThumbnailProvider ImmichLocalThumbnailProvider key, ImageDecoderCallback decode, ) { - final chunkEvents = StreamController(); + final cache = cacheManager ?? ThumbnailImageCacheManager(); return MultiImageStreamCompleter( - codec: _codec(key.asset, decode, chunkEvents), + codec: _codec(key.asset, cache, decode), scale: 1.0, - chunkEvents: chunkEvents.stream, informationCollector: () sync* { yield ErrorDescription(key.asset.fileName); }, @@ -50,25 +57,38 @@ class ImmichLocalThumbnailProvider // Streams in each stage of the image as we ask for it Stream _codec( Asset assetData, + CacheManager cache, ImageDecoderCallback decode, - StreamController chunkEvents, ) async* { - final thumbBytes = await assetData.local - ?.thumbnailDataWithSize(ThumbnailSize(width, height)); - if (thumbBytes == null) { - chunkEvents.close(); + final cacheKey = + '$userId${assetData.localId}${assetData.checksum}$width$height'; + final fileFromCache = await cache.getFileFromCache(cacheKey); + if (fileFromCache != null) { + try { + final buffer = + await ui.ImmutableBuffer.fromFilePath(fileFromCache.file.path); + final codec = await decode(buffer); + yield codec; + return; + } catch (error) { + log.severe('Found thumbnail in cache, but loading it failed', error); + } + } + + final thumbnailBytes = await assetData.local?.thumbnailDataWithSize( + ThumbnailSize(width, height), + quality: 80, + ); + if (thumbnailBytes == null) { throw StateError( - "Loading thumb for local photo ${asset.fileName} failed", + "Loading thumb for local photo ${assetData.fileName} failed", ); } - try { - final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); - final codec = await decode(buffer); - yield codec; - } finally { - chunkEvents.close(); - } + final buffer = await ui.ImmutableBuffer.fromUint8List(thumbnailBytes); + final codec = await decode(buffer); + yield codec; + await cache.putFile(cacheKey, thumbnailBytes); } @override diff --git a/mobile/lib/widgets/common/immich_thumbnail.dart b/mobile/lib/widgets/common/immich_thumbnail.dart index 2ebead0083..35729ead7b 100644 --- a/mobile/lib/widgets/common/immich_thumbnail.dart +++ b/mobile/lib/widgets/common/immich_thumbnail.dart @@ -1,7 +1,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart'; import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; @@ -9,8 +9,9 @@ import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; import 'package:immich_mobile/widgets/common/immich_image.dart'; import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart'; import 'package:octo_image/octo_image.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; -class ImmichThumbnail extends HookWidget { +class ImmichThumbnail extends HookConsumerWidget { const ImmichThumbnail({ this.asset, this.width = 250, @@ -31,6 +32,7 @@ class ImmichThumbnail extends HookWidget { static ImageProvider imageProvider({ Asset? asset, String? assetId, + String? userId, int thumbnailSize = 256, }) { if (asset == null && assetId == null) { @@ -48,6 +50,7 @@ class ImmichThumbnail extends HookWidget { asset: asset, height: thumbnailSize, width: thumbnailSize, + userId: userId, ); } else { return ImmichRemoteThumbnailProvider( @@ -59,8 +62,10 @@ class ImmichThumbnail extends HookWidget { } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { Uint8List? blurhash = useBlurHashRef(asset).value; + final userId = ref.watch(currentUserProvider)?.id; + if (asset == null) { return Container( color: Colors.grey, @@ -79,6 +84,7 @@ class ImmichThumbnail extends HookWidget { octoSet: blurHashOrPlaceholder(blurhash), image: ImmichThumbnail.imageProvider( asset: asset, + userId: userId, ), width: width, height: height, From 64000d9d766048c3f8267d9b111c02a0a3a8c42f Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Wed, 23 Apr 2025 17:49:06 +0200 Subject: [PATCH 045/356] feat: static analysis job for gha workflows (#17688) * fix: set persist-credentials explicitly for checkout https://woodruffw.github.io/zizmor/audits/#artipacked * fix: minimize permissions scope for workflows https://woodruffw.github.io/zizmor/audits/#excessive-permissions * fix: remove potential template injections https://woodruffw.github.io/zizmor/audits/#template-injection * fix: only pass needed secrets in workflow_call https://woodruffw.github.io/zizmor/audits/#secrets-inherit * fix: push perm for single-arch build jobs I hadn't realised these push to the registry too :x * chore: fix formatting * fix: $ * fix: retag job quoting * feat: static analysis job for gha workflows * chore: fix formatting * fix: clear last zizmor checks * fix: broken merge --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 6 +++-- .github/workflows/docs-deploy.yml | 14 ++++++------ .github/workflows/docs-destroy.yml | 2 +- .github/workflows/pr-label-validation.yml | 2 +- .github/workflows/pr-labeler.yml | 2 +- .github/workflows/prepare-release.yml | 5 ++++- .github/workflows/static_analysis.yml | 27 +++++++++++++++++++++++ .github/workflows/weblate-lock.yml | 1 + 8 files changed, 46 insertions(+), 13 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a78d3c25dc..9a65b05ace 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -224,7 +224,7 @@ jobs: BUILD_SOURCE_COMMIT=${{ github.sha }} - name: Export digest - run: | + run: | # zizmor: ignore[template-injection] mkdir -p ${{ runner.temp }}/digests digest="${{ steps.build.outputs.digest }}" touch "${{ runner.temp }}/digests/${digest#sha256:}" @@ -426,7 +426,7 @@ jobs: BUILD_SOURCE_COMMIT=${{ github.sha }} - name: Export digest - run: | + run: | # zizmor: ignore[template-injection] mkdir -p ${{ runner.temp }}/digests digest="${{ steps.build.outputs.digest }}" touch "${{ runner.temp }}/digests/${digest#sha256:}" @@ -535,6 +535,7 @@ jobs: run: exit 1 - name: All jobs passed or skipped if: ${{ !(contains(needs.*.result, 'failure')) }} + # zizmor: ignore[template-injection] run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" success-check-ml: @@ -549,4 +550,5 @@ jobs: run: exit 1 - name: All jobs passed or skipped if: ${{ !(contains(needs.*.result, 'failure')) }} + # zizmor: ignore[template-injection] run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 10277a0c5e..fd12423fd9 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -1,6 +1,6 @@ name: Docs deploy on: - workflow_run: + workflow_run: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here workflows: ['Docs build'] types: - completed @@ -115,22 +115,22 @@ jobs: - name: Load parameters id: parameters uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + env: + PARAM_JSON: ${{ needs.checks.outputs.parameters }} with: script: | - const json = `${{ needs.checks.outputs.parameters }}`; - const parameters = JSON.parse(json); + const parameters = JSON.parse(process.env.PARAM_JSON); core.setOutput("event", parameters.event); core.setOutput("name", parameters.name); core.setOutput("shouldDeploy", parameters.shouldDeploy); - - run: | - echo "Starting docs deployment for ${{ steps.parameters.outputs.event }} ${{ steps.parameters.outputs.name }}" - - name: Download artifact uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + env: + ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }} with: script: | - let artifact = ${{ needs.checks.outputs.artifact }}; + let artifact = JSON.parse(process.env.ARTIFACT_JSON); let download = await github.rest.actions.downloadArtifact({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml index 9d1e4b6612..0da258de09 100644 --- a/.github/workflows/docs-destroy.yml +++ b/.github/workflows/docs-destroy.yml @@ -1,6 +1,6 @@ name: Docs destroy on: - pull_request_target: + pull_request_target: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here types: [closed] permissions: {} diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml index 8d34597a08..c5e5131920 100644 --- a/.github/workflows/pr-label-validation.yml +++ b/.github/workflows/pr-label-validation.yml @@ -1,7 +1,7 @@ name: PR Label Validation on: - pull_request_target: + pull_request_target: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here types: [opened, labeled, unlabeled, synchronize] permissions: {} diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 5704f4275f..75c6836ab9 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -1,6 +1,6 @@ name: 'Pull Request Labeler' on: - - pull_request_target + - pull_request_target # zizmor: ignore[dangerous-triggers] no attacker inputs are used here permissions: {} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index dc171597e9..145418d72b 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -47,7 +47,10 @@ jobs: uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 - name: Bump version - run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}" + env: + SERVER_BUMP: ${{ inputs.serverBump }} + MOBILE_BUMP: ${{ inputs.mobileBump }} + run: misc/release/pump-version.sh -s "${SERVER_BUMP}" -m "${MOBILE_BUMP}" - name: Commit and tag id: push-tag diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 1a3c11d3d5..3efbc25de3 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -95,3 +95,30 @@ jobs: - name: Run dart custom_lint run: dart run custom_lint working-directory: ./mobile + + zizmor: + name: zizmor + runs-on: ubuntu-latest + permissions: + security-events: write + contents: read + actions: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v5 + + - name: Run zizmor 🌈 + run: uvx zizmor --format=sarif . > results.sarif + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif + category: zizmor diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index 2aef5c472a..2d644955bc 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -57,4 +57,5 @@ jobs: run: exit 1 - name: All jobs passed or skipped if: ${{ !(contains(needs.*.result, 'failure')) }} + # zizmor: ignore[template-injection] run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" From be1062474bd357f16ff9721eaf447aaeb20e4fe7 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 23 Apr 2025 11:02:49 -0500 Subject: [PATCH 046/356] chore: memory spacing (#17813) chore(web): memory spacing --- web/src/lib/components/photos-page/memory-lane.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte index acf66e5dde..280273d0d1 100644 --- a/web/src/lib/components/photos-page/memory-lane.svelte +++ b/web/src/lib/components/photos-page/memory-lane.svelte @@ -76,7 +76,7 @@
    (innerWidth = width)}> {#each memoryStore.memories as memory (memory.id)} Date: Wed, 23 Apr 2025 17:10:43 +0100 Subject: [PATCH 047/356] fix: retrieve version from lockfile and fallback to cli command (#17812) --- .../repositories/server-info.repository.ts | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts index cc7e1770fc..4500094899 100644 --- a/server/src/repositories/server-info.repository.ts +++ b/server/src/repositories/server-info.repository.ts @@ -75,27 +75,49 @@ export class ServerInfoRepository { buildVersions?: ServerBuildVersions; + private async retrieveVersionFallback( + command: string, + commandTransform?: (output: string) => string, + version?: string, + ): Promise { + if (!version) { + const output = await maybeFirstLine(command); + version = commandTransform ? commandTransform(output) : output; + } + return version; + } + async getBuildVersions(): Promise { if (!this.buildVersions) { const { nodeVersion, resourcePaths } = this.configRepository.getEnv(); - const [nodejsOutput, ffmpegOutput, magickOutput] = await Promise.all([ - maybeFirstLine('node --version'), - maybeFirstLine('ffmpeg -version'), - maybeFirstLine('convert --version'), - ]); - - const lockfile = await readFile(resourcePaths.lockFile) + const lockfile: BuildLockfile | undefined = await readFile(resourcePaths.lockFile) .then((buffer) => JSON.parse(buffer.toString())) .catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`)); + const [nodejsVersion, ffmpegVersion, magickVersion, exiftoolVersion] = await Promise.all([ + this.retrieveVersionFallback('node --version', undefined, nodeVersion), + this.retrieveVersionFallback( + 'ffmpeg -version', + (output) => output.replaceAll('ffmpeg version ', ''), + getLockfileVersion('ffmpeg', lockfile), + ), + this.retrieveVersionFallback( + 'magick --version', + (output) => output.replaceAll('Version: ImageMagick ', ''), + getLockfileVersion('imagemagick', lockfile), + ), + exiftool.version(), + ]); + + const libvipsVersion = getLockfileVersion('libvips', lockfile) || sharp.versions.vips; + this.buildVersions = { - nodejs: nodejsOutput || nodeVersion || '', - exiftool: await exiftool.version(), - ffmpeg: getLockfileVersion('ffmpeg', lockfile) || ffmpegOutput.replaceAll('ffmpeg version', '') || '', - libvips: getLockfileVersion('libvips', lockfile) || sharp.versions.vips, - imagemagick: - getLockfileVersion('imagemagick', lockfile) || magickOutput.replaceAll('Version: ImageMagick ', '') || '', + nodejs: nodejsVersion, + exiftool: exiftoolVersion, + ffmpeg: ffmpegVersion, + libvips: libvipsVersion, + imagemagick: magickVersion, }; } From 830b4dadcb89eeff807b3786f5b928d715014ea2 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Wed, 23 Apr 2025 18:26:58 +0200 Subject: [PATCH 048/356] chore(web): update translations (#17808) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aleksander Vae Haaland Co-authored-by: Bezruchenko Simon Co-authored-by: Bonov Co-authored-by: Bruno López Barcia Co-authored-by: Chris Axell Co-authored-by: Dymitr Co-authored-by: Florian Ostertag Co-authored-by: GiannosOB Co-authored-by: Happy Co-authored-by: Hurricane-32 Co-authored-by: Indrek Haav Co-authored-by: Jane Co-authored-by: Javier Villanueva García Co-authored-by: Junghyuk Kwon Co-authored-by: Karl Solgård Co-authored-by: Leo Bottaro Co-authored-by: Linerly Co-authored-by: MannyLama Co-authored-by: Matjaž T Co-authored-by: Miki Mrvos Co-authored-by: RWDai <869759838@qq.com> Co-authored-by: Roi Gabay Co-authored-by: Runskrift Co-authored-by: Sebastian Co-authored-by: Shawn Co-authored-by: Sidewave Tech Co-authored-by: Sylvain Pichon Co-authored-by: Temuri Doghonadze Co-authored-by: Xo Co-authored-by: Zvonimir Co-authored-by: adri1m64 Co-authored-by: catelixor Co-authored-by: eav5jhl0 Co-authored-by: kiwinho Co-authored-by: millallo Co-authored-by: pyccl Co-authored-by: stanciupaul Co-authored-by: thehijacker Co-authored-by: waclaw66 Co-authored-by: xuars Co-authored-by: Вячеслав Лукьяненко Co-authored-by: 灯笼 --- i18n/es.json | 66 +++++++++++++++++++++++++------------------------ i18n/hr.json | 6 +++++ i18n/id.json | 2 ++ i18n/ko.json | 1 + i18n/nb_NO.json | 10 +++++++- i18n/sl.json | 6 +++-- i18n/sv.json | 2 ++ 7 files changed, 58 insertions(+), 35 deletions(-) diff --git a/i18n/es.json b/i18n/es.json index 02cd6ab840..64521c1aa8 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "¿Estás seguro de que deseas desactivar todos los métodos de inicio de sesión? Esto desactivará por completo el inicio de sesión.", "authentication_settings_reenable": "Para reactivarlo, utiliza un Comando del servidor.", "background_task_job": "Tareas en segundo plano", - "backup_database": "Respaldar base de datos", - "backup_database_enable_description": "Activar respaldo de base de datos", - "backup_keep_last_amount": "Cantidad de respaldos previos a mantener", - "backup_settings": "Ajustes de respaldo", - "backup_settings_description": "Administrar configuración de respaldo de base de datos", + "backup_database": "Crear volcado de base de datos", + "backup_database_enable_description": "Activar volcado de base de datos", + "backup_keep_last_amount": "Cantidad de volcados previos a mantener", + "backup_settings": "Ajustes de volcado de base de datos", + "backup_settings_description": "Administrar configuración de volcado de base de datos. Nota: estas tareas no están monitorizadas y no se notificarán los fallos.", "check_all": "Verificar todo", "cleanup": "Limpieza", "cleared_jobs": "Trabajos borrados para: {job}", @@ -91,9 +91,9 @@ "image_thumbnail_quality_description": "Calidad de miniatura de 1 a 100. Es mejor cuanto más alto es el valor pero genera archivos más grandes y puede reducir la capacidad de respuesta de la aplicación.", "image_thumbnail_title": "Ajustes de las miniaturas", "job_concurrency": "{job}: Procesos simultáneos", - "job_created": "Trabajo creado", + "job_created": "Tarea creada", "job_not_concurrency_safe": "Esta tarea no es segura para la simultaneidad.", - "job_settings": "Configuración tareas", + "job_settings": "Configuración de tareas", "job_settings_description": "Administrar tareas simultáneas", "job_status": "Estado de la tarea", "jobs_delayed": "{jobCount, plural, one {# retrasado} other {# retrasados}}", @@ -169,7 +169,7 @@ "migration_job_description": "Migrar miniaturas de archivos y caras a la estructura de carpetas más reciente", "no_paths_added": "No se han añadido carpetas", "no_pattern_added": "No se han añadido patrones", - "note_apply_storage_label_previous_assets": "Nota: para aplicar una Etiqueta de Almacenamient a un elemento anteriormente cargado, lanza el", + "note_apply_storage_label_previous_assets": "Nota: para aplicar una Etiqueta de Almacenamiento a un elemento anteriormente cargado, lanza el", "note_cannot_be_changed_later": "NOTA: ¡No se puede cambiar posteriormente!", "notification_email_from_address": "Desde", "notification_email_from_address_description": "Dirección de correo electrónico del remitente, por ejemplo: \"Immich Photo Server \"", @@ -252,12 +252,12 @@ "storage_template_migration": "Migración de plantillas de almacenamiento", "storage_template_migration_description": "Aplicar la {template} actual a los elementos subidos previamente", "storage_template_migration_info": "La plantilla de almacenamiento convertirá todas las extensiones a minúscula. Los cambios en las plantillas solo se aplican a los elementos nuevos. Para aplicarlos retroactivamente a los elementos subidos previamente ejecute la {job}.", - "storage_template_migration_job": "Migración de la plantilla de almacenamiento", + "storage_template_migration_job": "Tarea de migración de la plantilla de almacenamiento", "storage_template_more_details": "Para obtener más detalles sobre esta función, consulte la Plantilla de almacenamiento y sus implicaciones", "storage_template_onboarding_description": "Cuando está habilitada, esta función organizará automáticamente los archivos según una plantilla definida por el usuario. Debido a problemas de estabilidad, la función se ha desactivado de forma predeterminada. Para obtener más información, consulte la documentación.", "storage_template_path_length": "Límite aproximado de la longitud de la ruta: {length, number}/{limit, number}", "storage_template_settings": "Plantilla de almacenamiento", - "storage_template_settings_description": "Administre la estructura de carpetas y el nombre de archivo del recurso cargado", + "storage_template_settings_description": "Administrar la estructura de carpetas y el nombre de archivo del recurso cargado", "storage_template_user_label": "{label} es la etiqueta de almacenamiento del usuario", "system_settings": "Ajustes del Sistema", "tag_cleanup_job": "Limpieza de etiquetas", @@ -345,7 +345,7 @@ "trash_settings": "Configuración papelera", "trash_settings_description": "Administrar la configuración de la papelera", "untracked_files": "Archivos sin seguimiento", - "untracked_files_description": "La aplicación no rastrea estos archivos. Puede ser el resultado de movimientos fallidos, cargas interrumpidas o sin procesar debido a un error", + "untracked_files_description": "La aplicación no rastrea estos archivos. Puede ser el resultado de movimientos fallidos, subidas interrumpidas o sin procesar debido a un error", "user_cleanup_job": "Limpieza de usuarios", "user_delete_delay": "La cuenta {user} y los archivos se programarán para su eliminación permanente en {delay, plural, one {# día} other {# días}}.", "user_delete_delay_settings": "Eliminar retardo", @@ -429,7 +429,7 @@ "allow_dark_mode": "Permitir modo oscuro", "allow_edits": "Permitir edición", "allow_public_user_to_download": "Permitir descargar al usuario público", - "allow_public_user_to_upload": "Permitir cargar al usuario publico", + "allow_public_user_to_upload": "Permitir subir al usuario publico", "alt_text_qr_code": "Código QR", "anti_clockwise": "En sentido antihorario", "api_key": "Clave API", @@ -473,7 +473,7 @@ "asset_skipped": "Omitido", "asset_skipped_in_trash": "En la papelera", "asset_uploaded": "Subido", - "asset_uploading": "Cargando…", + "asset_uploading": "Subiendo…", "asset_viewer_settings_subtitle": "Administra las configuracioens de tu visor de fotos", "asset_viewer_settings_title": "Visor de Archivos", "assets": "elementos", @@ -482,7 +482,7 @@ "assets_added_to_name_count": "Añadido {count, plural, one {# asset} other {# assets}} a {hasName, select, true {{name}} other {new album}}", "assets_count": "{count, plural, one {# activo} other {# activos}}", "assets_deleted_permanently": "{} elementos(s) eliminado(s) permanentemente", - "assets_deleted_permanently_from_server": "{} recurso(s) eliminados de forma permanente del servidor de Immich", + "assets_deleted_permanently_from_server": "{} recurso(s) eliminado(s) de forma permanente del servidor de Immich", "assets_moved_to_trash_count": "{count, plural, one {# elemento movido} other {# elementos movidos}} a la papelera", "assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# elemento} other {# elementos}}", "assets_removed_count": "Eliminado {count, plural, one {# elemento} other {# elementos}}", @@ -492,7 +492,7 @@ "assets_restored_successfully": "{} elemento(s) restaurado(s) exitosamente", "assets_trashed": "{} elemento(s) eliminado(s)", "assets_trashed_count": "Borrado {count, plural, one {# elemento} other {# elementos}}", - "assets_trashed_from_server": "{} recurso(s) enviados a la papelera desde el servidor de Immich", + "assets_trashed_from_server": "{} recurso(s) enviado(s) a la papelera desde el servidor de Immich", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} ya forma parte del álbum", "authorized_devices": "Dispositivos Autorizados", "automatic_endpoint_switching_subtitle": "Conectarse localmente a través de la Wi-Fi designada cuando esté disponible y usar conexiones alternativas en otros lugares", @@ -510,11 +510,11 @@ "backup_all": "Todos", "backup_background_service_backup_failed_message": "Error al copiar elementos. Reintentando…", "backup_background_service_connection_failed_message": "Error al conectar con el servidor. Reintentando…", - "backup_background_service_current_upload_notification": "Cargando {}", + "backup_background_service_current_upload_notification": "Subiendo {}", "backup_background_service_default_notification": "Comprobando nuevos elementos…", "backup_background_service_error_title": "Error de copia de seguridad", "backup_background_service_in_progress_notification": "Creando copia de seguridad de tus elementos…", - "backup_background_service_upload_failure_notification": "Error al cargar {}", + "backup_background_service_upload_failure_notification": "Error al subir {}", "backup_controller_page_albums": "Álbumes de copia de seguridad", "backup_controller_page_background_app_refresh_disabled_content": "Activa la actualización en segundo plano de la aplicación en Configuración > General > Actualización en segundo plano para usar la copia de seguridad en segundo plano.", "backup_controller_page_background_app_refresh_disabled_title": "Actualización en segundo plano desactivada", @@ -536,7 +536,7 @@ "backup_controller_page_backup_selected": "Seleccionado: ", "backup_controller_page_backup_sub": "Fotos y videos respaldados", "backup_controller_page_created": "Creado el: {}", - "backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos elementos al servidor.", + "backup_controller_page_desc_backup": "Active la copia de seguridad para subir automáticamente los nuevos elementos al servidor cuando se abre la aplicación.", "backup_controller_page_excluded": "Excluido: ", "backup_controller_page_failed": "Fallidos ({})", "backup_controller_page_filename": "Nombre del archivo: {} [{}]", @@ -554,11 +554,11 @@ "backup_controller_page_total_sub": "Todas las fotos y vídeos únicos de los álbumes seleccionados", "backup_controller_page_turn_off": "Apagar la copia de seguridad", "backup_controller_page_turn_on": "Activar la copia de seguridad", - "backup_controller_page_uploading_file_info": "Cargando información del archivo", + "backup_controller_page_uploading_file_info": "Subiendo información del archivo", "backup_err_only_album": "No se puede eliminar el único álbum", "backup_info_card_assets": "elementos", "backup_manual_cancelled": "Cancelado", - "backup_manual_in_progress": "Subida en progreso. Espere", + "backup_manual_in_progress": "Subida ya en progreso. Vuelve a intentarlo más tarde", "backup_manual_success": "Éxito", "backup_manual_title": "Estado de la subida", "backup_options_page_title": "Opciones de Copia de Seguridad", @@ -767,7 +767,7 @@ "download_enqueue": "Descarga en cola", "download_error": "Error al descargar", "download_failed": "Descarga fallida", - "download_filename": "Archivo: {}", + "download_filename": "archivo: {}", "download_finished": "Descarga completada", "download_include_embedded_motion_videos": "Vídeos incrustados", "download_include_embedded_motion_videos_description": "Incluir vídeos incrustados en fotografías en movimiento como un archivo separado", @@ -978,7 +978,7 @@ "external": "Externo", "external_libraries": "Bibliotecas Externas", "external_network": "Red externa", - "external_network_sheet_info": "Cuando no estés conectado a la red WiFi preferida, la aplicación se conectará al servidor utilizando la primera de las siguientes URLs a la que pueda acceder, comenzando desde la parte superior de la lista hacia abajo", + "external_network_sheet_info": "Cuando no estés conectado a la red Wi-Fi preferida, la aplicación se conectará al servidor utilizando la primera de las siguientes URLs a la que pueda acceder, comenzando desde la parte superior de la lista hacia abajo", "face_unassigned": "Sin asignar", "failed": "Fallido", "failed_to_load_assets": "Error al cargar los activos", @@ -1125,7 +1125,7 @@ "local_network": "Local network", "local_network_sheet_info": "La aplicación se conectará al servidor a través de esta URL cuando utilice la red Wi-Fi especificada", "location_permission": "Permiso de ubicación", - "location_permission_content": "Para usar la función de cambio automático, Immich necesita permiso de ubicación precisa para poder leer el nombre de la red WiFi actual", + "location_permission_content": "Para usar la función de cambio automático, Immich necesita permiso de ubicación precisa para poder leer el nombre de la red Wi-Fi actual", "location_picker_choose_on_map": "Elegir en el mapa", "location_picker_latitude_error": "Introduce una latitud válida", "location_picker_latitude_hint": "Introduce tu latitud aquí", @@ -1263,7 +1263,7 @@ "no_shared_albums_message": "Crea un álbum para compartir fotos y vídeos con personas de tu red", "not_in_any_album": "Sin álbum", "not_selected": "No seleccionado", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar la etiqueta de almacenamiento a los archivos cargados previamente, ejecute el", + "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar la etiqueta de almacenamiento a los archivos subidos previamente, ejecute el", "notes": "Notas", "notification_permission_dialog_content": "Para activar las notificaciones, ve a Configuración y selecciona permitir.", "notification_permission_list_tile_content": "Concede permiso para habilitar las notificaciones.", @@ -1432,6 +1432,8 @@ "recent_searches": "Búsquedas recientes", "recently_added": "Añadidos recientemente", "recently_added_page_title": "Recién Agregadas", + "recently_taken": "Recientemente tomado", + "recently_taken_page_title": "Recientemente Tomado", "refresh": "Actualizar", "refresh_encoded_videos": "Recargar los vídeos codificados", "refresh_faces": "Actualizar caras", @@ -1615,7 +1617,7 @@ "settings_saved": "Ajustes guardados", "share": "Compartir", "share_add_photos": "Agregar fotos", - "share_assets_selected": "{} seleccionados", + "share_assets_selected": "{} seleccionado(s)", "share_dialog_preparing": "Preparando...", "shared": "Compartido", "shared_album_activities_input_disable": "Los comentarios están deshabilitados", @@ -1629,7 +1631,7 @@ "shared_by_user": "Compartido por {user}", "shared_by_you": "Compartido por ti", "shared_from_partner": "Fotos de {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Cargados", + "shared_intent_upload_button_progress_text": "{} / {} Cargado(s)", "shared_link_app_bar_title": "Enlaces compartidos", "shared_link_clipboard_copied_massage": "Copiado al portapapeles", "shared_link_clipboard_text": "Enlace: {}\nContraseña: {}", @@ -1790,7 +1792,7 @@ "trash_no_results_message": "Las fotos y videos que se envíen a la papelera aparecerán aquí.", "trash_page_delete_all": "Eliminar todos", "trash_page_empty_trash_dialog_content": "¿Está seguro que quiere eliminar los elementos? Estos elementos serán eliminados de Immich permanentemente", - "trash_page_info": "Los archivos en la papelera serán eliminados automáticamente después de {} días", + "trash_page_info": "Los archivos en la papelera serán eliminados automáticamente de forma permanente después de {} días", "trash_page_no_assets": "No hay elementos en la papelera", "trash_page_restore_all": "Restaurar todos", "trash_page_select_assets_btn": "Seleccionar elementos", @@ -1818,22 +1820,22 @@ "unstack": "Desapilar", "unstacked_assets_count": "Desapilado(s) {count, plural, one {# elemento} other {# elementos}}", "untracked_files": "Archivos no monitorizados", - "untracked_files_decription": "Estos archivos no están siendo monitorizados por la aplicación. Es posible que sean resultado de errores al moverlos, cargas interrumpidas o por un fallo de la aplicación", + "untracked_files_decription": "Estos archivos no están siendo monitorizados por la aplicación. Es posible que sean resultado de errores al moverlos, subidas interrumpidas o por un fallo de la aplicación", "up_next": "A continuación", "updated_password": "Contraseña actualizada", "upload": "Subir", - "upload_concurrency": "Cargas simultáneas", + "upload_concurrency": "Subidas simultáneas", "upload_dialog_info": "¿Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?", "upload_dialog_title": "Subir elementos", - "upload_errors": "Carga completada con {count, plural, one {# error} other {# errores}}, actualice la página para ver los nuevos recursos de carga.", + "upload_errors": "Subida completada con {count, plural, one {# error} other {# errores}}, actualice la página para ver los nuevos recursos de la subida.", "upload_progress": "Restante {remaining, number} - Procesado {processed, number}/{total, number}", "upload_skipped_duplicates": "Saltado {count, plural, one {# duplicate asset} other {# duplicate assets}}", "upload_status_duplicates": "Duplicados", "upload_status_errors": "Errores", "upload_status_uploaded": "Subido", - "upload_success": "Carga realizada correctamente, actualice la página para ver los nuevos recursos de carga.", + "upload_success": "Subida realizada correctamente, actualice la página para ver los nuevos recursos de subida.", "upload_to_immich": "Subir a Immich ({})", - "uploading": "Cargando", + "uploading": "Subiendo", "url": "URL", "usage": "Uso", "use_current_connection": "Usar conexión actual", diff --git a/i18n/hr.json b/i18n/hr.json index 229ea91c03..a30de885cd 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -915,6 +915,8 @@ "hide_unnamed_people": "Sakrij neimenovane osobe", "host": "Domaćin", "hour": "Sat", + "ignore_icloud_photos": "Ignoriraj iCloud fotografije", + "ignore_icloud_photos_description": "Fotografije pohranjene na iCloudu neće biti učitane na Immich poslužitelj", "image": "Slika", "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} snimljeno {date}", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} snimljeno s {person1} {date}", @@ -926,6 +928,10 @@ "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1} i {person2} {date}", "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1}, {person2} i {person3} {date}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1}, {person2} i {additionalCount, number} drugih {date}", + "image_saved_successfully": "Slika je spremljena", + "image_viewer_page_state_provider_download_started": "Preuzimanje započelo", + "image_viewer_page_state_provider_download_success": "Uspješno Preuzimanje", + "image_viewer_page_state_provider_share_error": "Greška pri dijeljenju", "immich_logo": "Immich Logo", "immich_web_interface": "Immich Web Sučelje", "import_from_json": "Uvoz iz JSON-a", diff --git a/i18n/id.json b/i18n/id.json index ceda55340d..1d500654dc 100644 --- a/i18n/id.json +++ b/i18n/id.json @@ -1432,6 +1432,8 @@ "recent_searches": "Pencarian terkini", "recently_added": "Recently added", "recently_added_page_title": "Baru Ditambahkan", + "recently_taken": "Diambil terkini", + "recently_taken_page_title": "Diambil Terkini", "refresh": "Segarkan", "refresh_encoded_videos": "Segarkan video terenkode", "refresh_faces": "Segarkan wajah", diff --git a/i18n/ko.json b/i18n/ko.json index 1a129eb72a..03db17c396 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -1432,6 +1432,7 @@ "recent_searches": "최근 검색", "recently_added": "최근 추가", "recently_added_page_title": "최근 추가", + "recently_taken": "최근 촬영됨", "refresh": "새로고침", "refresh_encoded_videos": "동영상 재인코딩", "refresh_faces": "얼굴 새로고침", diff --git a/i18n/nb_NO.json b/i18n/nb_NO.json index 3a70867431..0995b81cc5 100644 --- a/i18n/nb_NO.json +++ b/i18n/nb_NO.json @@ -85,7 +85,7 @@ "image_quality": "Kvalitet", "image_resolution": "Oppløsning", "image_resolution_description": "Høyere oppløsninger kan bevare flere detaljer, men det tar lengre tid å kode, har større filstørrelser og kan redusere appresponsen.", - "image_settings": "Bildeinnstilliinger", + "image_settings": "Bildeinnstillinger", "image_settings_description": "Administrer kvalitet og oppløsning på genererte bilder", "image_thumbnail_description": "Små miniatyrbilder med strippet metadata, brukt når du ser på grupper av bilder som hovedtidslinjen", "image_thumbnail_quality_description": "Miniatyrbildekvalitet fra 1-100. Høyere er bedre, men produserer større filer og kan redusere appens respons.", @@ -371,6 +371,8 @@ "admin_password": "Administrator Passord", "administration": "Administrasjon", "advanced": "Avansert", + "advanced_settings_enable_alternate_media_filter_subtitle": "Bruk denne innstillingen for å filtrere mediefiler under synkronisering basert på alternative kriterier. Bruk kun denne innstillingen dersom man opplever problemer med at applikasjonen ikke oppdager alle album.", + "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTELT] Bruk alternativ enhet album synk filter", "advanced_settings_log_level_title": "Loggnivå: {}", "advanced_settings_prefer_remote_subtitle": "Noen enheter er veldige trege til å hente mikrobilder fra enheten. Aktiver denne innstillingen for å hente de eksternt istedenfor.", "advanced_settings_prefer_remote_title": "Foretrekk eksterne bilder", @@ -378,6 +380,8 @@ "advanced_settings_proxy_headers_title": "Proxy headere", "advanced_settings_self_signed_ssl_subtitle": "Hopper over SSL sertifikatverifikasjon for server-endepunkt. Påkrevet for selvsignerte sertifikater.", "advanced_settings_self_signed_ssl_title": "Tillat selvsignerte SSL sertifikater", + "advanced_settings_sync_remote_deletions_subtitle": "Automatisk slette eller gjenopprette filer på denne enheten hvis den handlingen har blitt gjort på nettsiden", + "advanced_settings_sync_remote_deletions_title": "Synk sletting fra nettsiden [EKSPERIMENTELT]", "advanced_settings_tile_subtitle": "Avanserte brukerinnstillinger", "advanced_settings_troubleshooting_subtitle": "Aktiver ekstra funksjoner for feilsøking", "advanced_settings_troubleshooting_title": "Feilsøking", @@ -992,6 +996,7 @@ "filetype": "Filtype", "filter": "Filter", "filter_people": "Filtrer personer", + "filter_places": "Filtrer steder", "find_them_fast": "Finn dem raskt ved søking av navn", "fix_incorrect_match": "Fiks feilaktig match", "folder": "Folder", @@ -1282,6 +1287,7 @@ "onboarding_welcome_user": "Velkommen, {user}", "online": "Tilkoblet", "only_favorites": "Bare favoritter", + "open": "Åpne", "open_in_map_view": "Åpne i kartvisning", "open_in_openstreetmap": "Åpne i OpenStreetMap", "open_the_search_filters": "Åpne søkefiltrene", @@ -1426,6 +1432,8 @@ "recent_searches": "Nylige søk", "recently_added": "Nylig lagt til", "recently_added_page_title": "Nylig lagt til", + "recently_taken": "Nylig tatt", + "recently_taken_page_title": "Nylig tatt", "refresh": "Oppdater", "refresh_encoded_videos": "Oppdater kodete videoer", "refresh_faces": "Oppdater ansikter", diff --git a/i18n/sl.json b/i18n/sl.json index c6b3e153d5..01f62d204e 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -978,7 +978,7 @@ "external": "Zunanji", "external_libraries": "Zunanje knjižnice", "external_network": "Zunanje omrežje", - "external_network_sheet_info": "Ko aplikacija ni v želenem omrežju WiFi, se bo povezala s strežnikom prek prvega od spodnjih URL-jev, ki jih lahko doseže, začenši od zgoraj navzdol", + "external_network_sheet_info": "Ko aplikacija ni v želenem omrežju Wi-Fi, se bo povezala s strežnikom prek prvega od spodnjih URL-jev, ki jih lahko doseže, začenši od zgoraj navzdol", "face_unassigned": "Nedodeljen", "failed": "Ni uspelo", "failed_to_load_assets": "Sredstev ni bilo mogoče naložiti", @@ -1125,7 +1125,7 @@ "local_network": "Lokalno omrežje", "local_network_sheet_info": "Aplikacija se bo povezala s strežnikom prek tega URL-ja, ko bo uporabljala navedeno omrežje Wi-Fi", "location_permission": "Dovoljenje za lokacijo", - "location_permission_content": "Za uporabo funkcije samodejnega preklapljanja potrebuje Immich dovoljenje za natančno lokacijo, da lahko prebere ime trenutnega omrežja WiFi", + "location_permission_content": "Za uporabo funkcije samodejnega preklapljanja potrebuje Immich dovoljenje za natančno lokacijo, da lahko prebere ime trenutnega omrežja Wi-Fi", "location_picker_choose_on_map": "Izberi na zemljevidu", "location_picker_latitude_error": "Vnesi veljavno zemljepisno širino", "location_picker_latitude_hint": "Tukaj vnesi svojo zemljepisno širino", @@ -1432,6 +1432,8 @@ "recent_searches": "Nedavna iskanja", "recently_added": "Nedavno dodano", "recently_added_page_title": "Nedavno dodano", + "recently_taken": "Nedavno uporabljen", + "recently_taken_page_title": "Nedavno Uporabljen", "refresh": "Osveži", "refresh_encoded_videos": "Osveži kodirane videoposnetke", "refresh_faces": "Osveži obraze", diff --git a/i18n/sv.json b/i18n/sv.json index 22f97a1cb0..fff282f99f 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -378,6 +378,7 @@ "advanced_settings_proxy_headers_title": "Proxy-headers", "advanced_settings_self_signed_ssl_subtitle": "Hoppar över SSL-certifikatverifiering för serverändpunkten. Krävs för självsignerade certifikat.", "advanced_settings_self_signed_ssl_title": "Tillåt självsignerade SSL-certifikat", + "advanced_settings_sync_remote_deletions_title": "Synkonisera fjärradering [EXPERIMENTELL]", "advanced_settings_tile_subtitle": "Avancerade användarinställningar", "advanced_settings_troubleshooting_subtitle": "Aktivera funktioner för felsökning", "advanced_settings_troubleshooting_title": "Felsökning", @@ -992,6 +993,7 @@ "filetype": "Filtyp", "filter": "Filter", "filter_people": "Filtrera personer", + "filter_places": "Filtrera platser", "find_them_fast": "Hitta dem snabbt efter namn med sök", "fix_incorrect_match": "Fixa inkorrekt matchning", "folder": "Mapp", From bb6cdc99ab0ae63704ed7ba47721993e5865978a Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Wed, 23 Apr 2025 17:38:43 +0100 Subject: [PATCH 049/356] ci: correct permissions for building mobile during release flow (#17814) --- .github/workflows/prepare-release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 145418d72b..2704246399 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -64,6 +64,8 @@ jobs: build_mobile: uses: ./.github/workflows/build-mobile.yml needs: bump_version + permissions: + contents: read secrets: KEY_JKS: ${{ secrets.KEY_JKS }} ALIAS: ${{ secrets.ALIAS }} From f659ef4b7aeb20f2fe433abb7fc6f7b4d95bb0ea Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:44:47 +0000 Subject: [PATCH 050/356] chore: version v1.132.0 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 17 files changed, 30 insertions(+), 26 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 17d8cbab8e..ae7ad2d4c7 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.61", + "version": "2.2.62", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.61", + "version": "2.2.62", "license": "GNU Affero General Public License version 3", "dependencies": { "chokidar": "^4.0.3", @@ -54,7 +54,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.131.3", + "version": "1.132.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index 0759eb13ee..16f18f3bbf 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.61", + "version": "2.2.62", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 247d5749e9..51acc2ce87 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.132.0", + "url": "https://v1.132.0.archive.immich.app" + }, { "label": "v1.131.3", "url": "https://v1.131.3.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index cfaff74545..6a11509f66 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.131.3", + "version": "1.132.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.131.3", + "version": "1.132.0", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -44,7 +44,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.61", + "version": "2.2.62", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -93,7 +93,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.131.3", + "version": "1.132.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 8027cb420e..787a87ee13 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.131.3", + "version": "1.132.0", "description": "", "main": "index.js", "type": "module", diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 612e5084d2..40e2bd0663 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 193, - "android.injected.version.name" => "1.131.3", + "android.injected.version.code" => 194, + "android.injected.version.name" => "1.132.0", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 4853e9be43..ccd2016d2d 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.131.3" + version_number: "1.132.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index fef299b5af..c9613ed85b 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.131.3 +- API version: 1.132.0 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 4e57b0fb3b..611585ab84 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.131.3+193 +version: 1.132.0+194 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index c9ea04ac5f..72402323c9 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7656,7 +7656,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.131.3", + "version": "1.132.0", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 1fe3ea7587..42a96708cd 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.131.3", + "version": "1.132.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.131.3", + "version": "1.132.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index a3d5d9d224..9151a82815 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.131.3", + "version": "1.132.0", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 9fa219a92b..021d59859d 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.131.3 + * 1.132.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index 402dfc164d..e3f8e6ee49 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.131.3", + "version": "1.132.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich", - "version": "1.131.3", + "version": "1.132.0", "hasInstallScript": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/server/package.json b/server/package.json index 178f0fb0a0..5678f56dc1 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.131.3", + "version": "1.132.0", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index af87772105..27748faf0a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.131.3", + "version": "1.132.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.131.3", + "version": "1.132.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", @@ -82,7 +82,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.131.3", + "version": "1.132.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index 80610e661e..cea34ec985 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.131.3", + "version": "1.132.0", "license": "GNU Affero General Public License version 3", "type": "module", "scripts": { From 6ce8a1deeb4bc034121aafc64d5293ad5d2c381c Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Wed, 23 Apr 2025 17:08:29 -0400 Subject: [PATCH 051/356] fix(server): bump sharp (#17818) * bump sharp * test linking * link in prod image too * force global * keep unnecessary libraries * override sharp version * revert dockerfile changes * add node-gyp and napi * dev dependency --- server/Dockerfile | 18 +- server/package-lock.json | 1257 ++++++++++++++++++++++++++++++++++++-- server/package.json | 8 +- 3 files changed, 1232 insertions(+), 51 deletions(-) diff --git a/server/Dockerfile b/server/Dockerfile index 84037031fd..969666f18d 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -6,14 +6,14 @@ WORKDIR /usr/src/app COPY server/package.json server/package-lock.json ./ COPY server/patches ./patches RUN npm ci && \ - # exiftool-vendored.pl, sharp-linux-x64 and sharp-linux-arm64 are the only ones we need - # they're marked as optional dependencies, so we need to copy them manually after pruning - rm -rf node_modules/@img/sharp-libvips* && \ - rm -rf node_modules/@img/sharp-linuxmusl-x64 + # exiftool-vendored.pl, sharp-linux-x64 and sharp-linux-arm64 are the only ones we need + # they're marked as optional dependencies, so we need to copy them manually after pruning + rm -rf node_modules/@img/sharp-libvips* && \ + rm -rf node_modules/@img/sharp-linuxmusl-x64 ENV PATH="${PATH}:/usr/src/app/bin" \ - IMMICH_ENV=development \ - NVIDIA_DRIVER_CAPABILITIES=all \ - NVIDIA_VISIBLE_DEVICES=all + IMMICH_ENV=development \ + NVIDIA_DRIVER_CAPABILITIES=all \ + NVIDIA_VISIBLE_DEVICES=all ENTRYPOINT ["tini", "--", "/bin/sh"] @@ -47,8 +47,8 @@ FROM ghcr.io/immich-app/base-server-prod:202504081114@sha256:8353bcbdb4e6579300a WORKDIR /usr/src/app ENV NODE_ENV=production \ - NVIDIA_DRIVER_CAPABILITIES=all \ - NVIDIA_VISIBLE_DEVICES=all + NVIDIA_DRIVER_CAPABILITIES=all \ + NVIDIA_VISIBLE_DEVICES=all COPY --from=prod /usr/src/app/node_modules ./node_modules COPY --from=prod /usr/src/app/dist ./dist COPY --from=prod /usr/src/app/bin ./bin diff --git a/server/package-lock.json b/server/package-lock.json index e3f8e6ee49..677cb57a03 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -63,7 +63,7 @@ "sanitize-filename": "^1.6.3", "sanitize-html": "^2.14.0", "semver": "^7.6.2", - "sharp": "^0.33.5", + "sharp": "^0.34.0", "sirv": "^3.0.0", "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", @@ -107,7 +107,8 @@ "globals": "^16.0.0", "jsdom": "^26.1.0", "mock-fs": "^5.2.0", - "node-addon-api": "^8.3.0", + "node-addon-api": "^8.3.1", + "node-gyp": "^11.2.0", "patch-package": "^8.0.0", "pngjs": "^7.0.0", "prettier": "^3.0.2", @@ -850,6 +851,16 @@ "node": ">=18" } }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/linux-x64": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", @@ -1165,13 +1176,169 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz", + "integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz", + "integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==", "cpu": [ "x64" ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", + "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", + "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", + "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", + "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", + "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", + "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", + "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", + "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", + "cpu": [ + "arm64" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1182,9 +1349,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", + "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", "cpu": [ "x64" ], @@ -1197,10 +1364,76 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz", + "integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz", + "integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz", + "integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.1.0" + } + }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz", + "integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==", "cpu": [ "x64" ], @@ -1216,13 +1449,35 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" + "@img/sharp-libvips-linux-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz", + "integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz", + "integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==", "cpu": [ "x64" ], @@ -1238,7 +1493,64 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + "@img/sharp-libvips-linuxmusl-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz", + "integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz", + "integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz", + "integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@inquirer/checkbox": { @@ -1634,6 +1946,19 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -2361,6 +2686,70 @@ "integrity": "sha512-Hm3jIGsoUl6RLB1vzY+dZeqb+/kWPZ+h34yiWxW0dV87l8Im/eMOwpOA+a0L78U0HM04syEjXuRlCozqpwuojQ==", "license": "MIT" }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.2.tgz", + "integrity": "sha512-b9TN7q+j5/7+rGLhFAVZiKJGIASuo8tWvInGfAd8wsULjB1uNGRCj1z1WZwwPWzVQbIKWFYqc+9L7W09qwt52w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.2.tgz", + "integrity": "sha512-caR62jNDUCU+qobStO6YJ05p9E+LR0EoXh1EEmyU69cYydsAy7drMcOlUlRtQihM6K6QfvNwJuLhsHcCzNpqtA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.2.tgz", + "integrity": "sha512-fHHXBusURjBmN6VBUtu6/5s7cCeEkuGAb/ZZiGHBLVBXMBy4D5QpM8P33Or8JD1nlOjm/ZT9sEE5HouQ0F+hUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.2.tgz", + "integrity": "sha512-9CF1Pnivij7+M3G74lxr+e9h6o2YNIe7QtExWq1KUK4hsOLTBv6FJikEwCaC3NeYTflzrm69E5UfwEAbV2U9/g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@next/swc-linux-x64-gnu": { "version": "15.1.2", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.2.tgz", @@ -2393,6 +2782,38 @@ "node": ">= 10" } }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.2.tgz", + "integrity": "sha512-wvg7MlfnaociP7k8lxLX4s2iBJm4BrNiNFhVUY+Yur5yhAJHfkS8qPPeDEUH8rQiY0PX3u/P7Q/wcg6Mv6GSAA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.2.tgz", + "integrity": "sha512-D3cNA8NoT3aWISWmo7HF5Eyko/0OdOO+VagkoJuiTk7pyX3P/b+n8XA/MYvyR+xSVcbKn68B1rY9fgqjNISqzQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2428,6 +2849,60 @@ "node": ">= 8" } }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/@nuxt/opencollective": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.1.tgz", @@ -6463,6 +6938,190 @@ "node": ">=8" } }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -7735,6 +8394,16 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -7869,6 +8538,23 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -8346,6 +9032,13 @@ "node": ">=12.0.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/express": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", @@ -8520,6 +9213,21 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -9413,6 +10121,13 @@ "entities": "^4.4.0" } }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -9687,6 +10402,20 @@ "url": "https://opencollective.com/ioredis" } }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -10077,6 +10806,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true, + "license": "MIT" + }, "node_modules/jsdom": { "version": "26.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", @@ -10602,6 +11338,29 @@ "semver": "bin/semver.js" } }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/marked": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.4.tgz", @@ -10803,6 +11562,128 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/minizlib": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", @@ -11318,6 +12199,31 @@ } } }, + "node_modules/node-gyp": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.2.0.tgz", + "integrity": "sha512-T0S1zqskVUSxcsSTkAsLc7xCycrRYmtDHadDinzocrThjyQCn5kMlEBSj6H4qDbgsIOSLmmlRIeb0lZXj+UArA==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/node-gyp-build-optional-packages": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", @@ -11333,6 +12239,125 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/node-gyp/node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/node-gyp/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -11640,6 +12665,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -12379,6 +13417,16 @@ "node": ">=6" } }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -12394,6 +13442,20 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -13559,15 +14621,15 @@ "license": "MIT" }, "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz", + "integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "semver": "^7.7.1" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -13576,25 +14638,26 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" + "@img/sharp-darwin-arm64": "0.34.1", + "@img/sharp-darwin-x64": "0.34.1", + "@img/sharp-libvips-darwin-arm64": "1.1.0", + "@img/sharp-libvips-darwin-x64": "1.1.0", + "@img/sharp-libvips-linux-arm": "1.1.0", + "@img/sharp-libvips-linux-arm64": "1.1.0", + "@img/sharp-libvips-linux-ppc64": "1.1.0", + "@img/sharp-libvips-linux-s390x": "1.1.0", + "@img/sharp-libvips-linux-x64": "1.1.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", + "@img/sharp-libvips-linuxmusl-x64": "1.1.0", + "@img/sharp-linux-arm": "0.34.1", + "@img/sharp-linux-arm64": "0.34.1", + "@img/sharp-linux-s390x": "0.34.1", + "@img/sharp-linux-x64": "0.34.1", + "@img/sharp-linuxmusl-arm64": "0.34.1", + "@img/sharp-linuxmusl-x64": "0.34.1", + "@img/sharp-wasm32": "0.34.1", + "@img/sharp-win32-ia32": "0.34.1", + "@img/sharp-win32-x64": "0.34.1" } }, "node_modules/shebang-command": { @@ -13760,6 +14823,17 @@ "integrity": "sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg==", "license": "BSD-3-Clause" }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -13895,6 +14969,46 @@ "node": ">= 0.6" } }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -13976,6 +15090,13 @@ "node": ">= 10.x" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/sql-formatter": { "version": "15.5.2", "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.5.2.tgz", @@ -14046,6 +15167,19 @@ "nan": "^2.20.0" } }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -15033,6 +16167,23 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tinypool": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", @@ -15666,6 +16817,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/server/package.json b/server/package.json index 5678f56dc1..e76fab0148 100644 --- a/server/package.json +++ b/server/package.json @@ -88,7 +88,7 @@ "sanitize-filename": "^1.6.3", "sanitize-html": "^2.14.0", "semver": "^7.6.2", - "sharp": "^0.33.5", + "sharp": "^0.34.0", "sirv": "^3.0.0", "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", @@ -132,7 +132,8 @@ "globals": "^16.0.0", "jsdom": "^26.1.0", "mock-fs": "^5.2.0", - "node-addon-api": "^8.3.0", + "node-addon-api": "^8.3.1", + "node-gyp": "^11.2.0", "patch-package": "^8.0.0", "pngjs": "^7.0.0", "prettier": "^3.0.2", @@ -152,5 +153,8 @@ }, "volta": { "node": "22.14.0" + }, + "overrides": { + "sharp": "^0.34.0" } } From c167e46ec75ecfe5e9635bcaa638d75046ff1aa7 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 23 Apr 2025 16:40:59 -0500 Subject: [PATCH 052/356] chore: revert #16732 (#17819) * chore: revert #16732 * lint --- .../android/app/src/main/AndroidManifest.xml | 3 +- .../immich/BackgroundServicePlugin.kt | 206 +----------------- .../app/alextran/immich/MainActivity.kt | 8 +- mobile/lib/domain/models/store.model.dart | 1 - .../local_files_manager.interface.dart | 5 - mobile/lib/providers/websocket.provider.dart | 28 +-- .../local_files_manager.repository.dart | 23 -- mobile/lib/services/app_settings.service.dart | 1 - mobile/lib/services/sync.service.dart | 47 +--- mobile/lib/utils/local_files_manager.dart | 39 ---- .../widgets/settings/advanced_settings.dart | 37 ---- .../modules/shared/sync_service_test.dart | 5 - mobile/test/repository.mocks.dart | 6 +- mobile/test/service.mocks.dart | 3 - 14 files changed, 16 insertions(+), 396 deletions(-) delete mode 100644 mobile/lib/interfaces/local_files_manager.interface.dart delete mode 100644 mobile/lib/repositories/local_files_manager.repository.dart delete mode 100644 mobile/lib/utils/local_files_manager.dart diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 58d7f0655a..eb81dc267b 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -6,7 +6,6 @@ android:maxSdkVersion="32" /> - @@ -125,4 +124,4 @@ - + \ No newline at end of file diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt index e7f787e8d8..8520413cff 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt @@ -1,40 +1,25 @@ package app.alextran.immich -import android.content.ContentResolver -import android.content.ContentUris -import android.content.ContentValues import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.os.Environment -import android.provider.MediaStore -import android.provider.Settings import android.util.Log import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry import java.security.MessageDigest import java.io.FileInputStream import kotlinx.coroutines.* /** - * Android plugin for Dart `BackgroundService` and file trash operations + * Android plugin for Dart `BackgroundService` + * + * Receives messages/method calls from the foreground Dart side to manage + * the background service, e.g. start (enqueue), stop (cancel) */ -class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener { +class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { private var methodChannel: MethodChannel? = null - private var fileTrashChannel: MethodChannel? = null private var context: Context? = null - private var pendingResult: Result? = null - private val PERMISSION_REQUEST_CODE = 1001 - private var activityBinding: ActivityPluginBinding? = null override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { onAttachedToEngine(binding.applicationContext, binding.binaryMessenger) @@ -44,10 +29,6 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, context = ctx methodChannel = MethodChannel(messenger, "immich/foregroundChannel") methodChannel?.setMethodCallHandler(this) - - // Add file trash channel - fileTrashChannel = MethodChannel(messenger, "file_trash") - fileTrashChannel?.setMethodCallHandler(this) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { @@ -57,14 +38,11 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, private fun onDetachedFromEngine() { methodChannel?.setMethodCallHandler(null) methodChannel = null - fileTrashChannel?.setMethodCallHandler(null) - fileTrashChannel = null } - override fun onMethodCall(call: MethodCall, result: Result) { + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { val ctx = context!! when (call.method) { - // Existing BackgroundService methods "enable" -> { val args = call.arguments>()!! ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) @@ -136,180 +114,10 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, } } - // File Trash methods moved from MainActivity - "moveToTrash" -> { - val fileName = call.argument("fileName") - if (fileName != null) { - if (hasManageStoragePermission()) { - val success = moveToTrash(fileName) - result.success(success) - } else { - result.error("PERMISSION_DENIED", "Storage permission required", null) - } - } else { - result.error("INVALID_NAME", "The file name is not specified.", null) - } - } - - "restoreFromTrash" -> { - val fileName = call.argument("fileName") - if (fileName != null) { - if (hasManageStoragePermission()) { - val success = untrashImage(fileName) - result.success(success) - } else { - result.error("PERMISSION_DENIED", "Storage permission required", null) - } - } else { - result.error("INVALID_NAME", "The file name is not specified.", null) - } - } - - "requestManageStoragePermission" -> { - if (!hasManageStoragePermission()) { - requestManageStoragePermission(result) - } else { - Log.e("Manage storage permission", "Permission already granted") - result.success(true) - } - } - else -> result.notImplemented() } } - - // File Trash methods moved from MainActivity - private fun hasManageStoragePermission(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - Environment.isExternalStorageManager() - } else { - true - } - } - - private fun requestManageStoragePermission(result: Result) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - pendingResult = result // Store the result callback - val activity = activityBinding?.activity ?: return - - val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) - intent.data = Uri.parse("package:${activity.packageName}") - activity.startActivityForResult(intent, PERMISSION_REQUEST_CODE) - } else { - result.success(true) - } - } - - private fun moveToTrash(fileName: String): Boolean { - val contentResolver = context?.contentResolver ?: return false - val uri = getFileUri(fileName) - Log.e("FILE_URI", uri.toString()) - return uri?.let { moveToTrash(it) } ?: false - } - - private fun moveToTrash(contentUri: Uri): Boolean { - val contentResolver = context?.contentResolver ?: return false - return try { - val values = ContentValues().apply { - put(MediaStore.MediaColumns.IS_TRASHED, 1) // Move to trash - } - val updated = contentResolver.update(contentUri, values, null, null) - updated > 0 - } catch (e: Exception) { - Log.e("TrashError", "Error moving to trash", e) - false - } - } - - private fun getFileUri(fileName: String): Uri? { - val contentResolver = context?.contentResolver ?: return null - val contentUri = MediaStore.Files.getContentUri("external") - val projection = arrayOf(MediaStore.Images.Media._ID) - val selection = "${MediaStore.Images.Media.DISPLAY_NAME} = ?" - val selectionArgs = arrayOf(fileName) - var fileUri: Uri? = null - - contentResolver.query(contentUri, projection, selection, selectionArgs, null)?.use { cursor -> - if (cursor.moveToFirst()) { - val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) - fileUri = ContentUris.withAppendedId(contentUri, id) - } - } - return fileUri - } - - private fun untrashImage(name: String): Boolean { - val contentResolver = context?.contentResolver ?: return false - val uri = getTrashedFileUri(contentResolver, name) - Log.e("FILE_URI", uri.toString()) - return uri?.let { untrashImage(it) } ?: false - } - - private fun untrashImage(contentUri: Uri): Boolean { - val contentResolver = context?.contentResolver ?: return false - return try { - val values = ContentValues().apply { - put(MediaStore.MediaColumns.IS_TRASHED, 0) // Restore file - } - val updated = contentResolver.update(contentUri, values, null, null) - updated > 0 - } catch (e: Exception) { - Log.e("TrashError", "Error restoring file", e) - false - } - } - - private fun getTrashedFileUri(contentResolver: ContentResolver, fileName: String): Uri? { - val contentUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) - val projection = arrayOf(MediaStore.Files.FileColumns._ID) - - val queryArgs = Bundle().apply { - putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?") - putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName)) - putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY) - } - - contentResolver.query(contentUri, projection, queryArgs, null)?.use { cursor -> - if (cursor.moveToFirst()) { - val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)) - return ContentUris.withAppendedId(contentUri, id) - } - } - return null - } - - // ActivityAware implementation - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activityBinding = binding - binding.addActivityResultListener(this) - } - - override fun onDetachedFromActivityForConfigChanges() { - activityBinding?.removeActivityResultListener(this) - activityBinding = null - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - activityBinding = binding - binding.addActivityResultListener(this) - } - - override fun onDetachedFromActivity() { - activityBinding?.removeActivityResultListener(this) - activityBinding = null - } - - // ActivityResultListener implementation - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - if (requestCode == PERMISSION_REQUEST_CODE) { - val granted = hasManageStoragePermission() - pendingResult?.success(granted) - pendingResult = null - return true - } - return false - } } private const val TAG = "BackgroundServicePlugin" -private const val BUFFER_SIZE = 2 * 1024 * 1024 +private const val BUFFER_SIZE = 2 * 1024 * 1024; diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt index 2b6bf81148..4ffb490c77 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt @@ -2,12 +2,14 @@ package app.alextran.immich import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine -import androidx.annotation.NonNull +import android.os.Bundle +import android.content.Intent class MainActivity : FlutterActivity() { - override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) flutterEngine.plugins.add(BackgroundServicePlugin()) - // No need to set up method channel here as it's now handled in the plugin } + } diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index 8a5a908e0d..e6d9ecaf48 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -65,7 +65,6 @@ enum StoreKey { // Video settings loadOriginalVideo._(136), - manageLocalMediaAndroid._(137), // Experimental stuff photoManagerCustomFilter._(1000); diff --git a/mobile/lib/interfaces/local_files_manager.interface.dart b/mobile/lib/interfaces/local_files_manager.interface.dart deleted file mode 100644 index c8b83a7c93..0000000000 --- a/mobile/lib/interfaces/local_files_manager.interface.dart +++ /dev/null @@ -1,5 +0,0 @@ -abstract interface class ILocalFilesManager { - Future moveToTrash(String fileName); - Future restoreFromTrash(String fileName); - Future requestManageStoragePermission(); -} diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index 72dbda8b6f..f92d2c8421 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -23,7 +23,6 @@ enum PendingAction { assetDelete, assetUploaded, assetHidden, - assetTrash, } class PendingChange { @@ -161,7 +160,7 @@ class WebsocketNotifier extends StateNotifier { socket.on('on_upload_success', _handleOnUploadSuccess); socket.on('on_config_update', _handleOnConfigUpdate); socket.on('on_asset_delete', _handleOnAssetDelete); - socket.on('on_asset_trash', _handleOnAssetTrash); + socket.on('on_asset_trash', _handleServerUpdates); socket.on('on_asset_restore', _handleServerUpdates); socket.on('on_asset_update', _handleServerUpdates); socket.on('on_asset_stack_update', _handleServerUpdates); @@ -208,26 +207,6 @@ class WebsocketNotifier extends StateNotifier { _debounce.run(handlePendingChanges); } - Future _handlePendingTrashes() async { - final trashChanges = state.pendingChanges - .where((c) => c.action == PendingAction.assetTrash) - .toList(); - if (trashChanges.isNotEmpty) { - List remoteIds = trashChanges - .expand((a) => (a.value as List).map((e) => e.toString())) - .toList(); - - await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); - await _ref.read(assetProvider.notifier).getAllAsset(); - - state = state.copyWith( - pendingChanges: state.pendingChanges - .whereNot((c) => trashChanges.contains(c)) - .toList(), - ); - } - } - Future _handlePendingDeletes() async { final deleteChanges = state.pendingChanges .where((c) => c.action == PendingAction.assetDelete) @@ -288,7 +267,6 @@ class WebsocketNotifier extends StateNotifier { await _handlePendingUploaded(); await _handlePendingDeletes(); await _handlingPendingHidden(); - await _handlePendingTrashes(); } void _handleOnConfigUpdate(dynamic _) { @@ -307,10 +285,6 @@ class WebsocketNotifier extends StateNotifier { void _handleOnAssetDelete(dynamic data) => addPendingChange(PendingAction.assetDelete, data); - void _handleOnAssetTrash(dynamic data) { - addPendingChange(PendingAction.assetTrash, data); - } - void _handleOnAssetHidden(dynamic data) => addPendingChange(PendingAction.assetHidden, data); diff --git a/mobile/lib/repositories/local_files_manager.repository.dart b/mobile/lib/repositories/local_files_manager.repository.dart deleted file mode 100644 index 522d7e7a05..0000000000 --- a/mobile/lib/repositories/local_files_manager.repository.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/interfaces/local_files_manager.interface.dart'; -import 'package:immich_mobile/utils/local_files_manager.dart'; - -final localFilesManagerRepositoryProvider = - Provider((ref) => LocalFilesManagerRepository()); - -class LocalFilesManagerRepository implements ILocalFilesManager { - @override - Future moveToTrash(String fileName) async { - return await LocalFilesManager.moveToTrash(fileName); - } - - @override - Future restoreFromTrash(String fileName) async { - return await LocalFilesManager.restoreFromTrash(fileName); - } - - @override - Future requestManageStoragePermission() async { - return await LocalFilesManager.requestManageStoragePermission(); - } -} diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 6413b69fce..cc57b8d3a3 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -61,7 +61,6 @@ enum AppSettingsEnum { 0, ), advancedTroubleshooting(StoreKey.advancedTroubleshooting, null, false), - manageLocalMediaAndroid(StoreKey.manageLocalMediaAndroid, null, false), logLevel(StoreKey.logLevel, null, 5), // Level.INFO = 5 preferRemoteImage(StoreKey.preferRemoteImage, null, false), loopVideo(StoreKey.loopVideo, "loopVideo", true), diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart index 547e49c1a0..11a9dcb56a 100644 --- a/mobile/lib/services/sync.service.dart +++ b/mobile/lib/services/sync.service.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -17,10 +16,8 @@ import 'package:immich_mobile/interfaces/album_api.interface.dart'; import 'package:immich_mobile/interfaces/album_media.interface.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/etag.interface.dart'; -import 'package:immich_mobile/interfaces/local_files_manager.interface.dart'; import 'package:immich_mobile/interfaces/partner.interface.dart'; import 'package:immich_mobile/interfaces/partner_api.interface.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/album.repository.dart'; @@ -28,10 +25,8 @@ import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/album_media.repository.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/etag.repository.dart'; -import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/repositories/partner.repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/entity.service.dart'; import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/utils/async_mutex.dart'; @@ -53,8 +48,6 @@ final syncServiceProvider = Provider( ref.watch(userRepositoryProvider), ref.watch(userServiceProvider), ref.watch(etagRepositoryProvider), - ref.watch(appSettingsServiceProvider), - ref.watch(localFilesManagerRepositoryProvider), ref.watch(partnerApiRepositoryProvider), ref.watch(userApiRepositoryProvider), ), @@ -76,8 +69,6 @@ class SyncService { final IUserApiRepository _userApiRepository; final AsyncMutex _lock = AsyncMutex(); final Logger _log = Logger('SyncService'); - final AppSettingsService _appSettingsService; - final ILocalFilesManager _localFilesManager; SyncService( this._hashService, @@ -91,8 +82,6 @@ class SyncService { this._userRepository, this._userService, this._eTagRepository, - this._appSettingsService, - this._localFilesManager, this._partnerApiRepository, this._userApiRepository, ); @@ -249,19 +238,8 @@ class SyncService { return null; } - Future _moveToTrashMatchedAssets(Iterable idsToDelete) async { - final List localAssets = await _assetRepository.getAllLocal(); - final List matchedAssets = localAssets - .where((asset) => idsToDelete.contains(asset.remoteId)) - .toList(); - - for (var asset in matchedAssets) { - _localFilesManager.moveToTrash(asset.fileName); - } - } - /// Deletes remote-only assets, updates merged assets to be local-only - Future handleRemoteAssetRemoval(List idsToDelete) async { + Future handleRemoteAssetRemoval(List idsToDelete) { return _assetRepository.transaction(() async { await _assetRepository.deleteAllByRemoteId( idsToDelete, @@ -271,12 +249,6 @@ class SyncService { idsToDelete, state: AssetState.merged, ); - if (Platform.isAndroid && - _appSettingsService.getSetting( - AppSettingsEnum.manageLocalMediaAndroid, - )) { - await _moveToTrashMatchedAssets(idsToDelete); - } if (merged.isEmpty) return; for (final Asset asset in merged) { asset.remoteId = null; @@ -818,27 +790,10 @@ class SyncService { return (existing, toUpsert); } - Future _toggleTrashStatusForAssets(List assetsList) async { - for (var asset in assetsList) { - if (asset.isTrashed) { - _localFilesManager.moveToTrash(asset.fileName); - } else { - _localFilesManager.restoreFromTrash(asset.fileName); - } - } - } - /// Inserts or updates the assets in the database with their ExifInfo (if any) Future upsertAssetsWithExif(List assets) async { if (assets.isEmpty) return; - if (Platform.isAndroid && - _appSettingsService.getSetting( - AppSettingsEnum.manageLocalMediaAndroid, - )) { - _toggleTrashStatusForAssets(assets); - } - try { await _assetRepository.transaction(() async { await _assetRepository.updateAll(assets); diff --git a/mobile/lib/utils/local_files_manager.dart b/mobile/lib/utils/local_files_manager.dart deleted file mode 100644 index da9308c3cf..0000000000 --- a/mobile/lib/utils/local_files_manager.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -class LocalFilesManager { - static const MethodChannel _channel = MethodChannel('file_trash'); - - static Future moveToTrash(String fileName) async { - try { - final bool success = - await _channel.invokeMethod('moveToTrash', {'fileName': fileName}); - return success; - } on PlatformException catch (e) { - debugPrint('Error moving to trash: ${e.message}'); - return false; - } - } - - static Future restoreFromTrash(String fileName) async { - try { - final bool success = await _channel - .invokeMethod('restoreFromTrash', {'fileName': fileName}); - return success; - } on PlatformException catch (e) { - debugPrint('Error restoring file: ${e.message}'); - return false; - } - } - - static Future requestManageStoragePermission() async { - try { - final bool success = - await _channel.invokeMethod('requestManageStoragePermission'); - return success; - } on PlatformException catch (e) { - debugPrint('Error requesting permission: ${e.message}'); - return false; - } - } -} diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index 98c8728298..a2e0e5b95c 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -1,13 +1,11 @@ import 'dart:io'; -import 'package:device_info_plus/device_info_plus.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; @@ -27,8 +25,6 @@ class AdvancedSettings extends HookConsumerWidget { final advancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); - final manageLocalMediaAndroid = - useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid); final levelId = useAppSettingsState(AppSettingsEnum.logLevel); final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage); final allowSelfSignedSSLCert = @@ -44,16 +40,6 @@ class AdvancedSettings extends HookConsumerWidget { LogService.I.setlogLevel(Level.LEVELS[levelId.value].toLogLevel()), ); - Future checkAndroidVersion() async { - if (Platform.isAndroid) { - DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); - AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; - int sdkVersion = androidInfo.version.sdkInt; - return sdkVersion >= 30; - } - return false; - } - final advancedSettings = [ SettingsSwitchListTile( enabled: true, @@ -61,29 +47,6 @@ class AdvancedSettings extends HookConsumerWidget { title: "advanced_settings_troubleshooting_title".tr(), subtitle: "advanced_settings_troubleshooting_subtitle".tr(), ), - FutureBuilder( - future: checkAndroidVersion(), - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data == true) { - return SettingsSwitchListTile( - enabled: true, - valueNotifier: manageLocalMediaAndroid, - title: "advanced_settings_sync_remote_deletions_title".tr(), - subtitle: "advanced_settings_sync_remote_deletions_subtitle".tr(), - onChanged: (value) async { - if (value) { - final result = await ref - .read(localFilesManagerRepositoryProvider) - .requestManageStoragePermission(); - manageLocalMediaAndroid.value = result; - } - }, - ); - } else { - return const SizedBox.shrink(); - } - }, - ), SettingsSliderListTile( text: "advanced_settings_log_level_title".tr(args: [logLevel]), valueNotifier: levelId, diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart index 2029ade018..3879e64237 100644 --- a/mobile/test/modules/shared/sync_service_test.dart +++ b/mobile/test/modules/shared/sync_service_test.dart @@ -60,9 +60,6 @@ void main() { final MockAlbumMediaRepository albumMediaRepository = MockAlbumMediaRepository(); final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository(); - final MockAppSettingService appSettingService = MockAppSettingService(); - final MockLocalFilesManagerRepository localFilesManagerRepository = - MockLocalFilesManagerRepository(); final MockPartnerApiRepository partnerApiRepository = MockPartnerApiRepository(); final MockUserApiRepository userApiRepository = MockUserApiRepository(); @@ -109,8 +106,6 @@ void main() { userRepository, userService, eTagRepository, - appSettingService, - localFilesManagerRepository, partnerApiRepository, userApiRepository, ); diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart index d2f0da4231..1c698297dc 100644 --- a/mobile/test/repository.mocks.dart +++ b/mobile/test/repository.mocks.dart @@ -10,7 +10,6 @@ import 'package:immich_mobile/interfaces/auth_api.interface.dart'; import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/interfaces/etag.interface.dart'; import 'package:immich_mobile/interfaces/file_media.interface.dart'; -import 'package:immich_mobile/interfaces/local_files_manager.interface.dart'; import 'package:immich_mobile/interfaces/partner.interface.dart'; import 'package:immich_mobile/interfaces/partner_api.interface.dart'; import 'package:mocktail/mocktail.dart'; @@ -42,9 +41,6 @@ class MockAuthApiRepository extends Mock implements IAuthApiRepository {} class MockAuthRepository extends Mock implements IAuthRepository {} -class MockPartnerRepository extends Mock implements IPartnerRepository {} - class MockPartnerApiRepository extends Mock implements IPartnerApiRepository {} -class MockLocalFilesManagerRepository extends Mock - implements ILocalFilesManager {} +class MockPartnerRepository extends Mock implements IPartnerRepository {} diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index 87a8c01cf0..d31a7e5d50 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -1,6 +1,5 @@ import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/entity.service.dart'; @@ -26,6 +25,4 @@ class MockNetworkService extends Mock implements NetworkService {} class MockSearchApi extends Mock implements SearchApi {} -class MockAppSettingService extends Mock implements AppSettingsService {} - class MockBackgroundService extends Mock implements BackgroundService {} From 57d622bc43b3bdd0f08c0a31c52e267accb2e0d4 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 23 Apr 2025 16:41:08 -0500 Subject: [PATCH 053/356] chore: post release tasks (#17816) --- mobile/ios/Runner.xcodeproj/project.pbxproj | 12 ++++++------ mobile/ios/Runner/Info.plist | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 69f122cf17..e4c25fefdf 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -541,7 +541,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 201; + CURRENT_PROJECT_VERSION = 202; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -685,7 +685,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 201; + CURRENT_PROJECT_VERSION = 202; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -715,7 +715,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 201; + CURRENT_PROJECT_VERSION = 202; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -748,7 +748,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 201; + CURRENT_PROJECT_VERSION = 202; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -791,7 +791,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 201; + CURRENT_PROJECT_VERSION = 202; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -831,7 +831,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 201; + CURRENT_PROJECT_VERSION = 202; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index bad1ea42f2..02fef7a965 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -78,7 +78,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.131.3 + 1.132.0 CFBundleSignature ???? CFBundleURLTypes @@ -93,7 +93,7 @@ CFBundleVersion - 201 + 202 FLTEnableImpeller ITSAppUsesNonExemptEncryption From 37f5e6e2cb4a81b035d49fea54b6b9385b4e4197 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 21:43:47 +0000 Subject: [PATCH 054/356] chore: version v1.132.1 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 17 files changed, 30 insertions(+), 26 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index ae7ad2d4c7..d15358b26e 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.62", + "version": "2.2.63", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.62", + "version": "2.2.63", "license": "GNU Affero General Public License version 3", "dependencies": { "chokidar": "^4.0.3", @@ -54,7 +54,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.132.0", + "version": "1.132.1", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index 16f18f3bbf..8a742cd0d7 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.62", + "version": "2.2.63", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 51acc2ce87..26eb3a2f9a 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.132.1", + "url": "https://v1.132.1.archive.immich.app" + }, { "label": "v1.132.0", "url": "https://v1.132.0.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 6a11509f66..7eb831b897 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.132.0", + "version": "1.132.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.132.0", + "version": "1.132.1", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -44,7 +44,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.62", + "version": "2.2.63", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -93,7 +93,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.132.0", + "version": "1.132.1", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 787a87ee13..3946f149d6 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.132.0", + "version": "1.132.1", "description": "", "main": "index.js", "type": "module", diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 40e2bd0663..13f3b0b850 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 194, - "android.injected.version.name" => "1.132.0", + "android.injected.version.code" => 195, + "android.injected.version.name" => "1.132.1", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index ccd2016d2d..f454d24973 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.132.0" + version_number: "1.132.1" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index c9613ed85b..073ae932ce 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.132.0 +- API version: 1.132.1 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 611585ab84..07f56fb341 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.132.0+194 +version: 1.132.1+195 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 72402323c9..53709f3f0c 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7656,7 +7656,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.132.0", + "version": "1.132.1", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 42a96708cd..fe398ed2bb 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.132.0", + "version": "1.132.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.132.0", + "version": "1.132.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 9151a82815..4afce16f23 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.132.0", + "version": "1.132.1", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 021d59859d..01f476517e 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.132.0 + * 1.132.1 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index 677cb57a03..cde8bd3a62 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.132.0", + "version": "1.132.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich", - "version": "1.132.0", + "version": "1.132.1", "hasInstallScript": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/server/package.json b/server/package.json index e76fab0148..f4435ced68 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.132.0", + "version": "1.132.1", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index 27748faf0a..91d0adb573 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.132.0", + "version": "1.132.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.132.0", + "version": "1.132.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", @@ -82,7 +82,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.132.0", + "version": "1.132.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index cea34ec985..ec53fd69d5 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.132.0", + "version": "1.132.1", "license": "GNU Affero General Public License version 3", "type": "module", "scripts": { From dab4870fed64538684958903c37a92b2771a7647 Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Wed, 23 Apr 2025 23:30:13 -0400 Subject: [PATCH 055/356] fix: flappy e2e test (#17832) * fix: flappy e2e test * lint --- e2e/src/api/specs/oauth.e2e-spec.ts | 2 +- e2e/src/web/specs/shared-link.e2e-spec.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/e2e/src/api/specs/oauth.e2e-spec.ts b/e2e/src/api/specs/oauth.e2e-spec.ts index 3b1e75d3e5..9e4d64892e 100644 --- a/e2e/src/api/specs/oauth.e2e-spec.ts +++ b/e2e/src/api/specs/oauth.e2e-spec.ts @@ -142,7 +142,7 @@ describe(`/oauth`, () => { it(`should throw an error if the state mismatches`, async () => { const callbackParams = await loginWithOAuth('oauth-auto-register'); const { state } = await loginWithOAuth('oauth-auto-register'); - const { status, body } = await request(app) + const { status } = await request(app) .post('/oauth/callback') .send({ ...callbackParams, state }); expect(status).toBeGreaterThanOrEqual(400); diff --git a/e2e/src/web/specs/shared-link.e2e-spec.ts b/e2e/src/web/specs/shared-link.e2e-spec.ts index 562a0b4e8c..aeddb86322 100644 --- a/e2e/src/web/specs/shared-link.e2e-spec.ts +++ b/e2e/src/web/specs/shared-link.e2e-spec.ts @@ -55,7 +55,6 @@ test.describe('Shared Links', () => { await page.goto(`/share/${sharedLink.key}`); await page.getByRole('heading', { name: 'Test Album' }).waitFor(); await page.getByRole('button', { name: 'Download' }).click(); - await page.getByText('DOWNLOADING', { exact: true }).waitFor(); await page.waitForEvent('download'); }); From 1d610ad9cb45e02929d9ddc850c0184b510ecaf4 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 24 Apr 2025 12:58:29 -0400 Subject: [PATCH 056/356] refactor: database connection parsing (#17852) --- server/src/app.module.ts | 2 +- server/src/bin/migrations.ts | 6 +- server/src/bin/sync-sql.ts | 2 +- .../repositories/config.repository.spec.ts | 106 ++---------------- server/src/repositories/config.repository.ts | 61 +++------- .../src/repositories/database.repository.ts | 25 ++++- server/src/services/backup.service.ts | 2 +- server/src/services/database.service.spec.ts | 66 +++-------- server/src/utils/database.spec.ts | 83 ++++++++++++++ server/src/utils/database.ts | 62 +++++++--- server/test/medium/globalSetup.ts | 12 +- .../repositories/config.repository.mock.ts | 19 +--- server/test/utils.ts | 28 ++--- 13 files changed, 217 insertions(+), 257 deletions(-) create mode 100644 server/src/utils/database.spec.ts diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 05dbc090fc..153b525fe5 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -44,7 +44,7 @@ const imports = [ BullModule.registerQueue(...bull.queues), ClsModule.forRoot(cls.config), OpenTelemetryModule.forRoot(otel), - KyselyModule.forRoot(getKyselyConfig(database.config.kysely)), + KyselyModule.forRoot(getKyselyConfig(database.config)), ]; class BaseModule implements OnModuleInit, OnModuleDestroy { diff --git a/server/src/bin/migrations.ts b/server/src/bin/migrations.ts index 2ddc6776fb..7b850f6166 100644 --- a/server/src/bin/migrations.ts +++ b/server/src/bin/migrations.ts @@ -10,7 +10,7 @@ import { DatabaseRepository } from 'src/repositories/database.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import 'src/schema'; import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools'; -import { getKyselyConfig } from 'src/utils/database'; +import { asPostgresConnectionConfig, getKyselyConfig } from 'src/utils/database'; const main = async () => { const command = process.argv[2]; @@ -56,7 +56,7 @@ const main = async () => { const getDatabaseClient = () => { const configRepository = new ConfigRepository(); const { database } = configRepository.getEnv(); - return new Kysely(getKyselyConfig(database.config.kysely)); + return new Kysely(getKyselyConfig(database.config)); }; const runQuery = async (query: string) => { @@ -105,7 +105,7 @@ const create = (path: string, up: string[], down: string[]) => { const compare = async () => { const configRepository = new ConfigRepository(); const { database } = configRepository.getEnv(); - const db = postgres(database.config.kysely); + const db = postgres(asPostgresConnectionConfig(database.config)); const source = schemaFromCode(); const target = await schemaFromDatabase(db, {}); diff --git a/server/src/bin/sync-sql.ts b/server/src/bin/sync-sql.ts index 47e6610a74..b791358a90 100644 --- a/server/src/bin/sync-sql.ts +++ b/server/src/bin/sync-sql.ts @@ -78,7 +78,7 @@ class SqlGenerator { const moduleFixture = await Test.createTestingModule({ imports: [ KyselyModule.forRoot({ - ...getKyselyConfig(database.config.kysely), + ...getKyselyConfig(database.config), log: (event) => { if (event.level === 'query') { this.sqlLogger.logQuery(event.query.sql); diff --git a/server/src/repositories/config.repository.spec.ts b/server/src/repositories/config.repository.spec.ts index 888d5c33ec..9e9ed71191 100644 --- a/server/src/repositories/config.repository.spec.ts +++ b/server/src/repositories/config.repository.spec.ts @@ -80,21 +80,12 @@ describe('getEnv', () => { const { database } = getEnv(); expect(database).toEqual({ config: { - kysely: expect.objectContaining({ - host: 'database', - port: 5432, - database: 'immich', - username: 'postgres', - password: 'postgres', - }), - typeorm: expect.objectContaining({ - type: 'postgres', - host: 'database', - port: 5432, - database: 'immich', - username: 'postgres', - password: 'postgres', - }), + connectionType: 'parts', + host: 'database', + port: 5432, + database: 'immich', + username: 'postgres', + password: 'postgres', }, skipMigrations: false, vectorExtension: 'vectors', @@ -110,88 +101,9 @@ describe('getEnv', () => { it('should use DB_URL', () => { process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich'; const { database } = getEnv(); - expect(database.config.kysely).toMatchObject({ - host: 'database1', - password: 'postgres2', - user: 'postgres1', - port: 54_320, - database: 'immich', - }); - }); - - it('should handle sslmode=require', () => { - process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=require'; - - const { database } = getEnv(); - - expect(database.config.kysely).toMatchObject({ ssl: {} }); - }); - - it('should handle sslmode=prefer', () => { - process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=prefer'; - - const { database } = getEnv(); - - expect(database.config.kysely).toMatchObject({ ssl: {} }); - }); - - it('should handle sslmode=verify-ca', () => { - process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-ca'; - - const { database } = getEnv(); - - expect(database.config.kysely).toMatchObject({ ssl: {} }); - }); - - it('should handle sslmode=verify-full', () => { - process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-full'; - - const { database } = getEnv(); - - expect(database.config.kysely).toMatchObject({ ssl: {} }); - }); - - it('should handle sslmode=no-verify', () => { - process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=no-verify'; - - const { database } = getEnv(); - - expect(database.config.kysely).toMatchObject({ ssl: { rejectUnauthorized: false } }); - }); - - it('should handle ssl=true', () => { - process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?ssl=true'; - - const { database } = getEnv(); - - expect(database.config.kysely).toMatchObject({ ssl: true }); - }); - - it('should reject invalid ssl', () => { - process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?ssl=invalid'; - - expect(() => getEnv()).toThrowError('Invalid ssl option: invalid'); - }); - - it('should handle socket: URLs', () => { - process.env.DB_URL = 'socket:/run/postgresql?db=database1'; - - const { database } = getEnv(); - - expect(database.config.kysely).toMatchObject({ - host: '/run/postgresql', - database: 'database1', - }); - }); - - it('should handle sockets in postgres: URLs', () => { - process.env.DB_URL = 'postgres:///database2?host=/path/to/socket'; - - const { database } = getEnv(); - - expect(database.config.kysely).toMatchObject({ - host: '/path/to/socket', - database: 'database2', + expect(database.config).toMatchObject({ + connectionType: 'url', + url: 'postgres://postgres1:postgres2@database1:54320/immich', }); }); }); diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index f689641d4f..9b88a78e6b 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -7,8 +7,7 @@ import { Request, Response } from 'express'; import { RedisOptions } from 'ioredis'; import { CLS_ID, ClsModuleOptions } from 'nestjs-cls'; import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces'; -import { join, resolve } from 'node:path'; -import { parse } from 'pg-connection-string'; +import { join } from 'node:path'; import { citiesFile, excludePaths, IWorker } from 'src/constants'; import { Telemetry } from 'src/decorators'; import { EnvDto } from 'src/dtos/env.dto'; @@ -22,9 +21,7 @@ import { QueueName, } from 'src/enum'; import { DatabaseConnectionParams, VectorExtension } from 'src/types'; -import { isValidSsl, PostgresConnectionConfig } from 'src/utils/database'; import { setDifference } from 'src/utils/set'; -import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js'; export interface EnvData { host?: string; @@ -59,7 +56,7 @@ export interface EnvData { }; database: { - config: { typeorm: PostgresConnectionOptions & DatabaseConnectionParams; kysely: PostgresConnectionConfig }; + config: DatabaseConnectionParams; skipMigrations: boolean; vectorExtension: VectorExtension; }; @@ -152,14 +149,10 @@ const getEnv = (): EnvData => { const isProd = environment === ImmichEnvironment.PRODUCTION; const buildFolder = dto.IMMICH_BUILD_DATA || '/build'; const folders = { - // eslint-disable-next-line unicorn/prefer-module - dist: resolve(`${__dirname}/..`), geodata: join(buildFolder, 'geodata'), web: join(buildFolder, 'www'), }; - const databaseUrl = dto.DB_URL; - let redisConfig = { host: dto.REDIS_HOSTNAME || 'redis', port: dto.REDIS_PORT || 6379, @@ -191,30 +184,16 @@ const getEnv = (): EnvData => { } } - const parts = { - connectionType: 'parts', - host: dto.DB_HOSTNAME || 'database', - port: dto.DB_PORT || 5432, - username: dto.DB_USERNAME || 'postgres', - password: dto.DB_PASSWORD || 'postgres', - database: dto.DB_DATABASE_NAME || 'immich', - } as const; - - let parsedOptions: PostgresConnectionConfig = parts; - if (dto.DB_URL) { - const parsed = parse(dto.DB_URL); - if (!isValidSsl(parsed.ssl)) { - throw new Error(`Invalid ssl option: ${parsed.ssl}`); - } - - parsedOptions = { - ...parsed, - ssl: parsed.ssl, - host: parsed.host ?? undefined, - port: parsed.port ? Number(parsed.port) : undefined, - database: parsed.database ?? undefined, - }; - } + const databaseConnection: DatabaseConnectionParams = dto.DB_URL + ? { connectionType: 'url', url: dto.DB_URL } + : { + connectionType: 'parts', + host: dto.DB_HOSTNAME || 'database', + port: dto.DB_PORT || 5432, + username: dto.DB_USERNAME || 'postgres', + password: dto.DB_PASSWORD || 'postgres', + database: dto.DB_DATABASE_NAME || 'immich', + }; return { host: dto.IMMICH_HOST, @@ -269,21 +248,7 @@ const getEnv = (): EnvData => { }, database: { - config: { - typeorm: { - type: 'postgres', - entities: [], - migrations: [`${folders.dist}/migrations` + '/*.{js,ts}'], - subscribers: [], - migrationsRun: false, - synchronize: false, - connectTimeoutMS: 10_000, // 10 seconds - parseInt8: true, - ...(databaseUrl ? { connectionType: 'url', url: databaseUrl } : parts), - }, - kysely: parsedOptions, - }, - + config: databaseConnection, skipMigrations: dto.DB_SKIP_MIGRATIONS ?? false, vectorExtension: dto.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS, }, diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index c70c2cbdd4..a402c9d28d 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -3,7 +3,7 @@ import AsyncLock from 'async-lock'; import { FileMigrationProvider, Kysely, Migrator, sql, Transaction } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { readdir } from 'node:fs/promises'; -import { join } from 'node:path'; +import { join, resolve } from 'node:path'; import semver from 'semver'; import { EXTENSION_NAMES, POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants'; import { DB } from 'src/db'; @@ -205,8 +205,29 @@ export class DatabaseRepository { const { rows } = await tableExists.execute(this.db); const hasTypeOrmMigrations = !!rows[0]?.result; if (hasTypeOrmMigrations) { + // eslint-disable-next-line unicorn/prefer-module + const dist = resolve(`${__dirname}/..`); + this.logger.debug('Running typeorm migrations'); - const dataSource = new DataSource(database.config.typeorm); + const dataSource = new DataSource({ + type: 'postgres', + entities: [], + subscribers: [], + migrations: [`${dist}/migrations` + '/*.{js,ts}'], + migrationsRun: false, + synchronize: false, + connectTimeoutMS: 10_000, // 10 seconds + parseInt8: true, + ...(database.config.connectionType === 'url' + ? { url: database.config.url } + : { + host: database.config.host, + port: database.config.port, + username: database.config.username, + password: database.config.password, + database: database.config.database, + }), + }); await dataSource.initialize(); await dataSource.runMigrations(options); await dataSource.destroy(); diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts index dc4f71b992..409d34ab73 100644 --- a/server/src/services/backup.service.ts +++ b/server/src/services/backup.service.ts @@ -70,7 +70,7 @@ export class BackupService extends BaseService { async handleBackupDatabase(): Promise { this.logger.debug(`Database Backup Started`); const { database } = this.configRepository.getEnv(); - const config = database.config.typeorm; + const config = database.config; const isUrlConnection = config.connectionType === 'url'; diff --git a/server/src/services/database.service.spec.ts b/server/src/services/database.service.spec.ts index 4e45ec3ae0..e0ab4a624d 100644 --- a/server/src/services/database.service.spec.ts +++ b/server/src/services/database.service.spec.ts @@ -53,22 +53,12 @@ describe(DatabaseService.name, () => { mockEnvData({ database: { config: { - kysely: { - host: 'database', - port: 5432, - user: 'postgres', - password: 'postgres', - database: 'immich', - }, - typeorm: { - connectionType: 'parts', - type: 'postgres', - host: 'database', - port: 5432, - username: 'postgres', - password: 'postgres', - database: 'immich', - }, + connectionType: 'parts', + host: 'database', + port: 5432, + username: 'postgres', + password: 'postgres', + database: 'immich', }, skipMigrations: false, vectorExtension: extension, @@ -292,22 +282,12 @@ describe(DatabaseService.name, () => { mockEnvData({ database: { config: { - kysely: { - host: 'database', - port: 5432, - user: 'postgres', - password: 'postgres', - database: 'immich', - }, - typeorm: { - connectionType: 'parts', - type: 'postgres', - host: 'database', - port: 5432, - username: 'postgres', - password: 'postgres', - database: 'immich', - }, + connectionType: 'parts', + host: 'database', + port: 5432, + username: 'postgres', + password: 'postgres', + database: 'immich', }, skipMigrations: true, vectorExtension: DatabaseExtension.VECTORS, @@ -325,22 +305,12 @@ describe(DatabaseService.name, () => { mockEnvData({ database: { config: { - kysely: { - host: 'database', - port: 5432, - user: 'postgres', - password: 'postgres', - database: 'immich', - }, - typeorm: { - connectionType: 'parts', - type: 'postgres', - host: 'database', - port: 5432, - username: 'postgres', - password: 'postgres', - database: 'immich', - }, + connectionType: 'parts', + host: 'database', + port: 5432, + username: 'postgres', + password: 'postgres', + database: 'immich', }, skipMigrations: true, vectorExtension: DatabaseExtension.VECTOR, diff --git a/server/src/utils/database.spec.ts b/server/src/utils/database.spec.ts new file mode 100644 index 0000000000..4c6a82ad8f --- /dev/null +++ b/server/src/utils/database.spec.ts @@ -0,0 +1,83 @@ +import { asPostgresConnectionConfig } from 'src/utils/database'; + +describe('database utils', () => { + describe('asPostgresConnectionConfig', () => { + it('should handle sslmode=require', () => { + expect( + asPostgresConnectionConfig({ + connectionType: 'url', + url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=require', + }), + ).toMatchObject({ ssl: {} }); + }); + + it('should handle sslmode=prefer', () => { + expect( + asPostgresConnectionConfig({ + connectionType: 'url', + url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=prefer', + }), + ).toMatchObject({ ssl: {} }); + }); + + it('should handle sslmode=verify-ca', () => { + expect( + asPostgresConnectionConfig({ + connectionType: 'url', + url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-ca', + }), + ).toMatchObject({ ssl: {} }); + }); + + it('should handle sslmode=verify-full', () => { + expect( + asPostgresConnectionConfig({ + connectionType: 'url', + url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-full', + }), + ).toMatchObject({ ssl: {} }); + }); + + it('should handle sslmode=no-verify', () => { + expect( + asPostgresConnectionConfig({ + connectionType: 'url', + url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=no-verify', + }), + ).toMatchObject({ ssl: { rejectUnauthorized: false } }); + }); + + it('should handle ssl=true', () => { + expect( + asPostgresConnectionConfig({ + connectionType: 'url', + url: 'postgres://postgres1:postgres2@database1:54320/immich?ssl=true', + }), + ).toMatchObject({ ssl: true }); + }); + + it('should reject invalid ssl', () => { + expect(() => + asPostgresConnectionConfig({ + connectionType: 'url', + url: 'postgres://postgres1:postgres2@database1:54320/immich?ssl=invalid', + }), + ).toThrowError('Invalid ssl option'); + }); + + it('should handle socket: URLs', () => { + expect( + asPostgresConnectionConfig({ connectionType: 'url', url: 'socket:/run/postgresql?db=database1' }), + ).toMatchObject({ host: '/run/postgresql', database: 'database1' }); + }); + + it('should handle sockets in postgres: URLs', () => { + expect( + asPostgresConnectionConfig({ connectionType: 'url', url: 'postgres:///database2?host=/path/to/socket' }), + ).toMatchObject({ + host: '/path/to/socket', + database: 'database2', + }); + }); + }); +}); diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 1af0aa4b4e..8f0b56597a 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -13,33 +13,57 @@ import { } from 'kysely'; import { PostgresJSDialect } from 'kysely-postgres-js'; import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'; +import { parse } from 'pg-connection-string'; import postgres, { Notice } from 'postgres'; import { columns, Exif, Person } from 'src/database'; import { DB } from 'src/db'; import { AssetFileType } from 'src/enum'; import { TimeBucketSize } from 'src/repositories/asset.repository'; import { AssetSearchBuilderOptions } from 'src/repositories/search.repository'; +import { DatabaseConnectionParams } from 'src/types'; type Ssl = 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object; -export type PostgresConnectionConfig = { - host?: string; - password?: string; - user?: string; - port?: number; - database?: string; - max?: number; - client_encoding?: string; - ssl?: Ssl; - application_name?: string; - fallback_application_name?: string; - options?: string; -}; - -export const isValidSsl = (ssl?: string | boolean | object): ssl is Ssl => +const isValidSsl = (ssl?: string | boolean | object): ssl is Ssl => typeof ssl !== 'string' || ssl === 'require' || ssl === 'allow' || ssl === 'prefer' || ssl === 'verify-full'; -export const getKyselyConfig = (options: PostgresConnectionConfig): KyselyConfig => { +export const asPostgresConnectionConfig = (params: DatabaseConnectionParams) => { + if (params.connectionType === 'parts') { + return { + host: params.host, + port: params.port, + username: params.username, + password: params.password, + database: params.database, + ssl: undefined, + }; + } + + const { host, port, user, password, database, ...rest } = parse(params.url); + let ssl: Ssl | undefined; + if (rest.ssl) { + if (!isValidSsl(rest.ssl)) { + throw new Error(`Invalid ssl option: ${rest.ssl}`); + } + ssl = rest.ssl; + } + + return { + host: host ?? undefined, + port: port ? Number(port) : undefined, + username: user, + password, + database: database ?? undefined, + ssl, + }; +}; + +export const getKyselyConfig = ( + params: DatabaseConnectionParams, + options: Partial>> = {}, +): KyselyConfig => { + const config = asPostgresConnectionConfig(params); + return { dialect: new PostgresJSDialect({ postgres: postgres({ @@ -66,6 +90,12 @@ export const getKyselyConfig = (options: PostgresConnectionConfig): KyselyConfig connection: { TimeZone: 'UTC', }, + host: config.host, + port: config.port, + username: config.username, + password: config.password, + database: config.database, + ssl: config.ssl, ...options, }), }), diff --git a/server/test/medium/globalSetup.ts b/server/test/medium/globalSetup.ts index 46eb1a733f..e63c9f5224 100644 --- a/server/test/medium/globalSetup.ts +++ b/server/test/medium/globalSetup.ts @@ -1,5 +1,4 @@ import { Kysely } from 'kysely'; -import { parse } from 'pg-connection-string'; import { DB } from 'src/db'; import { ConfigRepository } from 'src/repositories/config.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; @@ -37,19 +36,10 @@ const globalSetup = async () => { const postgresPort = postgresContainer.getMappedPort(5432); const postgresUrl = `postgres://postgres:postgres@localhost:${postgresPort}/immich`; - const parsed = parse(postgresUrl); process.env.IMMICH_TEST_POSTGRES_URL = postgresUrl; - const db = new Kysely( - getKyselyConfig({ - ...parsed, - ssl: false, - host: parsed.host ?? undefined, - port: parsed.port ? Number(parsed.port) : undefined, - database: parsed.database ?? undefined, - }), - ); + const db = new Kysely(getKyselyConfig({ connectionType: 'url', url: postgresUrl })); const configRepository = new ConfigRepository(); const logger = new LoggingRepository(undefined, configRepository); diff --git a/server/test/repositories/config.repository.mock.ts b/server/test/repositories/config.repository.mock.ts index 7c5450c36e..4943a56a33 100644 --- a/server/test/repositories/config.repository.mock.ts +++ b/server/test/repositories/config.repository.mock.ts @@ -21,19 +21,12 @@ const envData: EnvData = { database: { config: { - kysely: { database: 'immich', host: 'database', port: 5432 }, - typeorm: { - connectionType: 'parts', - database: 'immich', - type: 'postgres', - host: 'database', - port: 5432, - username: 'postgres', - password: 'postgres', - name: 'immich', - synchronize: false, - migrationsRun: true, - }, + connectionType: 'parts', + database: 'immich', + host: 'database', + port: 5432, + username: 'postgres', + password: 'postgres', }, skipMigrations: false, diff --git a/server/test/utils.ts b/server/test/utils.ts index e1d979fbfe..c7c29d310e 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -1,9 +1,9 @@ import { ClassConstructor } from 'class-transformer'; -import { Kysely, sql } from 'kysely'; +import { Kysely } from 'kysely'; import { ChildProcessWithoutNullStreams } from 'node:child_process'; import { Writable } from 'node:stream'; -import { parse } from 'pg-connection-string'; import { PNG } from 'pngjs'; +import postgres from 'postgres'; import { DB } from 'src/db'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; @@ -49,7 +49,7 @@ import { VersionHistoryRepository } from 'src/repositories/version-history.repos import { ViewRepository } from 'src/repositories/view-repository'; import { BaseService } from 'src/services/base.service'; import { RepositoryInterface } from 'src/types'; -import { getKyselyConfig } from 'src/utils/database'; +import { asPostgresConnectionConfig, getKyselyConfig } from 'src/utils/database'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; @@ -297,24 +297,20 @@ function* newPngFactory() { const pngFactory = newPngFactory(); +const withDatabase = (url: string, name: string) => url.replace('/immich', `/${name}`); + export const getKyselyDB = async (suffix?: string): Promise> => { - const parsed = parse(process.env.IMMICH_TEST_POSTGRES_URL!); + const testUrl = process.env.IMMICH_TEST_POSTGRES_URL!; + const sql = postgres({ + ...asPostgresConnectionConfig({ connectionType: 'url', url: withDatabase(testUrl, 'postgres') }), + max: 1, + }); - const parsedOptions = { - ...parsed, - ssl: false, - host: parsed.host ?? undefined, - port: parsed.port ? Number(parsed.port) : undefined, - database: parsed.database ?? undefined, - }; - - const kysely = new Kysely(getKyselyConfig({ ...parsedOptions, max: 1, database: 'postgres' })); const randomSuffix = Math.random().toString(36).slice(2, 7); const dbName = `immich_${suffix ?? randomSuffix}`; + await sql.unsafe(`CREATE DATABASE ${dbName} WITH TEMPLATE immich OWNER postgres;`); - await sql.raw(`CREATE DATABASE ${dbName} WITH TEMPLATE immich OWNER postgres;`).execute(kysely); - - return new Kysely(getKyselyConfig({ ...parsedOptions, database: dbName })); + return new Kysely(getKyselyConfig({ connectionType: 'url', url: withDatabase(testUrl, dbName) })); }; export const newRandomImage = () => { From a03902f1743c56b2584ae3c17d9a60d0062b8058 Mon Sep 17 00:00:00 2001 From: Daimolean <92239625+wuzihao051119@users.noreply.github.com> Date: Fri, 25 Apr 2025 07:40:52 +0800 Subject: [PATCH 057/356] fix(docs): incorrect date sorting (#17858) --- docs/src/pages/roadmap.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/src/pages/roadmap.tsx b/docs/src/pages/roadmap.tsx index 4dc391cb27..1e0914a651 100644 --- a/docs/src/pages/roadmap.tsx +++ b/docs/src/pages/roadmap.tsx @@ -252,6 +252,13 @@ const milestones: Item[] = [ description: 'Browse your photos and videos in their folder structure inside the mobile app', release: 'v1.130.0', }), + { + icon: mdiStar, + iconColor: 'gold', + title: '60,000 Stars', + description: 'Reached 60K Stars on GitHub!', + getDateLabel: withLanguage(new Date(2025, 2, 4)), + }, withRelease({ icon: mdiTagFaces, iconColor: 'teal', @@ -260,13 +267,6 @@ const milestones: Item[] = [ 'Manually tag or remove faces in photos and videos, even when automatic detection misses or misidentifies them.', release: 'v1.127.0', }), - { - icon: mdiStar, - iconColor: 'gold', - title: '60,000 Stars', - description: 'Reached 60K Stars on GitHub!', - getDateLabel: withLanguage(new Date(2025, 2, 4)), - }, withRelease({ icon: mdiLinkEdit, iconColor: 'crimson', From b0371580283420fd2a99b915fe7307f7d3488392 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Fri, 25 Apr 2025 05:39:50 +0530 Subject: [PATCH 058/356] fix(mobile): auto trash using MANAGE_MEDIA (#17828) fix: auto trash using MANAGE_MEDIA Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../immich/BackgroundServicePlugin.kt | 212 +++++++++++++++++- .../app/alextran/immich/MainActivity.kt | 8 +- mobile/lib/domain/models/store.model.dart | 1 + .../local_files_manager.interface.dart | 5 + mobile/lib/providers/websocket.provider.dart | 28 ++- .../local_files_manager.repository.dart | 25 +++ mobile/lib/services/app_settings.service.dart | 1 + mobile/lib/services/sync.service.dart | 66 +++++- mobile/lib/utils/local_files_manager.dart | 38 ++++ .../widgets/settings/advanced_settings.dart | 37 +++ .../modules/shared/sync_service_test.dart | 5 + mobile/test/repository.mocks.dart | 6 +- mobile/test/service.mocks.dart | 3 + 13 files changed, 420 insertions(+), 15 deletions(-) create mode 100644 mobile/lib/interfaces/local_files_manager.interface.dart create mode 100644 mobile/lib/repositories/local_files_manager.repository.dart create mode 100644 mobile/lib/utils/local_files_manager.dart diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt index 8520413cff..ae2ec22a71 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt @@ -1,25 +1,42 @@ package app.alextran.immich +import android.app.Activity +import android.content.ContentResolver +import android.content.ContentUris import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.MediaStore +import android.provider.Settings import android.util.Log +import androidx.annotation.RequiresApi import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.plugin.common.PluginRegistry import java.security.MessageDigest import java.io.FileInputStream import kotlinx.coroutines.* +import androidx.core.net.toUri /** - * Android plugin for Dart `BackgroundService` - * - * Receives messages/method calls from the foreground Dart side to manage - * the background service, e.g. start (enqueue), stop (cancel) + * Android plugin for Dart `BackgroundService` and file trash operations */ -class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { +class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener { private var methodChannel: MethodChannel? = null + private var fileTrashChannel: MethodChannel? = null private var context: Context? = null + private var pendingResult: Result? = null + private val permissionRequestCode = 1001 + private val trashRequestCode = 1002 + private var activityBinding: ActivityPluginBinding? = null override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { onAttachedToEngine(binding.applicationContext, binding.binaryMessenger) @@ -29,6 +46,10 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { context = ctx methodChannel = MethodChannel(messenger, "immich/foregroundChannel") methodChannel?.setMethodCallHandler(this) + + // Add file trash channel + fileTrashChannel = MethodChannel(messenger, "file_trash") + fileTrashChannel?.setMethodCallHandler(this) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { @@ -38,11 +59,14 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { private fun onDetachedFromEngine() { methodChannel?.setMethodCallHandler(null) methodChannel = null + fileTrashChannel?.setMethodCallHandler(null) + fileTrashChannel = null } - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + override fun onMethodCall(call: MethodCall, result: Result) { val ctx = context!! when (call.method) { + // Existing BackgroundService methods "enable" -> { val args = call.arguments>()!! ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) @@ -114,10 +138,184 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } } + // File Trash methods moved from MainActivity + "moveToTrash" -> { + val mediaUrls = call.argument>("mediaUrls") + if (mediaUrls != null) { + if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { + moveToTrash(mediaUrls, result) + } else { + result.error("PERMISSION_DENIED", "Media permission required", null) + } + } else { + result.error("INVALID_NAME", "The mediaUrls is not specified.", null) + } + } + + "restoreFromTrash" -> { + val fileName = call.argument("fileName") + val type = call.argument("type") + if (fileName != null && type != null) { + if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) { + restoreFromTrash(fileName, type, result) + } else { + result.error("PERMISSION_DENIED", "Media permission required", null) + } + } else { + result.error("INVALID_NAME", "The file name is not specified.", null) + } + } + + "requestManageMediaPermission" -> { + if (!hasManageMediaPermission()) { + requestManageMediaPermission(result) + } else { + Log.e("Manage storage permission", "Permission already granted") + result.success(true) + } + } + else -> result.notImplemented() } } + + private fun hasManageMediaPermission(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + MediaStore.canManageMedia(context!!); + } else { + false + } + } + + private fun requestManageMediaPermission(result: Result) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + pendingResult = result // Store the result callback + val activity = activityBinding?.activity ?: return + + val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA) + intent.data = "package:${activity.packageName}".toUri() + activity.startActivityForResult(intent, permissionRequestCode) + } else { + result.success(false) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + private fun moveToTrash(mediaUrls: List, result: Result) { + val urisToTrash = mediaUrls.map { it.toUri() } + if (urisToTrash.isEmpty()) { + result.error("INVALID_ARGS", "No valid URIs provided", null) + return + } + + toggleTrash(urisToTrash, true, result); + } + + @RequiresApi(Build.VERSION_CODES.R) + private fun restoreFromTrash(name: String, type: Int, result: Result) { + val uri = getTrashedFileUri(name, type) + if (uri == null) { + Log.e("TrashError", "Asset Uri cannot be found obtained") + result.error("TrashError", "Asset Uri cannot be found obtained", null) + return + } + Log.e("FILE_URI", uri.toString()) + uri.let { toggleTrash(listOf(it), false, result) } + } + + @RequiresApi(Build.VERSION_CODES.R) + private fun toggleTrash(contentUris: List, isTrashed: Boolean, result: Result) { + val activity = activityBinding?.activity + val contentResolver = context?.contentResolver + if (activity == null || contentResolver == null) { + result.error("TrashError", "Activity or ContentResolver not available", null) + return + } + + try { + val pendingIntent = MediaStore.createTrashRequest(contentResolver, contentUris, isTrashed) + pendingResult = result // Store for onActivityResult + activity.startIntentSenderForResult( + pendingIntent.intentSender, + trashRequestCode, + null, 0, 0, 0 + ) + } catch (e: Exception) { + Log.e("TrashError", "Error creating or starting trash request", e) + result.error("TrashError", "Error creating or starting trash request", null) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + private fun getTrashedFileUri(fileName: String, type: Int): Uri? { + val contentResolver = context?.contentResolver ?: return null + val queryUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) + val projection = arrayOf(MediaStore.Files.FileColumns._ID) + + val queryArgs = Bundle().apply { + putString( + ContentResolver.QUERY_ARG_SQL_SELECTION, + "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?" + ) + putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName)) + putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY) + } + + contentResolver.query(queryUri, projection, queryArgs, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)) + // same order as AssetType from dart + val contentUri = when (type) { + 1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + 2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + 3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + else -> queryUri + } + return ContentUris.withAppendedId(contentUri, id) + } + } + return null + } + + // ActivityAware implementation + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activityBinding = binding + binding.addActivityResultListener(this) + } + + override fun onDetachedFromActivityForConfigChanges() { + activityBinding?.removeActivityResultListener(this) + activityBinding = null + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + activityBinding = binding + binding.addActivityResultListener(this) + } + + override fun onDetachedFromActivity() { + activityBinding?.removeActivityResultListener(this) + activityBinding = null + } + + // ActivityResultListener implementation + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + if (requestCode == permissionRequestCode) { + val granted = hasManageMediaPermission() + pendingResult?.success(granted) + pendingResult = null + return true + } + + if (requestCode == trashRequestCode) { + val approved = resultCode == Activity.RESULT_OK + pendingResult?.success(approved) + pendingResult = null + return true + } + return false + } } private const val TAG = "BackgroundServicePlugin" -private const val BUFFER_SIZE = 2 * 1024 * 1024; +private const val BUFFER_SIZE = 2 * 1024 * 1024 diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt index 4ffb490c77..2b6bf81148 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt @@ -2,14 +2,12 @@ package app.alextran.immich import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine -import android.os.Bundle -import android.content.Intent +import androidx.annotation.NonNull class MainActivity : FlutterActivity() { - - override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) flutterEngine.plugins.add(BackgroundServicePlugin()) + // No need to set up method channel here as it's now handled in the plugin } - } diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index e6d9ecaf48..8a5a908e0d 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -65,6 +65,7 @@ enum StoreKey { // Video settings loadOriginalVideo._(136), + manageLocalMediaAndroid._(137), // Experimental stuff photoManagerCustomFilter._(1000); diff --git a/mobile/lib/interfaces/local_files_manager.interface.dart b/mobile/lib/interfaces/local_files_manager.interface.dart new file mode 100644 index 0000000000..07274b7e29 --- /dev/null +++ b/mobile/lib/interfaces/local_files_manager.interface.dart @@ -0,0 +1,5 @@ +abstract interface class ILocalFilesManager { + Future moveToTrash(List mediaUrls); + Future restoreFromTrash(String fileName, int type); + Future requestManageMediaPermission(); +} diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index f92d2c8421..72dbda8b6f 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -23,6 +23,7 @@ enum PendingAction { assetDelete, assetUploaded, assetHidden, + assetTrash, } class PendingChange { @@ -160,7 +161,7 @@ class WebsocketNotifier extends StateNotifier { socket.on('on_upload_success', _handleOnUploadSuccess); socket.on('on_config_update', _handleOnConfigUpdate); socket.on('on_asset_delete', _handleOnAssetDelete); - socket.on('on_asset_trash', _handleServerUpdates); + socket.on('on_asset_trash', _handleOnAssetTrash); socket.on('on_asset_restore', _handleServerUpdates); socket.on('on_asset_update', _handleServerUpdates); socket.on('on_asset_stack_update', _handleServerUpdates); @@ -207,6 +208,26 @@ class WebsocketNotifier extends StateNotifier { _debounce.run(handlePendingChanges); } + Future _handlePendingTrashes() async { + final trashChanges = state.pendingChanges + .where((c) => c.action == PendingAction.assetTrash) + .toList(); + if (trashChanges.isNotEmpty) { + List remoteIds = trashChanges + .expand((a) => (a.value as List).map((e) => e.toString())) + .toList(); + + await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); + await _ref.read(assetProvider.notifier).getAllAsset(); + + state = state.copyWith( + pendingChanges: state.pendingChanges + .whereNot((c) => trashChanges.contains(c)) + .toList(), + ); + } + } + Future _handlePendingDeletes() async { final deleteChanges = state.pendingChanges .where((c) => c.action == PendingAction.assetDelete) @@ -267,6 +288,7 @@ class WebsocketNotifier extends StateNotifier { await _handlePendingUploaded(); await _handlePendingDeletes(); await _handlingPendingHidden(); + await _handlePendingTrashes(); } void _handleOnConfigUpdate(dynamic _) { @@ -285,6 +307,10 @@ class WebsocketNotifier extends StateNotifier { void _handleOnAssetDelete(dynamic data) => addPendingChange(PendingAction.assetDelete, data); + void _handleOnAssetTrash(dynamic data) { + addPendingChange(PendingAction.assetTrash, data); + } + void _handleOnAssetHidden(dynamic data) => addPendingChange(PendingAction.assetHidden, data); diff --git a/mobile/lib/repositories/local_files_manager.repository.dart b/mobile/lib/repositories/local_files_manager.repository.dart new file mode 100644 index 0000000000..c2e234d14d --- /dev/null +++ b/mobile/lib/repositories/local_files_manager.repository.dart @@ -0,0 +1,25 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/local_files_manager.interface.dart'; +import 'package:immich_mobile/utils/local_files_manager.dart'; + +final localFilesManagerRepositoryProvider = + Provider((ref) => const LocalFilesManagerRepository()); + +class LocalFilesManagerRepository implements ILocalFilesManager { + const LocalFilesManagerRepository(); + + @override + Future moveToTrash(List mediaUrls) async { + return await LocalFilesManager.moveToTrash(mediaUrls); + } + + @override + Future restoreFromTrash(String fileName, int type) async { + return await LocalFilesManager.restoreFromTrash(fileName, type); + } + + @override + Future requestManageMediaPermission() async { + return await LocalFilesManager.requestManageMediaPermission(); + } +} diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index cc57b8d3a3..6413b69fce 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -61,6 +61,7 @@ enum AppSettingsEnum { 0, ), advancedTroubleshooting(StoreKey.advancedTroubleshooting, null, false), + manageLocalMediaAndroid(StoreKey.manageLocalMediaAndroid, null, false), logLevel(StoreKey.logLevel, null, 5), // Level.INFO = 5 preferRemoteImage(StoreKey.preferRemoteImage, null, false), loopVideo(StoreKey.loopVideo, "loopVideo", true), diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart index 11a9dcb56a..80950d8c00 100644 --- a/mobile/lib/services/sync.service.dart +++ b/mobile/lib/services/sync.service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -16,8 +17,10 @@ import 'package:immich_mobile/interfaces/album_api.interface.dart'; import 'package:immich_mobile/interfaces/album_media.interface.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/etag.interface.dart'; +import 'package:immich_mobile/interfaces/local_files_manager.interface.dart'; import 'package:immich_mobile/interfaces/partner.interface.dart'; import 'package:immich_mobile/interfaces/partner_api.interface.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/album.repository.dart'; @@ -25,8 +28,10 @@ import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/album_media.repository.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/etag.repository.dart'; +import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/repositories/partner.repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/entity.service.dart'; import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/utils/async_mutex.dart'; @@ -48,6 +53,8 @@ final syncServiceProvider = Provider( ref.watch(userRepositoryProvider), ref.watch(userServiceProvider), ref.watch(etagRepositoryProvider), + ref.watch(appSettingsServiceProvider), + ref.watch(localFilesManagerRepositoryProvider), ref.watch(partnerApiRepositoryProvider), ref.watch(userApiRepositoryProvider), ), @@ -69,6 +76,8 @@ class SyncService { final IUserApiRepository _userApiRepository; final AsyncMutex _lock = AsyncMutex(); final Logger _log = Logger('SyncService'); + final AppSettingsService _appSettingsService; + final ILocalFilesManager _localFilesManager; SyncService( this._hashService, @@ -82,6 +91,8 @@ class SyncService { this._userRepository, this._userService, this._eTagRepository, + this._appSettingsService, + this._localFilesManager, this._partnerApiRepository, this._userApiRepository, ); @@ -238,8 +249,22 @@ class SyncService { return null; } + Future _moveToTrashMatchedAssets(Iterable idsToDelete) async { + final List localAssets = await _assetRepository.getAllLocal(); + final List matchedAssets = localAssets + .where((asset) => idsToDelete.contains(asset.remoteId)) + .toList(); + + final mediaUrls = await Future.wait( + matchedAssets + .map((asset) => asset.local?.getMediaUrl() ?? Future.value(null)), + ); + + await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList()); + } + /// Deletes remote-only assets, updates merged assets to be local-only - Future handleRemoteAssetRemoval(List idsToDelete) { + Future handleRemoteAssetRemoval(List idsToDelete) async { return _assetRepository.transaction(() async { await _assetRepository.deleteAllByRemoteId( idsToDelete, @@ -249,6 +274,12 @@ class SyncService { idsToDelete, state: AssetState.merged, ); + if (Platform.isAndroid && + _appSettingsService.getSetting( + AppSettingsEnum.manageLocalMediaAndroid, + )) { + await _moveToTrashMatchedAssets(idsToDelete); + } if (merged.isEmpty) return; for (final Asset asset in merged) { asset.remoteId = null; @@ -790,10 +821,43 @@ class SyncService { return (existing, toUpsert); } + Future _toggleTrashStatusForAssets(List assetsList) async { + final trashMediaUrls = []; + + for (final asset in assetsList) { + if (asset.isTrashed) { + final mediaUrl = await asset.local?.getMediaUrl(); + if (mediaUrl == null) { + _log.warning( + "Failed to get media URL for asset ${asset.name} while moving to trash", + ); + continue; + } + trashMediaUrls.add(mediaUrl); + } else { + await _localFilesManager.restoreFromTrash( + asset.fileName, + asset.type.index, + ); + } + } + + if (trashMediaUrls.isNotEmpty) { + await _localFilesManager.moveToTrash(trashMediaUrls); + } + } + /// Inserts or updates the assets in the database with their ExifInfo (if any) Future upsertAssetsWithExif(List assets) async { if (assets.isEmpty) return; + if (Platform.isAndroid && + _appSettingsService.getSetting( + AppSettingsEnum.manageLocalMediaAndroid, + )) { + _toggleTrashStatusForAssets(assets); + } + try { await _assetRepository.transaction(() async { await _assetRepository.updateAll(assets); diff --git a/mobile/lib/utils/local_files_manager.dart b/mobile/lib/utils/local_files_manager.dart new file mode 100644 index 0000000000..a4cf41a6e6 --- /dev/null +++ b/mobile/lib/utils/local_files_manager.dart @@ -0,0 +1,38 @@ +import 'package:flutter/services.dart'; +import 'package:logging/logging.dart'; + +abstract final class LocalFilesManager { + static final Logger _logger = Logger('LocalFilesManager'); + static const MethodChannel _channel = MethodChannel('file_trash'); + + static Future moveToTrash(List mediaUrls) async { + try { + return await _channel + .invokeMethod('moveToTrash', {'mediaUrls': mediaUrls}); + } catch (e, s) { + _logger.warning('Error moving file to trash', e, s); + return false; + } + } + + static Future restoreFromTrash(String fileName, int type) async { + try { + return await _channel.invokeMethod( + 'restoreFromTrash', + {'fileName': fileName, 'type': type}, + ); + } catch (e, s) { + _logger.warning('Error restore file from trash', e, s); + return false; + } + } + + static Future requestManageMediaPermission() async { + try { + return await _channel.invokeMethod('requestManageMediaPermission'); + } catch (e, s) { + _logger.warning('Error requesting manage media permission', e, s); + return false; + } + } +} diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index a2e0e5b95c..d65186a191 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -1,11 +1,13 @@ import 'dart:io'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; @@ -25,6 +27,8 @@ class AdvancedSettings extends HookConsumerWidget { final advancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); + final manageLocalMediaAndroid = + useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid); final levelId = useAppSettingsState(AppSettingsEnum.logLevel); final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage); final allowSelfSignedSSLCert = @@ -40,6 +44,16 @@ class AdvancedSettings extends HookConsumerWidget { LogService.I.setlogLevel(Level.LEVELS[levelId.value].toLogLevel()), ); + Future checkAndroidVersion() async { + if (Platform.isAndroid) { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + int sdkVersion = androidInfo.version.sdkInt; + return sdkVersion >= 31; + } + return false; + } + final advancedSettings = [ SettingsSwitchListTile( enabled: true, @@ -47,6 +61,29 @@ class AdvancedSettings extends HookConsumerWidget { title: "advanced_settings_troubleshooting_title".tr(), subtitle: "advanced_settings_troubleshooting_subtitle".tr(), ), + FutureBuilder( + future: checkAndroidVersion(), + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data == true) { + return SettingsSwitchListTile( + enabled: true, + valueNotifier: manageLocalMediaAndroid, + title: "advanced_settings_sync_remote_deletions_title".tr(), + subtitle: "advanced_settings_sync_remote_deletions_subtitle".tr(), + onChanged: (value) async { + if (value) { + final result = await ref + .read(localFilesManagerRepositoryProvider) + .requestManageMediaPermission(); + manageLocalMediaAndroid.value = result; + } + }, + ); + } else { + return const SizedBox.shrink(); + } + }, + ), SettingsSliderListTile( text: "advanced_settings_log_level_title".tr(args: [logLevel]), valueNotifier: levelId, diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart index 3879e64237..2029ade018 100644 --- a/mobile/test/modules/shared/sync_service_test.dart +++ b/mobile/test/modules/shared/sync_service_test.dart @@ -60,6 +60,9 @@ void main() { final MockAlbumMediaRepository albumMediaRepository = MockAlbumMediaRepository(); final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository(); + final MockAppSettingService appSettingService = MockAppSettingService(); + final MockLocalFilesManagerRepository localFilesManagerRepository = + MockLocalFilesManagerRepository(); final MockPartnerApiRepository partnerApiRepository = MockPartnerApiRepository(); final MockUserApiRepository userApiRepository = MockUserApiRepository(); @@ -106,6 +109,8 @@ void main() { userRepository, userService, eTagRepository, + appSettingService, + localFilesManagerRepository, partnerApiRepository, userApiRepository, ); diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart index 1c698297dc..d2f0da4231 100644 --- a/mobile/test/repository.mocks.dart +++ b/mobile/test/repository.mocks.dart @@ -10,6 +10,7 @@ import 'package:immich_mobile/interfaces/auth_api.interface.dart'; import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/interfaces/etag.interface.dart'; import 'package:immich_mobile/interfaces/file_media.interface.dart'; +import 'package:immich_mobile/interfaces/local_files_manager.interface.dart'; import 'package:immich_mobile/interfaces/partner.interface.dart'; import 'package:immich_mobile/interfaces/partner_api.interface.dart'; import 'package:mocktail/mocktail.dart'; @@ -41,6 +42,9 @@ class MockAuthApiRepository extends Mock implements IAuthApiRepository {} class MockAuthRepository extends Mock implements IAuthRepository {} +class MockPartnerRepository extends Mock implements IPartnerRepository {} + class MockPartnerApiRepository extends Mock implements IPartnerApiRepository {} -class MockPartnerRepository extends Mock implements IPartnerRepository {} +class MockLocalFilesManagerRepository extends Mock + implements ILocalFilesManager {} diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index d31a7e5d50..87a8c01cf0 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -1,5 +1,6 @@ import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/entity.service.dart'; @@ -25,4 +26,6 @@ class MockNetworkService extends Mock implements NetworkService {} class MockSearchApi extends Mock implements SearchApi {} +class MockAppSettingService extends Mock implements AppSettingsService {} + class MockBackgroundService extends Mock implements BackgroundService {} From 765da7b1821a1fc53edfdcdd91d0108a744fe42e Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 24 Apr 2025 19:16:54 -0500 Subject: [PATCH 059/356] fix(mobile): mobile migration logic (#17865) * fix(mobile): mobile migration logic * add exception * remove unused comment * finalize --- mobile/lib/utils/migration.dart | 73 ++++++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index bebd7a027b..6a09f79ce2 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'dart:io'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/android_device_asset.entity.dart'; @@ -17,6 +17,8 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; +// ignore: import_rule_photo_manager +import 'package:photo_manager/photo_manager.dart'; const int targetVersion = 10; @@ -69,14 +71,45 @@ Future _migrateDeviceAsset(Isar db) async { : (await db.iOSDeviceAssets.where().findAll()) .map((i) => _DeviceAsset(assetId: i.id, hash: i.hash)) .toList(); - final localAssets = (await db.assets - .where() - .anyOf(ids, (query, id) => query.localIdEqualTo(id.assetId)) - .findAll()) - .map((a) => _DeviceAsset(assetId: a.localId!, dateTime: a.fileModifiedAt)) - .toList(); - debugPrint("Device Asset Ids length - ${ids.length}"); - debugPrint("Local Asset Ids length - ${localAssets.length}"); + + final PermissionState ps = await PhotoManager.requestPermissionExtend(); + if (!ps.hasAccess) { + if (kDebugMode) { + debugPrint( + "[MIGRATION] Photo library permission not granted. Skipping device asset migration.", + ); + } + + return; + } + + List<_DeviceAsset> localAssets = []; + final List paths = + await PhotoManager.getAssetPathList(onlyAll: true); + + if (paths.isEmpty) { + localAssets = (await db.assets + .where() + .anyOf(ids, (query, id) => query.localIdEqualTo(id.assetId)) + .findAll()) + .map( + (a) => _DeviceAsset(assetId: a.localId!, dateTime: a.fileModifiedAt), + ) + .toList(); + } else { + final AssetPathEntity albumWithAll = paths.first; + final int assetCount = await albumWithAll.assetCountAsync; + + final List allDeviceAssets = + await albumWithAll.getAssetListRange(start: 0, end: assetCount); + + localAssets = allDeviceAssets + .map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)) + .toList(); + } + + debugPrint("[MIGRATION] Device Asset Ids length - ${ids.length}"); + debugPrint("[MIGRATION] Local Asset Ids length - ${localAssets.length}"); ids.sort((a, b) => a.assetId.compareTo(b.assetId)); localAssets.sort((a, b) => a.assetId.compareTo(b.assetId)); final List toAdd = []; @@ -95,15 +128,27 @@ Future _migrateDeviceAsset(Isar db) async { return false; }, onlyFirst: (deviceAsset) { - debugPrint( - 'DeviceAsset not found in local assets: ${deviceAsset.assetId}', - ); + if (kDebugMode) { + debugPrint( + '[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}', + ); + } }, onlySecond: (asset) { - debugPrint('Local asset not found in DeviceAsset: ${asset.assetId}'); + if (kDebugMode) { + debugPrint( + '[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}', + ); + } }, ); - debugPrint("Total number of device assets migrated - ${toAdd.length}"); + + if (kDebugMode) { + debugPrint( + "[MIGRATION] Total number of device assets migrated - ${toAdd.length}", + ); + } + await db.writeTxn(() async { await db.deviceAssetEntitys.putAll(toAdd); }); From 0d60be3d87559a7ccb98e875b1b2502321e19284 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 03:07:06 +0000 Subject: [PATCH 060/356] chore: version v1.132.2 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 17 files changed, 30 insertions(+), 26 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index d15358b26e..fe75b7ce8b 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.63", + "version": "2.2.64", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.63", + "version": "2.2.64", "license": "GNU Affero General Public License version 3", "dependencies": { "chokidar": "^4.0.3", @@ -54,7 +54,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.132.1", + "version": "1.132.2", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index 8a742cd0d7..b47156f043 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.63", + "version": "2.2.64", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 26eb3a2f9a..60a7436afb 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.132.2", + "url": "https://v1.132.2.archive.immich.app" + }, { "label": "v1.132.1", "url": "https://v1.132.1.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 7eb831b897..c5364d95f8 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.132.1", + "version": "1.132.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.132.1", + "version": "1.132.2", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -44,7 +44,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.63", + "version": "2.2.64", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -93,7 +93,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.132.1", + "version": "1.132.2", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 3946f149d6..afbf06f34b 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.132.1", + "version": "1.132.2", "description": "", "main": "index.js", "type": "module", diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 13f3b0b850..013a0235d8 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 195, - "android.injected.version.name" => "1.132.1", + "android.injected.version.code" => 196, + "android.injected.version.name" => "1.132.2", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index f454d24973..5d46f0ebcf 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.132.1" + version_number: "1.132.2" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 073ae932ce..2ff35f9537 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.132.1 +- API version: 1.132.2 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 07f56fb341..bda35258f9 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.132.1+195 +version: 1.132.2+196 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 53709f3f0c..b242ade761 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7656,7 +7656,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.132.1", + "version": "1.132.2", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index fe398ed2bb..249ffe9960 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.132.1", + "version": "1.132.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.132.1", + "version": "1.132.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 4afce16f23..6194abe583 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.132.1", + "version": "1.132.2", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 01f476517e..5eca3c83eb 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.132.1 + * 1.132.2 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index cde8bd3a62..8214a6f874 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.132.1", + "version": "1.132.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich", - "version": "1.132.1", + "version": "1.132.2", "hasInstallScript": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/server/package.json b/server/package.json index f4435ced68..b3ce836f7c 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.132.1", + "version": "1.132.2", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index 91d0adb573..d6fcb816f8 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.132.1", + "version": "1.132.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.132.1", + "version": "1.132.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", @@ -82,7 +82,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.132.1", + "version": "1.132.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index ec53fd69d5..b4b9a80da1 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.132.1", + "version": "1.132.2", "license": "GNU Affero General Public License version 3", "type": "module", "scripts": { From 1fe3c7b9b3ed8864fcd6c6b495ebae93c210995c Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Fri, 25 Apr 2025 00:07:42 -0400 Subject: [PATCH 061/356] fix(docs): priorities (Capitalization) (#17866) priorities --- docs/docs/guides/database-queries.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/guides/database-queries.md b/docs/docs/guides/database-queries.md index 89a4f07bc0..209f673993 100644 --- a/docs/docs/guides/database-queries.md +++ b/docs/docs/guides/database-queries.md @@ -1,7 +1,7 @@ # Database Queries :::danger -Keep in mind that mucking around in the database might set the moon on fire. Avoid modifying the database directly when possible, and always have current backups. +Keep in mind that mucking around in the database might set the Moon on fire. Avoid modifying the database directly when possible, and always have current backups. ::: :::tip From 644defa4a1211cefecfea763712161f68c53790b Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 24 Apr 2025 23:14:40 -0500 Subject: [PATCH 062/356] chore: post release tasks (#17867) --- mobile/ios/Runner.xcodeproj/project.pbxproj | 15 +++++++++------ mobile/ios/Runner/Info.plist | 4 ++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index e4c25fefdf..09e362b057 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -541,7 +541,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 202; + CURRENT_PROJECT_VERSION = 203; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -685,7 +685,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 202; + CURRENT_PROJECT_VERSION = 203; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -715,7 +715,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 202; + CURRENT_PROJECT_VERSION = 203; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -748,7 +748,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 202; + CURRENT_PROJECT_VERSION = 203; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -769,6 +769,7 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -791,7 +792,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 202; + CURRENT_PROJECT_VERSION = 203; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -811,6 +812,7 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -831,7 +833,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 202; + CURRENT_PROJECT_VERSION = 203; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -851,6 +853,7 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 02fef7a965..fc259703c8 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -78,7 +78,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.132.0 + 1.132.2 CFBundleSignature ???? CFBundleURLTypes @@ -93,7 +93,7 @@ CFBundleVersion - 202 + 203 FLTEnableImpeller ITSAppUsesNonExemptEncryption From e822e3eca99285178407e206939070f530b80850 Mon Sep 17 00:00:00 2001 From: Martin Mikita Date: Fri, 25 Apr 2025 10:57:44 +0200 Subject: [PATCH 063/356] docs: update MapTiler name (#17863) --- docs/docs/guides/custom-map-styles.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/docs/guides/custom-map-styles.md b/docs/docs/guides/custom-map-styles.md index 3f52937432..1a61afc324 100644 --- a/docs/docs/guides/custom-map-styles.md +++ b/docs/docs/guides/custom-map-styles.md @@ -14,14 +14,14 @@ online generators you can use. 2. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.) 3. Save your selections. Reload the map, and enjoy your custom map style! -## Use Maptiler to build a custom style +## Use MapTiler to build a custom style -Customizing the map style can be done easily using Maptiler, if you do not want to write an entire JSON document by hand. +Customizing the map style can be done easily using MapTiler, if you do not want to write an entire JSON document by hand. 1. Create a free account at https://cloud.maptiler.com 2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there. 3. The **editor** interface is self-explanatory. You can change colors, remove visible layers, or add optional layers (e.g., administrative, topo, hydro, etc.) in the composer. 4. Once you have your map composed, click on **Save** at the top right. Give it a unique name to save it to your account. -5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. Maptiler will present an interactive side-by-side map with the original and your changes prior to publication.
    ![Maptiler Publication Settings](img/immich_map_styles_publish.webp) -6. Maptiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay. -7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to Maptiler. +5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. MapTiler will present an interactive side-by-side map with the original and your changes prior to publication.
    ![MapTiler Publication Settings](img/immich_map_styles_publish.webp) +6. MapTiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay. +7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to MapTiler. From d0014bdf94feecf84b375499a8e48bb9fa47a8b3 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 25 Apr 2025 08:36:31 -0400 Subject: [PATCH 064/356] refactor: event manager (#17862) * refactor: event manager * refactor: event manager --- e2e/src/web/specs/auth.e2e-spec.ts | 6 +-- .../navigation-bar/navigation-bar.svelte | 15 ++---- web/src/lib/stores/auth-manager.svelte.ts | 33 ++++++++++++ web/src/lib/stores/event-manager.svelte.ts | 54 +++++++++++++++++++ web/src/lib/stores/folders.svelte.ts | 5 ++ web/src/lib/stores/memory.store.svelte.ts | 5 ++ web/src/lib/stores/search.svelte.ts | 6 +++ web/src/lib/stores/user.store.ts | 3 ++ web/src/lib/stores/user.svelte.ts | 7 ++- web/src/lib/stores/websocket.ts | 5 +- web/src/lib/utils/auth.ts | 24 +-------- .../routes/auth/change-password/+page.svelte | 11 ++-- 12 files changed, 127 insertions(+), 47 deletions(-) create mode 100644 web/src/lib/stores/auth-manager.svelte.ts create mode 100644 web/src/lib/stores/event-manager.svelte.ts diff --git a/e2e/src/web/specs/auth.e2e-spec.ts b/e2e/src/web/specs/auth.e2e-spec.ts index e89f17a4e9..74bee64e0a 100644 --- a/e2e/src/web/specs/auth.e2e-spec.ts +++ b/e2e/src/web/specs/auth.e2e-spec.ts @@ -25,7 +25,7 @@ test.describe('Registration', () => { // login await expect(page).toHaveTitle(/Login/); - await page.goto('/auth/login'); + await page.goto('/auth/login?autoLaunch=0'); await page.getByLabel('Email').fill('admin@immich.app'); await page.getByLabel('Password').fill('password'); await page.getByRole('button', { name: 'Login' }).click(); @@ -59,7 +59,7 @@ test.describe('Registration', () => { await context.clearCookies(); // login - await page.goto('/auth/login'); + await page.goto('/auth/login?autoLaunch=0'); await page.getByLabel('Email').fill('user@immich.cloud'); await page.getByLabel('Password').fill('password'); await page.getByRole('button', { name: 'Login' }).click(); @@ -72,7 +72,7 @@ test.describe('Registration', () => { await page.getByRole('button', { name: 'Change password' }).click(); // login with new password - await expect(page).toHaveURL('/auth/login'); + await expect(page).toHaveURL('/auth/login?autoLaunch=0'); await page.getByLabel('Email').fill('user@immich.cloud'); await page.getByLabel('Password').fill('new-password'); await page.getByRole('button', { name: 'Login' }).click(); diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index f7f9b877f3..90f6b3c55b 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -10,11 +10,13 @@ import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte'; import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; import { AppRoute } from '$lib/constants'; + import { authManager } from '$lib/stores/auth-manager.svelte'; + import { mobileDevice } from '$lib/stores/mobile-device.svelte'; import { featureFlags } from '$lib/stores/server-config.store'; + import { sidebarStore } from '$lib/stores/sidebar.svelte'; import { user } from '$lib/stores/user.store'; import { userInteraction } from '$lib/stores/user.svelte'; - import { handleLogout } from '$lib/utils/auth'; - import { getAboutInfo, logout, type ServerAboutResponseDto } from '@immich/sdk'; + import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk'; import { Button, IconButton } from '@immich/ui'; import { mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js'; import { onMount } from 'svelte'; @@ -23,8 +25,6 @@ import ThemeButton from '../theme-button.svelte'; import UserAvatar from '../user-avatar.svelte'; import AccountInfoPanel from './account-info-panel.svelte'; - import { sidebarStore } from '$lib/stores/sidebar.svelte'; - import { mobileDevice } from '$lib/stores/mobile-device.svelte'; interface Props { showUploadButton?: boolean; @@ -38,11 +38,6 @@ let shouldShowHelpPanel = $state(false); let innerWidth: number = $state(0); - const onLogout = async () => { - const { redirectUri } = await logout(); - await handleLogout(redirectUri); - }; - let info: ServerAboutResponseDto | undefined = $state(); onMount(async () => { @@ -183,7 +178,7 @@ {/if} {#if shouldShowAccountInfoPanel} - + authManager.logout()} /> {/if}
    diff --git a/web/src/lib/stores/auth-manager.svelte.ts b/web/src/lib/stores/auth-manager.svelte.ts new file mode 100644 index 0000000000..72c966df0b --- /dev/null +++ b/web/src/lib/stores/auth-manager.svelte.ts @@ -0,0 +1,33 @@ +import { goto } from '$app/navigation'; +import { AppRoute } from '$lib/constants'; +import { eventManager } from '$lib/stores/event-manager.svelte'; +import { logout } from '@immich/sdk'; + +class AuthManager { + async logout() { + let redirectUri; + + try { + const response = await logout(); + if (response.redirectUri) { + redirectUri = response.redirectUri; + } + } catch (error) { + console.log('Error logging out:', error); + } + + redirectUri = redirectUri ?? AppRoute.AUTH_LOGIN; + + try { + if (redirectUri.startsWith('/')) { + await goto(redirectUri); + } else { + globalThis.location.href = redirectUri; + } + } finally { + eventManager.emit('auth.logout'); + } + } +} + +export const authManager = new AuthManager(); diff --git a/web/src/lib/stores/event-manager.svelte.ts b/web/src/lib/stores/event-manager.svelte.ts new file mode 100644 index 0000000000..09e9b45c3c --- /dev/null +++ b/web/src/lib/stores/event-manager.svelte.ts @@ -0,0 +1,54 @@ +type Listener, K extends keyof EventMap> = (...params: EventMap[K]) => void; + +class EventManager> { + private listeners: { + [K in keyof EventMap]?: { + listener: Listener; + once?: boolean; + }[]; + } = {}; + + on(key: T, listener: (...params: EventMap[T]) => void) { + return this.addListener(key, listener, false); + } + + once(key: T, listener: (...params: EventMap[T]) => void) { + return this.addListener(key, listener, true); + } + + off(key: K, listener: Listener) { + if (this.listeners[key]) { + this.listeners[key] = this.listeners[key].filter((item) => item.listener !== listener); + } + + return this; + } + + emit(key: T, ...params: EventMap[T]) { + if (!this.listeners[key]) { + return; + } + + for (const { listener } of this.listeners[key]) { + listener(...params); + } + + // remove one time listeners + this.listeners[key] = this.listeners[key].filter((item) => !item.once); + } + + private addListener(key: T, listener: (...params: EventMap[T]) => void, once: boolean) { + if (!this.listeners[key]) { + this.listeners[key] = []; + } + + this.listeners[key].push({ listener, once }); + + return this; + } +} + +export const eventManager = new EventManager<{ + 'user.login': []; + 'auth.logout': []; +}>(); diff --git a/web/src/lib/stores/folders.svelte.ts b/web/src/lib/stores/folders.svelte.ts index fb59687a38..c6fc7808b2 100644 --- a/web/src/lib/stores/folders.svelte.ts +++ b/web/src/lib/stores/folders.svelte.ts @@ -1,3 +1,4 @@ +import { eventManager } from '$lib/stores/event-manager.svelte'; import { getAssetsByOriginalPath, getUniqueOriginalPaths, @@ -16,6 +17,10 @@ class FoldersStore { uniquePaths = $state([]); assets = $state({}); + constructor() { + eventManager.on('auth.logout', () => this.clearCache()); + } + async fetchUniquePaths() { if (this.initialized) { return; diff --git a/web/src/lib/stores/memory.store.svelte.ts b/web/src/lib/stores/memory.store.svelte.ts index 7173b43d06..ef3f87a3aa 100644 --- a/web/src/lib/stores/memory.store.svelte.ts +++ b/web/src/lib/stores/memory.store.svelte.ts @@ -1,3 +1,4 @@ +import { eventManager } from '$lib/stores/event-manager.svelte'; import { asLocalTimeISO } from '$lib/utils/date-time'; import { type AssetResponseDto, @@ -24,6 +25,10 @@ export type MemoryAsset = MemoryIndex & { }; class MemoryStoreSvelte { + constructor() { + eventManager.on('auth.logout', () => this.clearCache()); + } + memories = $state([]); private initialized = false; private memoryAssets = $derived.by(() => { diff --git a/web/src/lib/stores/search.svelte.ts b/web/src/lib/stores/search.svelte.ts index 7d012922ca..f334f53460 100644 --- a/web/src/lib/stores/search.svelte.ts +++ b/web/src/lib/stores/search.svelte.ts @@ -1,7 +1,13 @@ +import { eventManager } from '$lib/stores/event-manager.svelte'; + class SearchStore { savedSearchTerms = $state([]); isSearchEnabled = $state(false); + constructor() { + eventManager.on('auth.logout', () => this.clearCache()); + } + clearCache() { this.savedSearchTerms = []; this.isSearchEnabled = false; diff --git a/web/src/lib/stores/user.store.ts b/web/src/lib/stores/user.store.ts index 5bffc08b80..fe2288c252 100644 --- a/web/src/lib/stores/user.store.ts +++ b/web/src/lib/stores/user.store.ts @@ -1,3 +1,4 @@ +import { eventManager } from '$lib/stores/event-manager.svelte'; import { purchaseStore } from '$lib/stores/purchase.store'; import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk'; import { writable } from 'svelte/store'; @@ -14,3 +15,5 @@ export const resetSavedUser = () => { preferences.set(undefined as unknown as UserPreferencesResponseDto); purchaseStore.setPurchaseStatus(false); }; + +eventManager.on('auth.logout', () => resetSavedUser()); diff --git a/web/src/lib/stores/user.svelte.ts b/web/src/lib/stores/user.svelte.ts index 71b2cdd847..093d90e4b5 100644 --- a/web/src/lib/stores/user.svelte.ts +++ b/web/src/lib/stores/user.svelte.ts @@ -1,3 +1,4 @@ +import { eventManager } from '$lib/stores/event-manager.svelte'; import type { AlbumResponseDto, ServerAboutResponseDto, @@ -19,8 +20,10 @@ const defaultUserInteraction: UserInteractions = { serverInfo: undefined, }; -export const resetUserInteraction = () => { +export const userInteraction = $state(defaultUserInteraction); + +const reset = () => { Object.assign(userInteraction, defaultUserInteraction); }; -export const userInteraction = $state(defaultUserInteraction); +eventManager.on('auth.logout', () => reset()); diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index d398ca52a9..90228a5cbd 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -1,5 +1,4 @@ -import { AppRoute } from '$lib/constants'; -import { handleLogout } from '$lib/utils/auth'; +import { authManager } from '$lib/stores/auth-manager.svelte'; import { createEventEmitter } from '$lib/utils/eventemitter'; import type { AssetResponseDto, ServerVersionResponseDto } from '@immich/sdk'; import { io, type Socket } from 'socket.io-client'; @@ -50,7 +49,7 @@ websocket .on('disconnect', () => websocketStore.connected.set(false)) .on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion)) .on('on_new_release', (releaseVersion) => websocketStore.release.set(releaseVersion)) - .on('on_session_delete', () => handleLogout(AppRoute.AUTH_LOGIN)) + .on('on_session_delete', () => authManager.logout()) .on('connect_error', (e) => console.log('Websocket Connect Error', e)); export const openWebsocketConnection = () => { diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts index 22b92dd988..9b78c345e2 100644 --- a/web/src/lib/utils/auth.ts +++ b/web/src/lib/utils/auth.ts @@ -1,11 +1,7 @@ import { browser } from '$app/environment'; -import { goto } from '$app/navigation'; -import { foldersStore } from '$lib/stores/folders.svelte'; -import { memoryStore } from '$lib/stores/memory.store.svelte'; import { purchaseStore } from '$lib/stores/purchase.store'; -import { searchStore } from '$lib/stores/search.svelte'; -import { preferences as preferences$, resetSavedUser, user as user$ } from '$lib/stores/user.store'; -import { resetUserInteraction, userInteraction } from '$lib/stores/user.svelte'; +import { preferences as preferences$, user as user$ } from '$lib/stores/user.store'; +import { userInteraction } from '$lib/stores/user.svelte'; import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk'; import { redirect } from '@sveltejs/kit'; import { DateTime } from 'luxon'; @@ -91,19 +87,3 @@ export const getAccountAge = (): number => { return Number(accountAge); }; - -export const handleLogout = async (redirectUri: string) => { - try { - if (redirectUri.startsWith('/')) { - await goto(redirectUri); - } else { - globalThis.location.href = redirectUri; - } - } finally { - resetSavedUser(); - resetUserInteraction(); - foldersStore.clearCache(); - memoryStore.clearCache(); - searchStore.clearCache(); - } -}; diff --git a/web/src/routes/auth/change-password/+page.svelte b/web/src/routes/auth/change-password/+page.svelte index 33d354552e..16a6ffc677 100644 --- a/web/src/routes/auth/change-password/+page.svelte +++ b/web/src/routes/auth/change-password/+page.svelte @@ -1,9 +1,8 @@ From d85ef19bfcc5b82b870d6807564e1d0610d68a9a Mon Sep 17 00:00:00 2001 From: Yaros Date: Fri, 25 Apr 2025 17:38:30 +0200 Subject: [PATCH 065/356] fix(mobile): revert get location on app start (#17882) --- mobile/lib/pages/library/library.page.dart | 97 +++++++++------------- 1 file changed, 40 insertions(+), 57 deletions(-) diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index c08a1c715d..1dc336d204 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -1,7 +1,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:geolocator/geolocator.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; @@ -13,7 +12,6 @@ import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/utils/map_utils.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; import 'package:immich_mobile/widgets/common/user_avatar.dart'; @@ -357,66 +355,51 @@ class PlacesCollectionCard extends StatelessWidget { final widthFactor = isTablet ? 0.25 : 0.5; final size = context.width * widthFactor - 20.0; - return FutureBuilder<(Position?, LocationPermission?)>( - future: MapUtils.checkPermAndGetLocation( - context: context, - silent: true, + return GestureDetector( + onTap: () => context.pushRoute( + PlacesCollectionRoute( + currentLocation: null, + ), ), - builder: (context, snapshot) { - var position = snapshot.data?.$1; - return GestureDetector( - onTap: () => context.pushRoute( - PlacesCollectionRoute( - currentLocation: position != null - ? LatLng(position.latitude, position.longitude) - : null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: size, + width: size, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20)), + color: + context.colorScheme.secondaryContainer.withAlpha(100), + ), + child: IgnorePointer( + child: MapThumbnail( + zoom: 8, + centre: const LatLng( + 21.44950, + -157.91959, + ), + showAttribution: false, + themeMode: context.isDarkTheme + ? ThemeMode.dark + : ThemeMode.light, + ), + ), ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: size, - width: size, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: - const BorderRadius.all(Radius.circular(20)), - color: context.colorScheme.secondaryContainer - .withAlpha(100), - ), - child: IgnorePointer( - child: snapshot.connectionState == - ConnectionState.waiting - ? const Center(child: CircularProgressIndicator()) - : MapThumbnail( - zoom: 8, - centre: LatLng( - position?.latitude ?? 21.44950, - position?.longitude ?? -157.91959, - ), - showAttribution: false, - themeMode: context.isDarkTheme - ? ThemeMode.dark - : ThemeMode.light, - ), - ), - ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'places'.tr(), + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w500, ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'places'.tr(), - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], + ), ), - ); - }, + ], + ), ); }, ); From a1f8150c30c09e163f85738ee13e55226281048f Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 25 Apr 2025 14:39:14 -0500 Subject: [PATCH 066/356] fix: Authelia OAuth code verifier value contains invalid characters (#17886) * fix(mobile): Authelia OAuth code verifier value contains invalid characters * Refactor * Refactoring with Jason * Refactoring with Jason --- .../lib/widgets/forms/login/login_form.dart | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 3433648e9f..5374d1ef33 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -207,9 +207,27 @@ class LoginForm extends HookConsumerWidget { } String generateRandomString(int length) { + const chars = + 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; final random = Random.secure(); - return base64Url - .encode(List.generate(32, (i) => random.nextInt(256))); + return String.fromCharCodes( + Iterable.generate( + length, + (_) => chars.codeUnitAt(random.nextInt(chars.length)), + ), + ); + } + + List randomBytes(int length) { + final random = Random.secure(); + return List.generate(length, (i) => random.nextInt(256)); + } + + /// Per specification, the code verifier must be 43-128 characters long + /// and consist of characters [A-Z, a-z, 0-9, "-", ".", "_", "~"] + /// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 + String randomCodeVerifier() { + return base64Url.encode(randomBytes(42)); } Future generatePKCECodeChallenge(String codeVerifier) async { @@ -223,7 +241,8 @@ class LoginForm extends HookConsumerWidget { String? oAuthServerUrl; final state = generateRandomString(32); - final codeVerifier = generateRandomString(64); + + final codeVerifier = randomCodeVerifier(); final codeChallenge = await generatePKCECodeChallenge(codeVerifier); try { From 02994883fe3f3972323bb6759d0170a4062f5236 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 19:44:05 +0000 Subject: [PATCH 067/356] chore: version v1.132.3 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 17 files changed, 30 insertions(+), 26 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index fe75b7ce8b..fe428b2714 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.64", + "version": "2.2.65", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.64", + "version": "2.2.65", "license": "GNU Affero General Public License version 3", "dependencies": { "chokidar": "^4.0.3", @@ -54,7 +54,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.132.2", + "version": "1.132.3", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index b47156f043..b2d29d6bb9 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.64", + "version": "2.2.65", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 60a7436afb..1e45c7a696 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.132.3", + "url": "https://v1.132.3.archive.immich.app" + }, { "label": "v1.132.2", "url": "https://v1.132.2.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index c5364d95f8..af8117da2c 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.132.2", + "version": "1.132.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.132.2", + "version": "1.132.3", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -44,7 +44,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.64", + "version": "2.2.65", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -93,7 +93,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.132.2", + "version": "1.132.3", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index afbf06f34b..c4da9b8a4a 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.132.2", + "version": "1.132.3", "description": "", "main": "index.js", "type": "module", diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 013a0235d8..a0b08bb316 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 196, - "android.injected.version.name" => "1.132.2", + "android.injected.version.code" => 197, + "android.injected.version.name" => "1.132.3", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 5d46f0ebcf..cca3ac33b3 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.132.2" + version_number: "1.132.3" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 2ff35f9537..4f9b062ba6 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.132.2 +- API version: 1.132.3 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index bda35258f9..08e9661d58 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.132.2+196 +version: 1.132.3+197 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index b242ade761..f2851d7cf1 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7656,7 +7656,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.132.2", + "version": "1.132.3", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 249ffe9960..c102f594cf 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.132.2", + "version": "1.132.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.132.2", + "version": "1.132.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 6194abe583..70f76512b4 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.132.2", + "version": "1.132.3", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 5eca3c83eb..51e17c08ac 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.132.2 + * 1.132.3 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index 8214a6f874..b1fdfa1f9d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.132.2", + "version": "1.132.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich", - "version": "1.132.2", + "version": "1.132.3", "hasInstallScript": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/server/package.json b/server/package.json index b3ce836f7c..f68ba71564 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.132.2", + "version": "1.132.3", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index d6fcb816f8..37f944d3bb 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.132.2", + "version": "1.132.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.132.2", + "version": "1.132.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", @@ -82,7 +82,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.132.2", + "version": "1.132.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index b4b9a80da1..c32e7b04a8 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.132.2", + "version": "1.132.3", "license": "GNU Affero General Public License version 3", "type": "module", "scripts": { From 3858973be5a267b2b4ef0d5de832156c967807c5 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 27 Apr 2025 23:00:40 -0500 Subject: [PATCH 068/356] chore(mobile): translation (#17920) --- mobile/lib/pages/onboarding/permission_onboarding.page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/pages/onboarding/permission_onboarding.page.dart b/mobile/lib/pages/onboarding/permission_onboarding.page.dart index a6768cc207..b0a1b34b06 100644 --- a/mobile/lib/pages/onboarding/permission_onboarding.page.dart +++ b/mobile/lib/pages/onboarding/permission_onboarding.page.dart @@ -44,7 +44,7 @@ class PermissionOnboardingPage extends HookConsumerWidget { } }), child: const Text( - 'grant_permission', + 'continue', ).tr(), ), ], From 205260d31c72c8782182e978607423bf50e0dc44 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 27 Apr 2025 23:02:03 -0500 Subject: [PATCH 069/356] chore: post release tasks (#17895) --- mobile/ios/Runner.xcodeproj/project.pbxproj | 14 ++++++++------ mobile/ios/Runner/Info.plist | 4 ++-- mobile/ios/fastlane/Fastfile | 3 +++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 09e362b057..744ddc053b 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -261,9 +261,11 @@ 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; }; FAC6F88F2D287C890078CB2F = { CreatedOnToolsVersion = 16.0; + ProvisioningStyle = Automatic; }; }; }; @@ -541,7 +543,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 203; + CURRENT_PROJECT_VERSION = 205; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -685,7 +687,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 203; + CURRENT_PROJECT_VERSION = 205; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -715,7 +717,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 203; + CURRENT_PROJECT_VERSION = 205; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -748,7 +750,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 203; + CURRENT_PROJECT_VERSION = 205; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -792,7 +794,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 203; + CURRENT_PROJECT_VERSION = 205; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -833,7 +835,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 203; + CURRENT_PROJECT_VERSION = 205; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index fc259703c8..38394f0f1b 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -78,7 +78,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.132.2 + 1.132.3 CFBundleSignature ???? CFBundleURLTypes @@ -93,7 +93,7 @@ CFBundleVersion - 203 + 205 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index cca3ac33b3..3306fef1e2 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -18,6 +18,9 @@ default_platform(:ios) platform :ios do desc "iOS Release" lane :release do + enable_automatic_code_signing( + path: "./Runner.xcodeproj", + ) increment_version_number( version_number: "1.132.3" ) From 85ac0512a6b64244bd2fd475ecd6e17d5e13d4f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Tollk=C3=B6tter?= <1518021+atollk@users.noreply.github.com> Date: Mon, 28 Apr 2025 15:53:26 +0200 Subject: [PATCH 070/356] fix(web): Make date-time formatting follow locale (#17899) * fixed missing $locale parameter to .toLocaleString * Remove unused types and functions in timeline-util * remove unused export * re-enable export because it is needed for tests * format --- .../memory-page/memory-viewer.svelte | 4 +- .../server-about-modal.svelte | 16 +++-- .../user-settings-page/device-card.svelte | 4 +- web/src/lib/utils/byte-units.ts | 1 + web/src/lib/utils/thumbnail-util.ts | 5 +- web/src/lib/utils/timeline-util.ts | 71 +++---------------- 6 files changed, 31 insertions(+), 70 deletions(-) diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index e39a3cfa74..45aaf85b67 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -544,7 +544,9 @@

    - {fromLocalDateTime(current.memory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL)} + {fromLocalDateTime(current.memory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL, { + locale: $locale, + })}

    {current.asset.exifInfo?.city || ''} diff --git a/web/src/lib/components/shared-components/server-about-modal.svelte b/web/src/lib/components/shared-components/server-about-modal.svelte index cf935cd314..1284bb126d 100644 --- a/web/src/lib/components/shared-components/server-about-modal.svelte +++ b/web/src/lib/components/shared-components/server-about-modal.svelte @@ -6,6 +6,7 @@ import { t } from 'svelte-i18n'; import { mdiAlert } from '@mdi/js'; import Icon from '$lib/components/elements/icon.svelte'; + import { locale } from '$lib/stores/preferences.store'; interface Props { onClose: () => void; @@ -177,16 +178,19 @@ {$t('version_history_item', { values: { version: item.version, - date: createdAt.toLocaleString({ - month: 'short', - day: 'numeric', - year: 'numeric', - }), + date: createdAt.toLocaleString( + { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + { locale: $locale }, + ), }, })} diff --git a/web/src/lib/components/user-settings-page/device-card.svelte b/web/src/lib/components/user-settings-page/device-card.svelte index 5b70b006be..74e6579dd0 100644 --- a/web/src/lib/components/user-settings-page/device-card.svelte +++ b/web/src/lib/components/user-settings-page/device-card.svelte @@ -64,7 +64,9 @@ {DateTime.fromISO(device.updatedAt, { locale: $locale }).toRelativeCalendar(options)} - - {DateTime.fromISO(device.updatedAt, { locale: $locale }).toLocaleString(DateTime.DATETIME_MED)} + {DateTime.fromISO(device.updatedAt, { locale: $locale }).toLocaleString(DateTime.DATETIME_MED, { + locale: $locale, + })}

    diff --git a/web/src/lib/utils/byte-units.ts b/web/src/lib/utils/byte-units.ts index dae44009e2..218e22f671 100644 --- a/web/src/lib/utils/byte-units.ts +++ b/web/src/lib/utils/byte-units.ts @@ -34,6 +34,7 @@ export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, Byte * * de: `1,5 KiB` * * @param bytes number of bytes + * @param locale locale to use, default is `navigator.language` * @param maxPrecision maximum number of decimal places, default is `1` * @returns localized bytes with unit as string */ diff --git a/web/src/lib/utils/thumbnail-util.ts b/web/src/lib/utils/thumbnail-util.ts index a53691e716..f0043790ea 100644 --- a/web/src/lib/utils/thumbnail-util.ts +++ b/web/src/lib/utils/thumbnail-util.ts @@ -1,6 +1,7 @@ +import { locale } from '$lib/stores/preferences.store'; import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk'; import { t } from 'svelte-i18n'; -import { derived } from 'svelte/store'; +import { derived, get } from 'svelte/store'; import { fromLocalDateTime } from './timeline-util'; /** @@ -43,7 +44,7 @@ export const getAltText = derived(t, ($t) => { return asset.exifInfo.description; } - const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' }); + const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' }, { locale: get(locale) }); const hasPlace = !!asset.exifInfo?.city && !!asset.exifInfo?.country; const names = asset.people?.filter((p) => p.name).map((p) => p.name) ?? []; const peopleCount = names.length; diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index f40e2bc3eb..21a7d23953 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -1,41 +1,13 @@ -import type { AssetBucket } from '$lib/stores/assets-store.svelte'; import { locale } from '$lib/stores/preferences.store'; -import { type CommonJustifiedLayout } from '$lib/utils/layout-utils'; - -import type { AssetResponseDto } from '@immich/sdk'; import { memoize } from 'lodash-es'; import { DateTime, type LocaleOptions } from 'luxon'; import { get } from 'svelte/store'; -export type DateGroup = { - bucket: AssetBucket; - index: number; - row: number; - col: number; - date: DateTime; - groupTitle: string; - assets: AssetResponseDto[]; - assetsIntersecting: boolean[]; - height: number; - intersecting: boolean; - geometry: CommonJustifiedLayout; -}; export type ScrubberListener = ( bucketDate: string | undefined, overallScrollPercent: number, bucketScrollPercent: number, ) => void | Promise; -export type ScrollTargetListener = ({ - bucket, - dateGroup, - asset, - offset, -}: { - bucket: AssetBucket; - dateGroup: DateGroup; - asset: AssetResponseDto; - offset: number; -}) => void; export const fromLocalDateTime = (localDateTime: string) => DateTime.fromISO(localDateTime, { zone: 'UTC', locale: get(locale) }); @@ -43,31 +15,6 @@ export const fromLocalDateTime = (localDateTime: string) => export const fromDateTimeOriginal = (dateTimeOriginal: string, timeZone: string) => DateTime.fromISO(dateTimeOriginal, { zone: timeZone }); -export type LayoutBox = { - aspectRatio: number; - top: number; - width: number; - height: number; - left: number; - forcedAspectRatio?: boolean; -}; - -export function findTotalOffset(element: HTMLElement, stop: HTMLElement) { - let offset = 0; - while (element.offsetParent && element !== stop) { - offset += element.offsetTop; - element = element.offsetParent as HTMLElement; - } - return offset; -} - -export const groupDateFormat: Intl.DateTimeFormatOptions = { - weekday: 'short', - month: 'short', - day: 'numeric', - year: 'numeric', -}; - export function formatGroupTitle(_date: DateTime): string { if (!_date.isValid) { return _date.toString(); @@ -87,20 +34,24 @@ export function formatGroupTitle(_date: DateTime): string { // Last week if (date >= today.minus({ days: 6 }) && date < today) { - return date.toLocaleString({ weekday: 'long' }); + return date.toLocaleString({ weekday: 'long' }, { locale: get(locale) }); } // This year if (today.hasSame(date, 'year')) { - return date.toLocaleString({ - weekday: 'short', - month: 'short', - day: 'numeric', - }); + return date.toLocaleString( + { + weekday: 'short', + month: 'short', + day: 'numeric', + }, + { locale: get(locale) }, + ); } - return getDateLocaleString(date); + return getDateLocaleString(date, { locale: get(locale) }); } + export const getDateLocaleString = (date: DateTime, opts?: LocaleOptions): string => date.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY, opts); From e6c575c33ebe559598a8e68eecbd31e0ccfc8a9a Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 28 Apr 2025 09:53:53 -0400 Subject: [PATCH 071/356] feat: rtl (#17860) --- web/package-lock.json | 8 +++---- web/package.json | 2 +- .../admin-page/jobs/job-tile.svelte | 6 ++--- .../server-stats/server-stats-panel.svelte | 4 ++-- .../admin-page/server-stats/stats-card.svelte | 2 +- .../settings/auth/auth-settings.svelte | 8 +++---- .../backup-settings/backup-settings.svelte | 2 +- .../settings/ffmpeg/ffmpeg-settings.svelte | 12 +++++----- .../settings/image/image-settings.svelte | 4 ++-- .../settings/job-settings/job-settings.svelte | 4 ++-- .../library-settings/library-settings.svelte | 6 ++--- .../logging-settings/logging-settings.svelte | 2 +- .../machine-learning-settings.svelte | 8 +++---- .../settings/map-settings/map-settings.svelte | 4 ++-- .../metadata-settings.svelte | 2 +- .../new-version-check-settings.svelte | 2 +- .../notification-settings.svelte | 2 +- .../settings/server/server-settings.svelte | 4 ++-- .../storage-template-settings.svelte | 2 +- .../template-settings.svelte | 4 ++-- .../settings/theme/theme-settings.svelte | 2 +- .../trash-settings/trash-settings.svelte | 2 +- .../user-settings/user-settings.svelte | 4 ++-- .../album-page/album-card-group.svelte | 4 ++-- .../components/album-page/album-card.svelte | 2 +- .../components/album-page/album-viewer.svelte | 2 +- .../album-page/albums-table-row.svelte | 4 ++-- .../components/album-page/albums-table.svelte | 8 +++---- .../album-page/user-selection-modal.svelte | 4 ++-- .../asset-viewer/activity-viewer.svelte | 14 ++++++------ .../asset-viewer/album-list-item.svelte | 2 +- .../asset-viewer/asset-viewer.svelte | 10 ++++----- .../asset-viewer/detail-panel-location.svelte | 4 ++-- .../asset-viewer/detail-panel-tags.svelte | 4 ++-- .../asset-viewer/detail-panel.svelte | 2 +- .../asset-viewer/download-panel.svelte | 6 ++--- .../face-editor/face-editor.svelte | 8 +++---- .../asset-viewer/photo-viewer.svelte | 2 +- .../assets/thumbnail/image-thumbnail.svelte | 2 +- .../assets/thumbnail/thumbnail.svelte | 12 +++++----- .../assets/thumbnail/video-thumbnail.svelte | 4 ++-- .../components/elements/buttons/button.svelte | 2 +- .../elements/buttons/skip-link.svelte | 2 +- .../lib/components/elements/dropdown.svelte | 4 ++-- .../faces-page/edit-name-input.svelte | 2 +- .../faces-page/face-thumbnail.svelte | 6 ++--- .../manage-people-visibility.svelte | 6 ++--- .../faces-page/merge-face-selector.svelte | 4 ++-- .../components/faces-page/people-card.svelte | 4 ++-- .../faces-page/person-side-panel.svelte | 16 +++++++------- .../faces-page/unmerge-face-selector.svelte | 6 ++--- .../forms/library-import-paths-form.svelte | 4 ++-- .../forms/library-scan-settings-form.svelte | 2 +- .../components/forms/tag-asset-form.svelte | 4 ++-- .../components/layouts/AuthPageLayout.svelte | 2 +- .../memory-page/memory-viewer.svelte | 20 ++++++++--------- .../photos-page/asset-date-group.svelte | 2 +- .../components/photos-page/asset-grid.svelte | 2 +- .../components/photos-page/memory-lane.svelte | 12 +++++----- .../components/photos-page/skeleton.svelte | 2 +- .../places-page/places-card-group.svelte | 4 ++-- .../shared-components/change-date.svelte | 2 +- .../shared-components/change-location.svelte | 4 ++-- .../shared-components/combobox.svelte | 16 +++++++------- .../context-menu/button-context-menu.svelte | 12 +++++++++- .../context-menu/context-menu.svelte | 14 ++++++++---- .../context-menu/menu-option.svelte | 4 ++-- .../right-click-context-menu.svelte | 4 ++-- .../shared-components/control-app-bar.svelte | 2 +- .../shared-components/duplicates-modal.svelte | 2 +- .../full-screen-modal.svelte | 2 +- .../immich-logo-small-link.svelte | 2 +- .../navigation-bar/account-info-panel.svelte | 4 ++-- .../navigation-bar/navigation-bar.svelte | 8 +++---- .../navigation-loading-bar.svelte | 2 +- .../notification/notification-card.svelte | 4 ++-- .../notification/notification-list.svelte | 2 +- .../shared-components/password-field.svelte | 2 +- .../progress-bar/progress-bar.svelte | 2 +- .../scrubber/scrubber.svelte | 20 ++++++++--------- .../search-bar/search-bar.svelte | 10 ++++----- .../search-bar/search-history-box.svelte | 4 ++-- .../search-bar/search-tags-section.svelte | 4 ++-- .../settings/setting-accordion.svelte | 4 ++-- .../settings/setting-input-field.svelte | 2 +- .../settings/setting-select.svelte | 4 ++-- .../settings/setting-switch.svelte | 2 +- .../shared-components/show-shortcuts.svelte | 4 ++-- .../side-bar/purchase-info.svelte | 4 ++-- .../side-bar/recent-albums.svelte | 2 +- .../side-bar/server-status.svelte | 2 +- .../side-bar/side-bar-link.svelte | 6 ++--- .../side-bar/side-bar-section.svelte | 4 ++-- .../side-bar/storage-space.svelte | 2 +- .../shared-components/tree/breadcrumbs.svelte | 2 +- .../shared-components/tree/tree-items.svelte | 2 +- .../shared-components/tree/tree.svelte | 4 ++-- .../shared-components/upload-panel.svelte | 6 ++--- .../user-settings-page/app-settings.svelte | 18 +++++++-------- .../change-password-settings.svelte | 2 +- .../user-settings-page/device-card.svelte | 4 ++-- .../download-settings.svelte | 2 +- .../feature-settings.svelte | 22 +++++++++---------- .../notifications-settings.svelte | 8 +++---- .../partner-selection-modal.svelte | 2 +- .../partner-settings.svelte | 2 +- .../user-api-key-list.svelte | 2 +- .../user-profile-settings.svelte | 2 +- .../user-purchase-settings.svelte | 4 ++-- .../user-usage-statistic.svelte | 4 ++-- .../duplicates/duplicate-asset.svelte | 10 ++++----- .../duplicates-compare-control.svelte | 20 +++++------------ web/src/lib/constants.ts | 12 +++++----- web/src/lib/stores/event-manager.svelte.ts | 1 + web/src/lib/stores/language-manager.svelte.ts | 21 ++++++++++++++++++ web/src/lib/utils/album-utils.ts | 2 +- .../[[assetId=id]]/+page.svelte | 4 ++-- web/src/routes/(user)/explore/+page.svelte | 6 ++--- .../[[assetId=id]]/+page.svelte | 4 ++-- web/src/routes/(user)/people/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 6 ++--- .../[[assetId=id]]/+page.svelte | 12 +++++----- web/src/routes/(user)/sharing/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- web/src/routes/+layout.svelte | 5 ++++- web/src/routes/admin/jobs-status/+page.svelte | 2 +- .../admin/library-management/+page.svelte | 4 ++-- web/src/routes/admin/repair/+page.svelte | 14 ++++++------ .../routes/admin/user-management/+page.svelte | 2 +- web/src/routes/auth/login/+page.svelte | 2 +- 130 files changed, 354 insertions(+), 323 deletions(-) create mode 100644 web/src/lib/stores/language-manager.svelte.ts diff --git a/web/package-lock.json b/web/package-lock.json index 37f944d3bb..37f7faf711 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.17.3", + "@immich/ui": "^0.18.1", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", @@ -1320,9 +1320,9 @@ "link": true }, "node_modules/@immich/ui": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.17.4.tgz", - "integrity": "sha512-a6M7Fxno5fwY5A0kxdluS8r+A4L6xZhSTKMW8c8hoFhQHvbBTHAsGFKQF3GOEQLOlUuvsS2Lt7dMevBlAPgo/A==", + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.18.1.tgz", + "integrity": "sha512-XWWO6OTfH3MektyxCn0hWefZyOGyWwwx/2zHinuShpxTHSyfveJ4mOkFP8DkyMz0dnvJ1EfdkPBMkld3y5R/Hw==", "license": "GNU Affero General Public License version 3", "dependencies": { "@mdi/js": "^7.4.47", diff --git a/web/package.json b/web/package.json index c32e7b04a8..4102765f70 100644 --- a/web/package.json +++ b/web/package.json @@ -27,7 +27,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.17.3", + "@immich/ui": "^0.18.1", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte index 80dd29e0be..c77ff60f22 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte @@ -51,7 +51,7 @@ let isIdle = $derived(!queueStatus.isActive && !queueStatus.isPaused); let multipleButtons = $derived(allText || refreshText); - const commonClasses = 'flex place-items-center justify-between w-full py-2 sm:py-4 pr-4 pl-6'; + const commonClasses = 'flex place-items-center justify-between w-full py-2 sm:py-4 pe-4 ps-6';

    {$t('active')}

    @@ -119,7 +119,7 @@

    {waitingCount.toLocaleString($locale)} diff --git a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte index bb288511ac..8bae8fee4b 100644 --- a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte +++ b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte @@ -79,7 +79,7 @@ {zeros(statsUsage)}{statsUsage} - {statsUsageUnit} + {statsUsageUnit}

    @@ -88,7 +88,7 @@

    {$t('user_usage_detail').toUpperCase()}

    - +
    diff --git a/web/src/lib/components/admin-page/server-stats/stats-card.svelte b/web/src/lib/components/admin-page/server-stats/stats-card.svelte index 14d32c055f..b1804427e9 100644 --- a/web/src/lib/components/admin-page/server-stats/stats-card.svelte +++ b/web/src/lib/components/admin-page/server-stats/stats-card.svelte @@ -31,7 +31,7 @@ class="text-immich-primary dark:text-immich-dark-primary">{value} {#if unit} - {unit} + {unit} {/if} diff --git a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte index 5380a76286..67da6bb7f2 100644 --- a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte +++ b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte @@ -76,13 +76,13 @@
    e.preventDefault()}> -
    +
    -
    +

    {#snippet children({ message })} @@ -243,8 +243,8 @@ title={$t('admin.password_settings')} subtitle={$t('admin.password_settings_description')} > -

    -
    +
    +
    -
    +
    -
    +

    @@ -70,7 +70,7 @@ title={$t('admin.transcoding_policy')} subtitle={$t('admin.transcoding_policy_description')} > -

    +
    -
    +
    -
    +
    -
    +
    -
    +
    onReset({ ...options, configKeys: ['ffmpeg'] })} onSave={() => onSave({ ffmpeg: config.ffmpeg })} diff --git a/web/src/lib/components/admin-page/settings/image/image-settings.svelte b/web/src/lib/components/admin-page/settings/image/image-settings.svelte index 9a66ad9c97..9a32e8e4e0 100644 --- a/web/src/lib/components/admin-page/settings/image/image-settings.svelte +++ b/web/src/lib/components/admin-page/settings/image/image-settings.svelte @@ -40,7 +40,7 @@
    -
    +
    -
    +
    onReset({ ...options, configKeys: ['image'] })} onSave={() => onSave({ image: config.image })} diff --git a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte index 5a95dbea30..e9f54e7ee8 100644 --- a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte +++ b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte @@ -47,7 +47,7 @@
    {#each jobNames as jobName (jobName)} -
    +
    {#if isSystemConfigJobDto(jobName)} {/each} -
    +
    onReset({ ...options, configKeys: ['job'] })} onSave={() => onSave({ job: config.job })} diff --git a/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte b/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte index b1012c0287..390b167a54 100644 --- a/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte +++ b/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte @@ -47,14 +47,14 @@
    -
    +
    -
    +
    -
    +
    -
    +
    1} -
    +
    -
    +
    -
    +
    -
    +

    {/snippet} -
    +
    -
    +
    -
    +
    -
    +
    -
    +
    -
    +
    onReset({ ...options, configKeys: ['server'] })} onSave={() => onSave({ server: config.server })} diff --git a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte index 67299d8f6b..efc42bf8b7 100644 --- a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte +++ b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte @@ -141,7 +141,7 @@

    {#await getTemplateOptions() then} -
    +
    -
    +

    {$t('admin.template_email_if_empty')} @@ -102,7 +102,7 @@ onclick={() => getTemplate(templateName, config.templates.email[templateKey])} title={$t('admin.template_email_preview')} > - + {$t('admin.template_email_preview')}

    diff --git a/web/src/lib/components/admin-page/settings/theme/theme-settings.svelte b/web/src/lib/components/admin-page/settings/theme/theme-settings.svelte index 79b5f838e3..64b4b92b5e 100644 --- a/web/src/lib/components/admin-page/settings/theme/theme-settings.svelte +++ b/web/src/lib/components/admin-page/settings/theme/theme-settings.svelte @@ -26,7 +26,7 @@
    -
    +
    -
    +

    diff --git a/web/src/lib/components/admin-page/settings/user-settings/user-settings.svelte b/web/src/lib/components/admin-page/settings/user-settings/user-settings.svelte index f96c3808a8..422e1ebe49 100644 --- a/web/src/lib/components/admin-page/settings/user-settings/user-settings.svelte +++ b/web/src/lib/components/admin-page/settings/user-settings/user-settings.svelte @@ -24,7 +24,7 @@
    e.preventDefault()}> -
    +
    -
    +
    onReset({ ...options, configKeys: ['user'] })} onSave={() => onSave({ user: config.user })} diff --git a/web/src/lib/components/album-page/album-card-group.svelte b/web/src/lib/components/album-page/album-card-group.svelte index 9b2aa11552..3556d9fea4 100644 --- a/web/src/lib/components/album-page/album-card-group.svelte +++ b/web/src/lib/components/album-page/album-card-group.svelte @@ -48,7 +48,7 @@
    diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte index cec4919e4e..06ec030bea 100644 --- a/web/src/lib/components/album-page/album-card.svelte +++ b/web/src/lib/components/album-page/album-card.svelte @@ -40,7 +40,7 @@ {#if onShowContextMenu}
    {#if album.description}

    {album.description}

    diff --git a/web/src/lib/components/album-page/albums-table-row.svelte b/web/src/lib/components/album-page/albums-table-row.svelte index c900930f8a..034ed62010 100644 --- a/web/src/lib/components/album-page/albums-table-row.svelte +++ b/web/src/lib/components/album-page/albums-table-row.svelte @@ -35,13 +35,13 @@ onclick={() => goto(`${AppRoute.ALBUMS}/${album.id}`)} {oncontextmenu} > -
    + {album.albumName} {#if album.shared} - +
    @@ -48,18 +48,18 @@ class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg" > toggleAlbumGroupCollapsing(albumGroup.id)} aria-expanded={!isCollapsed} > - diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/components/album-page/user-selection-modal.svelte index 1496c1ce66..9ee7cc550d 100644 --- a/web/src/lib/components/album-page/user-selection-modal.svelte +++ b/web/src/lib/components/album-page/user-selection-modal.svelte @@ -94,7 +94,7 @@ -
    +

    {user.name}

    @@ -136,7 +136,7 @@ class="flex w-full place-items-center gap-4 p-4" > -
    +

    {user.name}

    diff --git a/web/src/lib/components/asset-viewer/activity-viewer.svelte b/web/src/lib/components/asset-viewer/activity-viewer.svelte index caa1ced290..94b66d4c22 100644 --- a/web/src/lib/components/asset-viewer/activity-viewer.svelte +++ b/web/src/lib/components/asset-viewer/activity-viewer.svelte @@ -186,7 +186,7 @@ > {#each reactions as reaction, index (reaction.id)} {#if reaction.type === ReactionType.Comment} -
    +
    @@ -202,7 +202,7 @@ {/if} {#if reaction.user.id === user.id || albumOwnerId === user.id} -
    +
    -
    +
    @@ -255,7 +255,7 @@ {/if} {#if reaction.user.id === user.id || albumOwnerId === user.id} -
    +
    {#if isSendingMessage} -
    +
    {:else if message} -
    +
    diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 91461d574d..98bc087f71 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -422,7 +422,7 @@
    @@ -547,7 +547,7 @@ /> {/if} {#if $slideshowState === SlideshowState.None && isShared && ((album && album.isActivityEnabled) || numberOfComments > 0)} -
    +
    ($isShowDetail = false)} /> @@ -582,7 +582,7 @@
    @@ -631,7 +631,7 @@
    (isOwner ? (isShowChangeLocation = true) : null)} title={isOwner ? $t('edit_location') : ''} class:hover:dark:text-immich-dark-primary={isOwner} @@ -68,7 +68,7 @@ {:else if !asset.exifInfo?.city && isOwner} {$t('merge')} {/snippet} diff --git a/web/src/lib/components/faces-page/people-card.svelte b/web/src/lib/components/faces-page/people-card.svelte index 4aff4e96f8..b740953340 100644 --- a/web/src/lib/components/faces-page/people-card.svelte +++ b/web/src/lib/components/faces-page/people-card.svelte @@ -54,7 +54,7 @@ circle /> {#if person.isFavorite} -
    +
    {/if} @@ -62,7 +62,7 @@
    {#if showVerticalDots} -
    +
    ($boundingBoxesArray = [peopleWithFaces[index]])} onmouseover={() => ($boundingBoxesArray = [peopleWithFaces[index]])} onmouseleave={() => ($boundingBoxesArray = [])} @@ -303,7 +303,7 @@

    {/if} -
    +
    {#if selectedPersonToCreate[face.id] || selectedPersonToReassign[face.id]} handleReset(face.id)} /> {:else} @@ -321,29 +321,29 @@ title={$t('select_new_face')} size="18" padding="1" - class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform" + class="absolute start-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform" onclick={() => handleFacePicker(face)} /> {/if}
    -
    +
    {#if !selectedPersonToCreate[face.id] && !selectedPersonToReassign[face.id] && !face.person}
    {/if}
    {#if face.person != null} -
    +
    deleteAssetFace(face)} />
    diff --git a/web/src/lib/components/faces-page/unmerge-face-selector.svelte b/web/src/lib/components/faces-page/unmerge-face-selector.svelte index e808c98748..41c584d602 100644 --- a/web/src/lib/components/faces-page/unmerge-face-selector.svelte +++ b/web/src/lib/components/faces-page/unmerge-face-selector.svelte @@ -120,7 +120,7 @@
    {#snippet leading()} @@ -140,7 +140,7 @@ {:else} {/if} - {$t('create_new_person')} {$t('create_new_person')} {$t('reassign')}
    {/snippet} diff --git a/web/src/lib/components/forms/library-import-paths-form.svelte b/web/src/lib/components/forms/library-import-paths-form.svelte index 639b81071f..64c32532ef 100644 --- a/web/src/lib/components/forms/library-import-paths-form.svelte +++ b/web/src/lib/components/forms/library-import-paths-form.svelte @@ -173,7 +173,7 @@ {/if} -
    + {albumGroup.name} - + ({$t('albums_count', { values: { count: albumGroup.albums.length } })})
    +
    {#each validatedPaths as validatedPath, listIndex (validatedPath.importPath)} - +
    + {#if validatedPath.isValid} - +
    {#each exclusionPatterns as exclusionPattern, listIndex (exclusionPattern)}

    {tag.value} @@ -81,7 +81,7 @@


    diff --git a/web/src/lib/components/shared-components/change-date.svelte b/web/src/lib/components/shared-components/change-date.svelte index 13b2752f0c..ef682d9048 100644 --- a/web/src/lib/components/shared-components/change-date.svelte +++ b/web/src/lib/components/shared-components/change-date.svelte @@ -144,7 +144,7 @@ {#snippet promptSnippet()} -
    +
    diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte index a84838f1db..f981e85029 100644 --- a/web/src/lib/components/shared-components/change-location.svelte +++ b/web/src/lib/components/shared-components/change-location.svelte @@ -147,7 +147,7 @@ : ''}" onclick={() => handleUseSuggested(place.latitude, place.longitude)} > -

    +

    {getLocation(place.name, place.admin1name, place.admin2name)}

    @@ -189,7 +189,7 @@ {/await}
    -
    +
    {#if isActive} -
    +
    @@ -273,11 +273,11 @@ aria-expanded={isOpen} autocomplete="off" bind:this={input} - class:!pl-8={isActive} + class:!ps-8={isActive} class:!rounded-b-none={isOpen && dropdownDirection === 'bottom'} class:!rounded-t-none={isOpen && dropdownDirection === 'top'} class:cursor-pointer={!isActive} - class="immich-form-input text-sm text-left w-full !pr-12 transition-all" + class="immich-form-input text-sm w-full !pe-12 transition-all" id={inputId} onfocus={activate} oninput={onInput} @@ -325,8 +325,8 @@ />
    {#if selectedOption} @@ -341,7 +341,7 @@ role="listbox" id={listboxId} transition:fly={{ duration: 250 }} - class="fixed text-left text-sm w-full overflow-y-auto bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-900 z-[10000]" + class="fixed text-start text-sm w-full overflow-y-auto bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-900 z-[10000]" class:rounded-b-xl={dropdownDirection === 'bottom'} class:rounded-t-xl={dropdownDirection === 'top'} class:shadow={dropdownDirection === 'bottom'} @@ -360,7 +360,7 @@ role="option" aria-selected={selectedIndex === 0} aria-disabled={true} - class="text-left w-full px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-200 aria-selected:dark:bg-gray-700" + class="text-start w-full px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-200 aria-selected:dark:bg-gray-700" id={`${listboxId}-${0}`} onclick={closeDropdown} > @@ -372,7 +372,7 @@
  • handleSelect(option)} role="option" diff --git a/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte b/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte index a3e12e4f12..67a17db950 100644 --- a/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte +++ b/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte @@ -7,6 +7,7 @@ } from '$lib/components/elements/buttons/circle-icon-button.svelte'; import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte'; import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store'; + import { languageManager } from '$lib/stores/language-manager.svelte'; import { getContextMenuPositionFromBoundingRect, getContextMenuPositionFromEvent, @@ -26,6 +27,7 @@ /** * The direction in which the context menu should open. */ + // TODO change to start vs end direction?: 'left' | 'right'; color?: Color; size?: string | undefined; @@ -62,7 +64,15 @@ const menuId = `context-menu-${id}`; const openDropdown = (event: KeyboardEvent | MouseEvent) => { - contextMenuPosition = getContextMenuPositionFromEvent(event, align); + let layoutAlign = align; + if (languageManager.rtl) { + if (align.includes('left')) { + layoutAlign = align.replace('left', 'right') as Align; + } else if (align.includes('right')) { + layoutAlign = align.replace('right', 'left') as Align; + } + } + contextMenuPosition = getContextMenuPositionFromEvent(event, layoutAlign); isOpen = true; menuContainer?.focus(); }; diff --git a/web/src/lib/components/shared-components/context-menu/context-menu.svelte b/web/src/lib/components/shared-components/context-menu/context-menu.svelte index 976f86d3e5..a79a3bd385 100644 --- a/web/src/lib/components/shared-components/context-menu/context-menu.svelte +++ b/web/src/lib/components/shared-components/context-menu/context-menu.svelte @@ -3,6 +3,7 @@ import { slide } from 'svelte/transition'; import { clickOutside } from '$lib/actions/click-outside'; import type { Snippet } from 'svelte'; + import { languageManager } from '$lib/stores/language-manager.svelte'; interface Props { isVisible?: boolean; @@ -41,12 +42,17 @@ $effect(() => { if (menuElement) { + let layoutDirection = direction; + if (languageManager.rtl) { + layoutDirection = direction === 'left' ? 'right' : 'left'; + } + const rect = menuElement.getBoundingClientRect(); - const directionWidth = direction === 'left' ? rect.width : 0; + const directionWidth = layoutDirection === 'left' ? rect.width : 0; const menuHeight = Math.min(menuElement.clientHeight, height) || 0; - left = Math.min(window.innerWidth - rect.width, x - directionWidth); - top = Math.min(window.innerHeight - menuHeight, y); + left = Math.max(8, Math.min(window.innerWidth - rect.width, x - directionWidth)); + top = Math.max(8, Math.min(window.innerHeight - menuHeight, y)); } }); @@ -66,7 +72,7 @@ aria-labelledby={ariaLabelledBy} bind:this={menuElement} class="{isVisible - ? 'max-h-dvh max-h-svh' + ? 'max-h-dvh' : 'max-h-0'} flex flex-col transition-all duration-[250ms] ease-in-out outline-none overflow-auto" role="menu" tabindex="-1" diff --git a/web/src/lib/components/shared-components/context-menu/menu-option.svelte b/web/src/lib/components/shared-components/context-menu/menu-option.svelte index b3a6d41018..b331804958 100644 --- a/web/src/lib/components/shared-components/context-menu/menu-option.svelte +++ b/web/src/lib/components/shared-components/context-menu/menu-option.svelte @@ -53,7 +53,7 @@ onclick={handleClick} onmouseover={() => ($selectedIdStore = id)} onmouseleave={() => ($selectedIdStore = undefined)} - class="w-full p-4 text-left text-sm font-medium {textColor} focus:outline-none focus:ring-2 focus:ring-inset cursor-pointer border-gray-200 flex gap-2 items-center {isActive + class="w-full p-4 text-start text-sm font-medium {textColor} focus:outline-none focus:ring-2 focus:ring-inset cursor-pointer border-gray-200 flex gap-2 items-center {isActive ? activeColor : 'bg-slate-100'}" role="menuitem" @@ -65,7 +65,7 @@
    {text} {#if shortcutLabel} - + {shortcutLabel} {/if} diff --git a/web/src/lib/components/shared-components/context-menu/right-click-context-menu.svelte b/web/src/lib/components/shared-components/context-menu/right-click-context-menu.svelte index 9b9e68b6c5..27d50f4fe5 100644 --- a/web/src/lib/components/shared-components/context-menu/right-click-context-menu.svelte +++ b/web/src/lib/components/shared-components/context-menu/right-click-context-menu.svelte @@ -38,7 +38,7 @@ const elements = document.elementsFromPoint(event.x, event.y); if (menuContainer && elements.includes(menuContainer)) { - // User right-clicked on the context menu itself, we keep the context + // User end-clicked on the context menu itself, we keep the context // menu as is return; } @@ -91,7 +91,7 @@ }, ]} > -
  • +
    diff --git a/web/src/lib/components/user-settings-page/user-profile-settings.svelte b/web/src/lib/components/user-settings-page/user-profile-settings.svelte index c36c36d7cc..90487f532f 100644 --- a/web/src/lib/components/user-settings-page/user-profile-settings.svelte +++ b/web/src/lib/components/user-settings-page/user-profile-settings.svelte @@ -42,7 +42,7 @@
    -
    +
    {#if isServerProduct}
    @@ -152,7 +152,7 @@ {/if} {:else}
    diff --git a/web/src/lib/components/user-settings-page/user-usage-statistic.svelte b/web/src/lib/components/user-settings-page/user-usage-statistic.svelte index f7de1d8f64..ad77516d55 100644 --- a/web/src/lib/components/user-settings-page/user-usage-statistic.svelte +++ b/web/src/lib/components/user-settings-page/user-usage-statistic.svelte @@ -68,7 +68,7 @@

    {$t('photos_and_videos')}

    -
    +
    @@ -92,7 +92,7 @@

    {$t('albums')}

    -
    +
    diff --git a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte index 97f44e3ec4..b8409cb0ef 100644 --- a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte +++ b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte @@ -44,14 +44,14 @@ {#if asset.isFavorite} -
    +
    {/if}
    @@ -59,7 +59,7 @@
    -
    +
    {#if isFromExternalLibrary}
    {$t('external')} @@ -68,7 +68,7 @@ {#if asset.stack?.assetCount}
    -
    {asset.stack.assetCount}
    +
    {asset.stack.assetCount}
    @@ -79,7 +79,7 @@
    @@ -143,21 +143,11 @@
    {#if trashCount === 0} - {:else} - {/each}
    diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index c985104e3c..9d427e1ea7 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -251,7 +251,7 @@
    {#if assetInteraction.selectionActive} -
    +
    cancelMultiselect(assetInteraction)} @@ -289,13 +289,13 @@
    {:else} -
    +
    goto(previousRoute)} backIcon={mdiArrowLeft}>
    -
    +
    @@ -313,13 +313,13 @@
    {getHumanReadableSearchKey(key as keyof SearchTerms)}
    {#if value !== true} -
    +
    {#if (key === 'takenAfter' || key === 'takenBefore') && typeof value === 'string'} {getHumanReadableDate(value)} {:else if key === 'personIds' && Array.isArray(value)} @@ -349,7 +349,7 @@ > {#if searchResultAlbums.length > 0}
    -
    {$t('albums').toUpperCase()}
    +
    {$t('albums').toUpperCase()}
    diff --git a/web/src/routes/(user)/sharing/+page.svelte b/web/src/routes/(user)/sharing/+page.svelte index e3d6ac1ced..a55452b5d1 100644 --- a/web/src/routes/(user)/sharing/+page.svelte +++ b/web/src/routes/(user)/sharing/+page.svelte @@ -68,7 +68,7 @@ class="flex gap-4 rounded-lg px-5 py-4 transition-all hover:bg-gray-200 dark:hover:bg-gray-700" > -
    +

    {partner.name}

    diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte index 8bb43676e8..8d33a2eb6e 100644 --- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -146,7 +146,7 @@
    -
    {$t('explorer').toUpperCase()}
    +
    {$t('explorer').toUpperCase()}
    languageManager.setLanguage(lang)); }); onDestroy(() => { diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte index 21381081e0..07757614e5 100644 --- a/web/src/routes/admin/jobs-status/+page.svelte +++ b/web/src/routes/admin/jobs-status/+page.svelte @@ -107,7 +107,7 @@ > {#snippet promptSnippet()} -
    +
    {#if libraries.length > 0} -
    +
    @@ -369,7 +369,7 @@ {/if} {#if editScanSettings === index} -
    +
    {:else}
    -
    +
    @@ -265,7 +265,7 @@
    - +
    @@ -295,7 +295,7 @@ -
    copyToClipboard(orphan.pathValue)}> {}} /> + {orphan.pathValue} @@ -306,7 +306,7 @@
    - +
    @@ -337,11 +337,11 @@ -
    copyToClipboard(extra.filename)}> {}} /> - + {extra.filename} - + {#if extra.checksum} [sha1:{extra.checksum}] {/if} diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index 0ca17c4ed8..a25799588a 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -180,7 +180,7 @@ {/if} - +
    diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index c3d01b3c56..aa756ac2e8 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -132,7 +132,7 @@

    {$t('or').toUpperCase()} From 460d594791c5d4674f162345d40bb2e98e9bac8a Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Mon, 28 Apr 2025 14:54:11 +0100 Subject: [PATCH 072/356] feat: api response compression (#17878) --- server/package-lock.json | 66 +++++++++++++++++++++++++++++++++++++++ server/package.json | 2 ++ server/src/workers/api.ts | 2 ++ 3 files changed, 70 insertions(+) diff --git a/server/package-lock.json b/server/package-lock.json index b1fdfa1f9d..24180f7cac 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -32,6 +32,7 @@ "chokidar": "^3.5.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "compression": "^1.8.0", "cookie": "^1.0.2", "cookie-parser": "^1.4.7", "exiftool-vendored": "^28.3.1", @@ -83,6 +84,7 @@ "@types/archiver": "^6.0.0", "@types/async-lock": "^1.4.2", "@types/bcrypt": "^5.0.0", + "@types/compression": "^1.7.5", "@types/cookie-parser": "^1.4.8", "@types/express": "^4.17.17", "@types/fluent-ffmpeg": "^2.1.21", @@ -5009,6 +5011,16 @@ "@types/node": "*" } }, + "node_modules/@types/compression": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -7603,6 +7615,60 @@ "node": ">= 14" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", + "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", diff --git a/server/package.json b/server/package.json index f68ba71564..33d1450a53 100644 --- a/server/package.json +++ b/server/package.json @@ -57,6 +57,7 @@ "chokidar": "^3.5.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "compression": "^1.8.0", "cookie": "^1.0.2", "cookie-parser": "^1.4.7", "exiftool-vendored": "^28.3.1", @@ -108,6 +109,7 @@ "@types/archiver": "^6.0.0", "@types/async-lock": "^1.4.2", "@types/bcrypt": "^5.0.0", + "@types/compression": "^1.7.5", "@types/cookie-parser": "^1.4.8", "@types/express": "^4.17.17", "@types/fluent-ffmpeg": "^2.1.21", diff --git a/server/src/workers/api.ts b/server/src/workers/api.ts index ddf6e50aa2..4248b23d30 100644 --- a/server/src/workers/api.ts +++ b/server/src/workers/api.ts @@ -1,6 +1,7 @@ import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { json } from 'body-parser'; +import compression from 'compression'; import cookieParser from 'cookie-parser'; import { existsSync } from 'node:fs'; import sirv from 'sirv'; @@ -60,6 +61,7 @@ async function bootstrap() { ); } app.use(app.get(ApiService).ssr(excludePaths)); + app.use(compression()); const server = await (host ? app.listen(port, host) : app.listen(port)); server.requestTimeout = 24 * 60 * 60 * 1000; From ad272333dbf15b9a3419a9d67e5c9621664f9077 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 28 Apr 2025 09:54:51 -0400 Subject: [PATCH 073/356] refactor: user avatar color (#17753) --- e2e/src/api/specs/user-admin.e2e-spec.ts | 26 ++--- e2e/src/api/specs/user.e2e-spec.ts | 26 ++--- mobile/openapi/README.md | 1 - mobile/openapi/lib/api.dart | 1 - mobile/openapi/lib/api_client.dart | 2 - mobile/openapi/lib/model/avatar_response.dart | 99 ------------------- .../lib/model/user_admin_create_dto.dart | 13 ++- .../lib/model/user_admin_update_dto.dart | 13 ++- .../model/user_preferences_response_dto.dart | 10 +- .../openapi/lib/model/user_update_me_dto.dart | 13 ++- open-api/immich-openapi-specs.json | 43 ++++---- open-api/typescript-sdk/src/fetch-client.ts | 7 +- server/src/database.ts | 14 ++- server/src/dtos/user-preferences.dto.ts | 6 -- server/src/dtos/user.dto.ts | 28 +++++- server/src/queries/activity.repository.sql | 2 + server/src/queries/album.repository.sql | 9 ++ server/src/queries/partner.repository.sql | 8 ++ server/src/queries/user.repository.sql | 7 ++ .../1745244781846-AddUserAvatarColorColumn.ts | 14 +++ server/src/schema/tables/user.table.ts | 5 +- server/src/services/download.service.ts | 2 +- server/src/services/notification.service.ts | 4 +- server/src/services/user-admin.service.ts | 12 +-- server/src/services/user.service.ts | 9 +- server/src/types.ts | 4 - server/src/utils/preferences.ts | 20 ++-- server/test/fixtures/user.stub.ts | 12 +-- server/test/small.factory.ts | 3 + .../navigation-bar/account-info-panel.svelte | 7 +- 30 files changed, 200 insertions(+), 220 deletions(-) delete mode 100644 mobile/openapi/lib/model/avatar_response.dart create mode 100644 server/src/schema/migrations/1745244781846-AddUserAvatarColorColumn.ts diff --git a/e2e/src/api/specs/user-admin.e2e-spec.ts b/e2e/src/api/specs/user-admin.e2e-spec.ts index 9299e62b79..1fbee84c3f 100644 --- a/e2e/src/api/specs/user-admin.e2e-spec.ts +++ b/e2e/src/api/specs/user-admin.e2e-spec.ts @@ -215,6 +215,19 @@ describe('/admin/users', () => { const user = await getMyUser({ headers: asBearerAuth(token.accessToken) }); expect(user).toMatchObject({ email: nonAdmin.userEmail }); }); + + it('should update the avatar color', async () => { + const { status, body } = await request(app) + .put(`/admin/users/${admin.userId}`) + .send({ avatarColor: 'orange' }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toMatchObject({ avatarColor: 'orange' }); + + const after = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) }); + expect(after).toMatchObject({ avatarColor: 'orange' }); + }); }); describe('PUT /admin/users/:id/preferences', () => { @@ -240,19 +253,6 @@ describe('/admin/users', () => { expect(after).toMatchObject({ memories: { enabled: false } }); }); - it('should update the avatar color', async () => { - const { status, body } = await request(app) - .put(`/admin/users/${admin.userId}/preferences`) - .send({ avatar: { color: 'orange' } }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ avatar: { color: 'orange' } }); - - const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) }); - expect(after).toMatchObject({ avatar: { color: 'orange' } }); - }); - it('should update download archive size', async () => { const { status, body } = await request(app) .put(`/admin/users/${admin.userId}/preferences`) diff --git a/e2e/src/api/specs/user.e2e-spec.ts b/e2e/src/api/specs/user.e2e-spec.ts index 54d11e5049..b9eb140c56 100644 --- a/e2e/src/api/specs/user.e2e-spec.ts +++ b/e2e/src/api/specs/user.e2e-spec.ts @@ -139,6 +139,19 @@ describe('/users', () => { profileChangedAt: expect.anything(), }); }); + + it('should update avatar color', async () => { + const { status, body } = await request(app) + .put(`/users/me`) + .send({ avatarColor: 'blue' }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toMatchObject({ avatarColor: 'blue' }); + + const after = await getMyUser({ headers: asBearerAuth(admin.accessToken) }); + expect(after).toMatchObject({ avatarColor: 'blue' }); + }); }); describe('PUT /users/me/preferences', () => { @@ -158,19 +171,6 @@ describe('/users', () => { expect(after).toMatchObject({ memories: { enabled: false } }); }); - it('should update avatar color', async () => { - const { status, body } = await request(app) - .put(`/users/me/preferences`) - .send({ avatar: { color: 'blue' } }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ avatar: { color: 'blue' } }); - - const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(after).toMatchObject({ avatar: { color: 'blue' } }); - }); - it('should require an integer for download archive size', async () => { const { status, body } = await request(app) .put(`/users/me/preferences`) diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 4f9b062ba6..5a7a42cce5 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -300,7 +300,6 @@ Class | Method | HTTP request | Description - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md) - [AssetTypeEnum](doc//AssetTypeEnum.md) - [AudioCodec](doc//AudioCodec.md) - - [AvatarResponse](doc//AvatarResponse.md) - [AvatarUpdate](doc//AvatarUpdate.md) - [BulkIdResponseDto](doc//BulkIdResponseDto.md) - [BulkIdsDto](doc//BulkIdsDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index ff5a95bbbc..d08f9fda38 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -107,7 +107,6 @@ part 'model/asset_stack_response_dto.dart'; part 'model/asset_stats_response_dto.dart'; part 'model/asset_type_enum.dart'; part 'model/audio_codec.dart'; -part 'model/avatar_response.dart'; part 'model/avatar_update.dart'; part 'model/bulk_id_response_dto.dart'; part 'model/bulk_ids_dto.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 5759217f41..0d8e4c6ba9 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -270,8 +270,6 @@ class ApiClient { return AssetTypeEnumTypeTransformer().decode(value); case 'AudioCodec': return AudioCodecTypeTransformer().decode(value); - case 'AvatarResponse': - return AvatarResponse.fromJson(value); case 'AvatarUpdate': return AvatarUpdate.fromJson(value); case 'BulkIdResponseDto': diff --git a/mobile/openapi/lib/model/avatar_response.dart b/mobile/openapi/lib/model/avatar_response.dart deleted file mode 100644 index 8ce0287565..0000000000 --- a/mobile/openapi/lib/model/avatar_response.dart +++ /dev/null @@ -1,99 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class AvatarResponse { - /// Returns a new [AvatarResponse] instance. - AvatarResponse({ - required this.color, - }); - - UserAvatarColor color; - - @override - bool operator ==(Object other) => identical(this, other) || other is AvatarResponse && - other.color == color; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (color.hashCode); - - @override - String toString() => 'AvatarResponse[color=$color]'; - - Map toJson() { - final json = {}; - json[r'color'] = this.color; - return json; - } - - /// Returns a new [AvatarResponse] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AvatarResponse? fromJson(dynamic value) { - upgradeDto(value, "AvatarResponse"); - if (value is Map) { - final json = value.cast(); - - return AvatarResponse( - color: UserAvatarColor.fromJson(json[r'color'])!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AvatarResponse.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = AvatarResponse.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AvatarResponse-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = AvatarResponse.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'color', - }; -} - diff --git a/mobile/openapi/lib/model/user_admin_create_dto.dart b/mobile/openapi/lib/model/user_admin_create_dto.dart index 4bd1266426..1477c82ca1 100644 --- a/mobile/openapi/lib/model/user_admin_create_dto.dart +++ b/mobile/openapi/lib/model/user_admin_create_dto.dart @@ -13,6 +13,7 @@ part of openapi.api; class UserAdminCreateDto { /// Returns a new [UserAdminCreateDto] instance. UserAdminCreateDto({ + this.avatarColor, required this.email, required this.name, this.notify, @@ -22,6 +23,8 @@ class UserAdminCreateDto { this.storageLabel, }); + UserAvatarColor? avatarColor; + String email; String name; @@ -51,6 +54,7 @@ class UserAdminCreateDto { @override bool operator ==(Object other) => identical(this, other) || other is UserAdminCreateDto && + other.avatarColor == avatarColor && other.email == email && other.name == name && other.notify == notify && @@ -62,6 +66,7 @@ class UserAdminCreateDto { @override int get hashCode => // ignore: unnecessary_parenthesis + (avatarColor == null ? 0 : avatarColor!.hashCode) + (email.hashCode) + (name.hashCode) + (notify == null ? 0 : notify!.hashCode) + @@ -71,10 +76,15 @@ class UserAdminCreateDto { (storageLabel == null ? 0 : storageLabel!.hashCode); @override - String toString() => 'UserAdminCreateDto[email=$email, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; + String toString() => 'UserAdminCreateDto[avatarColor=$avatarColor, email=$email, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; Map toJson() { final json = {}; + if (this.avatarColor != null) { + json[r'avatarColor'] = this.avatarColor; + } else { + // json[r'avatarColor'] = null; + } json[r'email'] = this.email; json[r'name'] = this.name; if (this.notify != null) { @@ -110,6 +120,7 @@ class UserAdminCreateDto { final json = value.cast(); return UserAdminCreateDto( + avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), email: mapValueOfType(json, r'email')!, name: mapValueOfType(json, r'name')!, notify: mapValueOfType(json, r'notify'), diff --git a/mobile/openapi/lib/model/user_admin_update_dto.dart b/mobile/openapi/lib/model/user_admin_update_dto.dart index f0478c9b4c..951ee8ce84 100644 --- a/mobile/openapi/lib/model/user_admin_update_dto.dart +++ b/mobile/openapi/lib/model/user_admin_update_dto.dart @@ -13,6 +13,7 @@ part of openapi.api; class UserAdminUpdateDto { /// Returns a new [UserAdminUpdateDto] instance. UserAdminUpdateDto({ + this.avatarColor, this.email, this.name, this.password, @@ -21,6 +22,8 @@ class UserAdminUpdateDto { this.storageLabel, }); + UserAvatarColor? avatarColor; + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -60,6 +63,7 @@ class UserAdminUpdateDto { @override bool operator ==(Object other) => identical(this, other) || other is UserAdminUpdateDto && + other.avatarColor == avatarColor && other.email == email && other.name == name && other.password == password && @@ -70,6 +74,7 @@ class UserAdminUpdateDto { @override int get hashCode => // ignore: unnecessary_parenthesis + (avatarColor == null ? 0 : avatarColor!.hashCode) + (email == null ? 0 : email!.hashCode) + (name == null ? 0 : name!.hashCode) + (password == null ? 0 : password!.hashCode) + @@ -78,10 +83,15 @@ class UserAdminUpdateDto { (storageLabel == null ? 0 : storageLabel!.hashCode); @override - String toString() => 'UserAdminUpdateDto[email=$email, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; + String toString() => 'UserAdminUpdateDto[avatarColor=$avatarColor, email=$email, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; Map toJson() { final json = {}; + if (this.avatarColor != null) { + json[r'avatarColor'] = this.avatarColor; + } else { + // json[r'avatarColor'] = null; + } if (this.email != null) { json[r'email'] = this.email; } else { @@ -124,6 +134,7 @@ class UserAdminUpdateDto { final json = value.cast(); return UserAdminUpdateDto( + avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), email: mapValueOfType(json, r'email'), name: mapValueOfType(json, r'name'), password: mapValueOfType(json, r'password'), diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart index b244284eb0..215e691cb1 100644 --- a/mobile/openapi/lib/model/user_preferences_response_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_response_dto.dart @@ -13,7 +13,6 @@ part of openapi.api; class UserPreferencesResponseDto { /// Returns a new [UserPreferencesResponseDto] instance. UserPreferencesResponseDto({ - required this.avatar, required this.download, required this.emailNotifications, required this.folders, @@ -25,8 +24,6 @@ class UserPreferencesResponseDto { required this.tags, }); - AvatarResponse avatar; - DownloadResponse download; EmailNotificationsResponse emailNotifications; @@ -47,7 +44,6 @@ class UserPreferencesResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto && - other.avatar == avatar && other.download == download && other.emailNotifications == emailNotifications && other.folders == folders && @@ -61,7 +57,6 @@ class UserPreferencesResponseDto { @override int get hashCode => // ignore: unnecessary_parenthesis - (avatar.hashCode) + (download.hashCode) + (emailNotifications.hashCode) + (folders.hashCode) + @@ -73,11 +68,10 @@ class UserPreferencesResponseDto { (tags.hashCode); @override - String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; + String toString() => 'UserPreferencesResponseDto[download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; Map toJson() { final json = {}; - json[r'avatar'] = this.avatar; json[r'download'] = this.download; json[r'emailNotifications'] = this.emailNotifications; json[r'folders'] = this.folders; @@ -99,7 +93,6 @@ class UserPreferencesResponseDto { final json = value.cast(); return UserPreferencesResponseDto( - avatar: AvatarResponse.fromJson(json[r'avatar'])!, download: DownloadResponse.fromJson(json[r'download'])!, emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!, folders: FoldersResponse.fromJson(json[r'folders'])!, @@ -156,7 +149,6 @@ class UserPreferencesResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'avatar', 'download', 'emailNotifications', 'folders', diff --git a/mobile/openapi/lib/model/user_update_me_dto.dart b/mobile/openapi/lib/model/user_update_me_dto.dart index 8f3f4df37a..779e07ffa6 100644 --- a/mobile/openapi/lib/model/user_update_me_dto.dart +++ b/mobile/openapi/lib/model/user_update_me_dto.dart @@ -13,11 +13,14 @@ part of openapi.api; class UserUpdateMeDto { /// Returns a new [UserUpdateMeDto] instance. UserUpdateMeDto({ + this.avatarColor, this.email, this.name, this.password, }); + UserAvatarColor? avatarColor; + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -44,6 +47,7 @@ class UserUpdateMeDto { @override bool operator ==(Object other) => identical(this, other) || other is UserUpdateMeDto && + other.avatarColor == avatarColor && other.email == email && other.name == name && other.password == password; @@ -51,15 +55,21 @@ class UserUpdateMeDto { @override int get hashCode => // ignore: unnecessary_parenthesis + (avatarColor == null ? 0 : avatarColor!.hashCode) + (email == null ? 0 : email!.hashCode) + (name == null ? 0 : name!.hashCode) + (password == null ? 0 : password!.hashCode); @override - String toString() => 'UserUpdateMeDto[email=$email, name=$name, password=$password]'; + String toString() => 'UserUpdateMeDto[avatarColor=$avatarColor, email=$email, name=$name, password=$password]'; Map toJson() { final json = {}; + if (this.avatarColor != null) { + json[r'avatarColor'] = this.avatarColor; + } else { + // json[r'avatarColor'] = null; + } if (this.email != null) { json[r'email'] = this.email; } else { @@ -87,6 +97,7 @@ class UserUpdateMeDto { final json = value.cast(); return UserUpdateMeDto( + avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), email: mapValueOfType(json, r'email'), name: mapValueOfType(json, r'name'), password: mapValueOfType(json, r'password'), diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index f2851d7cf1..1471020cd4 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8884,21 +8884,6 @@ ], "type": "string" }, - "AvatarResponse": { - "properties": { - "color": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ] - } - }, - "required": [ - "color" - ], - "type": "object" - }, "AvatarUpdate": { "properties": { "color": { @@ -13621,6 +13606,14 @@ }, "UserAdminCreateDto": { "properties": { + "avatarColor": { + "allOf": [ + { + "$ref": "#/components/schemas/UserAvatarColor" + } + ], + "nullable": true + }, "email": { "format": "email", "type": "string" @@ -13763,6 +13756,14 @@ }, "UserAdminUpdateDto": { "properties": { + "avatarColor": { + "allOf": [ + { + "$ref": "#/components/schemas/UserAvatarColor" + } + ], + "nullable": true + }, "email": { "format": "email", "type": "string" @@ -13826,9 +13827,6 @@ }, "UserPreferencesResponseDto": { "properties": { - "avatar": { - "$ref": "#/components/schemas/AvatarResponse" - }, "download": { "$ref": "#/components/schemas/DownloadResponse" }, @@ -13858,7 +13856,6 @@ } }, "required": [ - "avatar", "download", "emailNotifications", "folders", @@ -13952,6 +13949,14 @@ }, "UserUpdateMeDto": { "properties": { + "avatarColor": { + "allOf": [ + { + "$ref": "#/components/schemas/UserAvatarColor" + } + ], + "nullable": true + }, "email": { "format": "email", "type": "string" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 51e17c08ac..1ba4d3e231 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -64,6 +64,7 @@ export type UserAdminResponseDto = { updatedAt: string; }; export type UserAdminCreateDto = { + avatarColor?: (UserAvatarColor) | null; email: string; name: string; notify?: boolean; @@ -76,6 +77,7 @@ export type UserAdminDeleteDto = { force?: boolean; }; export type UserAdminUpdateDto = { + avatarColor?: (UserAvatarColor) | null; email?: string; name?: string; password?: string; @@ -83,9 +85,6 @@ export type UserAdminUpdateDto = { shouldChangePassword?: boolean; storageLabel?: string | null; }; -export type AvatarResponse = { - color: UserAvatarColor; -}; export type DownloadResponse = { archiveSize: number; includeEmbeddedVideos: boolean; @@ -122,7 +121,6 @@ export type TagsResponse = { sidebarWeb: boolean; }; export type UserPreferencesResponseDto = { - avatar: AvatarResponse; download: DownloadResponse; emailNotifications: EmailNotificationsResponse; folders: FoldersResponse; @@ -1388,6 +1386,7 @@ export type TrashResponseDto = { count: number; }; export type UserUpdateMeDto = { + avatarColor?: (UserAvatarColor) | null; email?: string; name?: string; password?: string; diff --git a/server/src/database.ts b/server/src/database.ts index 27094958ed..0dab61cbe0 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -9,6 +9,7 @@ import { Permission, SharedLinkType, SourceType, + UserAvatarColor, UserStatus, } from 'src/enum'; import { OnThisDayData, UserMetadataItem } from 'src/types'; @@ -122,6 +123,7 @@ export type User = { id: string; name: string; email: string; + avatarColor: UserAvatarColor | null; profileImagePath: string; profileChangedAt: Date; }; @@ -264,7 +266,15 @@ export type AssetFace = { person?: Person | null; }; -const userColumns = ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'] as const; +const userColumns = ['id', 'name', 'email', 'avatarColor', 'profileImagePath', 'profileChangedAt'] as const; +const userWithPrefixColumns = [ + 'users.id', + 'users.name', + 'users.email', + 'users.avatarColor', + 'users.profileImagePath', + 'users.profileChangedAt', +] as const; export const columns = { asset: [ @@ -306,7 +316,7 @@ export const columns = { 'shared_links.password', ], user: userColumns, - userWithPrefix: ['users.id', 'users.name', 'users.email', 'users.profileImagePath', 'users.profileChangedAt'], + userWithPrefix: userWithPrefixColumns, userAdmin: [ ...userColumns, 'createdAt', diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index fe92838fdb..a9d32523ae 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -137,11 +137,6 @@ export class UserPreferencesUpdateDto { purchase?: PurchaseUpdate; } -class AvatarResponse { - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) - color!: UserAvatarColor; -} - class RatingsResponse { enabled: boolean = false; } @@ -195,7 +190,6 @@ export class UserPreferencesResponseDto implements UserPreferences { ratings!: RatingsResponse; sharedLinks!: SharedLinksResponse; tags!: TagsResponse; - avatar!: AvatarResponse; emailNotifications!: EmailNotificationsResponse; download!: DownloadResponse; purchase!: PurchaseResponse; diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index 72e5c83b35..31275f9c28 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -1,10 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator'; +import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator'; import { User, UserAdmin } from 'src/database'; import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; import { UserMetadataItem } from 'src/types'; -import { getPreferences } from 'src/utils/preferences'; import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation'; export class UserUpdateMeDto { @@ -23,6 +22,11 @@ export class UserUpdateMeDto { @IsString() @IsNotEmpty() name?: string; + + @Optional({ nullable: true }) + @IsEnum(UserAvatarColor) + @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + avatarColor?: UserAvatarColor | null; } export class UserResponseDto { @@ -41,13 +45,21 @@ export class UserLicense { activatedAt!: Date; } +const emailToAvatarColor = (email: string): UserAvatarColor => { + const values = Object.values(UserAvatarColor); + const randomIndex = Math.floor( + [...email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length, + ); + return values[randomIndex]; +}; + export const mapUser = (entity: User | UserAdmin): UserResponseDto => { return { id: entity.id, email: entity.email, name: entity.name, profileImagePath: entity.profileImagePath, - avatarColor: getPreferences(entity.email, (entity as UserAdmin).metadata || []).avatar.color, + avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email), profileChangedAt: entity.profileChangedAt, }; }; @@ -69,6 +81,11 @@ export class UserAdminCreateDto { @IsString() name!: string; + @Optional({ nullable: true }) + @IsEnum(UserAvatarColor) + @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + avatarColor?: UserAvatarColor | null; + @Optional({ nullable: true }) @IsString() @Transform(toSanitized) @@ -104,6 +121,11 @@ export class UserAdminUpdateDto { @IsNotEmpty() name?: string; + @Optional({ nullable: true }) + @IsEnum(UserAvatarColor) + @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + avatarColor?: UserAvatarColor | null; + @Optional({ nullable: true }) @IsString() @Transform(toSanitized) diff --git a/server/src/queries/activity.repository.sql b/server/src/queries/activity.repository.sql index c6e4c60a19..3040de8e03 100644 --- a/server/src/queries/activity.repository.sql +++ b/server/src/queries/activity.repository.sql @@ -13,6 +13,7 @@ from "users"."id", "users"."name", "users"."email", + "users"."avatarColor", "users"."profileImagePath", "users"."profileChangedAt" from @@ -44,6 +45,7 @@ returning "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index b89cbfb0b9..f4eb6a9929 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -12,6 +12,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from @@ -36,6 +37,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from @@ -100,6 +102,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from @@ -124,6 +127,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from @@ -191,6 +195,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from @@ -215,6 +220,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from @@ -269,6 +275,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from @@ -292,6 +299,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from @@ -353,6 +361,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from diff --git a/server/src/queries/partner.repository.sql b/server/src/queries/partner.repository.sql index e115dc34b9..e7170f367e 100644 --- a/server/src/queries/partner.repository.sql +++ b/server/src/queries/partner.repository.sql @@ -12,6 +12,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from @@ -29,6 +30,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from @@ -61,6 +63,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from @@ -78,6 +81,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from @@ -112,6 +116,7 @@ returning "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from @@ -129,6 +134,7 @@ returning "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from @@ -156,6 +162,7 @@ returning "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from @@ -173,6 +180,7 @@ returning "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt" from diff --git a/server/src/queries/user.repository.sql b/server/src/queries/user.repository.sql index 1212d0f2bd..e8ab5018fc 100644 --- a/server/src/queries/user.repository.sql +++ b/server/src/queries/user.repository.sql @@ -5,6 +5,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt", "createdAt", @@ -43,6 +44,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt", "createdAt", @@ -90,6 +92,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt", "createdAt", @@ -128,6 +131,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt", "createdAt", @@ -152,6 +156,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt", "createdAt", @@ -198,6 +203,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt", "createdAt", @@ -235,6 +241,7 @@ select "id", "name", "email", + "avatarColor", "profileImagePath", "profileChangedAt", "createdAt", diff --git a/server/src/schema/migrations/1745244781846-AddUserAvatarColorColumn.ts b/server/src/schema/migrations/1745244781846-AddUserAvatarColorColumn.ts new file mode 100644 index 0000000000..5f3fdbedc8 --- /dev/null +++ b/server/src/schema/migrations/1745244781846-AddUserAvatarColorColumn.ts @@ -0,0 +1,14 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "users" ADD "avatarColor" character varying;`.execute(db); + await sql` + UPDATE "users" + SET "avatarColor" = "user_metadata"."value"->'avatar'->>'color' + FROM "user_metadata" + WHERE "users"."id" = "user_metadata"."userId" AND "user_metadata"."key" = 'preferences';`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "users" DROP COLUMN "avatarColor";`.execute(db); +} diff --git a/server/src/schema/tables/user.table.ts b/server/src/schema/tables/user.table.ts index eeef923796..7525a739a6 100644 --- a/server/src/schema/tables/user.table.ts +++ b/server/src/schema/tables/user.table.ts @@ -1,6 +1,6 @@ import { ColumnType } from 'kysely'; import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { UserStatus } from 'src/enum'; +import { UserAvatarColor, UserStatus } from 'src/enum'; import { users_delete_audit } from 'src/schema/functions'; import { AfterDeleteTrigger, @@ -49,6 +49,9 @@ export class UserTable { @Column({ type: 'boolean', default: true }) shouldChangePassword!: Generated; + @Column({ default: null }) + avatarColor!: UserAvatarColor | null; + @DeleteDateColumn() deletedAt!: Timestamp | null; diff --git a/server/src/services/download.service.ts b/server/src/services/download.service.ts index cb664aea32..02711b9bfd 100644 --- a/server/src/services/download.service.ts +++ b/server/src/services/download.service.ts @@ -33,7 +33,7 @@ export class DownloadService extends BaseService { const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4; const metadata = await this.userRepository.getMetadata(auth.user.id); - const preferences = getPreferences(auth.user.email, metadata); + const preferences = getPreferences(metadata); const motionIds = new Set(); const archives: DownloadArchiveInfo[] = []; let archive: DownloadArchiveInfo = { size: 0, assetIds: [] }; diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 2e456718ca..573be90f93 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -271,7 +271,7 @@ export class NotificationService extends BaseService { return JobStatus.SKIPPED; } - const { emailNotifications } = getPreferences(recipient.email, recipient.metadata); + const { emailNotifications } = getPreferences(recipient.metadata); if (!emailNotifications.enabled || !emailNotifications.albumInvite) { return JobStatus.SKIPPED; @@ -333,7 +333,7 @@ export class NotificationService extends BaseService { continue; } - const { emailNotifications } = getPreferences(user.email, user.metadata); + const { emailNotifications } = getPreferences(user.metadata); if (!emailNotifications.enabled || !emailNotifications.albumUpdate) { continue; diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index 0cba749d36..c1c6cc49ec 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -106,21 +106,19 @@ export class UserAdminService extends BaseService { } async getPreferences(auth: AuthDto, id: string): Promise { - const { email } = await this.findOrFail(id, { withDeleted: true }); + await this.findOrFail(id, { withDeleted: true }); const metadata = await this.userRepository.getMetadata(id); - const preferences = getPreferences(email, metadata); - return mapPreferences(preferences); + return mapPreferences(getPreferences(metadata)); } async updatePreferences(auth: AuthDto, id: string, dto: UserPreferencesUpdateDto) { - const { email } = await this.findOrFail(id, { withDeleted: false }); + await this.findOrFail(id, { withDeleted: false }); const metadata = await this.userRepository.getMetadata(id); - const preferences = getPreferences(email, metadata); - const newPreferences = mergePreferences(preferences, dto); + const newPreferences = mergePreferences(getPreferences(metadata), dto); await this.userRepository.upsertMetadata(id, { key: UserMetadataKey.PREFERENCES, - value: getPreferencesPartial({ email }, newPreferences), + value: getPreferencesPartial(newPreferences), }); return mapPreferences(newPreferences); diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index 327328eb1c..a0304d51ad 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -53,6 +53,7 @@ export class UserService extends BaseService { const update: Updateable = { email: dto.email, name: dto.name, + avatarColor: dto.avatarColor, }; if (dto.password) { @@ -68,18 +69,16 @@ export class UserService extends BaseService { async getMyPreferences(auth: AuthDto): Promise { const metadata = await this.userRepository.getMetadata(auth.user.id); - const preferences = getPreferences(auth.user.email, metadata); - return mapPreferences(preferences); + return mapPreferences(getPreferences(metadata)); } async updateMyPreferences(auth: AuthDto, dto: UserPreferencesUpdateDto) { const metadata = await this.userRepository.getMetadata(auth.user.id); - const current = getPreferences(auth.user.email, metadata); - const updated = mergePreferences(current, dto); + const updated = mergePreferences(getPreferences(metadata), dto); await this.userRepository.upsertMetadata(auth.user.id, { key: UserMetadataKey.PREFERENCES, - value: getPreferencesPartial(auth.user, updated), + value: getPreferencesPartial(updated), }); return mapPreferences(updated); diff --git a/server/src/types.ts b/server/src/types.ts index 88ba644739..c5375ae727 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -11,7 +11,6 @@ import { SyncEntityType, SystemMetadataKey, TranscodeTarget, - UserAvatarColor, UserMetadataKey, VideoCodec, } from 'src/enum'; @@ -486,9 +485,6 @@ export interface UserPreferences { enabled: boolean; sidebarWeb: boolean; }; - avatar: { - color: UserAvatarColor; - }; emailNotifications: { enabled: boolean; albumInvite: boolean; diff --git a/server/src/utils/preferences.ts b/server/src/utils/preferences.ts index 584c5300cd..a013c0b74e 100644 --- a/server/src/utils/preferences.ts +++ b/server/src/utils/preferences.ts @@ -1,16 +1,11 @@ import _ from 'lodash'; import { UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; -import { UserAvatarColor, UserMetadataKey } from 'src/enum'; +import { UserMetadataKey } from 'src/enum'; import { DeepPartial, UserMetadataItem, UserPreferences } from 'src/types'; import { HumanReadableSize } from 'src/utils/bytes'; import { getKeysDeep } from 'src/utils/misc'; -const getDefaultPreferences = (user: { email: string }): UserPreferences => { - const values = Object.values(UserAvatarColor); - const randomIndex = Math.floor( - [...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length, - ); - +const getDefaultPreferences = (): UserPreferences => { return { folders: { enabled: false, @@ -34,9 +29,6 @@ const getDefaultPreferences = (user: { email: string }): UserPreferences => { enabled: false, sidebarWeb: false, }, - avatar: { - color: values[randomIndex], - }, emailNotifications: { enabled: true, albumInvite: true, @@ -53,8 +45,8 @@ const getDefaultPreferences = (user: { email: string }): UserPreferences => { }; }; -export const getPreferences = (email: string, metadata: UserMetadataItem[]): UserPreferences => { - const preferences = getDefaultPreferences({ email }); +export const getPreferences = (metadata: UserMetadataItem[]): UserPreferences => { + const preferences = getDefaultPreferences(); const item = metadata.find(({ key }) => key === UserMetadataKey.PREFERENCES); const partial = item?.value || {}; for (const property of getKeysDeep(partial)) { @@ -64,8 +56,8 @@ export const getPreferences = (email: string, metadata: UserMetadataItem[]): Use return preferences; }; -export const getPreferencesPartial = (user: { email: string }, newPreferences: UserPreferences) => { - const defaultPreferences = getDefaultPreferences(user); +export const getPreferencesPartial = (newPreferences: UserPreferences) => { + const defaultPreferences = getDefaultPreferences(); const partial: DeepPartial = {}; for (const property of getKeysDeep(defaultPreferences)) { const newValue = _.get(newPreferences, property); diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index f0043d174a..0db58e2eed 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -1,5 +1,5 @@ import { UserAdmin } from 'src/database'; -import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; +import { UserStatus } from 'src/enum'; import { authStub } from 'test/fixtures/auth.stub'; export const userStub = { @@ -12,6 +12,7 @@ export const userStub = { storageLabel: 'admin', oauthId: '', shouldChangePassword: false, + avatarColor: null, profileImagePath: '', createdAt: new Date('2021-01-01'), deletedAt: null, @@ -28,16 +29,12 @@ export const userStub = { storageLabel: null, oauthId: '', shouldChangePassword: false, + avatarColor: null, profileImagePath: '', createdAt: new Date('2021-01-01'), deletedAt: null, updatedAt: new Date('2021-01-01'), - metadata: [ - { - key: UserMetadataKey.PREFERENCES, - value: { avatar: { color: UserAvatarColor.PRIMARY } }, - }, - ], + metadata: [], quotaSizeInBytes: null, quotaUsageInBytes: 0, }, @@ -50,6 +47,7 @@ export const userStub = { storageLabel: null, oauthId: '', shouldChangePassword: false, + avatarColor: null, profileImagePath: '', createdAt: new Date('2021-01-01'), deletedAt: null, diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 29eef7002e..919cdd4b1c 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -140,6 +140,7 @@ const userFactory = (user: Partial = {}) => ({ id: newUuid(), name: 'Test User', email: 'test@immich.cloud', + avatarColor: null, profileImagePath: '', profileChangedAt: newDate(), ...user, @@ -155,6 +156,7 @@ const userAdminFactory = (user: Partial = {}) => { storageLabel = null, shouldChangePassword = false, isAdmin = false, + avatarColor = null, createdAt = newDate(), updatedAt = newDate(), deletedAt = null, @@ -173,6 +175,7 @@ const userAdminFactory = (user: Partial = {}) => { storageLabel, shouldChangePassword, isAdmin, + avatarColor, createdAt, updatedAt, deletedAt, diff --git a/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte b/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte index 92db67eba0..5b778cf227 100644 --- a/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte @@ -5,9 +5,9 @@ import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import Icon from '$lib/components/elements/icon.svelte'; import { AppRoute } from '$lib/constants'; - import { preferences, user } from '$lib/stores/user.store'; + import { user } from '$lib/stores/user.store'; import { handleError } from '$lib/utils/handle-error'; - import { deleteProfileImage, updateMyPreferences, type UserAvatarColor } from '@immich/sdk'; + import { deleteProfileImage, updateMyUser, type UserAvatarColor } from '@immich/sdk'; import { mdiCog, mdiLogout, mdiPencil, mdiWrench } from '@mdi/js'; import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; @@ -30,8 +30,7 @@ await deleteProfileImage(); } - $preferences = await updateMyPreferences({ userPreferencesUpdateDto: { avatar: { color } } }); - $user = { ...$user, profileImagePath: '', avatarColor: $preferences.avatar.color }; + $user = await updateMyUser({ userUpdateMeDto: { avatarColor: color } }); isShowSelectAvatar = false; notificationController.show({ From 21c7d7033671869c4b4bb6f4f2c27cb07f29a7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Tollk=C3=B6tter?= <1518021+atollk@users.noreply.github.com> Date: Mon, 28 Apr 2025 15:56:36 +0200 Subject: [PATCH 074/356] feat(mobile): Capitalize month names in asset grid (#17898) * capitalize month titles * capitalize day titles as well --- mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart index a7141c33b2..060898e270 100644 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart +++ b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart @@ -755,7 +755,7 @@ class _MonthTitle extends StatelessWidget { key: Key("month-$title"), padding: const EdgeInsets.only(left: 12.0, top: 24.0), child: Text( - title, + toBeginningOfSentenceCase(title, context.locale.languageCode), style: const TextStyle( fontSize: 26, fontWeight: FontWeight.w500, @@ -786,7 +786,7 @@ class _Title extends StatelessWidget { @override Widget build(BuildContext context) { return GroupDividerTitle( - text: title, + text: toBeginningOfSentenceCase(title, context.locale.languageCode), multiselectEnabled: selectionActive, onSelect: () => selectAssets(assets), onDeselect: () => deselectAssets(assets), From c664d99a348999d16cda45a8dd57db436b8f9fba Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Mon, 28 Apr 2025 10:11:19 -0400 Subject: [PATCH 075/356] refactor: vscode - format/organize on save (#17928) --- .vscode/settings.json | 80 ++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 49692809bc..396755a634 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,45 +1,63 @@ { - "editor.formatOnSave": true, - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.tabSize": 2, - "editor.formatOnSave": true - }, - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.tabSize": 2, - "editor.formatOnSave": true - }, "[css]": { "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.tabSize": 2, - "editor.formatOnSave": true - }, - "[svelte]": { - "editor.defaultFormatter": "svelte.svelte-vscode", + "editor.formatOnSave": true, "editor.tabSize": 2 }, - "svelte.enable-ts-plugin": true, - "eslint.validate": [ - "javascript", - "svelte" - ], - "typescript.preferences.importModuleSpecifier": "non-relative", "[dart]": { + "editor.defaultFormatter": "Dart-Code.dart-code", "editor.formatOnSave": true, "editor.selectionHighlight": false, "editor.suggest.snippetsPreventQuickSuggestions": false, "editor.suggestSelection": "first", "editor.tabCompletion": "onlySnippets", - "editor.wordBasedSuggestions": "off", - "editor.defaultFormatter": "Dart-Code.dart-code" + "editor.wordBasedSuggestions": "off" }, - "cSpell.words": [ - "immich" - ], + "[javascript]": { + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", + "source.removeUnusedImports": "explicit" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2 + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2 + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2 + }, + "[svelte]": { + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", + "source.removeUnusedImports": "explicit" + }, + "editor.defaultFormatter": "svelte.svelte-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2 + }, + "[typescript]": { + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", + "source.removeUnusedImports": "explicit" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2 + }, + "cSpell.words": ["immich"], + "editor.formatOnSave": true, + "eslint.validate": ["javascript", "svelte"], "explorer.fileNesting.enabled": true, "explorer.fileNesting.patterns": { - "*.ts": "${capture}.spec.ts,${capture}.mock.ts", - "*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart" - } -} \ No newline at end of file + "*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart", + "*.ts": "${capture}.spec.ts,${capture}.mock.ts" + }, + "svelte.enable-ts-plugin": true, + "typescript.preferences.importModuleSpecifier": "non-relative" +} From 2fd05e84470f94d461db2cda743dd237ab66c1a9 Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Mon, 28 Apr 2025 10:23:05 -0400 Subject: [PATCH 076/356] feat: preload and cancel images with a service worker (#16893) * feat: Service Worker to preload/cancel images and other resources * Remove caddy configuration, localhost is secure if port-forwarded * fix e2e tests * Cache/return the app.html for all web entry points * Only handle preload/cancel * fix e2e * fix e2e * e2e-2 * that'll do it * format * fix test * lint * refactor common code to conditionals --------- Co-authored-by: Alex --- Makefile | 3 + e2e/src/web/specs/photo-viewer.e2e-spec.ts | 14 --- web/eslint.config.js | 2 + .../asset-viewer/photo-viewer.svelte | 5 +- .../assets/thumbnail/image-thumbnail.svelte | 7 +- web/src/lib/utils/sw-messaging.ts | 8 ++ web/src/service-worker/index.ts | 86 +++++++++++++++++++ 7 files changed, 108 insertions(+), 17 deletions(-) create mode 100644 web/src/lib/utils/sw-messaging.ts create mode 100644 web/src/service-worker/index.ts diff --git a/Makefile b/Makefile index e15faa8051..1e7760ae68 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,9 @@ e2e: prod: docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans +prod-down: + docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans + prod-scale: docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans diff --git a/e2e/src/web/specs/photo-viewer.e2e-spec.ts b/e2e/src/web/specs/photo-viewer.e2e-spec.ts index 4871e7522c..c8a9b42b2a 100644 --- a/e2e/src/web/specs/photo-viewer.e2e-spec.ts +++ b/e2e/src/web/specs/photo-viewer.e2e-spec.ts @@ -21,23 +21,9 @@ test.describe('Photo Viewer', () => { test.beforeEach(async ({ context, page }) => { // before each test, login as user await utils.setAuthCookies(context, admin.accessToken); - await page.goto('/photos'); await page.waitForLoadState('networkidle'); }); - test('initially shows a loading spinner', async ({ page }) => { - await page.route(`/api/assets/${asset.id}/thumbnail**`, async (route) => { - // slow down the request for thumbnail, so spinner has chance to show up - await new Promise((f) => setTimeout(f, 2000)); - await route.continue(); - }); - await page.goto(`/photos/${asset.id}`); - await page.waitForLoadState('load'); - // this is the spinner - await page.waitForSelector('svg[role=status]'); - await expect(page.getByTestId('loading-spinner')).toBeVisible(); - }); - test('loads original photo when zoomed', async ({ page }) => { await page.goto(`/photos/${asset.id}`); await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail'); diff --git a/web/eslint.config.js b/web/eslint.config.js index 5c24cd1aeb..9ced619504 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -58,6 +58,8 @@ export default typescriptEslint.config( }, }, + ignores: ['**/service-worker/**'], + rules: { '@typescript-eslint/no-unused-vars': [ 'warn', diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index fdb986786e..531f075b86 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -21,6 +21,7 @@ import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte'; import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; + import { cancelImageUrl, preloadImageUrl } from '$lib/utils/sw-messaging'; interface Props { asset: AssetResponseDto; @@ -71,8 +72,7 @@ const preload = (targetSize: AssetMediaSize | 'original', preloadAssets?: AssetResponseDto[]) => { for (const preloadAsset of preloadAssets || []) { if (preloadAsset.type === AssetTypeEnum.Image) { - let img = new Image(); - img.src = getAssetUrl(preloadAsset.id, targetSize, preloadAsset.thumbhash); + preloadImageUrl(getAssetUrl(preloadAsset.id, targetSize, preloadAsset.thumbhash)); } } }; @@ -168,6 +168,7 @@ return () => { loader?.removeEventListener('load', onload); loader?.removeEventListener('error', onerror); + cancelImageUrl(imageLoaderUrl); }; }); diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte index 2e8ad6ca32..04493b273c 100644 --- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte @@ -2,9 +2,11 @@ import { thumbhash } from '$lib/actions/thumbhash'; import BrokenAsset from '$lib/components/assets/broken-asset.svelte'; import Icon from '$lib/components/elements/icon.svelte'; + import { cancelImageUrl } from '$lib/utils/sw-messaging'; import { TUNABLES } from '$lib/utils/tunables'; import { mdiEyeOffOutline } from '@mdi/js'; import type { ClassValue } from 'svelte/elements'; + import type { ActionReturn } from 'svelte/action'; import { fade } from 'svelte/transition'; interface Props { @@ -59,11 +61,14 @@ onComplete?.(true); }; - function mount(elem: HTMLImageElement) { + function mount(elem: HTMLImageElement): ActionReturn { if (elem.complete) { loaded = true; onComplete?.(false); } + return { + destroy: () => cancelImageUrl(url), + }; } let optionalClasses = $derived( diff --git a/web/src/lib/utils/sw-messaging.ts b/web/src/lib/utils/sw-messaging.ts new file mode 100644 index 0000000000..1a19d3c134 --- /dev/null +++ b/web/src/lib/utils/sw-messaging.ts @@ -0,0 +1,8 @@ +const broadcast = new BroadcastChannel('immich'); + +export function cancelImageUrl(url: string) { + broadcast.postMessage({ type: 'cancel', url }); +} +export function preloadImageUrl(url: string) { + broadcast.postMessage({ type: 'preload', url }); +} diff --git a/web/src/service-worker/index.ts b/web/src/service-worker/index.ts new file mode 100644 index 0000000000..797f4754b6 --- /dev/null +++ b/web/src/service-worker/index.ts @@ -0,0 +1,86 @@ +/// +/// +/// +/// +import { version } from '$service-worker'; + +const useCache = true; +const sw = globalThis as unknown as ServiceWorkerGlobalScope; +const pendingLoads = new Map(); + +// Create a unique cache name for this deployment +const CACHE = `cache-${version}`; + +sw.addEventListener('install', (event) => { + event.waitUntil(sw.skipWaiting()); +}); + +sw.addEventListener('activate', (event) => { + event.waitUntil(sw.clients.claim()); + // Remove previous cached data from disk + event.waitUntil(deleteOldCaches()); +}); + +sw.addEventListener('fetch', (event) => { + if (event.request.method !== 'GET') { + return; + } + const url = new URL(event.request.url); + if (/^\/api\/assets\/[a-f0-9-]+\/(original|thumbnail)/.test(url.pathname)) { + event.respondWith(immichAsset(url)); + } +}); + +async function deleteOldCaches() { + for (const key of await caches.keys()) { + if (key !== CACHE) { + await caches.delete(key); + } + } +} + +async function immichAsset(url: URL) { + const cache = await caches.open(CACHE); + let response = useCache ? await cache.match(url) : undefined; + if (response) { + return response; + } + try { + const cancelToken = new AbortController(); + const request = fetch(url, { + signal: cancelToken.signal, + }); + pendingLoads.set(url.toString(), cancelToken); + response = await request; + if (!(response instanceof Response)) { + throw new TypeError('invalid response from fetch'); + } + if (response.status === 200) { + cache.put(url, response.clone()); + } + return response; + } catch { + return Response.error(); + } finally { + pendingLoads.delete(url.toString()); + } +} + +const broadcast = new BroadcastChannel('immich'); +// eslint-disable-next-line unicorn/prefer-add-event-listener +broadcast.onmessage = (event) => { + if (!event.data) { + return; + } + const urlstring = event.data.url; + const url = new URL(urlstring, event.origin); + if (event.data.type === 'cancel') { + const pending = pendingLoads.get(url.toString()); + if (pending) { + pending.abort(); + pendingLoads.delete(url.toString()); + } + } else if (event.data.type === 'preload') { + immichAsset(url); + } +}; From 23717ce98155382b7cc2bb52fdc88395f81496b6 Mon Sep 17 00:00:00 2001 From: Yaros Date: Mon, 28 Apr 2025 16:23:33 +0200 Subject: [PATCH 077/356] feat(mobile): save grid size on gesture resize (#17891) --- mobile/lib/widgets/asset_grid/immich_asset_grid.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid.dart index 2ec01e871f..da4c47e466 100644 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid.dart +++ b/mobile/lib/widgets/asset_grid/immich_asset_grid.dart @@ -97,6 +97,7 @@ class ImmichAssetGrid extends HookConsumerWidget { ); if (7 - scaleFactor.value.toInt() != perRow.value) { perRow.value = 7 - scaleFactor.value.toInt(); + settings.setSetting(AppSettingsEnum.tilesPerRow, perRow.value); } }; }), From 1b5fc9c66588e33b7818135af6cb3b9f4e1f04f3 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 28 Apr 2025 10:36:14 -0400 Subject: [PATCH 078/356] feat: notifications (#17701) * feat: notifications * UI works * chore: pr feedback * initial fetch and clear notification upon logging out * fix: merge --------- Co-authored-by: Alex Tran --- i18n/en.json | 5 + mobile/openapi/README.md | 18 +- mobile/openapi/lib/api.dart | 8 + .../lib/api/notifications_admin_api.dart | 55 +- mobile/openapi/lib/api/notifications_api.dart | 311 ++++++++++ mobile/openapi/lib/api_client.dart | 14 + mobile/openapi/lib/api_helper.dart | 6 + .../lib/model/notification_create_dto.dart | 180 ++++++ .../model/notification_delete_all_dto.dart | 101 ++++ .../openapi/lib/model/notification_dto.dart | 182 ++++++ .../openapi/lib/model/notification_level.dart | 91 +++ .../openapi/lib/model/notification_type.dart | 91 +++ .../model/notification_update_all_dto.dart | 112 ++++ .../lib/model/notification_update_dto.dart | 102 ++++ mobile/openapi/lib/model/permission.dart | 12 + open-api/immich-openapi-specs.json | 555 ++++++++++++++++-- open-api/typescript-sdk/src/fetch-client.ts | 194 ++++-- server/src/controllers/index.ts | 2 + .../notification-admin.controller.ts | 20 +- .../controllers/notification.controller.ts | 60 ++ server/src/database.ts | 1 + server/src/db.d.ts | 18 + server/src/dtos/notification.dto.ts | 108 +++- server/src/enum.ts | 20 + server/src/queries/access.repository.sql | 9 + .../src/queries/notification.repository.sql | 58 ++ server/src/repositories/access.repository.ts | 22 + server/src/repositories/event.repository.ts | 3 + server/src/repositories/index.ts | 4 +- .../repositories/notification.repository.ts | 103 ++++ server/src/schema/index.ts | 2 + .../1744991379464-AddNotificationsTable.ts | 22 + .../src/schema/tables/notification.table.ts | 52 ++ server/src/services/backup.service.spec.ts | 27 +- server/src/services/backup.service.ts | 2 +- server/src/services/base.service.ts | 2 + server/src/services/index.ts | 2 + server/src/services/job.service.ts | 6 +- .../notification-admin.service.spec.ts | 111 ++++ .../services/notification-admin.service.ts | 120 ++++ .../src/services/notification.service.spec.ts | 77 --- server/src/services/notification.service.ts | 93 ++- server/src/types.ts | 4 + server/src/utils/access.ts | 6 + server/test/medium.factory.ts | 24 +- .../notification.controller.spec.ts | 86 +++ .../repositories/access.repository.mock.ts | 4 + server/test/small.factory.ts | 1 + server/test/utils.ts | 4 + .../navigation-bar/navigation-bar.svelte | 27 +- .../navigation-bar/notification-item.svelte | 114 ++++ .../navigation-bar/notification-panel.svelte | 82 +++ .../lib/stores/notification-manager.svelte.ts | 38 ++ web/src/lib/stores/websocket.ts | 5 +- web/src/routes/auth/login/+page.svelte | 6 +- 55 files changed, 3186 insertions(+), 196 deletions(-) create mode 100644 mobile/openapi/lib/api/notifications_api.dart create mode 100644 mobile/openapi/lib/model/notification_create_dto.dart create mode 100644 mobile/openapi/lib/model/notification_delete_all_dto.dart create mode 100644 mobile/openapi/lib/model/notification_dto.dart create mode 100644 mobile/openapi/lib/model/notification_level.dart create mode 100644 mobile/openapi/lib/model/notification_type.dart create mode 100644 mobile/openapi/lib/model/notification_update_all_dto.dart create mode 100644 mobile/openapi/lib/model/notification_update_dto.dart create mode 100644 server/src/controllers/notification.controller.ts create mode 100644 server/src/queries/notification.repository.sql create mode 100644 server/src/repositories/notification.repository.ts create mode 100644 server/src/schema/migrations/1744991379464-AddNotificationsTable.ts create mode 100644 server/src/schema/tables/notification.table.ts create mode 100644 server/src/services/notification-admin.service.spec.ts create mode 100644 server/src/services/notification-admin.service.ts create mode 100644 server/test/medium/specs/controllers/notification.controller.spec.ts create mode 100644 web/src/lib/components/shared-components/navigation-bar/notification-item.svelte create mode 100644 web/src/lib/components/shared-components/navigation-bar/notification-panel.svelte create mode 100644 web/src/lib/stores/notification-manager.svelte.ts diff --git a/i18n/en.json b/i18n/en.json index eafb3415d5..8404d6d1d0 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -857,6 +857,7 @@ "failed_to_remove_product_key": "Failed to remove product key", "failed_to_stack_assets": "Failed to stack assets", "failed_to_unstack_assets": "Failed to un-stack assets", + "failed_to_update_notification_status": "Failed to update notification status", "import_path_already_exists": "This import path already exists.", "incorrect_email_or_password": "Incorrect email or password", "paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation", @@ -1199,6 +1200,9 @@ "map_settings_only_show_favorites": "Show Favorite Only", "map_settings_theme_settings": "Map Theme", "map_zoom_to_see_photos": "Zoom out to see photos", + "mark_as_read": "Mark as read", + "mark_all_as_read": "Mark all as read", + "marked_all_as_read": "Marked all as read", "matches": "Matches", "media_type": "Media type", "memories": "Memories", @@ -1260,6 +1264,7 @@ "no_places": "No places", "no_results": "No results", "no_results_description": "Try a synonym or more general keyword", + "no_notifications": "No notifications", "no_shared_albums_message": "Create an album to share photos and videos with people in your network", "not_in_any_album": "Not in any album", "not_selected": "Not selected", diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 5a7a42cce5..b8ea4b924c 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -145,8 +145,15 @@ Class | Method | HTTP request | Description *MemoriesApi* | [**removeMemoryAssets**](doc//MemoriesApi.md#removememoryassets) | **DELETE** /memories/{id}/assets | *MemoriesApi* | [**searchMemories**](doc//MemoriesApi.md#searchmemories) | **GET** /memories | *MemoriesApi* | [**updateMemory**](doc//MemoriesApi.md#updatememory) | **PUT** /memories/{id} | -*NotificationsAdminApi* | [**getNotificationTemplateAdmin**](doc//NotificationsAdminApi.md#getnotificationtemplateadmin) | **POST** /notifications/admin/templates/{name} | -*NotificationsAdminApi* | [**sendTestEmailAdmin**](doc//NotificationsAdminApi.md#sendtestemailadmin) | **POST** /notifications/admin/test-email | +*NotificationsApi* | [**deleteNotification**](doc//NotificationsApi.md#deletenotification) | **DELETE** /notifications/{id} | +*NotificationsApi* | [**deleteNotifications**](doc//NotificationsApi.md#deletenotifications) | **DELETE** /notifications | +*NotificationsApi* | [**getNotification**](doc//NotificationsApi.md#getnotification) | **GET** /notifications/{id} | +*NotificationsApi* | [**getNotifications**](doc//NotificationsApi.md#getnotifications) | **GET** /notifications | +*NotificationsApi* | [**updateNotification**](doc//NotificationsApi.md#updatenotification) | **PUT** /notifications/{id} | +*NotificationsApi* | [**updateNotifications**](doc//NotificationsApi.md#updatenotifications) | **PUT** /notifications | +*NotificationsAdminApi* | [**createNotification**](doc//NotificationsAdminApi.md#createnotification) | **POST** /admin/notifications | +*NotificationsAdminApi* | [**getNotificationTemplateAdmin**](doc//NotificationsAdminApi.md#getnotificationtemplateadmin) | **POST** /admin/notifications/templates/{name} | +*NotificationsAdminApi* | [**sendTestEmailAdmin**](doc//NotificationsAdminApi.md#sendtestemailadmin) | **POST** /admin/notifications/test-email | *OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback | *OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link | *OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect | @@ -360,6 +367,13 @@ Class | Method | HTTP request | Description - [MemoryUpdateDto](doc//MemoryUpdateDto.md) - [MergePersonDto](doc//MergePersonDto.md) - [MetadataSearchDto](doc//MetadataSearchDto.md) + - [NotificationCreateDto](doc//NotificationCreateDto.md) + - [NotificationDeleteAllDto](doc//NotificationDeleteAllDto.md) + - [NotificationDto](doc//NotificationDto.md) + - [NotificationLevel](doc//NotificationLevel.md) + - [NotificationType](doc//NotificationType.md) + - [NotificationUpdateAllDto](doc//NotificationUpdateAllDto.md) + - [NotificationUpdateDto](doc//NotificationUpdateDto.md) - [OAuthAuthorizeResponseDto](doc//OAuthAuthorizeResponseDto.md) - [OAuthCallbackDto](doc//OAuthCallbackDto.md) - [OAuthConfigDto](doc//OAuthConfigDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index d08f9fda38..e845099bd2 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -44,6 +44,7 @@ part 'api/jobs_api.dart'; part 'api/libraries_api.dart'; part 'api/map_api.dart'; part 'api/memories_api.dart'; +part 'api/notifications_api.dart'; part 'api/notifications_admin_api.dart'; part 'api/o_auth_api.dart'; part 'api/partners_api.dart'; @@ -167,6 +168,13 @@ part 'model/memory_type.dart'; part 'model/memory_update_dto.dart'; part 'model/merge_person_dto.dart'; part 'model/metadata_search_dto.dart'; +part 'model/notification_create_dto.dart'; +part 'model/notification_delete_all_dto.dart'; +part 'model/notification_dto.dart'; +part 'model/notification_level.dart'; +part 'model/notification_type.dart'; +part 'model/notification_update_all_dto.dart'; +part 'model/notification_update_dto.dart'; part 'model/o_auth_authorize_response_dto.dart'; part 'model/o_auth_callback_dto.dart'; part 'model/o_auth_config_dto.dart'; diff --git a/mobile/openapi/lib/api/notifications_admin_api.dart b/mobile/openapi/lib/api/notifications_admin_api.dart index c58bf8978d..409683a950 100644 --- a/mobile/openapi/lib/api/notifications_admin_api.dart +++ b/mobile/openapi/lib/api/notifications_admin_api.dart @@ -16,7 +16,54 @@ class NotificationsAdminApi { final ApiClient apiClient; - /// Performs an HTTP 'POST /notifications/admin/templates/{name}' operation and returns the [Response]. + /// Performs an HTTP 'POST /admin/notifications' operation and returns the [Response]. + /// Parameters: + /// + /// * [NotificationCreateDto] notificationCreateDto (required): + Future createNotificationWithHttpInfo(NotificationCreateDto notificationCreateDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/notifications'; + + // ignore: prefer_final_locals + Object? postBody = notificationCreateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [NotificationCreateDto] notificationCreateDto (required): + Future createNotification(NotificationCreateDto notificationCreateDto,) async { + final response = await createNotificationWithHttpInfo(notificationCreateDto,); + 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), 'NotificationDto',) as NotificationDto; + + } + return null; + } + + /// Performs an HTTP 'POST /admin/notifications/templates/{name}' operation and returns the [Response]. /// Parameters: /// /// * [String] name (required): @@ -24,7 +71,7 @@ class NotificationsAdminApi { /// * [TemplateDto] templateDto (required): Future getNotificationTemplateAdminWithHttpInfo(String name, TemplateDto templateDto,) async { // ignore: prefer_const_declarations - final apiPath = r'/notifications/admin/templates/{name}' + final apiPath = r'/admin/notifications/templates/{name}' .replaceAll('{name}', name); // ignore: prefer_final_locals @@ -68,13 +115,13 @@ class NotificationsAdminApi { return null; } - /// Performs an HTTP 'POST /notifications/admin/test-email' operation and returns the [Response]. + /// Performs an HTTP 'POST /admin/notifications/test-email' operation and returns the [Response]. /// Parameters: /// /// * [SystemConfigSmtpDto] systemConfigSmtpDto (required): Future sendTestEmailAdminWithHttpInfo(SystemConfigSmtpDto systemConfigSmtpDto,) async { // ignore: prefer_const_declarations - final apiPath = r'/notifications/admin/test-email'; + final apiPath = r'/admin/notifications/test-email'; // ignore: prefer_final_locals Object? postBody = systemConfigSmtpDto; diff --git a/mobile/openapi/lib/api/notifications_api.dart b/mobile/openapi/lib/api/notifications_api.dart new file mode 100644 index 0000000000..501cc70a29 --- /dev/null +++ b/mobile/openapi/lib/api/notifications_api.dart @@ -0,0 +1,311 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class NotificationsApi { + NotificationsApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'DELETE /notifications/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future deleteNotificationWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final apiPath = r'/notifications/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future deleteNotification(String id,) async { + final response = await deleteNotificationWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'DELETE /notifications' operation and returns the [Response]. + /// Parameters: + /// + /// * [NotificationDeleteAllDto] notificationDeleteAllDto (required): + Future deleteNotificationsWithHttpInfo(NotificationDeleteAllDto notificationDeleteAllDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/notifications'; + + // ignore: prefer_final_locals + Object? postBody = notificationDeleteAllDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [NotificationDeleteAllDto] notificationDeleteAllDto (required): + Future deleteNotifications(NotificationDeleteAllDto notificationDeleteAllDto,) async { + final response = await deleteNotificationsWithHttpInfo(notificationDeleteAllDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'GET /notifications/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future getNotificationWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final apiPath = r'/notifications/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future getNotification(String id,) async { + final response = await getNotificationWithHttpInfo(id,); + 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), 'NotificationDto',) as NotificationDto; + + } + return null; + } + + /// Performs an HTTP 'GET /notifications' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id: + /// + /// * [NotificationLevel] level: + /// + /// * [NotificationType] type: + /// + /// * [bool] unread: + Future getNotificationsWithHttpInfo({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/notifications'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (id != null) { + queryParams.addAll(_queryParams('', 'id', id)); + } + if (level != null) { + queryParams.addAll(_queryParams('', 'level', level)); + } + if (type != null) { + queryParams.addAll(_queryParams('', 'type', type)); + } + if (unread != null) { + queryParams.addAll(_queryParams('', 'unread', unread)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id: + /// + /// * [NotificationLevel] level: + /// + /// * [NotificationType] type: + /// + /// * [bool] unread: + Future?> getNotifications({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async { + final response = await getNotificationsWithHttpInfo( id: id, level: level, type: type, unread: unread, ); + 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) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// Performs an HTTP 'PUT /notifications/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [NotificationUpdateDto] notificationUpdateDto (required): + Future updateNotificationWithHttpInfo(String id, NotificationUpdateDto notificationUpdateDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/notifications/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = notificationUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [NotificationUpdateDto] notificationUpdateDto (required): + Future updateNotification(String id, NotificationUpdateDto notificationUpdateDto,) async { + final response = await updateNotificationWithHttpInfo(id, notificationUpdateDto,); + 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), 'NotificationDto',) as NotificationDto; + + } + return null; + } + + /// Performs an HTTP 'PUT /notifications' operation and returns the [Response]. + /// Parameters: + /// + /// * [NotificationUpdateAllDto] notificationUpdateAllDto (required): + Future updateNotificationsWithHttpInfo(NotificationUpdateAllDto notificationUpdateAllDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/notifications'; + + // ignore: prefer_final_locals + Object? postBody = notificationUpdateAllDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [NotificationUpdateAllDto] notificationUpdateAllDto (required): + Future updateNotifications(NotificationUpdateAllDto notificationUpdateAllDto,) async { + final response = await updateNotificationsWithHttpInfo(notificationUpdateAllDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } +} diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 0d8e4c6ba9..7586cc1ae2 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -390,6 +390,20 @@ class ApiClient { return MergePersonDto.fromJson(value); case 'MetadataSearchDto': return MetadataSearchDto.fromJson(value); + case 'NotificationCreateDto': + return NotificationCreateDto.fromJson(value); + case 'NotificationDeleteAllDto': + return NotificationDeleteAllDto.fromJson(value); + case 'NotificationDto': + return NotificationDto.fromJson(value); + case 'NotificationLevel': + return NotificationLevelTypeTransformer().decode(value); + case 'NotificationType': + return NotificationTypeTypeTransformer().decode(value); + case 'NotificationUpdateAllDto': + return NotificationUpdateAllDto.fromJson(value); + case 'NotificationUpdateDto': + return NotificationUpdateDto.fromJson(value); case 'OAuthAuthorizeResponseDto': return OAuthAuthorizeResponseDto.fromJson(value); case 'OAuthCallbackDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 1ebf8314ad..cc517d48ab 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -100,6 +100,12 @@ String parameterToString(dynamic value) { if (value is MemoryType) { return MemoryTypeTypeTransformer().encode(value).toString(); } + if (value is NotificationLevel) { + return NotificationLevelTypeTransformer().encode(value).toString(); + } + if (value is NotificationType) { + return NotificationTypeTypeTransformer().encode(value).toString(); + } if (value is PartnerDirection) { return PartnerDirectionTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/notification_create_dto.dart b/mobile/openapi/lib/model/notification_create_dto.dart new file mode 100644 index 0000000000..07985353b2 --- /dev/null +++ b/mobile/openapi/lib/model/notification_create_dto.dart @@ -0,0 +1,180 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class NotificationCreateDto { + /// Returns a new [NotificationCreateDto] instance. + NotificationCreateDto({ + this.data, + this.description, + this.level, + this.readAt, + required this.title, + this.type, + required this.userId, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + Object? data; + + String? description; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + NotificationLevel? level; + + DateTime? readAt; + + String title; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + NotificationType? type; + + String userId; + + @override + bool operator ==(Object other) => identical(this, other) || other is NotificationCreateDto && + other.data == data && + other.description == description && + other.level == level && + other.readAt == readAt && + other.title == title && + other.type == type && + other.userId == userId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (data == null ? 0 : data!.hashCode) + + (description == null ? 0 : description!.hashCode) + + (level == null ? 0 : level!.hashCode) + + (readAt == null ? 0 : readAt!.hashCode) + + (title.hashCode) + + (type == null ? 0 : type!.hashCode) + + (userId.hashCode); + + @override + String toString() => 'NotificationCreateDto[data=$data, description=$description, level=$level, readAt=$readAt, title=$title, type=$type, userId=$userId]'; + + Map toJson() { + final json = {}; + if (this.data != null) { + json[r'data'] = this.data; + } else { + // json[r'data'] = null; + } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } + if (this.level != null) { + json[r'level'] = this.level; + } else { + // json[r'level'] = null; + } + if (this.readAt != null) { + json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + } else { + // json[r'readAt'] = null; + } + json[r'title'] = this.title; + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + json[r'userId'] = this.userId; + return json; + } + + /// Returns a new [NotificationCreateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static NotificationCreateDto? fromJson(dynamic value) { + upgradeDto(value, "NotificationCreateDto"); + if (value is Map) { + final json = value.cast(); + + return NotificationCreateDto( + data: mapValueOfType(json, r'data'), + description: mapValueOfType(json, r'description'), + level: NotificationLevel.fromJson(json[r'level']), + readAt: mapDateTime(json, r'readAt', r''), + title: mapValueOfType(json, r'title')!, + type: NotificationType.fromJson(json[r'type']), + userId: mapValueOfType(json, r'userId')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = NotificationCreateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = NotificationCreateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of NotificationCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = NotificationCreateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'title', + 'userId', + }; +} + diff --git a/mobile/openapi/lib/model/notification_delete_all_dto.dart b/mobile/openapi/lib/model/notification_delete_all_dto.dart new file mode 100644 index 0000000000..4be1b89e92 --- /dev/null +++ b/mobile/openapi/lib/model/notification_delete_all_dto.dart @@ -0,0 +1,101 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class NotificationDeleteAllDto { + /// Returns a new [NotificationDeleteAllDto] instance. + NotificationDeleteAllDto({ + this.ids = const [], + }); + + List ids; + + @override + bool operator ==(Object other) => identical(this, other) || other is NotificationDeleteAllDto && + _deepEquality.equals(other.ids, ids); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (ids.hashCode); + + @override + String toString() => 'NotificationDeleteAllDto[ids=$ids]'; + + Map toJson() { + final json = {}; + json[r'ids'] = this.ids; + return json; + } + + /// Returns a new [NotificationDeleteAllDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static NotificationDeleteAllDto? fromJson(dynamic value) { + upgradeDto(value, "NotificationDeleteAllDto"); + if (value is Map) { + final json = value.cast(); + + return NotificationDeleteAllDto( + ids: json[r'ids'] is Iterable + ? (json[r'ids'] as Iterable).cast().toList(growable: false) + : const [], + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = NotificationDeleteAllDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = NotificationDeleteAllDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of NotificationDeleteAllDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = NotificationDeleteAllDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'ids', + }; +} + diff --git a/mobile/openapi/lib/model/notification_dto.dart b/mobile/openapi/lib/model/notification_dto.dart new file mode 100644 index 0000000000..4f730b4e50 --- /dev/null +++ b/mobile/openapi/lib/model/notification_dto.dart @@ -0,0 +1,182 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class NotificationDto { + /// Returns a new [NotificationDto] instance. + NotificationDto({ + required this.createdAt, + this.data, + this.description, + required this.id, + required this.level, + this.readAt, + required this.title, + required this.type, + }); + + DateTime createdAt; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + Object? data; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? description; + + String id; + + NotificationLevel level; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? readAt; + + String title; + + NotificationType type; + + @override + bool operator ==(Object other) => identical(this, other) || other is NotificationDto && + other.createdAt == createdAt && + other.data == data && + other.description == description && + other.id == id && + other.level == level && + other.readAt == readAt && + other.title == title && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (createdAt.hashCode) + + (data == null ? 0 : data!.hashCode) + + (description == null ? 0 : description!.hashCode) + + (id.hashCode) + + (level.hashCode) + + (readAt == null ? 0 : readAt!.hashCode) + + (title.hashCode) + + (type.hashCode); + + @override + String toString() => 'NotificationDto[createdAt=$createdAt, data=$data, description=$description, id=$id, level=$level, readAt=$readAt, title=$title, type=$type]'; + + Map toJson() { + final json = {}; + json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + if (this.data != null) { + json[r'data'] = this.data; + } else { + // json[r'data'] = null; + } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } + json[r'id'] = this.id; + json[r'level'] = this.level; + if (this.readAt != null) { + json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + } else { + // json[r'readAt'] = null; + } + json[r'title'] = this.title; + json[r'type'] = this.type; + return json; + } + + /// Returns a new [NotificationDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static NotificationDto? fromJson(dynamic value) { + upgradeDto(value, "NotificationDto"); + if (value is Map) { + final json = value.cast(); + + return NotificationDto( + createdAt: mapDateTime(json, r'createdAt', r'')!, + data: mapValueOfType(json, r'data'), + description: mapValueOfType(json, r'description'), + id: mapValueOfType(json, r'id')!, + level: NotificationLevel.fromJson(json[r'level'])!, + readAt: mapDateTime(json, r'readAt', r''), + title: mapValueOfType(json, r'title')!, + type: NotificationType.fromJson(json[r'type'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = NotificationDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = NotificationDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of NotificationDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = NotificationDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'createdAt', + 'id', + 'level', + 'title', + 'type', + }; +} + diff --git a/mobile/openapi/lib/model/notification_level.dart b/mobile/openapi/lib/model/notification_level.dart new file mode 100644 index 0000000000..554863ae4f --- /dev/null +++ b/mobile/openapi/lib/model/notification_level.dart @@ -0,0 +1,91 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class NotificationLevel { + /// Instantiate a new enum with the provided [value]. + const NotificationLevel._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const success = NotificationLevel._(r'success'); + static const error = NotificationLevel._(r'error'); + static const warning = NotificationLevel._(r'warning'); + static const info = NotificationLevel._(r'info'); + + /// List of all possible values in this [enum][NotificationLevel]. + static const values = [ + success, + error, + warning, + info, + ]; + + static NotificationLevel? fromJson(dynamic value) => NotificationLevelTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = NotificationLevel.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [NotificationLevel] to String, +/// and [decode] dynamic data back to [NotificationLevel]. +class NotificationLevelTypeTransformer { + factory NotificationLevelTypeTransformer() => _instance ??= const NotificationLevelTypeTransformer._(); + + const NotificationLevelTypeTransformer._(); + + String encode(NotificationLevel data) => data.value; + + /// Decodes a [dynamic value][data] to a NotificationLevel. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + NotificationLevel? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'success': return NotificationLevel.success; + case r'error': return NotificationLevel.error; + case r'warning': return NotificationLevel.warning; + case r'info': return NotificationLevel.info; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [NotificationLevelTypeTransformer] instance. + static NotificationLevelTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/notification_type.dart b/mobile/openapi/lib/model/notification_type.dart new file mode 100644 index 0000000000..436d2d190f --- /dev/null +++ b/mobile/openapi/lib/model/notification_type.dart @@ -0,0 +1,91 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class NotificationType { + /// Instantiate a new enum with the provided [value]. + const NotificationType._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const jobFailed = NotificationType._(r'JobFailed'); + static const backupFailed = NotificationType._(r'BackupFailed'); + static const systemMessage = NotificationType._(r'SystemMessage'); + static const custom = NotificationType._(r'Custom'); + + /// List of all possible values in this [enum][NotificationType]. + static const values = [ + jobFailed, + backupFailed, + systemMessage, + custom, + ]; + + static NotificationType? fromJson(dynamic value) => NotificationTypeTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = NotificationType.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [NotificationType] to String, +/// and [decode] dynamic data back to [NotificationType]. +class NotificationTypeTypeTransformer { + factory NotificationTypeTypeTransformer() => _instance ??= const NotificationTypeTypeTransformer._(); + + const NotificationTypeTypeTransformer._(); + + String encode(NotificationType data) => data.value; + + /// Decodes a [dynamic value][data] to a NotificationType. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + NotificationType? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'JobFailed': return NotificationType.jobFailed; + case r'BackupFailed': return NotificationType.backupFailed; + case r'SystemMessage': return NotificationType.systemMessage; + case r'Custom': return NotificationType.custom; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [NotificationTypeTypeTransformer] instance. + static NotificationTypeTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/notification_update_all_dto.dart b/mobile/openapi/lib/model/notification_update_all_dto.dart new file mode 100644 index 0000000000..a6393b275a --- /dev/null +++ b/mobile/openapi/lib/model/notification_update_all_dto.dart @@ -0,0 +1,112 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class NotificationUpdateAllDto { + /// Returns a new [NotificationUpdateAllDto] instance. + NotificationUpdateAllDto({ + this.ids = const [], + this.readAt, + }); + + List ids; + + DateTime? readAt; + + @override + bool operator ==(Object other) => identical(this, other) || other is NotificationUpdateAllDto && + _deepEquality.equals(other.ids, ids) && + other.readAt == readAt; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (ids.hashCode) + + (readAt == null ? 0 : readAt!.hashCode); + + @override + String toString() => 'NotificationUpdateAllDto[ids=$ids, readAt=$readAt]'; + + Map toJson() { + final json = {}; + json[r'ids'] = this.ids; + if (this.readAt != null) { + json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + } else { + // json[r'readAt'] = null; + } + return json; + } + + /// Returns a new [NotificationUpdateAllDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static NotificationUpdateAllDto? fromJson(dynamic value) { + upgradeDto(value, "NotificationUpdateAllDto"); + if (value is Map) { + final json = value.cast(); + + return NotificationUpdateAllDto( + ids: json[r'ids'] is Iterable + ? (json[r'ids'] as Iterable).cast().toList(growable: false) + : const [], + readAt: mapDateTime(json, r'readAt', r''), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = NotificationUpdateAllDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = NotificationUpdateAllDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of NotificationUpdateAllDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = NotificationUpdateAllDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'ids', + }; +} + diff --git a/mobile/openapi/lib/model/notification_update_dto.dart b/mobile/openapi/lib/model/notification_update_dto.dart new file mode 100644 index 0000000000..e76496eb97 --- /dev/null +++ b/mobile/openapi/lib/model/notification_update_dto.dart @@ -0,0 +1,102 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class NotificationUpdateDto { + /// Returns a new [NotificationUpdateDto] instance. + NotificationUpdateDto({ + this.readAt, + }); + + DateTime? readAt; + + @override + bool operator ==(Object other) => identical(this, other) || other is NotificationUpdateDto && + other.readAt == readAt; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (readAt == null ? 0 : readAt!.hashCode); + + @override + String toString() => 'NotificationUpdateDto[readAt=$readAt]'; + + Map toJson() { + final json = {}; + if (this.readAt != null) { + json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + } else { + // json[r'readAt'] = null; + } + return json; + } + + /// Returns a new [NotificationUpdateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static NotificationUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "NotificationUpdateDto"); + if (value is Map) { + final json = value.cast(); + + return NotificationUpdateDto( + readAt: mapDateTime(json, r'readAt', r''), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = NotificationUpdateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = NotificationUpdateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of NotificationUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = NotificationUpdateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index 1244a434b6..1735bc2eb5 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -66,6 +66,10 @@ class Permission { static const memoryPeriodRead = Permission._(r'memory.read'); static const memoryPeriodUpdate = Permission._(r'memory.update'); static const memoryPeriodDelete = Permission._(r'memory.delete'); + static const notificationPeriodCreate = Permission._(r'notification.create'); + static const notificationPeriodRead = Permission._(r'notification.read'); + static const notificationPeriodUpdate = Permission._(r'notification.update'); + static const notificationPeriodDelete = Permission._(r'notification.delete'); static const partnerPeriodCreate = Permission._(r'partner.create'); static const partnerPeriodRead = Permission._(r'partner.read'); static const partnerPeriodUpdate = Permission._(r'partner.update'); @@ -147,6 +151,10 @@ class Permission { memoryPeriodRead, memoryPeriodUpdate, memoryPeriodDelete, + notificationPeriodCreate, + notificationPeriodRead, + notificationPeriodUpdate, + notificationPeriodDelete, partnerPeriodCreate, partnerPeriodRead, partnerPeriodUpdate, @@ -263,6 +271,10 @@ class PermissionTypeTransformer { case r'memory.read': return Permission.memoryPeriodRead; case r'memory.update': return Permission.memoryPeriodUpdate; case r'memory.delete': return Permission.memoryPeriodDelete; + case r'notification.create': return Permission.notificationPeriodCreate; + case r'notification.read': return Permission.notificationPeriodRead; + case r'notification.update': return Permission.notificationPeriodUpdate; + case r'notification.delete': return Permission.notificationPeriodDelete; case r'partner.create': return Permission.partnerPeriodCreate; case r'partner.read': return Permission.partnerPeriodRead; case r'partner.update': return Permission.partnerPeriodUpdate; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 1471020cd4..f4ec929373 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -206,6 +206,141 @@ ] } }, + "/admin/notifications": { + "post": { + "operationId": "createNotification", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationCreateDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Notifications (Admin)" + ] + } + }, + "/admin/notifications/templates/{name}": { + "post": { + "operationId": "getNotificationTemplateAdmin", + "parameters": [ + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Notifications (Admin)" + ] + } + }, + "/admin/notifications/test-email": { + "post": { + "operationId": "sendTestEmailAdmin", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SystemConfigSmtpDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestEmailResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Notifications (Admin)" + ] + } + }, "/admin/users": { "get": { "operationId": "searchUsersAdmin", @@ -3485,15 +3620,224 @@ ] } }, - "/notifications/admin/templates/{name}": { - "post": { - "operationId": "getNotificationTemplateAdmin", + "/notifications": { + "delete": { + "operationId": "deleteNotifications", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationDeleteAllDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Notifications" + ] + }, + "get": { + "operationId": "getNotifications", "parameters": [ { - "name": "name", + "name": "id", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "level", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/NotificationLevel" + } + }, + { + "name": "type", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/NotificationType" + } + }, + { + "name": "unread", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NotificationDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Notifications" + ] + }, + "put": { + "operationId": "updateNotifications", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationUpdateAllDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Notifications" + ] + } + }, + "/notifications/{id}": { + "delete": { + "operationId": "deleteNotification", + "parameters": [ + { + "name": "id", "required": true, "in": "path", "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Notifications" + ] + }, + "get": { + "operationId": "getNotification", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Notifications" + ] + }, + "put": { + "operationId": "updateNotification", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", "type": "string" } } @@ -3502,7 +3846,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateDto" + "$ref": "#/components/schemas/NotificationUpdateDto" } } }, @@ -3513,7 +3857,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TemplateResponseDto" + "$ref": "#/components/schemas/NotificationDto" } } }, @@ -3532,49 +3876,7 @@ } ], "tags": [ - "Notifications (Admin)" - ] - } - }, - "/notifications/admin/test-email": { - "post": { - "operationId": "sendTestEmailAdmin", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SystemConfigSmtpDto" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TestEmailResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Notifications (Admin)" + "Notifications" ] } }, @@ -10326,6 +10628,157 @@ }, "type": "object" }, + "NotificationCreateDto": { + "properties": { + "data": { + "type": "object" + }, + "description": { + "nullable": true, + "type": "string" + }, + "level": { + "allOf": [ + { + "$ref": "#/components/schemas/NotificationLevel" + } + ] + }, + "readAt": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/NotificationType" + } + ] + }, + "userId": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "title", + "userId" + ], + "type": "object" + }, + "NotificationDeleteAllDto": { + "properties": { + "ids": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "ids" + ], + "type": "object" + }, + "NotificationDto": { + "properties": { + "createdAt": { + "format": "date-time", + "type": "string" + }, + "data": { + "type": "object" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "level": { + "allOf": [ + { + "$ref": "#/components/schemas/NotificationLevel" + } + ] + }, + "readAt": { + "format": "date-time", + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/NotificationType" + } + ] + } + }, + "required": [ + "createdAt", + "id", + "level", + "title", + "type" + ], + "type": "object" + }, + "NotificationLevel": { + "enum": [ + "success", + "error", + "warning", + "info" + ], + "type": "string" + }, + "NotificationType": { + "enum": [ + "JobFailed", + "BackupFailed", + "SystemMessage", + "Custom" + ], + "type": "string" + }, + "NotificationUpdateAllDto": { + "properties": { + "ids": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, + "readAt": { + "format": "date-time", + "nullable": true, + "type": "string" + } + }, + "required": [ + "ids" + ], + "type": "object" + }, + "NotificationUpdateDto": { + "properties": { + "readAt": { + "format": "date-time", + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, "OAuthAuthorizeResponseDto": { "properties": { "url": { @@ -10600,6 +11053,10 @@ "memory.read", "memory.update", "memory.delete", + "notification.create", + "notification.read", + "notification.update", + "notification.delete", "partner.create", "partner.read", "partner.update", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 1ba4d3e231..647c5c4ada 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -39,6 +39,48 @@ export type ActivityCreateDto = { export type ActivityStatisticsResponseDto = { comments: number; }; +export type NotificationCreateDto = { + data?: object; + description?: string | null; + level?: NotificationLevel; + readAt?: string | null; + title: string; + "type"?: NotificationType; + userId: string; +}; +export type NotificationDto = { + createdAt: string; + data?: object; + description?: string; + id: string; + level: NotificationLevel; + readAt?: string; + title: string; + "type": NotificationType; +}; +export type TemplateDto = { + template: string; +}; +export type TemplateResponseDto = { + html: string; + name: string; +}; +export type SystemConfigSmtpTransportDto = { + host: string; + ignoreCert: boolean; + password: string; + port: number; + username: string; +}; +export type SystemConfigSmtpDto = { + enabled: boolean; + "from": string; + replyTo: string; + transport: SystemConfigSmtpTransportDto; +}; +export type TestEmailResponseDto = { + messageId: string; +}; export type UserLicense = { activatedAt: string; activationKey: string; @@ -661,28 +703,15 @@ export type MemoryUpdateDto = { memoryAt?: string; seenAt?: string; }; -export type TemplateDto = { - template: string; +export type NotificationDeleteAllDto = { + ids: string[]; }; -export type TemplateResponseDto = { - html: string; - name: string; +export type NotificationUpdateAllDto = { + ids: string[]; + readAt?: string | null; }; -export type SystemConfigSmtpTransportDto = { - host: string; - ignoreCert: boolean; - password: string; - port: number; - username: string; -}; -export type SystemConfigSmtpDto = { - enabled: boolean; - "from": string; - replyTo: string; - transport: SystemConfigSmtpTransportDto; -}; -export type TestEmailResponseDto = { - messageId: string; +export type NotificationUpdateDto = { + readAt?: string | null; }; export type OAuthConfigDto = { codeChallenge?: string; @@ -1453,6 +1482,43 @@ export function deleteActivity({ id }: { method: "DELETE" })); } +export function createNotification({ notificationCreateDto }: { + notificationCreateDto: NotificationCreateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: NotificationDto; + }>("/admin/notifications", oazapfts.json({ + ...opts, + method: "POST", + body: notificationCreateDto + }))); +} +export function getNotificationTemplateAdmin({ name, templateDto }: { + name: string; + templateDto: TemplateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TemplateResponseDto; + }>(`/admin/notifications/templates/${encodeURIComponent(name)}`, oazapfts.json({ + ...opts, + method: "POST", + body: templateDto + }))); +} +export function sendTestEmailAdmin({ systemConfigSmtpDto }: { + systemConfigSmtpDto: SystemConfigSmtpDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TestEmailResponseDto; + }>("/admin/notifications/test-email", oazapfts.json({ + ...opts, + method: "POST", + body: systemConfigSmtpDto + }))); +} export function searchUsersAdmin({ withDeleted }: { withDeleted?: boolean; }, opts?: Oazapfts.RequestOpts) { @@ -2321,29 +2387,71 @@ export function addMemoryAssets({ id, bulkIdsDto }: { body: bulkIdsDto }))); } -export function getNotificationTemplateAdmin({ name, templateDto }: { - name: string; - templateDto: TemplateDto; +export function deleteNotifications({ notificationDeleteAllDto }: { + notificationDeleteAllDto: NotificationDeleteAllDto; }, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: TemplateResponseDto; - }>(`/notifications/admin/templates/${encodeURIComponent(name)}`, oazapfts.json({ + return oazapfts.ok(oazapfts.fetchText("/notifications", oazapfts.json({ ...opts, - method: "POST", - body: templateDto + method: "DELETE", + body: notificationDeleteAllDto }))); } -export function sendTestEmailAdmin({ systemConfigSmtpDto }: { - systemConfigSmtpDto: SystemConfigSmtpDto; +export function getNotifications({ id, level, $type, unread }: { + id?: string; + level?: NotificationLevel; + $type?: NotificationType; + unread?: boolean; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: TestEmailResponseDto; - }>("/notifications/admin/test-email", oazapfts.json({ + data: NotificationDto[]; + }>(`/notifications${QS.query(QS.explode({ + id, + level, + "type": $type, + unread + }))}`, { + ...opts + })); +} +export function updateNotifications({ notificationUpdateAllDto }: { + notificationUpdateAllDto: NotificationUpdateAllDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/notifications", oazapfts.json({ ...opts, - method: "POST", - body: systemConfigSmtpDto + method: "PUT", + body: notificationUpdateAllDto + }))); +} +export function deleteNotification({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/notifications/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +export function getNotification({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: NotificationDto; + }>(`/notifications/${encodeURIComponent(id)}`, { + ...opts + })); +} +export function updateNotification({ id, notificationUpdateDto }: { + id: string; + notificationUpdateDto: NotificationUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: NotificationDto; + }>(`/notifications/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: notificationUpdateDto }))); } export function startOAuth({ oAuthConfigDto }: { @@ -3452,6 +3560,18 @@ export enum UserAvatarColor { Gray = "gray", Amber = "amber" } +export enum NotificationLevel { + Success = "success", + Error = "error", + Warning = "warning", + Info = "info" +} +export enum NotificationType { + JobFailed = "JobFailed", + BackupFailed = "BackupFailed", + SystemMessage = "SystemMessage", + Custom = "Custom" +} export enum UserStatus { Active = "active", Removing = "removing", @@ -3526,6 +3646,10 @@ export enum Permission { MemoryRead = "memory.read", MemoryUpdate = "memory.update", MemoryDelete = "memory.delete", + NotificationCreate = "notification.create", + NotificationRead = "notification.read", + NotificationUpdate = "notification.update", + NotificationDelete = "notification.delete", PartnerCreate = "partner.create", PartnerRead = "partner.read", PartnerUpdate = "partner.update", diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index 0da0aac8b1..e36793b3d7 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -14,6 +14,7 @@ import { LibraryController } from 'src/controllers/library.controller'; import { MapController } from 'src/controllers/map.controller'; import { MemoryController } from 'src/controllers/memory.controller'; import { NotificationAdminController } from 'src/controllers/notification-admin.controller'; +import { NotificationController } from 'src/controllers/notification.controller'; import { OAuthController } from 'src/controllers/oauth.controller'; import { PartnerController } from 'src/controllers/partner.controller'; import { PersonController } from 'src/controllers/person.controller'; @@ -47,6 +48,7 @@ export const controllers = [ LibraryController, MapController, MemoryController, + NotificationController, NotificationAdminController, OAuthController, PartnerController, diff --git a/server/src/controllers/notification-admin.controller.ts b/server/src/controllers/notification-admin.controller.ts index 937244fc56..9bac865bdf 100644 --- a/server/src/controllers/notification-admin.controller.ts +++ b/server/src/controllers/notification-admin.controller.ts @@ -1,16 +1,28 @@ import { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; -import { TemplateDto, TemplateResponseDto, TestEmailResponseDto } from 'src/dtos/notification.dto'; +import { + NotificationCreateDto, + NotificationDto, + TemplateDto, + TemplateResponseDto, + TestEmailResponseDto, +} from 'src/dtos/notification.dto'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { EmailTemplate } from 'src/repositories/email.repository'; -import { NotificationService } from 'src/services/notification.service'; +import { NotificationAdminService } from 'src/services/notification-admin.service'; @ApiTags('Notifications (Admin)') -@Controller('notifications/admin') +@Controller('admin/notifications') export class NotificationAdminController { - constructor(private service: NotificationService) {} + constructor(private service: NotificationAdminService) {} + + @Post() + @Authenticated({ admin: true }) + createNotification(@Auth() auth: AuthDto, @Body() dto: NotificationCreateDto): Promise { + return this.service.create(auth, dto); + } @Post('test-email') @HttpCode(HttpStatus.OK) diff --git a/server/src/controllers/notification.controller.ts b/server/src/controllers/notification.controller.ts new file mode 100644 index 0000000000..c64f786850 --- /dev/null +++ b/server/src/controllers/notification.controller.ts @@ -0,0 +1,60 @@ +import { Body, Controller, Delete, Get, Param, Put, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { + NotificationDeleteAllDto, + NotificationDto, + NotificationSearchDto, + NotificationUpdateAllDto, + NotificationUpdateDto, +} from 'src/dtos/notification.dto'; +import { Permission } from 'src/enum'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { NotificationService } from 'src/services/notification.service'; +import { UUIDParamDto } from 'src/validation'; + +@ApiTags('Notifications') +@Controller('notifications') +export class NotificationController { + constructor(private service: NotificationService) {} + + @Get() + @Authenticated({ permission: Permission.NOTIFICATION_READ }) + getNotifications(@Auth() auth: AuthDto, @Query() dto: NotificationSearchDto): Promise { + return this.service.search(auth, dto); + } + + @Put() + @Authenticated({ permission: Permission.NOTIFICATION_UPDATE }) + updateNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationUpdateAllDto): Promise { + return this.service.updateAll(auth, dto); + } + + @Delete() + @Authenticated({ permission: Permission.NOTIFICATION_DELETE }) + deleteNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationDeleteAllDto): Promise { + return this.service.deleteAll(auth, dto); + } + + @Get(':id') + @Authenticated({ permission: Permission.NOTIFICATION_READ }) + getNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.get(auth, id); + } + + @Put(':id') + @Authenticated({ permission: Permission.NOTIFICATION_UPDATE }) + updateNotification( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Body() dto: NotificationUpdateDto, + ): Promise { + return this.service.update(auth, id, dto); + } + + @Delete(':id') + @Authenticated({ permission: Permission.NOTIFICATION_DELETE }) + deleteNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.delete(auth, id); + } +} diff --git a/server/src/database.ts b/server/src/database.ts index 0dab61cbe0..a93873ef42 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -333,6 +333,7 @@ export const columns = { ], tag: ['tags.id', 'tags.value', 'tags.createdAt', 'tags.updatedAt', 'tags.color', 'tags.parentId'], apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'], + notification: ['id', 'createdAt', 'level', 'type', 'title', 'description', 'data', 'readAt'], syncAsset: [ 'id', 'ownerId', diff --git a/server/src/db.d.ts b/server/src/db.d.ts index 4e9738ecec..85be9d5208 100644 --- a/server/src/db.d.ts +++ b/server/src/db.d.ts @@ -11,6 +11,8 @@ import { AssetStatus, AssetType, MemoryType, + NotificationLevel, + NotificationType, Permission, SharedLinkType, SourceType, @@ -263,6 +265,21 @@ export interface Memories { updateId: Generated; } +export interface Notifications { + id: Generated; + createdAt: Generated; + updatedAt: Generated; + deletedAt: Timestamp | null; + updateId: Generated; + userId: string; + level: Generated; + type: NotificationType; + title: string; + description: string | null; + data: any | null; + readAt: Timestamp | null; +} + export interface MemoriesAssetsAssets { assetsId: string; memoriesId: string; @@ -463,6 +480,7 @@ export interface DB { memories: Memories; memories_assets_assets: MemoriesAssetsAssets; migrations: Migrations; + notifications: Notifications; move_history: MoveHistory; naturalearth_countries: NaturalearthCountries; partners_audit: PartnersAudit; diff --git a/server/src/dtos/notification.dto.ts b/server/src/dtos/notification.dto.ts index c1a09c801c..d9847cda17 100644 --- a/server/src/dtos/notification.dto.ts +++ b/server/src/dtos/notification.dto.ts @@ -1,4 +1,7 @@ -import { IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsString } from 'class-validator'; +import { NotificationLevel, NotificationType } from 'src/enum'; +import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; export class TestEmailResponseDto { messageId!: string; @@ -11,3 +14,106 @@ export class TemplateDto { @IsString() template!: string; } + +export class NotificationDto { + id!: string; + @ValidateDate() + createdAt!: Date; + @ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' }) + level!: NotificationLevel; + @ApiProperty({ enum: NotificationType, enumName: 'NotificationType' }) + type!: NotificationType; + title!: string; + description?: string; + data?: any; + readAt?: Date; +} + +export class NotificationSearchDto { + @Optional() + @ValidateUUID({ optional: true }) + id?: string; + + @IsEnum(NotificationLevel) + @Optional() + @ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' }) + level?: NotificationLevel; + + @IsEnum(NotificationType) + @Optional() + @ApiProperty({ enum: NotificationType, enumName: 'NotificationType' }) + type?: NotificationType; + + @ValidateBoolean({ optional: true }) + unread?: boolean; +} + +export class NotificationCreateDto { + @Optional() + @IsEnum(NotificationLevel) + @ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' }) + level?: NotificationLevel; + + @IsEnum(NotificationType) + @Optional() + @ApiProperty({ enum: NotificationType, enumName: 'NotificationType' }) + type?: NotificationType; + + @IsString() + title!: string; + + @IsString() + @Optional({ nullable: true }) + description?: string | null; + + @Optional({ nullable: true }) + data?: any; + + @ValidateDate({ optional: true, nullable: true }) + readAt?: Date | null; + + @ValidateUUID() + userId!: string; +} + +export class NotificationUpdateDto { + @ValidateDate({ optional: true, nullable: true }) + readAt?: Date | null; +} + +export class NotificationUpdateAllDto { + @ValidateUUID({ each: true, optional: true }) + ids!: string[]; + + @ValidateDate({ optional: true, nullable: true }) + readAt?: Date | null; +} + +export class NotificationDeleteAllDto { + @ValidateUUID({ each: true }) + ids!: string[]; +} + +export type MapNotification = { + id: string; + createdAt: Date; + updateId?: string; + level: NotificationLevel; + type: NotificationType; + data: any | null; + title: string; + description: string | null; + readAt: Date | null; +}; +export const mapNotification = (notification: MapNotification): NotificationDto => { + return { + id: notification.id, + createdAt: notification.createdAt, + level: notification.level, + type: notification.type, + title: notification.title, + description: notification.description ?? undefined, + data: notification.data ?? undefined, + readAt: notification.readAt ?? undefined, + }; +}; diff --git a/server/src/enum.ts b/server/src/enum.ts index b9a914671a..9fb6168b1a 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -126,6 +126,11 @@ export enum Permission { MEMORY_UPDATE = 'memory.update', MEMORY_DELETE = 'memory.delete', + NOTIFICATION_CREATE = 'notification.create', + NOTIFICATION_READ = 'notification.read', + NOTIFICATION_UPDATE = 'notification.update', + NOTIFICATION_DELETE = 'notification.delete', + PARTNER_CREATE = 'partner.create', PARTNER_READ = 'partner.read', PARTNER_UPDATE = 'partner.update', @@ -515,6 +520,7 @@ export enum JobName { NOTIFY_SIGNUP = 'notify-signup', NOTIFY_ALBUM_INVITE = 'notify-album-invite', NOTIFY_ALBUM_UPDATE = 'notify-album-update', + NOTIFICATIONS_CLEANUP = 'notifications-cleanup', SEND_EMAIL = 'notification-send-email', // Version check @@ -580,3 +586,17 @@ export enum SyncEntityType { PartnerAssetDeleteV1 = 'PartnerAssetDeleteV1', PartnerAssetExifV1 = 'PartnerAssetExifV1', } + +export enum NotificationLevel { + Success = 'success', + Error = 'error', + Warning = 'warning', + Info = 'info', +} + +export enum NotificationType { + JobFailed = 'JobFailed', + BackupFailed = 'BackupFailed', + SystemMessage = 'SystemMessage', + Custom = 'Custom', +} diff --git a/server/src/queries/access.repository.sql b/server/src/queries/access.repository.sql index dd58aebcb2..03f1af3b28 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -157,6 +157,15 @@ where and "memories"."ownerId" = $2 and "memories"."deletedAt" is null +-- AccessRepository.notification.checkOwnerAccess +select + "notifications"."id" +from + "notifications" +where + "notifications"."id" in ($1) + and "notifications"."userId" = $2 + -- AccessRepository.person.checkOwnerAccess select "person"."id" diff --git a/server/src/queries/notification.repository.sql b/server/src/queries/notification.repository.sql new file mode 100644 index 0000000000..c55e00d226 --- /dev/null +++ b/server/src/queries/notification.repository.sql @@ -0,0 +1,58 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- NotificationRepository.cleanup +delete from "notifications" +where + ( + ( + "deletedAt" is not null + and "deletedAt" < $1 + ) + or ( + "readAt" > $2 + and "createdAt" < $3 + ) + or ( + "readAt" = $4 + and "createdAt" < $5 + ) + ) + +-- NotificationRepository.search +select + "id", + "createdAt", + "level", + "type", + "title", + "description", + "data", + "readAt" +from + "notifications" +where + "userId" = $1 + and "deletedAt" is null +order by + "createdAt" desc + +-- NotificationRepository.search (unread) +select + "id", + "createdAt", + "level", + "type", + "title", + "description", + "data", + "readAt" +from + "notifications" +where + ( + "userId" = $1 + and "readAt" is null + ) + and "deletedAt" is null +order by + "createdAt" desc diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index 961cccbf3e..c24209e482 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -279,6 +279,26 @@ class AuthDeviceAccess { } } +class NotificationAccess { + constructor(private db: Kysely) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) + async checkOwnerAccess(userId: string, notificationIds: Set) { + if (notificationIds.size === 0) { + return new Set(); + } + + return this.db + .selectFrom('notifications') + .select('notifications.id') + .where('notifications.id', 'in', [...notificationIds]) + .where('notifications.userId', '=', userId) + .execute() + .then((stacks) => new Set(stacks.map((stack) => stack.id))); + } +} + class StackAccess { constructor(private db: Kysely) {} @@ -426,6 +446,7 @@ export class AccessRepository { asset: AssetAccess; authDevice: AuthDeviceAccess; memory: MemoryAccess; + notification: NotificationAccess; person: PersonAccess; partner: PartnerAccess; stack: StackAccess; @@ -438,6 +459,7 @@ export class AccessRepository { this.asset = new AssetAccess(db); this.authDevice = new AuthDeviceAccess(db); this.memory = new MemoryAccess(db); + this.notification = new NotificationAccess(db); this.person = new PersonAccess(db); this.partner = new PartnerAccess(db); this.stack = new StackAccess(db); diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index 3156804d09..b41c007ef5 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -14,6 +14,7 @@ import { SystemConfig } from 'src/config'; import { EventConfig } from 'src/decorators'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { NotificationDto } from 'src/dtos/notification.dto'; import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; import { ImmichWorker, MetadataKey, QueueName } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; @@ -64,6 +65,7 @@ type EventMap = { 'assets.restore': [{ assetIds: string[]; userId: string }]; 'job.start': [QueueName, JobItem]; + 'job.failed': [{ job: JobItem; error: Error | any }]; // session events 'session.delete': [{ sessionId: string }]; @@ -104,6 +106,7 @@ export interface ClientEventMap { on_server_version: [ServerVersionResponseDto]; on_config_update: []; on_new_release: [ReleaseNotification]; + on_notification: [NotificationDto]; on_session_delete: [string]; } diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index bd2e5c6774..453e515fe0 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -22,6 +22,7 @@ import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; import { MetadataRepository } from 'src/repositories/metadata.repository'; import { MoveRepository } from 'src/repositories/move.repository'; +import { NotificationRepository } from 'src/repositories/notification.repository'; import { OAuthRepository } from 'src/repositories/oauth.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; import { PersonRepository } from 'src/repositories/person.repository'; @@ -55,6 +56,7 @@ export const repositories = [ CryptoRepository, DatabaseRepository, DownloadRepository, + EmailRepository, EventRepository, JobRepository, LibraryRepository, @@ -65,7 +67,7 @@ export const repositories = [ MemoryRepository, MetadataRepository, MoveRepository, - EmailRepository, + NotificationRepository, OAuthRepository, PartnerRepository, PersonRepository, diff --git a/server/src/repositories/notification.repository.ts b/server/src/repositories/notification.repository.ts new file mode 100644 index 0000000000..112bb97e60 --- /dev/null +++ b/server/src/repositories/notification.repository.ts @@ -0,0 +1,103 @@ +import { Insertable, Kysely, Updateable } from 'kysely'; +import { DateTime } from 'luxon'; +import { InjectKysely } from 'nestjs-kysely'; +import { columns } from 'src/database'; +import { DB, Notifications } from 'src/db'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { NotificationSearchDto } from 'src/dtos/notification.dto'; + +export class NotificationRepository { + constructor(@InjectKysely() private db: Kysely) {} + + @GenerateSql({ params: [DummyValue.UUID] }) + cleanup() { + return this.db + .deleteFrom('notifications') + .where((eb) => + eb.or([ + // remove soft-deleted notifications + eb.and([eb('deletedAt', 'is not', null), eb('deletedAt', '<', DateTime.now().minus({ days: 3 }).toJSDate())]), + + // remove old, read notifications + eb.and([ + // keep recently read messages around for a few days + eb('readAt', '>', DateTime.now().minus({ days: 2 }).toJSDate()), + eb('createdAt', '<', DateTime.now().minus({ days: 15 }).toJSDate()), + ]), + + eb.and([ + // remove super old, unread notifications + eb('readAt', '=', null), + eb('createdAt', '<', DateTime.now().minus({ days: 30 }).toJSDate()), + ]), + ]), + ) + .execute(); + } + + @GenerateSql({ params: [DummyValue.UUID, {}] }, { name: 'unread', params: [DummyValue.UUID, { unread: true }] }) + search(userId: string, dto: NotificationSearchDto) { + return this.db + .selectFrom('notifications') + .select(columns.notification) + .where((qb) => + qb.and({ + userId, + id: dto.id, + level: dto.level, + type: dto.type, + readAt: dto.unread ? null : undefined, + }), + ) + .where('deletedAt', 'is', null) + .orderBy('createdAt', 'desc') + .execute(); + } + + create(notification: Insertable) { + return this.db + .insertInto('notifications') + .values(notification) + .returning(columns.notification) + .executeTakeFirstOrThrow(); + } + + get(id: string) { + return this.db + .selectFrom('notifications') + .select(columns.notification) + .where('id', '=', id) + .where('deletedAt', 'is not', null) + .executeTakeFirst(); + } + + update(id: string, notification: Updateable) { + return this.db + .updateTable('notifications') + .set(notification) + .where('deletedAt', 'is', null) + .where('id', '=', id) + .returning(columns.notification) + .executeTakeFirstOrThrow(); + } + + async updateAll(ids: string[], notification: Updateable) { + await this.db.updateTable('notifications').set(notification).where('id', 'in', ids).execute(); + } + + async delete(id: string) { + await this.db + .updateTable('notifications') + .set({ deletedAt: DateTime.now().toJSDate() }) + .where('id', '=', id) + .execute(); + } + + async deleteAll(ids: string[]) { + await this.db + .updateTable('notifications') + .set({ deletedAt: DateTime.now().toJSDate() }) + .where('id', 'in', ids) + .execute(); + } +} diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index fe4b86d65c..d297b2217d 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -28,6 +28,7 @@ import { MemoryTable } from 'src/schema/tables/memory.table'; import { MemoryAssetTable } from 'src/schema/tables/memory_asset.table'; import { MoveTable } from 'src/schema/tables/move.table'; import { NaturalEarthCountriesTable } from 'src/schema/tables/natural-earth-countries.table'; +import { NotificationTable } from 'src/schema/tables/notification.table'; import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table'; import { PartnerTable } from 'src/schema/tables/partner.table'; import { PersonTable } from 'src/schema/tables/person.table'; @@ -76,6 +77,7 @@ export class ImmichDatabase { MemoryTable, MoveTable, NaturalEarthCountriesTable, + NotificationTable, PartnerAuditTable, PartnerTable, PersonTable, diff --git a/server/src/schema/migrations/1744991379464-AddNotificationsTable.ts b/server/src/schema/migrations/1744991379464-AddNotificationsTable.ts new file mode 100644 index 0000000000..28dca6658c --- /dev/null +++ b/server/src/schema/migrations/1744991379464-AddNotificationsTable.ts @@ -0,0 +1,22 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE TABLE "notifications" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7(), "userId" uuid, "level" character varying NOT NULL DEFAULT 'info', "type" character varying NOT NULL DEFAULT 'info', "data" jsonb, "title" character varying NOT NULL, "description" text, "readAt" timestamp with time zone);`.execute(db); + await sql`ALTER TABLE "notifications" ADD CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "notifications" ADD CONSTRAINT "FK_692a909ee0fa9383e7859f9b406" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`CREATE INDEX "IDX_notifications_update_id" ON "notifications" ("updateId")`.execute(db); + await sql`CREATE INDEX "IDX_692a909ee0fa9383e7859f9b40" ON "notifications" ("userId")`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "notifications_updated_at" + BEFORE UPDATE ON "notifications" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP TRIGGER "notifications_updated_at" ON "notifications";`.execute(db); + await sql`DROP INDEX "IDX_notifications_update_id";`.execute(db); + await sql`DROP INDEX "IDX_692a909ee0fa9383e7859f9b40";`.execute(db); + await sql`ALTER TABLE "notifications" DROP CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a";`.execute(db); + await sql`ALTER TABLE "notifications" DROP CONSTRAINT "FK_692a909ee0fa9383e7859f9b406";`.execute(db); + await sql`DROP TABLE "notifications";`.execute(db); +} diff --git a/server/src/schema/tables/notification.table.ts b/server/src/schema/tables/notification.table.ts new file mode 100644 index 0000000000..bf9b8bdf3b --- /dev/null +++ b/server/src/schema/tables/notification.table.ts @@ -0,0 +1,52 @@ +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { NotificationLevel, NotificationType } from 'src/enum'; +import { UserTable } from 'src/schema/tables/user.table'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + ForeignKeyColumn, + PrimaryGeneratedColumn, + Table, + UpdateDateColumn, +} from 'src/sql-tools'; + +@Table('notifications') +@UpdatedAtTrigger('notifications_updated_at') +export class NotificationTable { + @PrimaryGeneratedColumn() + id!: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @DeleteDateColumn() + deletedAt?: Date; + + @UpdateIdColumn({ indexName: 'IDX_notifications_update_id' }) + updateId?: string; + + @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true }) + userId!: string; + + @Column({ default: NotificationLevel.Info }) + level!: NotificationLevel; + + @Column({ default: NotificationLevel.Info }) + type!: NotificationType; + + @Column({ type: 'jsonb', nullable: true }) + data!: any | null; + + @Column() + title!: string; + + @Column({ type: 'text', nullable: true }) + description!: string; + + @Column({ type: 'timestamp with time zone', nullable: true }) + readAt?: Date | null; +} diff --git a/server/src/services/backup.service.spec.ts b/server/src/services/backup.service.spec.ts index 704087ab05..aa72fd588a 100644 --- a/server/src/services/backup.service.spec.ts +++ b/server/src/services/backup.service.spec.ts @@ -142,52 +142,55 @@ describe(BackupService.name, () => { mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); }); + it('should run a database backup successfully', async () => { const result = await sut.handleBackupDatabase(); expect(result).toBe(JobStatus.SUCCESS); expect(mocks.storage.createWriteStream).toHaveBeenCalled(); }); + it('should rename file on success', async () => { const result = await sut.handleBackupDatabase(); expect(result).toBe(JobStatus.SUCCESS); expect(mocks.storage.rename).toHaveBeenCalled(); }); + it('should fail if pg_dumpall fails', async () => { mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error')); - const result = await sut.handleBackupDatabase(); - expect(result).toBe(JobStatus.FAILED); + await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1'); }); + it('should not rename file if pgdump fails and gzip succeeds', async () => { mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error')); - const result = await sut.handleBackupDatabase(); - expect(result).toBe(JobStatus.FAILED); + await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1'); expect(mocks.storage.rename).not.toHaveBeenCalled(); }); + it('should fail if gzip fails', async () => { mocks.process.spawn.mockReturnValueOnce(mockSpawn(0, 'data', '')); mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error')); - const result = await sut.handleBackupDatabase(); - expect(result).toBe(JobStatus.FAILED); + await expect(sut.handleBackupDatabase()).rejects.toThrow('Gzip failed with code 1'); }); + it('should fail if write stream fails', async () => { mocks.storage.createWriteStream.mockImplementation(() => { throw new Error('error'); }); - const result = await sut.handleBackupDatabase(); - expect(result).toBe(JobStatus.FAILED); + await expect(sut.handleBackupDatabase()).rejects.toThrow('error'); }); + it('should fail if rename fails', async () => { mocks.storage.rename.mockRejectedValue(new Error('error')); - const result = await sut.handleBackupDatabase(); - expect(result).toBe(JobStatus.FAILED); + await expect(sut.handleBackupDatabase()).rejects.toThrow('error'); }); + it('should ignore unlink failing and still return failed job status', async () => { mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error')); mocks.storage.unlink.mockRejectedValue(new Error('error')); - const result = await sut.handleBackupDatabase(); + await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1'); expect(mocks.storage.unlink).toHaveBeenCalled(); - expect(result).toBe(JobStatus.FAILED); }); + it.each` postgresVersion | expectedVersion ${'14.10'} | ${14} diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts index 409d34ab73..10f7becc7d 100644 --- a/server/src/services/backup.service.ts +++ b/server/src/services/backup.service.ts @@ -174,7 +174,7 @@ export class BackupService extends BaseService { await this.storageRepository .unlink(backupFilePath) .catch((error) => this.logger.error('Failed to delete failed backup file', error)); - return JobStatus.FAILED; + throw error; } this.logger.log(`Database Backup Success`); diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 23ddb1b63e..3381ad7222 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -29,6 +29,7 @@ import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; import { MetadataRepository } from 'src/repositories/metadata.repository'; import { MoveRepository } from 'src/repositories/move.repository'; +import { NotificationRepository } from 'src/repositories/notification.repository'; import { OAuthRepository } from 'src/repositories/oauth.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; import { PersonRepository } from 'src/repositories/person.repository'; @@ -80,6 +81,7 @@ export class BaseService { protected memoryRepository: MemoryRepository, protected metadataRepository: MetadataRepository, protected moveRepository: MoveRepository, + protected notificationRepository: NotificationRepository, protected oauthRepository: OAuthRepository, protected partnerRepository: PartnerRepository, protected personRepository: PersonRepository, diff --git a/server/src/services/index.ts b/server/src/services/index.ts index b214dd14f6..88b68d2c13 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -17,6 +17,7 @@ import { MapService } from 'src/services/map.service'; import { MediaService } from 'src/services/media.service'; import { MemoryService } from 'src/services/memory.service'; import { MetadataService } from 'src/services/metadata.service'; +import { NotificationAdminService } from 'src/services/notification-admin.service'; import { NotificationService } from 'src/services/notification.service'; import { PartnerService } from 'src/services/partner.service'; import { PersonService } from 'src/services/person.service'; @@ -60,6 +61,7 @@ export const services = [ MemoryService, MetadataService, NotificationService, + NotificationAdminService, PartnerService, PersonService, SearchService, diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index b81256de81..a387e6e099 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -215,11 +215,7 @@ export class JobService extends BaseService { await this.onDone(job); } } catch (error: Error | any) { - this.logger.error( - `Unable to run job handler (${queueName}/${job.name}): ${error}`, - error?.stack, - JSON.stringify(job.data), - ); + await this.eventRepository.emit('job.failed', { job, error }); } finally { this.telemetryRepository.jobs.addToGauge(queueMetric, -1); } diff --git a/server/src/services/notification-admin.service.spec.ts b/server/src/services/notification-admin.service.spec.ts new file mode 100644 index 0000000000..4a747d41a3 --- /dev/null +++ b/server/src/services/notification-admin.service.spec.ts @@ -0,0 +1,111 @@ +import { defaults, SystemConfig } from 'src/config'; +import { EmailTemplate } from 'src/repositories/email.repository'; +import { NotificationService } from 'src/services/notification.service'; +import { userStub } from 'test/fixtures/user.stub'; +import { newTestService, ServiceMocks } from 'test/utils'; + +const smtpTransport = Object.freeze({ + ...defaults, + notifications: { + smtp: { + ...defaults.notifications.smtp, + enabled: true, + transport: { + ignoreCert: false, + host: 'localhost', + port: 587, + username: 'test', + password: 'test', + }, + }, + }, +}); + +describe(NotificationService.name, () => { + let sut: NotificationService; + let mocks: ServiceMocks; + + beforeEach(() => { + ({ sut, mocks } = newTestService(NotificationService)); + }); + + it('should work', () => { + expect(sut).toBeDefined(); + }); + + describe('sendTestEmail', () => { + it('should throw error if user could not be found', async () => { + await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).rejects.toThrow('User not found'); + }); + + it('should throw error if smtp validation fails', async () => { + mocks.user.get.mockResolvedValue(userStub.admin); + mocks.email.verifySmtp.mockRejectedValue(''); + + await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).rejects.toThrow( + 'Failed to verify SMTP configuration', + ); + }); + + it('should send email to default domain', async () => { + mocks.user.get.mockResolvedValue(userStub.admin); + mocks.email.verifySmtp.mockResolvedValue(true); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); + + await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).resolves.not.toThrow(); + expect(mocks.email.renderEmail).toHaveBeenCalledWith({ + template: EmailTemplate.TEST_EMAIL, + data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name }, + }); + expect(mocks.email.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + subject: 'Test email from Immich', + smtp: smtpTransport.notifications.smtp.transport, + }), + ); + }); + + it('should send email to external domain', async () => { + mocks.user.get.mockResolvedValue(userStub.admin); + mocks.email.verifySmtp.mockResolvedValue(true); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.systemMetadata.get.mockResolvedValue({ server: { externalDomain: 'https://demo.immich.app' } }); + mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); + + await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).resolves.not.toThrow(); + expect(mocks.email.renderEmail).toHaveBeenCalledWith({ + template: EmailTemplate.TEST_EMAIL, + data: { baseUrl: 'https://demo.immich.app', displayName: userStub.admin.name }, + }); + expect(mocks.email.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + subject: 'Test email from Immich', + smtp: smtpTransport.notifications.smtp.transport, + }), + ); + }); + + it('should send email with replyTo', async () => { + mocks.user.get.mockResolvedValue(userStub.admin); + mocks.email.verifySmtp.mockResolvedValue(true); + mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); + + await expect( + sut.sendTestEmail('', { ...smtpTransport.notifications.smtp, replyTo: 'demo@immich.app' }), + ).resolves.not.toThrow(); + expect(mocks.email.renderEmail).toHaveBeenCalledWith({ + template: EmailTemplate.TEST_EMAIL, + data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name }, + }); + expect(mocks.email.sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + subject: 'Test email from Immich', + smtp: smtpTransport.notifications.smtp.transport, + replyTo: 'demo@immich.app', + }), + ); + }); + }); +}); diff --git a/server/src/services/notification-admin.service.ts b/server/src/services/notification-admin.service.ts new file mode 100644 index 0000000000..bf0d2bba41 --- /dev/null +++ b/server/src/services/notification-admin.service.ts @@ -0,0 +1,120 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { mapNotification, NotificationCreateDto } from 'src/dtos/notification.dto'; +import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; +import { NotificationLevel, NotificationType } from 'src/enum'; +import { EmailTemplate } from 'src/repositories/email.repository'; +import { BaseService } from 'src/services/base.service'; +import { getExternalDomain } from 'src/utils/misc'; + +@Injectable() +export class NotificationAdminService extends BaseService { + async create(auth: AuthDto, dto: NotificationCreateDto) { + const item = await this.notificationRepository.create({ + userId: dto.userId, + type: dto.type ?? NotificationType.Custom, + level: dto.level ?? NotificationLevel.Info, + title: dto.title, + description: dto.description, + data: dto.data, + }); + + return mapNotification(item); + } + + async sendTestEmail(id: string, dto: SystemConfigSmtpDto, tempTemplate?: string) { + const user = await this.userRepository.get(id, { withDeleted: false }); + if (!user) { + throw new Error('User not found'); + } + + try { + await this.emailRepository.verifySmtp(dto.transport); + } catch (error) { + throw new BadRequestException('Failed to verify SMTP configuration', { cause: error }); + } + + const { server } = await this.getConfig({ withCache: false }); + const { html, text } = await this.emailRepository.renderEmail({ + template: EmailTemplate.TEST_EMAIL, + data: { + baseUrl: getExternalDomain(server), + displayName: user.name, + }, + customTemplate: tempTemplate!, + }); + const { messageId } = await this.emailRepository.sendEmail({ + to: user.email, + subject: 'Test email from Immich', + html, + text, + from: dto.from, + replyTo: dto.replyTo || dto.from, + smtp: dto.transport, + }); + + return { messageId }; + } + + async getTemplate(name: EmailTemplate, customTemplate: string) { + const { server, templates } = await this.getConfig({ withCache: false }); + + let templateResponse = ''; + + switch (name) { + case EmailTemplate.WELCOME: { + const { html: _welcomeHtml } = await this.emailRepository.renderEmail({ + template: EmailTemplate.WELCOME, + data: { + baseUrl: getExternalDomain(server), + displayName: 'John Doe', + username: 'john@doe.com', + password: 'thisIsAPassword123', + }, + customTemplate: customTemplate || templates.email.welcomeTemplate, + }); + + templateResponse = _welcomeHtml; + break; + } + case EmailTemplate.ALBUM_UPDATE: { + const { html: _updateAlbumHtml } = await this.emailRepository.renderEmail({ + template: EmailTemplate.ALBUM_UPDATE, + data: { + baseUrl: getExternalDomain(server), + albumId: '1', + albumName: 'Favorite Photos', + recipientName: 'Jane Doe', + cid: undefined, + }, + customTemplate: customTemplate || templates.email.albumInviteTemplate, + }); + templateResponse = _updateAlbumHtml; + break; + } + + case EmailTemplate.ALBUM_INVITE: { + const { html } = await this.emailRepository.renderEmail({ + template: EmailTemplate.ALBUM_INVITE, + data: { + baseUrl: getExternalDomain(server), + albumId: '1', + albumName: "John Doe's Favorites", + senderName: 'John Doe', + recipientName: 'Jane Doe', + cid: undefined, + }, + customTemplate: customTemplate || templates.email.albumInviteTemplate, + }); + templateResponse = html; + break; + } + default: { + templateResponse = ''; + break; + } + } + + return { name, html: templateResponse }; + } +} diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 5830260753..133eb9e7f6 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -3,7 +3,6 @@ import { defaults, SystemConfig } from 'src/config'; import { AlbumUser } from 'src/database'; import { SystemConfigDto } from 'src/dtos/system-config.dto'; import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum'; -import { EmailTemplate } from 'src/repositories/email.repository'; import { NotificationService } from 'src/services/notification.service'; import { INotifyAlbumUpdateJob } from 'src/types'; import { albumStub } from 'test/fixtures/album.stub'; @@ -241,82 +240,6 @@ describe(NotificationService.name, () => { }); }); - describe('sendTestEmail', () => { - it('should throw error if user could not be found', async () => { - await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).rejects.toThrow('User not found'); - }); - - it('should throw error if smtp validation fails', async () => { - mocks.user.get.mockResolvedValue(userStub.admin); - mocks.email.verifySmtp.mockRejectedValue(''); - - await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).rejects.toThrow( - 'Failed to verify SMTP configuration', - ); - }); - - it('should send email to default domain', async () => { - mocks.user.get.mockResolvedValue(userStub.admin); - mocks.email.verifySmtp.mockResolvedValue(true); - mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); - - await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow(); - expect(mocks.email.renderEmail).toHaveBeenCalledWith({ - template: EmailTemplate.TEST_EMAIL, - data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name }, - }); - expect(mocks.email.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - subject: 'Test email from Immich', - smtp: configs.smtpTransport.notifications.smtp.transport, - }), - ); - }); - - it('should send email to external domain', async () => { - mocks.user.get.mockResolvedValue(userStub.admin); - mocks.email.verifySmtp.mockResolvedValue(true); - mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.systemMetadata.get.mockResolvedValue({ server: { externalDomain: 'https://demo.immich.app' } }); - mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); - - await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow(); - expect(mocks.email.renderEmail).toHaveBeenCalledWith({ - template: EmailTemplate.TEST_EMAIL, - data: { baseUrl: 'https://demo.immich.app', displayName: userStub.admin.name }, - }); - expect(mocks.email.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - subject: 'Test email from Immich', - smtp: configs.smtpTransport.notifications.smtp.transport, - }), - ); - }); - - it('should send email with replyTo', async () => { - mocks.user.get.mockResolvedValue(userStub.admin); - mocks.email.verifySmtp.mockResolvedValue(true); - mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); - - await expect( - sut.sendTestEmail('', { ...configs.smtpTransport.notifications.smtp, replyTo: 'demo@immich.app' }), - ).resolves.not.toThrow(); - expect(mocks.email.renderEmail).toHaveBeenCalledWith({ - template: EmailTemplate.TEST_EMAIL, - data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name }, - }); - expect(mocks.email.sendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - subject: 'Test email from Immich', - smtp: configs.smtpTransport.notifications.smtp.transport, - replyTo: 'demo@immich.app', - }), - ); - }); - }); - describe('handleUserSignup', () => { it('should skip if user could not be found', async () => { await expect(sut.handleUserSignup({ id: '' })).resolves.toBe(JobStatus.SKIPPED); diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 573be90f93..be475d1dca 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -1,7 +1,24 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { OnEvent, OnJob } from 'src/decorators'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { + mapNotification, + NotificationDeleteAllDto, + NotificationDto, + NotificationSearchDto, + NotificationUpdateAllDto, + NotificationUpdateDto, +} from 'src/dtos/notification.dto'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; -import { AssetFileType, JobName, JobStatus, QueueName } from 'src/enum'; +import { + AssetFileType, + JobName, + JobStatus, + NotificationLevel, + NotificationType, + Permission, + QueueName, +} from 'src/enum'; import { EmailTemplate } from 'src/repositories/email.repository'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; @@ -15,6 +32,80 @@ import { getPreferences } from 'src/utils/preferences'; export class NotificationService extends BaseService { private static albumUpdateEmailDelayMs = 300_000; + async search(auth: AuthDto, dto: NotificationSearchDto): Promise { + const items = await this.notificationRepository.search(auth.user.id, dto); + return items.map((item) => mapNotification(item)); + } + + async updateAll(auth: AuthDto, dto: NotificationUpdateAllDto) { + await this.requireAccess({ auth, ids: dto.ids, permission: Permission.NOTIFICATION_UPDATE }); + await this.notificationRepository.updateAll(dto.ids, { + readAt: dto.readAt, + }); + } + + async deleteAll(auth: AuthDto, dto: NotificationDeleteAllDto) { + await this.requireAccess({ auth, ids: dto.ids, permission: Permission.NOTIFICATION_DELETE }); + await this.notificationRepository.deleteAll(dto.ids); + } + + async get(auth: AuthDto, id: string) { + await this.requireAccess({ auth, ids: [id], permission: Permission.NOTIFICATION_READ }); + const item = await this.notificationRepository.get(id); + if (!item) { + throw new BadRequestException('Notification not found'); + } + return mapNotification(item); + } + + async update(auth: AuthDto, id: string, dto: NotificationUpdateDto) { + await this.requireAccess({ auth, ids: [id], permission: Permission.NOTIFICATION_UPDATE }); + const item = await this.notificationRepository.update(id, { + readAt: dto.readAt, + }); + return mapNotification(item); + } + + async delete(auth: AuthDto, id: string) { + await this.requireAccess({ auth, ids: [id], permission: Permission.NOTIFICATION_DELETE }); + await this.notificationRepository.delete(id); + } + + @OnJob({ name: JobName.NOTIFICATIONS_CLEANUP, queue: QueueName.BACKGROUND_TASK }) + async onNotificationsCleanup() { + await this.notificationRepository.cleanup(); + } + + @OnEvent({ name: 'job.failed' }) + async onJobFailed({ job, error }: ArgOf<'job.failed'>) { + const admin = await this.userRepository.getAdmin(); + if (!admin) { + return; + } + + this.logger.error(`Unable to run job handler (${job.name}): ${error}`, error?.stack, JSON.stringify(job.data)); + + switch (job.name) { + case JobName.BACKUP_DATABASE: { + const errorMessage = error instanceof Error ? error.message : error; + const item = await this.notificationRepository.create({ + userId: admin.id, + type: NotificationType.JobFailed, + level: NotificationLevel.Error, + title: 'Job Failed', + description: `Job ${[job.name]} failed with error: ${errorMessage}`, + }); + + this.eventRepository.clientSend('on_notification', admin.id, mapNotification(item)); + break; + } + + default: { + return; + } + } + } + @OnEvent({ name: 'config.update' }) onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) { this.eventRepository.clientBroadcast('on_config_update'); diff --git a/server/src/types.ts b/server/src/types.ts index c5375ae727..ba33e97aad 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -297,6 +297,10 @@ export type JobItem = // Metadata Extraction | { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob } | { name: JobName.METADATA_EXTRACTION; data: IEntityJob } + + // Notifications + | { name: JobName.NOTIFICATIONS_CLEANUP; data?: IBaseJob } + // Sidecar Scanning | { name: JobName.QUEUE_SIDECAR; data: IBaseJob } | { name: JobName.SIDECAR_DISCOVERY; data: IEntityJob } diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index 4e21a9226e..b04d23f114 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -221,6 +221,12 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe return access.person.checkFaceOwnerAccess(auth.user.id, ids); } + case Permission.NOTIFICATION_READ: + case Permission.NOTIFICATION_UPDATE: + case Permission.NOTIFICATION_DELETE: { + return access.notification.checkOwnerAccess(auth.user.id, ids); + } + case Permission.TAG_ASSET: case Permission.TAG_READ: case Permission.TAG_UPDATE: diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 671a8a50ca..3684837baa 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -13,9 +13,11 @@ import { AssetRepository } from 'src/repositories/asset.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; +import { EmailRepository } from 'src/repositories/email.repository'; import { JobRepository } from 'src/repositories/job.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; +import { NotificationRepository } from 'src/repositories/notification.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; import { PersonRepository } from 'src/repositories/person.repository'; import { SearchRepository } from 'src/repositories/search.repository'; @@ -42,10 +44,12 @@ type RepositoriesTypes = { config: ConfigRepository; crypto: CryptoRepository; database: DatabaseRepository; + email: EmailRepository; job: JobRepository; user: UserRepository; logger: LoggingRepository; memory: MemoryRepository; + notification: NotificationRepository; partner: PartnerRepository; person: PersonRepository; search: SearchRepository; @@ -142,6 +146,11 @@ export const getRepository = (key: K, db: Kys return new DatabaseRepository(db, new LoggingRepository(undefined, configRepo), configRepo); } + case 'email': { + const logger = new LoggingRepository(undefined, new ConfigRepository()); + return new EmailRepository(logger); + } + case 'logger': { const configMock = { getEnv: () => ({ noColor: false }) }; return new LoggingRepository(undefined, configMock as ConfigRepository); @@ -151,6 +160,10 @@ export const getRepository = (key: K, db: Kys return new MemoryRepository(db); } + case 'notification': { + return new NotificationRepository(db); + } + case 'partner': { return new PartnerRepository(db); } @@ -221,6 +234,10 @@ const getRepositoryMock = (key: K) => { }); } + case 'email': { + return automock(EmailRepository, { args: [{ setContext: () => {} }] }); + } + case 'job': { return automock(JobRepository, { args: [undefined, undefined, undefined, { setContext: () => {} }] }); } @@ -234,6 +251,10 @@ const getRepositoryMock = (key: K) => { return automock(MemoryRepository); } + case 'notification': { + return automock(NotificationRepository); + } + case 'partner': { return automock(PartnerRepository); } @@ -284,7 +305,7 @@ export const asDeps = (repositories: ServiceOverrides) => { repositories.crypto || getRepositoryMock('crypto'), repositories.database || getRepositoryMock('database'), repositories.downloadRepository, - repositories.email, + repositories.email || getRepositoryMock('email'), repositories.event, repositories.job || getRepositoryMock('job'), repositories.library, @@ -294,6 +315,7 @@ export const asDeps = (repositories: ServiceOverrides) => { repositories.memory || getRepositoryMock('memory'), repositories.metadata, repositories.move, + repositories.notification || getRepositoryMock('notification'), repositories.oauth, repositories.partner || getRepositoryMock('partner'), repositories.person || getRepositoryMock('person'), diff --git a/server/test/medium/specs/controllers/notification.controller.spec.ts b/server/test/medium/specs/controllers/notification.controller.spec.ts new file mode 100644 index 0000000000..f4a0ec82d5 --- /dev/null +++ b/server/test/medium/specs/controllers/notification.controller.spec.ts @@ -0,0 +1,86 @@ +import { NotificationController } from 'src/controllers/notification.controller'; +import { AuthService } from 'src/services/auth.service'; +import { NotificationService } from 'src/services/notification.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { createControllerTestApp, TestControllerApp } from 'test/medium/utils'; +import { factory } from 'test/small.factory'; + +describe(NotificationController.name, () => { + let realApp: TestControllerApp; + let mockApp: TestControllerApp; + + beforeEach(async () => { + realApp = await createControllerTestApp({ authType: 'real' }); + mockApp = await createControllerTestApp({ authType: 'mock' }); + }); + + describe('GET /notifications', () => { + it('should require authentication', async () => { + const { status, body } = await request(realApp.getHttpServer()).get('/notifications'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should call the service with an auth dto', async () => { + const auth = factory.auth({ user: factory.user() }); + mockApp.getMockedService(AuthService).authenticate.mockResolvedValue(auth); + const service = mockApp.getMockedService(NotificationService); + + const { status } = await request(mockApp.getHttpServer()) + .get('/notifications') + .set('Authorization', `Bearer token`); + + expect(status).toBe(200); + expect(service.search).toHaveBeenCalledWith(auth, {}); + }); + + it(`should reject an invalid notification level`, async () => { + const auth = factory.auth({ user: factory.user() }); + mockApp.getMockedService(AuthService).authenticate.mockResolvedValue(auth); + const service = mockApp.getMockedService(NotificationService); + + const { status, body } = await request(mockApp.getHttpServer()) + .get(`/notifications`) + .query({ level: 'invalid' }) + .set('Authorization', `Bearer token`); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('level must be one of the following values')])); + expect(service.search).not.toHaveBeenCalled(); + }); + }); + + describe('PUT /notifications', () => { + it('should require authentication', async () => { + const { status, body } = await request(realApp.getHttpServer()) + .put(`/notifications`) + .send({ ids: [], readAt: new Date().toISOString() }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + }); + + describe('GET /notifications/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(realApp.getHttpServer()).get(`/notifications/${factory.uuid()}`); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + }); + + describe('PUT /notifications/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(realApp.getHttpServer()) + .put(`/notifications/${factory.uuid()}`) + .send({ readAt: factory.date() }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + }); + + afterAll(async () => { + await realApp.close(); + await mockApp.close(); + }); +}); diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index ec5115b839..5b98b95e27 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -37,6 +37,10 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => { checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), }, + notification: { + checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), + }, + person: { checkFaceOwnerAccess: vitest.fn().mockResolvedValue(new Set()), checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 919cdd4b1c..d2742f7f80 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -314,4 +314,5 @@ export const factory = { sidecarWrite: assetSidecarWriteFactory, }, uuid: newUuid, + date: newDate, }; diff --git a/server/test/utils.ts b/server/test/utils.ts index c7c29d310e..2c444f491e 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -29,6 +29,7 @@ import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; import { MetadataRepository } from 'src/repositories/metadata.repository'; import { MoveRepository } from 'src/repositories/move.repository'; +import { NotificationRepository } from 'src/repositories/notification.repository'; import { OAuthRepository } from 'src/repositories/oauth.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; import { PersonRepository } from 'src/repositories/person.repository'; @@ -135,6 +136,7 @@ export type ServiceOverrides = { memory: MemoryRepository; metadata: MetadataRepository; move: MoveRepository; + notification: NotificationRepository; oauth: OAuthRepository; partner: PartnerRepository; person: PersonRepository; @@ -202,6 +204,7 @@ export const newTestService = ( memory: automock(MemoryRepository), metadata: newMetadataRepositoryMock(), move: automock(MoveRepository, { strict: false }), + notification: automock(NotificationRepository), oauth: automock(OAuthRepository, { args: [loggerMock] }), partner: automock(PartnerRepository, { strict: false }), person: newPersonRepositoryMock(), @@ -250,6 +253,7 @@ export const newTestService = ( overrides.memory || (mocks.memory as As), overrides.metadata || (mocks.metadata as As), overrides.move || (mocks.move as As), + overrides.notification || (mocks.notification as As), overrides.oauth || (mocks.oauth as As), overrides.partner || (mocks.partner as As), overrides.person || (mocks.person as As), diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index e91db5cc3a..2ebe4febab 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -8,6 +8,7 @@ import SkipLink from '$lib/components/elements/buttons/skip-link.svelte'; import HelpAndFeedbackModal from '$lib/components/shared-components/help-and-feedback-modal.svelte'; import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte'; + import NotificationPanel from '$lib/components/shared-components/navigation-bar/notification-panel.svelte'; import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; import { AppRoute } from '$lib/constants'; import { authManager } from '$lib/stores/auth-manager.svelte'; @@ -18,13 +19,14 @@ import { userInteraction } from '$lib/stores/user.svelte'; import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk'; import { Button, IconButton } from '@immich/ui'; - import { mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js'; + import { mdiBellBadge, mdiBellOutline, mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; import ThemeButton from '../theme-button.svelte'; import UserAvatar from '../user-avatar.svelte'; import AccountInfoPanel from './account-info-panel.svelte'; + import { notificationManager } from '$lib/stores/notification-manager.svelte'; interface Props { showUploadButton?: boolean; @@ -36,7 +38,9 @@ let shouldShowAccountInfo = $state(false); let shouldShowAccountInfoPanel = $state(false); let shouldShowHelpPanel = $state(false); + let shouldShowNotificationPanel = $state(false); let innerWidth: number = $state(0); + const hasUnreadNotifications = $derived(notificationManager.notifications.length > 0); let info: ServerAboutResponseDto | undefined = $state(); @@ -146,6 +150,27 @@ /> +
    (shouldShowNotificationPanel = false), + onEscape: () => (shouldShowNotificationPanel = false), + }} + > + (shouldShowNotificationPanel = !shouldShowNotificationPanel)} + aria-label={$t('notifications')} + /> + + {#if shouldShowNotificationPanel} + + {/if} +
    +
    (shouldShowAccountInfoPanel = false), diff --git a/web/src/lib/components/shared-components/navigation-bar/notification-item.svelte b/web/src/lib/components/shared-components/navigation-bar/notification-item.svelte new file mode 100644 index 0000000000..0d05e2d6d7 --- /dev/null +++ b/web/src/lib/components/shared-components/navigation-bar/notification-item.svelte @@ -0,0 +1,114 @@ + + + diff --git a/web/src/lib/components/shared-components/navigation-bar/notification-panel.svelte b/web/src/lib/components/shared-components/navigation-bar/notification-panel.svelte new file mode 100644 index 0000000000..be9fcd2a44 --- /dev/null +++ b/web/src/lib/components/shared-components/navigation-bar/notification-panel.svelte @@ -0,0 +1,82 @@ + + +
    + +
    + {$t('notifications')} +
    + +
    +
    + +
    + + {#if noUnreadNotifications} + + + {$t('no_notifications')} + + {:else} + + + {#each notificationManager.notifications as notification (notification.id)} +
    + markAsRead(id)} /> +
    + {/each} +
    +
    + {/if} +
    +
    diff --git a/web/src/lib/stores/notification-manager.svelte.ts b/web/src/lib/stores/notification-manager.svelte.ts new file mode 100644 index 0000000000..c06400fd16 --- /dev/null +++ b/web/src/lib/stores/notification-manager.svelte.ts @@ -0,0 +1,38 @@ +import { eventManager } from '$lib/stores/event-manager.svelte'; +import { getNotifications, updateNotification, updateNotifications, type NotificationDto } from '@immich/sdk'; + +class NotificationStore { + notifications = $state([]); + + constructor() { + // TODO replace this with an `auth.login` event + this.refresh().catch(() => {}); + + eventManager.on('auth.logout', () => this.clear()); + } + + get hasUnread() { + return this.notifications.length > 0; + } + + refresh = async () => { + this.notifications = await getNotifications({ unread: true }); + }; + + markAsRead = async (id: string) => { + this.notifications = this.notifications.filter((notification) => notification.id !== id); + await updateNotification({ id, notificationUpdateDto: { readAt: new Date().toISOString() } }); + }; + + markAllAsRead = async () => { + const ids = this.notifications.map(({ id }) => id); + this.notifications = []; + await updateNotifications({ notificationUpdateAllDto: { ids, readAt: new Date().toISOString() } }); + }; + + clear = () => { + this.notifications = []; + }; +} + +export const notificationManager = new NotificationStore(); diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index 90228a5cbd..ccfcfb7805 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -1,6 +1,7 @@ import { authManager } from '$lib/stores/auth-manager.svelte'; +import { notificationManager } from '$lib/stores/notification-manager.svelte'; import { createEventEmitter } from '$lib/utils/eventemitter'; -import type { AssetResponseDto, ServerVersionResponseDto } from '@immich/sdk'; +import { type AssetResponseDto, type NotificationDto, type ServerVersionResponseDto } from '@immich/sdk'; import { io, type Socket } from 'socket.io-client'; import { get, writable } from 'svelte/store'; import { user } from './user.store'; @@ -26,6 +27,7 @@ export interface Events { on_config_update: () => void; on_new_release: (newRelase: ReleaseEvent) => void; on_session_delete: (sessionId: string) => void; + on_notification: (notification: NotificationDto) => void; } const websocket: Socket = io({ @@ -50,6 +52,7 @@ websocket .on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion)) .on('on_new_release', (releaseVersion) => websocketStore.release.set(releaseVersion)) .on('on_session_delete', () => authManager.logout()) + .on('on_notification', () => notificationManager.refresh()) .on('connect_error', (e) => console.log('Websocket Connect Error', e)); export const openWebsocketConnection = () => { diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index aa756ac2e8..1dcb91f996 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -10,6 +10,7 @@ import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; + import { notificationManager } from '$lib/stores/notification-manager.svelte'; interface Props { data: PageData; @@ -24,7 +25,10 @@ let loading = $state(false); let oauthLoading = $state(true); - const onSuccess = async () => await goto(AppRoute.PHOTOS, { invalidateAll: true }); + const onSuccess = async () => { + await notificationManager.refresh(); + await goto(AppRoute.PHOTOS, { invalidateAll: true }); + }; const onFirstLogin = async () => await goto(AppRoute.AUTH_CHANGE_PASSWORD); const onOnboarding = async () => await goto(AppRoute.AUTH_ONBOARDING); From a17390a4228b01f804e7d2efe0486235f8171048 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 28 Apr 2025 16:56:04 +0200 Subject: [PATCH 079/356] refactor: move managers to new folder (#17929) --- .../context-menu/button-context-menu.svelte | 2 +- .../shared-components/context-menu/context-menu.svelte | 6 +++--- .../shared-components/navigation-bar/navigation-bar.svelte | 2 +- web/src/lib/{stores => managers}/auth-manager.svelte.ts | 2 +- web/src/lib/{stores => managers}/event-manager.svelte.ts | 0 web/src/lib/{stores => managers}/language-manager.svelte.ts | 2 +- web/src/lib/stores/folders.svelte.ts | 2 +- web/src/lib/stores/memory.store.svelte.ts | 2 +- web/src/lib/stores/notification-manager.svelte.ts | 2 +- web/src/lib/stores/search.svelte.ts | 2 +- web/src/lib/stores/user.store.ts | 2 +- web/src/lib/stores/user.svelte.ts | 2 +- web/src/lib/stores/websocket.ts | 2 +- web/src/routes/+layout.svelte | 2 +- web/src/routes/auth/change-password/+page.svelte | 2 +- 15 files changed, 16 insertions(+), 16 deletions(-) rename web/src/lib/{stores => managers}/auth-manager.svelte.ts (91%) rename web/src/lib/{stores => managers}/event-manager.svelte.ts (100%) rename web/src/lib/{stores => managers}/language-manager.svelte.ts (86%) diff --git a/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte b/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte index 67a17db950..593baafc7c 100644 --- a/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte +++ b/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte @@ -6,8 +6,8 @@ type Padding, } from '$lib/components/elements/buttons/circle-icon-button.svelte'; import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte'; + import { languageManager } from '$lib/managers/language-manager.svelte'; import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store'; - import { languageManager } from '$lib/stores/language-manager.svelte'; import { getContextMenuPositionFromBoundingRect, getContextMenuPositionFromEvent, diff --git a/web/src/lib/components/shared-components/context-menu/context-menu.svelte b/web/src/lib/components/shared-components/context-menu/context-menu.svelte index a79a3bd385..7d1be35944 100644 --- a/web/src/lib/components/shared-components/context-menu/context-menu.svelte +++ b/web/src/lib/components/shared-components/context-menu/context-menu.svelte @@ -1,9 +1,9 @@ -{#if downloadStore.isDownloading} +{#if downloadManager.isDownloading}

    {$t('downloading').toUpperCase()}

    - {#each Object.keys(downloadStore.assets) as downloadKey (downloadKey)} - {@const download = downloadStore.assets[downloadKey]} + {#each Object.keys(downloadManager.assets) as downloadKey (downloadKey)} + {@const download = downloadManager.assets[downloadKey]}
    diff --git a/web/src/lib/stores/download-store.svelte.ts b/web/src/lib/managers/download-manager.svelte.ts similarity index 74% rename from web/src/lib/stores/download-store.svelte.ts rename to web/src/lib/managers/download-manager.svelte.ts index 8c03671e73..107f80b8dc 100644 --- a/web/src/lib/stores/download-store.svelte.ts +++ b/web/src/lib/managers/download-manager.svelte.ts @@ -5,7 +5,7 @@ export interface DownloadProgress { abort: AbortController | null; } -class DownloadStore { +class DownloadManager { assets = $state>({}); isDownloading = $derived(Object.keys(this.assets).length > 0); @@ -42,10 +42,4 @@ class DownloadStore { } } -export const downloadStore = new DownloadStore(); - -export const downloadManager = { - add: (key: string, total: number, abort?: AbortController) => downloadStore.add(key, total, abort), - clear: (key: string) => downloadStore.clear(key), - update: (key: string, progress: number, total?: number) => downloadStore.update(key, progress, total), -}; +export const downloadManager = new DownloadManager(); diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index bd3cb416b5..35aea7eb9e 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -3,9 +3,9 @@ import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte'; import type { InterpolationValues } from '$lib/components/i18n/format-message'; import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification'; import { AppRoute } from '$lib/constants'; +import { downloadManager } from '$lib/managers/download-manager.svelte'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetsSnapshot, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.svelte'; -import { downloadManager } from '$lib/stores/download-store.svelte'; import { preferences } from '$lib/stores/user.store'; import { downloadRequest, getKey, withError } from '$lib/utils'; import { createAlbum } from '$lib/utils/album-utils'; diff --git a/web/src/routes/admin/repair/+page.svelte b/web/src/routes/admin/repair/+page.svelte index b04d8f1944..2d8ceca4da 100644 --- a/web/src/routes/admin/repair/+page.svelte +++ b/web/src/routes/admin/repair/+page.svelte @@ -7,7 +7,7 @@ NotificationType, notificationController, } from '$lib/components/shared-components/notification/notification'; - import { downloadManager } from '$lib/stores/download-store.svelte'; + import { downloadManager } from '$lib/managers/download-manager.svelte'; import { locale } from '$lib/stores/preferences.store'; import { copyToClipboard } from '$lib/utils'; import { downloadBlob } from '$lib/utils/asset-utils'; diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index 6512461ee9..1ac9f0b6fd 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -1,15 +1,16 @@ -{#if !isSharedLink() && $preferences?.ratings.enabled} +{#if !authManager.key && $preferences?.ratings.enabled}
    handlePromiseError(handleChangeRating(rating))} />
    diff --git a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte index 592279e353..eee7a7c0b6 100644 --- a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte @@ -3,7 +3,7 @@ import TagAssetForm from '$lib/components/forms/tag-asset-form.svelte'; import Portal from '$lib/components/shared-components/portal/portal.svelte'; import { AppRoute } from '$lib/constants'; - import { isSharedLink } from '$lib/utils'; + import { authManager } from '$lib/managers/auth-manager.svelte'; import { removeTag, tagAssets } from '$lib/utils/asset-utils'; import { getAssetInfo, type AssetResponseDto } from '@immich/sdk'; import { mdiClose, mdiPlus } from '@mdi/js'; @@ -41,7 +41,7 @@ }; -{#if isOwner && !isSharedLink()} +{#if isOwner && !authManager.key}

    {$t('tags').toUpperCase()}

    diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 5ef0ac0d73..15bc42d001 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -6,15 +6,19 @@ import DetailPanelTags from '$lib/components/asset-viewer/detail-panel-tags.svelte'; import Icon from '$lib/components/elements/icon.svelte'; import ChangeDate from '$lib/components/shared-components/change-date.svelte'; + import Portal from '$lib/components/shared-components/portal/portal.svelte'; import { AppRoute, QueryParameter, timeToLoadTheMap } from '$lib/constants'; + import { authManager } from '$lib/managers/auth-manager.svelte'; + import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { boundingBoxesArray } from '$lib/stores/people.store'; 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, isSharedLink } from '$lib/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 { fromDateTimeOriginal, fromLocalDateTime } from '$lib/utils/timeline-util'; import { AssetMediaSize, @@ -44,9 +48,6 @@ import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import UserAvatar from '../shared-components/user-avatar.svelte'; import AlbumListItemDetails from './album-list-item-details.svelte'; - import Portal from '$lib/components/shared-components/portal/portal.svelte'; - import { getMetadataSearchQuery } from '$lib/utils/metadata-search'; - import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; interface Props { asset: AssetResponseDto; @@ -84,7 +85,7 @@ const handleNewAsset = async (newAsset: AssetResponseDto) => { // TODO: check if reloading asset data is necessary - if (newAsset.id && !isSharedLink()) { + if (newAsset.id && !authManager.key) { const data = await getAssetInfo({ id: asset.id }); people = data?.people || []; unassignedFaces = data?.unassignedFaces || []; @@ -187,7 +188,7 @@ - {#if !isSharedLink() && isOwner} + {#if !authManager.key && isOwner}

    {$t('people').toUpperCase()}

    diff --git a/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte b/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte index 7b9fd85b4a..d678b00ddb 100644 --- a/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte +++ b/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte @@ -1,10 +1,11 @@ diff --git a/web/src/lib/components/assets/thumbnail/__test__/thumbnail.spec.ts b/web/src/lib/components/assets/thumbnail/__test__/thumbnail.spec.ts index f7447551f0..503ea8aefd 100644 --- a/web/src/lib/components/assets/thumbnail/__test__/thumbnail.spec.ts +++ b/web/src/lib/components/assets/thumbnail/__test__/thumbnail.spec.ts @@ -25,6 +25,7 @@ describe('Thumbnail component', () => { vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock()); vi.mock('$lib/utils/navigation', () => ({ currentUrlReplaceAssetId: vi.fn(), + isSharedLinkRoute: vi.fn().mockReturnValue(false), })); }); diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index eba10317aa..076b0b17cd 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -2,7 +2,7 @@ import Icon from '$lib/components/elements/icon.svelte'; import { ProjectionType } from '$lib/constants'; import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.store'; - import { getAssetPlaybackUrl, getAssetThumbnailUrl, isSharedLink } from '$lib/utils'; + import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils'; import { timeToSeconds } from '$lib/utils/date-time'; import { getAltText } from '$lib/utils/thumbnail-util'; import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto } from '@immich/sdk'; @@ -17,15 +17,16 @@ } from '@mdi/js'; import { thumbhash } from '$lib/actions/thumbhash'; + import { authManager } from '$lib/managers/auth-manager.svelte'; import { mobileDevice } from '$lib/stores/mobile-device.svelte'; + import { getFocusable } from '$lib/utils/focus-util'; import { currentUrlReplaceAssetId } from '$lib/utils/navigation'; import { TUNABLES } from '$lib/utils/tunables'; + import { onMount } from 'svelte'; import type { ClassValue } from 'svelte/elements'; import { fade } from 'svelte/transition'; import ImageThumbnail from './image-thumbnail.svelte'; import VideoThumbnail from './video-thumbnail.svelte'; - import { onMount } from 'svelte'; - import { getFocusable } from '$lib/utils/focus-util'; interface Props { asset: AssetResponseDto; @@ -331,13 +332,13 @@ >
    - {#if !isSharedLink() && asset.isFavorite} + {#if !authManager.key && asset.isFavorite}
    {/if} - {#if !isSharedLink() && showArchiveIcon && asset.isArchived} + {#if !authManager.key && showArchiveIcon && asset.isArchived}
    diff --git a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte index 1639a642b5..e1e803ad50 100644 --- a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte +++ b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte @@ -1,13 +1,13 @@
    -
    {@render trailing?.()}
    -
    +
    diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index 65f984134f..c8009a8fd9 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -55,7 +55,7 @@ (shouldShowHelpPanel = false)} {info} /> {/if} -
    @@ -209,4 +209,4 @@
    - + diff --git a/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte b/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte index af08fb4ce1..b8f02202a7 100644 --- a/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte @@ -7,14 +7,12 @@ import { t } from 'svelte-i18n'; - - + + + + + + diff --git a/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte b/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte index 9ef0bd1b9f..62dda02cea 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte @@ -7,10 +7,11 @@ import { onMount, type Snippet } from 'svelte'; interface Props { + ariaLabel?: string; children?: Snippet; } - let { children }: Props = $props(); + let { ariaLabel, children }: Props = $props(); const isHidden = $derived(!sidebarStore.isOpen && !mobileDevice.isFullSidebar); const isExpanded = $derived(sidebarStore.isOpen && !mobileDevice.isFullSidebar); @@ -30,8 +31,9 @@ }; -
    - + diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte index ec9c2a06da..94b064c23e 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte @@ -42,102 +42,100 @@ let isUtilitiesSelected: boolean = $state(false); - - + {/if} From 0e4cf9ac573d142b77b2153d292d9ca370ae481e Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Tue, 29 Apr 2025 13:59:06 -0400 Subject: [PATCH 095/356] feat(web): responsive date group header height (#17944) * feat: responsive date group header height * update tests * feat(web): improve perf when changing mobile orientation (#17945) fix: improve perf when changing mobile orientation --- .../photos-page/asset-date-group.svelte | 2 +- .../components/photos-page/asset-grid.svelte | 11 +++- .../components/photos-page/skeleton.svelte | 2 +- web/src/lib/stores/assets-store.spec.ts | 6 +-- web/src/lib/stores/assets-store.svelte.ts | 54 ++++++++++++++----- 5 files changed, 55 insertions(+), 20 deletions(-) diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index ecb25b0697..f20c04d3d4 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -131,7 +131,7 @@ >
    {#if !singleSelect && ((hoveredDateGroup === dateGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dateGroup.groupTitle))} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index da4387a490..e46aaefe0e 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -88,7 +88,16 @@ const usingMobileDevice = $derived(mobileDevice.pointerCoarse); $effect(() => { - assetStore.rowHeight = maxMd ? 100 : 235; + const layoutOptions = maxMd + ? { + rowHeight: 100, + headerHeight: 32, + } + : { + rowHeight: 235, + headerHeight: 48, + }; + assetStore.setLayoutOptions(layoutOptions); }); const scrollTo = (top: number) => { diff --git a/web/src/lib/components/photos-page/skeleton.svelte b/web/src/lib/components/photos-page/skeleton.svelte index 5c43887450..87ff91c511 100644 --- a/web/src/lib/components/photos-page/skeleton.svelte +++ b/web/src/lib/components/photos-page/skeleton.svelte @@ -9,7 +9,7 @@
    {title}
    diff --git a/web/src/lib/stores/assets-store.spec.ts b/web/src/lib/stores/assets-store.spec.ts index 0685103a1b..3d0292bde8 100644 --- a/web/src/lib/stores/assets-store.spec.ts +++ b/web/src/lib/stores/assets-store.spec.ts @@ -48,15 +48,15 @@ describe('AssetStore', () => { expect(plainBuckets).toEqual( expect.arrayContaining([ - expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 304 }), - expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 4515.333_333_333_333 }), + expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 303 }), + expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 4514.333_333_333_333 }), expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 286 }), ]), ); }); it('calculates timeline height', () => { - expect(assetStore.timelineHeight).toBe(5105.333_333_333_333); + expect(assetStore.timelineHeight).toBe(5103.333_333_333_333); }); }); diff --git a/web/src/lib/stores/assets-store.svelte.ts b/web/src/lib/stores/assets-store.svelte.ts index e048fabbc8..b4b4a4ade2 100644 --- a/web/src/lib/stores/assets-store.svelte.ts +++ b/web/src/lib/stores/assets-store.svelte.ts @@ -35,9 +35,7 @@ export type AssetStoreOptions = Omit & { timelineAlbumId?: string; deferInit?: boolean; }; -export type AssetStoreLayoutOptions = { - rowHeight: number; -}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any function updateObject(target: any, source: any): boolean { if (!target) { @@ -110,7 +108,6 @@ export class AssetDateGroup { readonly date: DateTime; readonly dayOfMonth: number; intersetingAssets: IntersectingAsset[] = $state([]); - dodo: IntersectingAsset[] = $state([]); height = $state(0); width = $state(0); @@ -121,6 +118,7 @@ export class AssetDateGroup { left: number = $state(0); row = $state(0); col = $state(0); + deferredLayout = false; constructor(bucket: AssetBucket, index: number, date: DateTime, dayOfMonth: number) { this.index = index; @@ -195,6 +193,10 @@ export class AssetDateGroup { } layout(options: CommonLayoutOptions) { + if (!this.bucket.intersecting) { + this.deferredLayout = true; + return; + } const assets = this.intersetingAssets.map((intersetingAsset) => intersetingAsset.asset!); const geometry = getJustifiedLayoutFromAssets(assets, options); this.width = geometry.containerWidth; @@ -547,6 +549,11 @@ export type LiteBucket = { bucketDateFormattted: string; }; +type AssetStoreLayoutOptions = { + rowHeight?: number; + headerHeight?: number; + gap?: number; +}; export class AssetStore { // --- public ---- isInitialized = $state(false); @@ -596,7 +603,7 @@ export class AssetStore { #unsubscribers: Unsubscriber[] = []; #rowHeight = $state(235); - #headerHeight = $state(49); + #headerHeight = $state(48); #gap = $state(12); #options: AssetStoreOptions = AssetStore.#INIT_OPTIONS; @@ -608,36 +615,46 @@ export class AssetStore { constructor() {} - set headerHeight(value) { + setLayoutOptions({ headerHeight = 48, rowHeight = 235, gap = 12 }: AssetStoreLayoutOptions) { + let changed = false; + changed ||= this.#setHeaderHeight(headerHeight); + changed ||= this.#setGap(gap); + changed ||= this.#setRowHeight(rowHeight); + if (changed) { + this.refreshLayout(); + } + } + + #setHeaderHeight(value: number) { if (this.#headerHeight == value) { - return; + return false; } this.#headerHeight = value; - this.refreshLayout(); + return true; } get headerHeight() { return this.#headerHeight; } - set gap(value) { + #setGap(value: number) { if (this.#gap == value) { - return; + return false; } this.#gap = value; - this.refreshLayout(); + return true; } get gap() { return this.#gap; } - set rowHeight(value) { + #setRowHeight(value: number) { if (this.#rowHeight == value) { - return; + return false; } this.#rowHeight = value; - this.refreshLayout(); + return true; } get rowHeight() { @@ -815,6 +832,15 @@ export class AssetStore { } bucket.intersecting = actuallyIntersecting || preIntersecting; bucket.actuallyIntersecting = actuallyIntersecting; + if (preIntersecting || actuallyIntersecting) { + const hasDeferred = bucket.dateGroups.some((group) => group.deferredLayout); + if (hasDeferred) { + this.#updateGeometry(bucket, true); + for (const group of bucket.dateGroups) { + group.deferredLayout = false; + } + } + } } #processPendingChanges = throttle(() => { From 3ce353393aa47ac9448cf2fd79e0c6b1f253e9f6 Mon Sep 17 00:00:00 2001 From: Thomas <9749173+uhthomas@users.noreply.github.com> Date: Tue, 29 Apr 2025 19:23:01 +0100 Subject: [PATCH 096/356] chore(server): don't insert embeddings if the model has changed (#17885) * chore(server): don't insert embeddings if the model has changed We're moving away from the heuristic of waiting for queues to complete. The job which inserts embeddings can simply check if the model has changed before inserting, rather than attempting to lock the queue. * more robust dim size update * use check constraint * index command cleanup * add create statement * update medium test, create appropriate extension * new line * set dimension size when running on all assets * why does it want braces smh * take 2 --------- Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com> --- .../1700713994428-AddCLIPEmbeddingIndex.ts | 10 +-- .../1700714033632-AddFaceEmbeddingIndex.ts | 10 +-- .../1718486162779-AddFaceSearchRelation.ts | 18 ++--- .../src/repositories/database.repository.ts | 8 +-- server/src/repositories/search.repository.ts | 33 ++++++--- server/src/schema/index.ts | 8 +-- .../1744910873969-InitialMigration.ts | 8 +-- .../src/services/smart-info.service.spec.ts | 71 +------------------ server/src/services/smart-info.service.ts | 21 +++--- server/src/utils/database.ts | 29 +++++++- server/test/medium.factory.ts | 2 +- 11 files changed, 82 insertions(+), 136 deletions(-) diff --git a/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts b/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts index 993e12f822..b5d47bb8cd 100644 --- a/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts +++ b/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts @@ -1,5 +1,5 @@ -import { DatabaseExtension } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; +import { vectorIndexQuery } from 'src/utils/database'; import { MigrationInterface, QueryRunner } from 'typeorm'; const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension; @@ -8,15 +8,9 @@ export class AddCLIPEmbeddingIndex1700713994428 implements MigrationInterface { name = 'AddCLIPEmbeddingIndex1700713994428'; public async up(queryRunner: QueryRunner): Promise { - if (vectorExtension === DatabaseExtension.VECTORS) { - await queryRunner.query(`SET vectors.pgvector_compatibility=on`); - } await queryRunner.query(`SET search_path TO "$user", public, vectors`); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS clip_index ON smart_search - USING hnsw (embedding vector_cosine_ops) - WITH (ef_construction = 300, m = 16)`); + await queryRunner.query(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: 'clip_index' })); } public async down(queryRunner: QueryRunner): Promise { diff --git a/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts b/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts index 182aae4e42..2b41788fe4 100644 --- a/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts +++ b/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts @@ -1,5 +1,5 @@ -import { DatabaseExtension } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; +import { vectorIndexQuery } from 'src/utils/database'; import { MigrationInterface, QueryRunner } from 'typeorm'; const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension; @@ -8,15 +8,9 @@ export class AddFaceEmbeddingIndex1700714033632 implements MigrationInterface { name = 'AddFaceEmbeddingIndex1700714033632'; public async up(queryRunner: QueryRunner): Promise { - if (vectorExtension === DatabaseExtension.VECTORS) { - await queryRunner.query(`SET vectors.pgvector_compatibility=on`); - } await queryRunner.query(`SET search_path TO "$user", public, vectors`); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS face_index ON asset_faces - USING hnsw (embedding vector_cosine_ops) - WITH (ef_construction = 300, m = 16)`); + await queryRunner.query(vectorIndexQuery({ vectorExtension, table: 'asset_faces', indexName: 'face_index' })); } public async down(queryRunner: QueryRunner): Promise { diff --git a/server/src/migrations/1718486162779-AddFaceSearchRelation.ts b/server/src/migrations/1718486162779-AddFaceSearchRelation.ts index e08bcb8e25..64849708d2 100644 --- a/server/src/migrations/1718486162779-AddFaceSearchRelation.ts +++ b/server/src/migrations/1718486162779-AddFaceSearchRelation.ts @@ -1,5 +1,6 @@ import { DatabaseExtension } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; +import { vectorIndexQuery } from 'src/utils/database'; import { MigrationInterface, QueryRunner } from 'typeorm'; const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension; @@ -8,7 +9,6 @@ export class AddFaceSearchRelation1718486162779 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { if (vectorExtension === DatabaseExtension.VECTORS) { await queryRunner.query(`SET search_path TO "$user", public, vectors`); - await queryRunner.query(`SET vectors.pgvector_compatibility=on`); } const hasEmbeddings = async (tableName: string): Promise => { @@ -47,21 +47,14 @@ export class AddFaceSearchRelation1718486162779 implements MigrationInterface { await queryRunner.query(`ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE real[]`); await queryRunner.query(`ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE vector(512)`); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS clip_index ON smart_search - USING hnsw (embedding vector_cosine_ops) - WITH (ef_construction = 300, m = 16)`); + await queryRunner.query(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: 'clip_index' })); - await queryRunner.query(` - CREATE INDEX face_index ON face_search - USING hnsw (embedding vector_cosine_ops) - WITH (ef_construction = 300, m = 16)`); + await queryRunner.query(vectorIndexQuery({ vectorExtension, table: 'face_search', indexName: 'face_index' })); } public async down(queryRunner: QueryRunner): Promise { if (vectorExtension === DatabaseExtension.VECTORS) { await queryRunner.query(`SET search_path TO "$user", public, vectors`); - await queryRunner.query(`SET vectors.pgvector_compatibility=on`); } await queryRunner.query(`ALTER TABLE asset_faces ADD COLUMN "embedding" vector(512)`); @@ -74,9 +67,6 @@ export class AddFaceSearchRelation1718486162779 implements MigrationInterface { WHERE id = fs."faceId"`); await queryRunner.query(`DROP TABLE face_search`); - await queryRunner.query(` - CREATE INDEX face_index ON asset_faces - USING hnsw (embedding vector_cosine_ops) - WITH (ef_construction = 300, m = 16)`); + await queryRunner.query(vectorIndexQuery({ vectorExtension, table: 'asset_faces', indexName: 'face_index' })); } } diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index a402c9d28d..addf6bcff0 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -12,6 +12,7 @@ import { DatabaseExtension, DatabaseLock, VectorIndex } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { ExtensionVersion, VectorExtension, VectorUpdateResult } from 'src/types'; +import { vectorIndexQuery } from 'src/utils/database'; import { isValidInteger } from 'src/validation'; import { DataSource } from 'typeorm'; @@ -119,12 +120,7 @@ export class DatabaseRepository { await sql`ALTER TABLE ${sql.raw(table)} ALTER COLUMN embedding SET DATA TYPE vector(${sql.raw(String(dimSize))})`.execute( tx, ); - await sql`SET vectors.pgvector_compatibility=on`.execute(tx); - await sql` - CREATE INDEX IF NOT EXISTS ${sql.raw(index)} ON ${sql.raw(table)} - USING hnsw (embedding vector_cosine_ops) - WITH (ef_construction = 300, m = 16) - `.execute(tx); + await sql.raw(vectorIndexQuery({ vectorExtension: this.vectorExtension, table, indexName: index })).execute(tx); }); } } diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 5c1ebae69d..0c958fec02 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -6,7 +6,8 @@ import { DB, Exif } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetStatus, AssetType } from 'src/enum'; -import { anyUuid, asUuid, searchAssetBuilder } from 'src/utils/database'; +import { ConfigRepository } from 'src/repositories/config.repository'; +import { anyUuid, asUuid, searchAssetBuilder, vectorIndexQuery } from 'src/utils/database'; import { isValidInteger } from 'src/validation'; export interface SearchResult { @@ -201,7 +202,10 @@ export interface GetCameraMakesOptions { @Injectable() export class SearchRepository { - constructor(@InjectKysely() private db: Kysely) {} + constructor( + @InjectKysely() private db: Kysely, + private configRepository: ConfigRepository, + ) {} @GenerateSql({ params: [ @@ -446,8 +450,8 @@ export class SearchRepository { async upsert(assetId: string, embedding: string): Promise { await this.db .insertInto('smart_search') - .values({ assetId: asUuid(assetId), embedding } as any) - .onConflict((oc) => oc.column('assetId').doUpdateSet({ embedding } as any)) + .values({ assetId, embedding }) + .onConflict((oc) => oc.column('assetId').doUpdateSet((eb) => ({ embedding: eb.ref('excluded.embedding') }))) .execute(); } @@ -469,19 +473,32 @@ export class SearchRepository { return dimSize; } - setDimensionSize(dimSize: number): Promise { + async setDimensionSize(dimSize: number): Promise { if (!isValidInteger(dimSize, { min: 1, max: 2 ** 16 })) { throw new Error(`Invalid CLIP dimension size: ${dimSize}`); } - return this.db.transaction().execute(async (trx) => { - await sql`truncate ${sql.table('smart_search')}`.execute(trx); + // this is done in two transactions to handle concurrent writes + await this.db.transaction().execute(async (trx) => { + await sql`delete from ${sql.table('smart_search')}`.execute(trx); + await trx.schema.alterTable('smart_search').dropConstraint('dim_size_constraint').ifExists().execute(); + await sql`alter table ${sql.table('smart_search')} add constraint dim_size_constraint check (array_length(embedding::real[], 1) = ${sql.lit(dimSize)})`.execute( + trx, + ); + }); + + const vectorExtension = this.configRepository.getEnv().database.vectorExtension; + await this.db.transaction().execute(async (trx) => { + await sql`drop index if exists clip_index`.execute(trx); await trx.schema .alterTable('smart_search') .alterColumn('embedding', (col) => col.setDataType(sql.raw(`vector(${dimSize})`))) .execute(); - await sql`reindex index clip_index`.execute(trx); + await sql.raw(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: 'clip_index' })).execute(trx); + await trx.schema.alterTable('smart_search').dropConstraint('dim_size_constraint').ifExists().execute(); }); + + await sql`vacuum analyze ${sql.table('smart_search')}`.execute(this.db); } async deleteAllSearchEmbeddings(): Promise { diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index d297b2217d..c62681d049 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -47,14 +47,8 @@ import { UserTable } from 'src/schema/tables/user.table'; import { VersionHistoryTable } from 'src/schema/tables/version-history.table'; import { ConfigurationParameter, Database, Extensions } from 'src/sql-tools'; -@Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'vectors', 'plpgsql']) +@Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql']) @ConfigurationParameter({ name: 'search_path', value: () => '"$user", public, vectors', scope: 'database' }) -@ConfigurationParameter({ - name: 'vectors.pgvector_compatibility', - value: () => 'on', - scope: 'user', - synchronize: false, -}) @Database({ name: 'immich' }) export class ImmichDatabase { tables = [ diff --git a/server/src/schema/migrations/1744910873969-InitialMigration.ts b/server/src/schema/migrations/1744910873969-InitialMigration.ts index 459534a26a..ce4a37ae3b 100644 --- a/server/src/schema/migrations/1744910873969-InitialMigration.ts +++ b/server/src/schema/migrations/1744910873969-InitialMigration.ts @@ -2,6 +2,7 @@ import { Kysely, sql } from 'kysely'; import { DatabaseExtension } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { vectorIndexQuery } from 'src/utils/database'; const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension; const lastMigrationSql = sql<{ name: string }>`SELECT "name" FROM "migrations" ORDER BY "timestamp" DESC LIMIT 1;`; @@ -29,7 +30,7 @@ export async function up(db: Kysely): Promise { await sql`CREATE EXTENSION IF NOT EXISTS "cube";`.execute(db); await sql`CREATE EXTENSION IF NOT EXISTS "earthdistance";`.execute(db); await sql`CREATE EXTENSION IF NOT EXISTS "pg_trgm";`.execute(db); - await sql`CREATE EXTENSION IF NOT EXISTS "vectors";`.execute(db); + await sql`CREATE EXTENSION IF NOT EXISTS ${sql.raw(vectorExtension)}`.execute(db); await sql`CREATE OR REPLACE FUNCTION immich_uuid_v7(p_timestamp timestamp with time zone default clock_timestamp()) RETURNS uuid VOLATILE LANGUAGE SQL @@ -108,7 +109,6 @@ export async function up(db: Kysely): Promise { $$;`.execute(db); if (vectorExtension === DatabaseExtension.VECTORS) { await sql`SET search_path TO "$user", public, vectors`.execute(db); - await sql`SET vectors.pgvector_compatibility=on`.execute(db); } await sql`CREATE TYPE "assets_status_enum" AS ENUM ('active','trashed','deleted');`.execute(db); await sql`CREATE TYPE "sourcetype" AS ENUM ('machine-learning','exif','manual');`.execute(db); @@ -293,7 +293,7 @@ export async function up(db: Kysely): Promise { await sql`CREATE INDEX "IDX_live_photo_cid" ON "exif" ("livePhotoCID")`.execute(db); await sql`CREATE INDEX "IDX_auto_stack_id" ON "exif" ("autoStackId")`.execute(db); await sql`CREATE INDEX "IDX_asset_exif_update_id" ON "exif" ("updateId")`.execute(db); - await sql`CREATE INDEX "face_index" ON "face_search" USING hnsw (embedding vector_cosine_ops) WITH (ef_construction = 300, m = 16)`.execute(db); + await sql.raw(vectorIndexQuery({ vectorExtension, table: 'face_search', indexName: 'face_index' })).execute(db); await sql`CREATE INDEX "IDX_geodata_gist_earthcoord" ON "geodata_places" (ll_to_earth_public(latitude, longitude))`.execute(db); await sql`CREATE INDEX "idx_geodata_places_name" ON "geodata_places" USING gin (f_unaccent("name") gin_trgm_ops)`.execute(db); await sql`CREATE INDEX "idx_geodata_places_admin2_name" ON "geodata_places" USING gin (f_unaccent("admin2Name") gin_trgm_ops)`.execute(db); @@ -316,7 +316,7 @@ export async function up(db: Kysely): Promise { await sql`CREATE INDEX "IDX_sharedlink_albumId" ON "shared_links" ("albumId")`.execute(db); await sql`CREATE INDEX "IDX_5b7decce6c8d3db9593d6111a6" ON "shared_link__asset" ("assetsId")`.execute(db); await sql`CREATE INDEX "IDX_c9fab4aa97ffd1b034f3d6581a" ON "shared_link__asset" ("sharedLinksId")`.execute(db); - await sql`CREATE INDEX "clip_index" ON "smart_search" USING hnsw (embedding vector_cosine_ops) WITH (ef_construction = 300, m = 16)`.execute(db); + await sql.raw(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: 'clip_index' })).execute(db); await sql`CREATE INDEX "IDX_d8ddd9d687816cc490432b3d4b" ON "session_sync_checkpoints" ("sessionId")`.execute(db); await sql`CREATE INDEX "IDX_session_sync_checkpoints_update_id" ON "session_sync_checkpoints" ("updateId")`.execute(db); await sql`CREATE INDEX "IDX_92e67dc508c705dd66c9461557" ON "tags" ("userId")`.execute(db); diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index df26e69108..ee94b89d72 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -58,10 +58,6 @@ describe(SmartInfoService.name, () => { expect(mocks.search.getDimensionSize).not.toHaveBeenCalled(); expect(mocks.search.setDimensionSize).not.toHaveBeenCalled(); expect(mocks.search.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); - expect(mocks.job.getQueueStatus).not.toHaveBeenCalled(); - expect(mocks.job.pause).not.toHaveBeenCalled(); - expect(mocks.job.waitForQueueCompletion).not.toHaveBeenCalled(); - expect(mocks.job.resume).not.toHaveBeenCalled(); }); it('should return if model and DB dimension size are equal', async () => { @@ -72,38 +68,15 @@ describe(SmartInfoService.name, () => { expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1); expect(mocks.search.setDimensionSize).not.toHaveBeenCalled(); expect(mocks.search.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); - expect(mocks.job.getQueueStatus).not.toHaveBeenCalled(); - expect(mocks.job.pause).not.toHaveBeenCalled(); - expect(mocks.job.waitForQueueCompletion).not.toHaveBeenCalled(); - expect(mocks.job.resume).not.toHaveBeenCalled(); }); it('should update DB dimension size if model and DB have different values', async () => { mocks.search.getDimensionSize.mockResolvedValue(768); - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningEnabled as SystemConfig }); expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1); expect(mocks.search.setDimensionSize).toHaveBeenCalledWith(512); - expect(mocks.job.getQueueStatus).toHaveBeenCalledTimes(1); - expect(mocks.job.pause).toHaveBeenCalledTimes(1); - expect(mocks.job.waitForQueueCompletion).toHaveBeenCalledTimes(1); - expect(mocks.job.resume).toHaveBeenCalledTimes(1); - }); - - it('should skip pausing and resuming queue if already paused', async () => { - mocks.search.getDimensionSize.mockResolvedValue(768); - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true }); - - await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningEnabled as SystemConfig }); - - expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1); - expect(mocks.search.setDimensionSize).toHaveBeenCalledWith(512); - expect(mocks.job.getQueueStatus).toHaveBeenCalledTimes(1); - expect(mocks.job.pause).not.toHaveBeenCalled(); - expect(mocks.job.waitForQueueCompletion).toHaveBeenCalledTimes(1); - expect(mocks.job.resume).not.toHaveBeenCalled(); }); }); @@ -120,10 +93,6 @@ describe(SmartInfoService.name, () => { expect(mocks.search.getDimensionSize).not.toHaveBeenCalled(); expect(mocks.search.setDimensionSize).not.toHaveBeenCalled(); expect(mocks.search.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); - expect(mocks.job.getQueueStatus).not.toHaveBeenCalled(); - expect(mocks.job.pause).not.toHaveBeenCalled(); - expect(mocks.job.waitForQueueCompletion).not.toHaveBeenCalled(); - expect(mocks.job.resume).not.toHaveBeenCalled(); }); it('should return if model and DB dimension size are equal', async () => { @@ -141,15 +110,10 @@ describe(SmartInfoService.name, () => { expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1); expect(mocks.search.setDimensionSize).not.toHaveBeenCalled(); expect(mocks.search.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); - expect(mocks.job.getQueueStatus).not.toHaveBeenCalled(); - expect(mocks.job.pause).not.toHaveBeenCalled(); - expect(mocks.job.waitForQueueCompletion).not.toHaveBeenCalled(); - expect(mocks.job.resume).not.toHaveBeenCalled(); }); it('should update DB dimension size if model and DB have different values', async () => { mocks.search.getDimensionSize.mockResolvedValue(512); - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); await sut.onConfigUpdate({ newConfig: { @@ -162,15 +126,10 @@ describe(SmartInfoService.name, () => { expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1); expect(mocks.search.setDimensionSize).toHaveBeenCalledWith(768); - expect(mocks.job.getQueueStatus).toHaveBeenCalledTimes(1); - expect(mocks.job.pause).toHaveBeenCalledTimes(1); - expect(mocks.job.waitForQueueCompletion).toHaveBeenCalledTimes(1); - expect(mocks.job.resume).toHaveBeenCalledTimes(1); }); it('should clear embeddings if old and new models are different', async () => { mocks.search.getDimensionSize.mockResolvedValue(512); - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); await sut.onConfigUpdate({ newConfig: { @@ -184,31 +143,6 @@ describe(SmartInfoService.name, () => { expect(mocks.search.deleteAllSearchEmbeddings).toHaveBeenCalled(); expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1); expect(mocks.search.setDimensionSize).not.toHaveBeenCalled(); - expect(mocks.job.getQueueStatus).toHaveBeenCalledTimes(1); - expect(mocks.job.pause).toHaveBeenCalledTimes(1); - expect(mocks.job.waitForQueueCompletion).toHaveBeenCalledTimes(1); - expect(mocks.job.resume).toHaveBeenCalledTimes(1); - }); - - it('should skip pausing and resuming queue if already paused', async () => { - mocks.search.getDimensionSize.mockResolvedValue(512); - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true }); - - await sut.onConfigUpdate({ - newConfig: { - machineLearning: { clip: { modelName: 'ViT-B-32__openai', enabled: true }, enabled: true }, - } as SystemConfig, - oldConfig: { - machineLearning: { clip: { modelName: 'ViT-B-16__openai', enabled: true }, enabled: true }, - } as SystemConfig, - }); - - expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1); - expect(mocks.search.setDimensionSize).not.toHaveBeenCalled(); - expect(mocks.job.getQueueStatus).toHaveBeenCalledTimes(1); - expect(mocks.job.pause).not.toHaveBeenCalled(); - expect(mocks.job.waitForQueueCompletion).toHaveBeenCalledTimes(1); - expect(mocks.job.resume).not.toHaveBeenCalled(); }); }); @@ -220,6 +154,7 @@ describe(SmartInfoService.name, () => { expect(mocks.asset.getAll).not.toHaveBeenCalled(); expect(mocks.asset.getWithout).not.toHaveBeenCalled(); + expect(mocks.search.setDimensionSize).not.toHaveBeenCalled(); }); it('should queue the assets without clip embeddings', async () => { @@ -234,7 +169,7 @@ describe(SmartInfoService.name, () => { { name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }, ]); expect(mocks.asset.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.SMART_SEARCH); - expect(mocks.search.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); + expect(mocks.search.setDimensionSize).not.toHaveBeenCalled(); }); it('should queue all the assets', async () => { @@ -249,7 +184,7 @@ describe(SmartInfoService.name, () => { { name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }, ]); expect(mocks.asset.getAll).toHaveBeenCalled(); - expect(mocks.search.deleteAllSearchEmbeddings).toHaveBeenCalled(); + expect(mocks.search.setDimensionSize).toHaveBeenCalledExactlyOnceWith(512); }); }); diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 411114eb17..42fefb60b9 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -50,12 +50,6 @@ export class SmartInfoService extends BaseService { return; } - const { isPaused } = await this.jobRepository.getQueueStatus(QueueName.SMART_SEARCH); - if (!isPaused) { - await this.jobRepository.pause(QueueName.SMART_SEARCH); - } - await this.jobRepository.waitForQueueCompletion(QueueName.SMART_SEARCH); - if (dimSizeChange) { this.logger.log( `Dimension size of model ${newConfig.machineLearning.clip.modelName} is ${dimSize}, but database expects ${dbDimSize}.`, @@ -67,9 +61,8 @@ export class SmartInfoService extends BaseService { await this.searchRepository.deleteAllSearchEmbeddings(); } - if (!isPaused) { - await this.jobRepository.resume(QueueName.SMART_SEARCH); - } + // TODO: A job to reindex all assets should be scheduled, though user + // confirmation should probably be requested before doing that. }); } @@ -81,7 +74,9 @@ export class SmartInfoService extends BaseService { } if (force) { - await this.searchRepository.deleteAllSearchEmbeddings(); + const { dimSize } = getCLIPModelInfo(machineLearning.clip.modelName); + // in addition to deleting embeddings, update the dimension size in case it failed earlier + await this.searchRepository.setDimensionSize(dimSize); } const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { @@ -126,6 +121,12 @@ export class SmartInfoService extends BaseService { await this.databaseRepository.wait(DatabaseLock.CLIPDimSize); } + const newConfig = await this.getConfig({ withCache: true }); + if (machineLearning.clip.modelName !== newConfig.machineLearning.clip.modelName) { + // Skip the job if the the model has changed since the embedding was generated. + return JobStatus.SKIPPED; + } + await this.searchRepository.upsert(asset.id, embedding); return JobStatus.SUCCESS; diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 8f0b56597a..b44ea5da46 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -17,10 +17,10 @@ import { parse } from 'pg-connection-string'; import postgres, { Notice } from 'postgres'; import { columns, Exif, Person } from 'src/database'; import { DB } from 'src/db'; -import { AssetFileType } from 'src/enum'; +import { AssetFileType, DatabaseExtension } from 'src/enum'; import { TimeBucketSize } from 'src/repositories/asset.repository'; import { AssetSearchBuilderOptions } from 'src/repositories/search.repository'; -import { DatabaseConnectionParams } from 'src/types'; +import { DatabaseConnectionParams, VectorExtension } from 'src/types'; type Ssl = 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object; @@ -373,3 +373,28 @@ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuild .$if(!!(options.withFaces || options.withPeople || options.personIds), (qb) => qb.select(withFacesAndPeople)) .$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null)); } + +type VectorIndexOptions = { vectorExtension: VectorExtension; table: string; indexName: string }; + +export function vectorIndexQuery({ vectorExtension, table, indexName }: VectorIndexOptions): string { + switch (vectorExtension) { + case DatabaseExtension.VECTORS: { + return ` + CREATE INDEX IF NOT EXISTS ${indexName} ON ${table} + USING vectors (embedding vector_cos_ops) WITH (options = $$ + [indexing.hnsw] + m = 16 + ef_construction = 300 + $$)`; + } + case DatabaseExtension.VECTOR: { + return ` + CREATE INDEX IF NOT EXISTS ${indexName} ON ${table} + USING hnsw (embedding vector_cosine_ops) + WITH (ef_construction = 300, m = 16)`; + } + default: { + throw new Error(`Unsupported vector extension: '${vectorExtension}'`); + } + } +} diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 3684837baa..388c4df96b 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -173,7 +173,7 @@ export const getRepository = (key: K, db: Kys } case 'search': { - return new SearchRepository(db); + return new SearchRepository(db, new ConfigRepository()); } case 'session': { From d89e88bb3f04bee8434cfb06b7a07b4f4b789b0f Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 29 Apr 2025 15:17:48 -0400 Subject: [PATCH 097/356] feat: configure token endpoint auth method (#17968) --- i18n/en.json | 10 +-- mobile/openapi/README.md | 1 + mobile/openapi/lib/api.dart | 1 + mobile/openapi/lib/api_client.dart | 2 + mobile/openapi/lib/api_helper.dart | 3 + .../o_auth_token_endpoint_auth_method.dart | 85 +++++++++++++++++++ .../lib/model/system_config_o_auth_dto.dart | 23 ++++- open-api/immich-openapi-specs.json | 22 ++++- open-api/typescript-sdk/src/fetch-client.ts | 6 ++ server/src/config.ts | 5 ++ server/src/dtos/system-config.dto.ts | 14 ++- server/src/enum.ts | 5 ++ server/src/repositories/logging.repository.ts | 2 +- server/src/repositories/oauth.repository.ts | 45 ++++++++-- .../services/system-config.service.spec.ts | 3 + .../settings/auth/auth-settings.svelte | 48 ++++++++--- .../settings/setting-input-field.svelte | 10 +-- .../settings/setting-select.svelte | 8 +- 18 files changed, 249 insertions(+), 44 deletions(-) create mode 100644 mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart diff --git a/i18n/en.json b/i18n/en.json index 239936471d..f1ab30a6d0 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -192,26 +192,22 @@ "oauth_auto_register": "Auto register", "oauth_auto_register_description": "Automatically register new users after signing in with OAuth", "oauth_button_text": "Button text", - "oauth_client_id": "Client ID", - "oauth_client_secret": "Client Secret", + "oauth_client_secret_description": "Required if PKCE (Proof Key for Code Exchange) is not supported by the OAuth provider", "oauth_enable_description": "Login with OAuth", - "oauth_issuer_url": "Issuer URL", "oauth_mobile_redirect_uri": "Mobile redirect URI", "oauth_mobile_redirect_uri_override": "Mobile redirect URI override", "oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like '{callback}'", - "oauth_profile_signing_algorithm": "Profile signing algorithm", - "oauth_profile_signing_algorithm_description": "Algorithm used to sign the user profile.", - "oauth_scope": "Scope", "oauth_settings": "OAuth", "oauth_settings_description": "Manage OAuth login settings", "oauth_settings_more_details": "For more details about this feature, refer to the docs.", - "oauth_signing_algorithm": "Signing algorithm", "oauth_storage_label_claim": "Storage label claim", "oauth_storage_label_claim_description": "Automatically set the user's storage label to the value of this claim.", "oauth_storage_quota_claim": "Storage quota claim", "oauth_storage_quota_claim_description": "Automatically set the user's storage quota to the value of this claim.", "oauth_storage_quota_default": "Default storage quota (GiB)", "oauth_storage_quota_default_description": "Quota in GiB to be used when no claim is provided (Enter 0 for unlimited quota).", + "oauth_timeout": "Request Timeout", + "oauth_timeout_description": "Timeout for requests in milliseconds", "offline_paths": "Offline Paths", "offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.", "password_enable_description": "Login with email and password", diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index b8ea4b924c..d46945f640 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -377,6 +377,7 @@ Class | Method | HTTP request | Description - [OAuthAuthorizeResponseDto](doc//OAuthAuthorizeResponseDto.md) - [OAuthCallbackDto](doc//OAuthCallbackDto.md) - [OAuthConfigDto](doc//OAuthConfigDto.md) + - [OAuthTokenEndpointAuthMethod](doc//OAuthTokenEndpointAuthMethod.md) - [OnThisDayDto](doc//OnThisDayDto.md) - [PartnerDirection](doc//PartnerDirection.md) - [PartnerResponseDto](doc//PartnerResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index e845099bd2..ba64363c97 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -178,6 +178,7 @@ part 'model/notification_update_dto.dart'; part 'model/o_auth_authorize_response_dto.dart'; part 'model/o_auth_callback_dto.dart'; part 'model/o_auth_config_dto.dart'; +part 'model/o_auth_token_endpoint_auth_method.dart'; part 'model/on_this_day_dto.dart'; part 'model/partner_direction.dart'; part 'model/partner_response_dto.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 7586cc1ae2..6abe576aca 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -410,6 +410,8 @@ class ApiClient { return OAuthCallbackDto.fromJson(value); case 'OAuthConfigDto': return OAuthConfigDto.fromJson(value); + case 'OAuthTokenEndpointAuthMethod': + return OAuthTokenEndpointAuthMethodTypeTransformer().decode(value); case 'OnThisDayDto': return OnThisDayDto.fromJson(value); case 'PartnerDirection': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index cc517d48ab..5f9d15c089 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -106,6 +106,9 @@ String parameterToString(dynamic value) { if (value is NotificationType) { return NotificationTypeTypeTransformer().encode(value).toString(); } + if (value is OAuthTokenEndpointAuthMethod) { + return OAuthTokenEndpointAuthMethodTypeTransformer().encode(value).toString(); + } if (value is PartnerDirection) { return PartnerDirectionTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart b/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart new file mode 100644 index 0000000000..fc528888b3 --- /dev/null +++ b/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart @@ -0,0 +1,85 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class OAuthTokenEndpointAuthMethod { + /// Instantiate a new enum with the provided [value]. + const OAuthTokenEndpointAuthMethod._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const post = OAuthTokenEndpointAuthMethod._(r'client_secret_post'); + static const basic = OAuthTokenEndpointAuthMethod._(r'client_secret_basic'); + + /// List of all possible values in this [enum][OAuthTokenEndpointAuthMethod]. + static const values = [ + post, + basic, + ]; + + static OAuthTokenEndpointAuthMethod? fromJson(dynamic value) => OAuthTokenEndpointAuthMethodTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = OAuthTokenEndpointAuthMethod.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [OAuthTokenEndpointAuthMethod] to String, +/// and [decode] dynamic data back to [OAuthTokenEndpointAuthMethod]. +class OAuthTokenEndpointAuthMethodTypeTransformer { + factory OAuthTokenEndpointAuthMethodTypeTransformer() => _instance ??= const OAuthTokenEndpointAuthMethodTypeTransformer._(); + + const OAuthTokenEndpointAuthMethodTypeTransformer._(); + + String encode(OAuthTokenEndpointAuthMethod data) => data.value; + + /// Decodes a [dynamic value][data] to a OAuthTokenEndpointAuthMethod. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + OAuthTokenEndpointAuthMethod? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'client_secret_post': return OAuthTokenEndpointAuthMethod.post; + case r'client_secret_basic': return OAuthTokenEndpointAuthMethod.basic; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [OAuthTokenEndpointAuthMethodTypeTransformer] instance. + static OAuthTokenEndpointAuthMethodTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/system_config_o_auth_dto.dart b/mobile/openapi/lib/model/system_config_o_auth_dto.dart index 9125bb7bba..24384a47b1 100644 --- a/mobile/openapi/lib/model/system_config_o_auth_dto.dart +++ b/mobile/openapi/lib/model/system_config_o_auth_dto.dart @@ -28,6 +28,8 @@ class SystemConfigOAuthDto { required this.signingAlgorithm, required this.storageLabelClaim, required this.storageQuotaClaim, + required this.timeout, + required this.tokenEndpointAuthMethod, }); bool autoLaunch; @@ -61,6 +63,11 @@ class SystemConfigOAuthDto { String storageQuotaClaim; + /// Minimum value: 1 + int timeout; + + OAuthTokenEndpointAuthMethod tokenEndpointAuthMethod; + @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigOAuthDto && other.autoLaunch == autoLaunch && @@ -77,7 +84,9 @@ class SystemConfigOAuthDto { other.scope == scope && other.signingAlgorithm == signingAlgorithm && other.storageLabelClaim == storageLabelClaim && - other.storageQuotaClaim == storageQuotaClaim; + other.storageQuotaClaim == storageQuotaClaim && + other.timeout == timeout && + other.tokenEndpointAuthMethod == tokenEndpointAuthMethod; @override int get hashCode => @@ -96,10 +105,12 @@ class SystemConfigOAuthDto { (scope.hashCode) + (signingAlgorithm.hashCode) + (storageLabelClaim.hashCode) + - (storageQuotaClaim.hashCode); + (storageQuotaClaim.hashCode) + + (timeout.hashCode) + + (tokenEndpointAuthMethod.hashCode); @override - String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim]'; + String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]'; Map toJson() { final json = {}; @@ -118,6 +129,8 @@ class SystemConfigOAuthDto { json[r'signingAlgorithm'] = this.signingAlgorithm; json[r'storageLabelClaim'] = this.storageLabelClaim; json[r'storageQuotaClaim'] = this.storageQuotaClaim; + json[r'timeout'] = this.timeout; + json[r'tokenEndpointAuthMethod'] = this.tokenEndpointAuthMethod; return json; } @@ -145,6 +158,8 @@ class SystemConfigOAuthDto { signingAlgorithm: mapValueOfType(json, r'signingAlgorithm')!, storageLabelClaim: mapValueOfType(json, r'storageLabelClaim')!, storageQuotaClaim: mapValueOfType(json, r'storageQuotaClaim')!, + timeout: mapValueOfType(json, r'timeout')!, + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.fromJson(json[r'tokenEndpointAuthMethod'])!, ); } return null; @@ -207,6 +222,8 @@ class SystemConfigOAuthDto { 'signingAlgorithm', 'storageLabelClaim', 'storageQuotaClaim', + 'timeout', + 'tokenEndpointAuthMethod', }; } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index f4ec929373..826af5a2ec 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -10824,6 +10824,13 @@ ], "type": "object" }, + "OAuthTokenEndpointAuthMethod": { + "enum": [ + "client_secret_post", + "client_secret_basic" + ], + "type": "string" + }, "OnThisDayDto": { "properties": { "year": { @@ -13404,6 +13411,17 @@ }, "storageQuotaClaim": { "type": "string" + }, + "timeout": { + "minimum": 1, + "type": "integer" + }, + "tokenEndpointAuthMethod": { + "allOf": [ + { + "$ref": "#/components/schemas/OAuthTokenEndpointAuthMethod" + } + ] } }, "required": [ @@ -13421,7 +13439,9 @@ "scope", "signingAlgorithm", "storageLabelClaim", - "storageQuotaClaim" + "storageQuotaClaim", + "timeout", + "tokenEndpointAuthMethod" ], "type": "object" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 647c5c4ada..743eeadf03 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1315,6 +1315,8 @@ export type SystemConfigOAuthDto = { signingAlgorithm: string; storageLabelClaim: string; storageQuotaClaim: string; + timeout: number; + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod; }; export type SystemConfigPasswordLoginDto = { enabled: boolean; @@ -3859,6 +3861,10 @@ export enum LogLevel { Error = "error", Fatal = "fatal" } +export enum OAuthTokenEndpointAuthMethod { + ClientSecretPost = "client_secret_post", + ClientSecretBasic = "client_secret_basic" +} export enum TimeBucketSize { Day = "DAY", Month = "MONTH" diff --git a/server/src/config.ts b/server/src/config.ts index 566adbd693..a9fdffbd62 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -5,6 +5,7 @@ import { CQMode, ImageFormat, LogLevel, + OAuthTokenEndpointAuthMethod, QueueName, ToneMapping, TranscodeHWAccel, @@ -96,6 +97,8 @@ export interface SystemConfig { scope: string; signingAlgorithm: string; profileSigningAlgorithm: string; + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod; + timeout: number; storageLabelClaim: string; storageQuotaClaim: string; }; @@ -260,6 +263,8 @@ export const defaults = Object.freeze({ profileSigningAlgorithm: 'none', storageLabelClaim: 'preferred_username', storageQuotaClaim: 'immich_quota', + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST, + timeout: 30_000, }, passwordLogin: { enabled: true, diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index eaef40a5e1..6991baf109 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -25,6 +25,7 @@ import { Colorspace, ImageFormat, LogLevel, + OAuthTokenEndpointAuthMethod, QueueName, ToneMapping, TranscodeHWAccel, @@ -33,7 +34,7 @@ import { VideoContainer, } from 'src/enum'; import { ConcurrentQueueName } from 'src/types'; -import { IsCronExpression, ValidateBoolean } from 'src/validation'; +import { IsCronExpression, Optional, ValidateBoolean } from 'src/validation'; const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled; @@ -344,10 +345,19 @@ class SystemConfigOAuthDto { clientId!: string; @ValidateIf(isOAuthEnabled) - @IsNotEmpty() @IsString() clientSecret!: string; + @IsEnum(OAuthTokenEndpointAuthMethod) + @ApiProperty({ enum: OAuthTokenEndpointAuthMethod, enumName: 'OAuthTokenEndpointAuthMethod' }) + tokenEndpointAuthMethod!: OAuthTokenEndpointAuthMethod; + + @IsInt() + @IsPositive() + @Optional() + @ApiProperty({ type: 'integer' }) + timeout!: number; + @IsNumber() @Min(0) defaultStorageQuota!: number; diff --git a/server/src/enum.ts b/server/src/enum.ts index c88e2e942c..4e725e1c13 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -605,3 +605,8 @@ export enum NotificationType { SystemMessage = 'SystemMessage', Custom = 'Custom', } + +export enum OAuthTokenEndpointAuthMethod { + CLIENT_SECRET_POST = 'client_secret_post', + CLIENT_SECRET_BASIC = 'client_secret_basic', +} diff --git a/server/src/repositories/logging.repository.ts b/server/src/repositories/logging.repository.ts index 05d2d45f4d..2ac3715a50 100644 --- a/server/src/repositories/logging.repository.ts +++ b/server/src/repositories/logging.repository.ts @@ -5,7 +5,7 @@ import { Telemetry } from 'src/decorators'; import { LogLevel } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; -type LogDetails = any[]; +type LogDetails = any; type LogFunction = () => string; const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; diff --git a/server/src/repositories/oauth.repository.ts b/server/src/repositories/oauth.repository.ts index d3e0372089..ea9f0b1901 100644 --- a/server/src/repositories/oauth.repository.ts +++ b/server/src/repositories/oauth.repository.ts @@ -1,16 +1,19 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import type { UserInfoResponse } from 'openid-client' with { 'resolution-mode': 'import' }; +import { OAuthTokenEndpointAuthMethod } from 'src/enum'; import { LoggingRepository } from 'src/repositories/logging.repository'; export type OAuthConfig = { clientId: string; - clientSecret: string; + clientSecret?: string; issuerUrl: string; mobileOverrideEnabled: boolean; mobileRedirectUri: string; profileSigningAlgorithm: string; scope: string; signingAlgorithm: string; + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod; + timeout: number; }; export type OAuthProfile = UserInfoResponse; @@ -76,12 +79,10 @@ export class OAuthRepository { ); } - if (error.code === 'OAUTH_INVALID_RESPONSE') { - this.logger.warn(`Invalid response from authorization server. Cause: ${error.cause?.message}`); - throw error.cause; - } + this.logger.error(`OAuth login failed: ${error.message}`); + this.logger.error(error); - throw error; + throw new Error('OAuth login failed', { cause: error }); } } @@ -103,6 +104,8 @@ export class OAuthRepository { clientSecret, profileSigningAlgorithm, signingAlgorithm, + tokenEndpointAuthMethod, + timeout, }: OAuthConfig) { try { const { allowInsecureRequests, discovery } = await import('openid-client'); @@ -114,14 +117,38 @@ export class OAuthRepository { response_types: ['code'], userinfo_signed_response_alg: profileSigningAlgorithm === 'none' ? undefined : profileSigningAlgorithm, id_token_signed_response_alg: signingAlgorithm, - timeout: 30_000, }, - undefined, - { execute: [allowInsecureRequests] }, + await this.getTokenAuthMethod(tokenEndpointAuthMethod, clientSecret), + { + execute: [allowInsecureRequests], + timeout, + }, ); } catch (error: any | AggregateError) { this.logger.error(`Error in OAuth discovery: ${error}`, error?.stack, error?.errors); throw new InternalServerErrorException(`Error in OAuth discovery: ${error}`, { cause: error }); } } + + private async getTokenAuthMethod(tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod, clientSecret?: string) { + const { None, ClientSecretPost, ClientSecretBasic } = await import('openid-client'); + + if (!clientSecret) { + return None(); + } + + switch (tokenEndpointAuthMethod) { + case OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST: { + return ClientSecretPost(clientSecret); + } + + case OAuthTokenEndpointAuthMethod.CLIENT_SECRET_BASIC: { + return ClientSecretBasic(clientSecret); + } + + default: { + return None(); + } + } + } } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 936acf27ad..176e6d6f04 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -6,6 +6,7 @@ import { CQMode, ImageFormat, LogLevel, + OAuthTokenEndpointAuthMethod, QueueName, ToneMapping, TranscodeHWAccel, @@ -119,6 +120,8 @@ const updatedConfig = Object.freeze({ scope: 'openid email profile', signingAlgorithm: 'RS256', profileSigningAlgorithm: 'none', + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST, + timeout: 30_000, storageLabelClaim: 'preferred_username', storageQuotaClaim: 'immich_quota', }, diff --git a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte index 67da6bb7f2..b2454b06c3 100644 --- a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte +++ b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte @@ -1,16 +1,17 @@ -{#if !$colorTheme.system} +{#if !themeManager.theme.system} themeManager.toggleTheme()} {padding} /> {/if} diff --git a/web/src/lib/components/user-settings-page/app-settings.svelte b/web/src/lib/components/user-settings-page/app-settings.svelte index 5b4a19c34f..f1d8e14787 100644 --- a/web/src/lib/components/user-settings-page/app-settings.svelte +++ b/web/src/lib/components/user-settings-page/app-settings.svelte @@ -1,11 +1,12 @@ - - - {#snippet buttons()} - - - - - - - {/snippet} -
    -
    - {#if matches.length + extras.length + orphans.length === 0} -
    - -
    - {:else} -
    -
    - - - - - - - {#each matches as match (match.extra.filename)} - handleSplit(match)} - > - - - - {/each} - -
    -
    -

    - {$t('matches').toUpperCase()} - {matches.length > 0 ? `(${matches.length.toLocaleString($locale)})` : ''} -

    -

    {$t('admin.these_files_matched_by_checksum')}

    -
    -
    - {match.orphan.pathValue} => - {match.extra.filename} - - ({match.orphan.entityType}/{match.orphan.pathType}) -
    - - - - - - - - - {#each orphans as orphan, index (index)} - - - - - - {/each} - -
    -
    -

    - {$t('admin.offline_paths').toUpperCase()} - {orphans.length > 0 ? `(${orphans.length.toLocaleString($locale)})` : ''} -

    -

    - {$t('admin.offline_paths_description')} -

    -
    -
    copyToClipboard(orphan.pathValue)}> - {}} /> - - {orphan.pathValue} - - ({orphan.entityType}) -
    - - - - - - - - - {#each extras as extra (extra.filename)} - handleCheckOne(extra.filename)} - title={extra.filename} - > - - - - {/each} - -
    -
    -

    - {$t('admin.untracked_files').toUpperCase()} - {extras.length > 0 ? `(${extras.length.toLocaleString($locale)})` : ''} -

    -

    - {$t('admin.untracked_files_description')} -

    -
    -
    copyToClipboard(extra.filename)}> - {}} /> - - {extra.filename} - - {#if extra.checksum} - [sha1:{extra.checksum}] - {/if} - -
    - - {/if} - - - diff --git a/web/src/routes/admin/repair/+page.ts b/web/src/routes/admin/repair/+page.ts deleted file mode 100644 index 9e52abb573..0000000000 --- a/web/src/routes/admin/repair/+page.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { authenticate } from '$lib/utils/auth'; -import { getFormatter } from '$lib/utils/i18n'; -import { getAuditFiles } from '@immich/sdk'; -import type { PageLoad } from './$types'; - -export const load = (async () => { - await authenticate({ admin: true }); - const { orphans, extras } = await getAuditFiles(); - const $t = await getFormatter(); - - return { - orphans, - extras, - meta: { - title: $t('repair'), - }, - }; -}) satisfies PageLoad; From be5cc2cdf56b75c11da386667867f9febea7bce8 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 30 Apr 2025 11:25:30 -0400 Subject: [PATCH 109/356] refactor: stream detect faces (#17996) --- server/src/queries/asset.job.repository.sql | 14 +++++++++ .../src/repositories/asset-job.repository.ts | 9 ++++++ server/src/repositories/asset.repository.ts | 16 +--------- server/src/services/person.service.spec.ts | 29 +++++-------------- server/src/services/person.service.ts | 26 +++++++---------- 5 files changed, 42 insertions(+), 52 deletions(-) diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index 48a233ffe4..ecf8f0b475 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -469,3 +469,17 @@ from "assets" where "assets"."deletedAt" <= $1 + +-- AssetJobRepository.streamForDetectFacesJob +select + "assets"."id" +from + "assets" + inner join "asset_job_status" as "job_status" on "assetId" = "assets"."id" +where + "assets"."isVisible" = $1 + and "assets"."deletedAt" is null + and "job_status"."previewAt" is not null + and "job_status"."facesRecognizedAt" is null +order by + "assets"."createdAt" desc diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index 693cd193c1..78ddeae3e2 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -322,4 +322,13 @@ export class AssetJobRepository { .where('assets.deletedAt', '<=', trashedBefore) .stream(); } + + @GenerateSql({ params: [], stream: true }) + streamForDetectFacesJob(force?: boolean) { + return this.assetsWithPreviews() + .$if(!force, (qb) => qb.where('job_status.facesRecognizedAt', 'is', null)) + .select(['assets.id']) + .orderBy('assets.createdAt', 'desc') + .stream(); + } } diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 7b6fb862d0..ade6201cf1 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -48,8 +48,6 @@ export interface LivePhotoSearchOptions { export enum WithoutProperty { THUMBNAIL = 'thumbnail', ENCODED_VIDEO = 'encoded-video', - EXIF = 'exif', - FACES = 'faces', SIDECAR = 'sidecar', } @@ -543,19 +541,7 @@ export class AssetRepository { .where('assets.type', '=', AssetType.VIDEO) .where((eb) => eb.or([eb('assets.encodedVideoPath', 'is', null), eb('assets.encodedVideoPath', '=', '')])), ) - .$if(property === WithoutProperty.EXIF, (qb) => - qb - .leftJoin('asset_job_status as job_status', 'assets.id', 'job_status.assetId') - .where((eb) => eb.or([eb('job_status.metadataExtractedAt', 'is', null), eb('assetId', 'is', null)])) - .where('assets.isVisible', '=', true), - ) - .$if(property === WithoutProperty.FACES, (qb) => - qb - .innerJoin('asset_job_status as job_status', 'assetId', 'assets.id') - .where('job_status.previewAt', 'is not', null) - .where('job_status.facesRecognizedAt', 'is', null) - .where('assets.isVisible', '=', true), - ) + .$if(property === WithoutProperty.SIDECAR, (qb) => qb .where((eb) => eb.or([eb('assets.sidecarPath', '=', ''), eb('assets.sidecarPath', 'is', null)])) diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 9808522434..5b88883472 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -2,7 +2,6 @@ import { BadRequestException, NotFoundException } from '@nestjs/common'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { mapFaces, mapPerson, PersonResponseDto } from 'src/dtos/person.dto'; import { CacheControl, Colorspace, ImageFormat, JobName, JobStatus, SourceType, SystemMetadataKey } from 'src/enum'; -import { WithoutProperty } from 'src/repositories/asset.repository'; import { DetectedFaces } from 'src/repositories/machine-learning.repository'; import { FaceSearchResult } from 'src/repositories/search.repository'; import { PersonService } from 'src/services/person.service'; @@ -455,14 +454,11 @@ describe(PersonService.name, () => { }); it('should queue missing assets', async () => { - mocks.asset.getWithout.mockResolvedValue({ - items: [assetStub.image], - hasNextPage: false, - }); + mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image])); await sut.handleQueueDetectFaces({ force: false }); - expect(mocks.asset.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.FACES); + expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(false); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FACE_DETECTION, @@ -472,10 +468,7 @@ describe(PersonService.name, () => { }); it('should queue all assets', async () => { - mocks.asset.getAll.mockResolvedValue({ - items: [assetStub.image], - hasNextPage: false, - }); + mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image])); mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.withName]); await sut.handleQueueDetectFaces({ force: true }); @@ -483,7 +476,7 @@ describe(PersonService.name, () => { expect(mocks.person.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING }); expect(mocks.person.delete).toHaveBeenCalledWith([personStub.withName.id]); expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.withName.thumbnailPath); - expect(mocks.asset.getAll).toHaveBeenCalled(); + expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(true); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FACE_DETECTION, @@ -493,17 +486,14 @@ describe(PersonService.name, () => { }); it('should refresh all assets', async () => { - mocks.asset.getAll.mockResolvedValue({ - items: [assetStub.image], - hasNextPage: false, - }); + mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image])); await sut.handleQueueDetectFaces({ force: undefined }); expect(mocks.person.delete).not.toHaveBeenCalled(); expect(mocks.person.deleteFaces).not.toHaveBeenCalled(); expect(mocks.storage.unlink).not.toHaveBeenCalled(); - expect(mocks.asset.getAll).toHaveBeenCalled(); + expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(undefined); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FACE_DETECTION, @@ -516,16 +506,13 @@ describe(PersonService.name, () => { it('should delete existing people and faces if forced', async () => { mocks.person.getAll.mockReturnValue(makeStream([faceStub.face1.person, personStub.randomPerson])); mocks.person.getAllFaces.mockReturnValue(makeStream([faceStub.face1])); - mocks.asset.getAll.mockResolvedValue({ - items: [assetStub.image], - hasNextPage: false, - }); + mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image])); mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]); mocks.person.deleteFaces.mockResolvedValue(); await sut.handleQueueDetectFaces({ force: true }); - expect(mocks.asset.getAll).toHaveBeenCalled(); + expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(true); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FACE_DETECTION, diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 66d68857a0..227ea3c1c2 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -36,7 +36,6 @@ import { SourceType, SystemMetadataKey, } from 'src/enum'; -import { WithoutProperty } from 'src/repositories/asset.repository'; import { BoundingBox } from 'src/repositories/machine-learning.repository'; import { UpdateFacesData } from 'src/repositories/person.repository'; import { BaseService } from 'src/services/base.service'; @@ -44,7 +43,6 @@ import { CropOptions, ImageDimensions, InputDimensions, JobItem, JobOf } from 's import { ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; import { isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc'; -import { usePagination } from 'src/utils/pagination'; @Injectable() export class PersonService extends BaseService { @@ -265,23 +263,19 @@ export class PersonService extends BaseService { await this.handlePersonCleanup(); } - const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { - return force === false - ? this.assetRepository.getWithout(pagination, WithoutProperty.FACES) - : this.assetRepository.getAll(pagination, { - orderDirection: 'desc', - withFaces: true, - withArchived: true, - isVisible: true, - }); - }); + let jobs: JobItem[] = []; + const assets = this.assetJobRepository.streamForDetectFacesJob(force); + for await (const asset of assets) { + jobs.push({ name: JobName.FACE_DETECTION, data: { id: asset.id } }); - for await (const assets of assetPagination) { - await this.jobRepository.queueAll( - assets.map((asset) => ({ name: JobName.FACE_DETECTION, data: { id: asset.id } })), - ); + if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { + await this.jobRepository.queueAll(jobs); + jobs = []; + } } + await this.jobRepository.queueAll(jobs); + if (force === undefined) { await this.jobRepository.queue({ name: JobName.PERSON_CLEANUP }); } From 436cff72b56b0155d0bc42c65d2d0e371a5c139e Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:50:38 +0200 Subject: [PATCH 110/356] refactor: activity manager (#17943) --- .../asset-viewer/activity-viewer.svelte | 89 ++++---------- .../asset-viewer/asset-viewer.svelte | 79 +++--------- .../lib/managers/activity-manager.svelte.ts | 113 ++++++++++++++++++ web/src/lib/stores/activity.store.ts | 11 -- .../[[assetId=id]]/+page.svelte | 89 +++++--------- 5 files changed, 175 insertions(+), 206 deletions(-) create mode 100644 web/src/lib/managers/activity-manager.svelte.ts delete mode 100644 web/src/lib/stores/activity.store.ts diff --git a/web/src/lib/components/asset-viewer/activity-viewer.svelte b/web/src/lib/components/asset-viewer/activity-viewer.svelte index 94b66d4c22..e98769d495 100644 --- a/web/src/lib/components/asset-viewer/activity-viewer.svelte +++ b/web/src/lib/components/asset-viewer/activity-viewer.svelte @@ -1,32 +1,24 @@ @@ -71,9 +74,6 @@ cancelColor="secondary" confirmColor="danger" confirmText={$t('close')} - onCancel={() => { - $showCancelConfirmDialog = false; - }} - onConfirm={() => (typeof $showCancelConfirmDialog === 'boolean' ? null : $showCancelConfirmDialog())} + onClose={(confirmed) => (confirmed ? onConfirm() : ($showCancelConfirmDialog = false))} /> {/if} diff --git a/web/src/lib/components/photos-page/delete-asset-dialog.svelte b/web/src/lib/components/photos-page/delete-asset-dialog.svelte index 3053600a47..fbdec86244 100644 --- a/web/src/lib/components/photos-page/delete-asset-dialog.svelte +++ b/web/src/lib/components/photos-page/delete-asset-dialog.svelte @@ -1,9 +1,9 @@ - + (confirmed ? handleConfirm() : onCancel())} +> {#snippet promptSnippet()}
    diff --git a/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte b/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte index dad16d52ca..32f4b6a8f4 100644 --- a/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte +++ b/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte @@ -1,8 +1,8 @@ - + onClose(false)} {width}>
    {#if promptSnippet}{@render promptSnippet()}{:else}

    {prompt}

    @@ -48,7 +46,7 @@ {#snippet stickyBottom()} {#if !hideCancelButton} - {/if} diff --git a/web/src/lib/components/shared-components/dialog/dialog.ts b/web/src/lib/components/shared-components/dialog/dialog.ts index 8efff58da0..69a64aad21 100644 --- a/web/src/lib/components/shared-components/dialog/dialog.ts +++ b/web/src/lib/components/shared-components/dialog/dialog.ts @@ -1,8 +1,7 @@ import { writable } from 'svelte/store'; type DialogActions = { - onConfirm: () => void; - onCancel: () => void; + onClose: (confirmed: boolean) => void; }; type DialogOptions = { @@ -24,13 +23,9 @@ function createDialogWrapper() { return new Promise((resolve) => { const newDialog: Dialog = { ...options, - onConfirm: () => { + onClose: (confirmed) => { dialog.set(undefined); - resolve(true); - }, - onCancel: () => { - dialog.set(undefined); - resolve(false); + resolve(confirmed); }, }; diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte index 07757614e5..af7f3d11af 100644 --- a/web/src/routes/admin/jobs-status/+page.svelte +++ b/web/src/routes/admin/jobs-status/+page.svelte @@ -102,8 +102,7 @@ confirmColor="primary" title={$t('admin.create_job')} disabled={!selectedJob} - onConfirm={handleCreate} - onCancel={handleCancel} + onClose={(confirmed) => (confirmed ? handleCreate() : handleCancel())} > {#snippet promptSnippet()} diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index a25799588a..8b96c6c922 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -152,8 +152,7 @@ (shouldShowPasswordResetSuccess = false)} - onCancel={() => (shouldShowPasswordResetSuccess = false)} + onClose={() => (shouldShowPasswordResetSuccess = false)} hideCancelButton={true} confirmColor="success" > From 62fc5b3c7db526856832a778fe877305a51e34d0 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Sat, 3 May 2025 00:41:42 +0200 Subject: [PATCH 120/356] refactor: introduce modal manager (#18039) --- web/package-lock.json | 8 +- web/package.json | 2 +- web/src/app.css | 3 +- .../components/forms/create-user-form.svelte | 126 ++++++++------- .../components/forms/edit-user-form.svelte | 148 +++++++++--------- .../shared-components/change-location.svelte | 2 +- .../dialog/confirm-dialog.svelte | 35 +++-- .../lib/forms/password-reset-success.svelte | 43 +++++ web/src/lib/managers/modal-manager.svelte.ts | 33 ++++ .../routes/admin/user-management/+page.svelte | 99 +++--------- web/tailwind.config.js | 2 +- 11 files changed, 265 insertions(+), 236 deletions(-) create mode 100644 web/src/lib/forms/password-reset-success.svelte create mode 100644 web/src/lib/managers/modal-manager.svelte.ts diff --git a/web/package-lock.json b/web/package-lock.json index c76dd64840..75c55aa779 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.18.1", + "@immich/ui": "^0.19.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", @@ -1320,9 +1320,9 @@ "link": true }, "node_modules/@immich/ui": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.18.1.tgz", - "integrity": "sha512-XWWO6OTfH3MektyxCn0hWefZyOGyWwwx/2zHinuShpxTHSyfveJ4mOkFP8DkyMz0dnvJ1EfdkPBMkld3y5R/Hw==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.19.0.tgz", + "integrity": "sha512-XVjSUoQVIoe83pxM4q8kmlejb2xep/TZEfoGbasI7takEGKNiWEyXr5eZaXZCSVgq78fcNRr4jyWz290ZAXh7A==", "license": "GNU Affero General Public License version 3", "dependencies": { "@mdi/js": "^7.4.47", diff --git a/web/package.json b/web/package.json index 9aa9bee6bc..7c5a0147bb 100644 --- a/web/package.json +++ b/web/package.json @@ -27,7 +27,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.18.1", + "@immich/ui": "^0.19.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", diff --git a/web/src/app.css b/web/src/app.css index 2c8d150b4f..61759eb1b0 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -8,7 +8,6 @@ --immich-primary: 66 80 175; --immich-bg: 255 255 255; --immich-fg: 0 0 0; - --immich-gray: 246 246 244; --immich-error: 229 115 115; --immich-success: 129 199 132; --immich-warning: 255 183 77; @@ -33,6 +32,7 @@ --immich-ui-warning: 255 170 0; --immich-ui-info: 14 165 233; --immich-ui-default-border: 209 213 219; + --immich-ui-gray: 246 246 246; } .dark { @@ -45,6 +45,7 @@ --immich-ui-warning: 255 170 0; --immich-ui-info: 14 165 233; --immich-ui-default-border: 55 65 81; + --immich-ui-gray: 33 33 33; } } diff --git a/web/src/lib/components/forms/create-user-form.svelte b/web/src/lib/components/forms/create-user-form.svelte index 83b3154d4b..34e498ce1c 100644 --- a/web/src/lib/components/forms/create-user-form.svelte +++ b/web/src/lib/components/forms/create-user-form.svelte @@ -1,21 +1,29 @@ - - - {#if error} - - {/if} - - {#if success} -

    {$t('new_user_created')}

    - {/if} - - - - - - - {#if $featureFlags.email} - - - + + + + {#if error} + {/if} - - - + {#if success} +

    {$t('new_user_created')}

    + {/if} - - - {passwordMismatchMessage} - + + + + - - - - - - - - - - - {#if quotaSizeWarning} - {$t('errors.quota_higher_than_disk_size')} + {#if $featureFlags.email} + + + {/if} - - - {#snippet stickyBottom()} - - - {/snippet} -
    - + + + + + + + {passwordMismatchMessage} + + + + + + + + + + + + + {#if quotaSizeWarning} + {$t('errors.quota_higher_than_disk_size')} + {/if} + + + + + + +
    + + +
    +
    + diff --git a/web/src/lib/components/forms/edit-user-form.svelte b/web/src/lib/components/forms/edit-user-form.svelte index ab914e6430..d2f56a974a 100644 --- a/web/src/lib/components/forms/edit-user-form.svelte +++ b/web/src/lib/components/forms/edit-user-form.svelte @@ -1,34 +1,26 @@ - -
    -
    - - + + + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + + +

    + {$t('admin.note_apply_storage_label_previous_assets')} + + {$t('admin.storage_template_migration_job')} + +

    +
    + +
    + + +
    + {#if canResetPassword} + + {/if} +
    - -
    - - -
    - -
    - - -
    - -
    - - - -

    - {$t('admin.note_apply_storage_label_previous_assets')} - - {$t('admin.storage_template_migration_job')} - -

    -
    - - - {#snippet stickyBottom()} - {#if canResetPassword} - - {/if} - - {/snippet} - +
    +
    diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte index 1987596026..3539945911 100644 --- a/web/src/lib/components/shared-components/change-location.svelte +++ b/web/src/lib/components/shared-components/change-location.svelte @@ -115,7 +115,7 @@ (confirmed ? handleConfirm() : onCancel())} > {#snippet promptSnippet()} diff --git a/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte b/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte index 32f4b6a8f4..75c07aebc6 100644 --- a/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte +++ b/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte @@ -1,8 +1,7 @@ - onClose(false)} {width}> -
    + onClose(false)} {size} class="bg-light text-dark"> + {#if promptSnippet}{@render promptSnippet()}{:else}

    {prompt}

    {/if} -
    + - {#snippet stickyBottom()} - {#if !hideCancelButton} - + {/if} + - {/if} - - {/snippet} -
    +
    + + diff --git a/web/src/lib/forms/password-reset-success.svelte b/web/src/lib/forms/password-reset-success.svelte new file mode 100644 index 0000000000..7091047eb8 --- /dev/null +++ b/web/src/lib/forms/password-reset-success.svelte @@ -0,0 +1,43 @@ + + + + {#snippet promptSnippet()} +
    + {$t('admin.user_password_has_been_reset')} + +
    + {newPassword} + copyToClipboard(newPassword)} + title={$t('copy_password')} + aria-label={$t('copy_password')} + /> +
    + + {$t('admin.user_password_reset_description')} +
    + {/snippet} +
    diff --git a/web/src/lib/managers/modal-manager.svelte.ts b/web/src/lib/managers/modal-manager.svelte.ts new file mode 100644 index 0000000000..c8cefe8a58 --- /dev/null +++ b/web/src/lib/managers/modal-manager.svelte.ts @@ -0,0 +1,33 @@ +import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte'; +import { mount, unmount, type Component, type ComponentProps } from 'svelte'; + +type OnCloseData = T extends { onClose: (data: infer R) => void } ? R : never; +// TODO make `props` optional if component only has `onClose` +// type OptionalIfEmpty = keyof T extends never ? undefined : T; + +class ModalManager { + open>(Component: Component, props: Omit) { + return new Promise((resolve) => { + let modal: object = {}; + + const onClose = async (data: K) => { + await unmount(modal); + resolve(data); + }; + + modal = mount(Component, { + target: document.body, + props: { + ...(props as T), + onClose, + }, + }); + }); + } + + openDialog(options: Omit, 'onClose'>) { + return this.open(ConfirmDialog, options); + } +} + +export const modalManager = new ModalManager(); diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index 8b96c6c922..42d1404177 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -6,20 +6,20 @@ import CreateUserForm from '$lib/components/forms/create-user-form.svelte'; import EditUserForm from '$lib/components/forms/edit-user-form.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; - import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte'; import { NotificationType, notificationController, } from '$lib/components/shared-components/notification/notification'; + import PasswordResetSuccess from '$lib/forms/password-reset-success.svelte'; + import { modalManager } from '$lib/managers/modal-manager.svelte'; import { locale } from '$lib/stores/preferences.store'; - import { featureFlags, serverConfig } from '$lib/stores/server-config.store'; + import { serverConfig } from '$lib/stores/server-config.store'; import { user } from '$lib/stores/user.store'; import { websocketEvents } from '$lib/stores/websocket'; - import { copyToClipboard } from '$lib/utils'; import { getByteUnitString } from '$lib/utils/byte-units'; import { UserStatus, searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk'; - import { Button, Code, IconButton, Text } from '@immich/ui'; - import { mdiContentCopy, mdiDeleteRestore, mdiInfinity, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'; + import { Button, IconButton } from '@immich/ui'; + import { mdiDeleteRestore, mdiInfinity, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'; import { DateTime } from 'luxon'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -32,13 +32,9 @@ let { data }: Props = $props(); let allUsers: UserAdminResponseDto[] = $state([]); - let shouldShowEditUserForm = $state(false); - let shouldShowCreateUserForm = $state(false); - let shouldShowPasswordResetSuccess = $state(false); let shouldShowDeleteConfirmDialog = $state(false); let shouldShowRestoreDialog = $state(false); let selectedUser = $state(); - let newPassword = $state(''); const refresh = async () => { allUsers = await searchUsersAdmin({ withDeleted: true }); @@ -65,25 +61,23 @@ return DateTime.fromISO(deletedAt).plus({ days: $serverConfig.userDeleteDelay }).toJSDate(); }; - const onUserCreated = async () => { + const handleCreate = async () => { + await modalManager.open(CreateUserForm, {}); await refresh(); - shouldShowCreateUserForm = false; }; - const editUserHandler = (user: UserAdminResponseDto) => { - selectedUser = user; - shouldShowEditUserForm = true; - }; - - const onEditUserSuccess = async () => { - await refresh(); - shouldShowEditUserForm = false; - }; - - const onEditPasswordSuccess = async () => { - await refresh(); - shouldShowEditUserForm = false; - shouldShowPasswordResetSuccess = true; + const handleEdit = async (dto: UserAdminResponseDto) => { + const result = await modalManager.open(EditUserForm, { user: dto, canResetPassword: dto.id !== $user.id }); + switch (result?.action) { + case 'resetPassword': { + await modalManager.open(PasswordResetSuccess, { newPassword: result.data }); + break; + } + case 'update': { + await refresh(); + break; + } + } }; const deleteUserHandler = (user: UserAdminResponseDto) => { @@ -110,26 +104,6 @@
    - {#if shouldShowCreateUserForm} - (shouldShowCreateUserForm = false)} - onClose={() => (shouldShowCreateUserForm = false)} - oauthEnabled={$featureFlags.oauth} - /> - {/if} - - {#if shouldShowEditUserForm && selectedUser} - (shouldShowEditUserForm = false)} - /> - {/if} - {#if shouldShowDeleteConfirmDialog && selectedUser} {/if} - {#if shouldShowPasswordResetSuccess} - (shouldShowPasswordResetSuccess = false)} - hideCancelButton={true} - confirmColor="success" - > - {#snippet promptSnippet()} -
    - {$t('admin.user_password_has_been_reset')} - -
    - {newPassword} - copyToClipboard(newPassword)} - title={$t('copy_password')} - aria-label={$t('copy_password')} - /> -
    - - {$t('admin.user_password_reset_description')} -
    - {/snippet} -
    - {/if} - editUserHandler(immichUser)} + onclick={() => handleEdit(immichUser)} aria-label={$t('edit_user')} /> {#if immichUser.id !== $user.id} @@ -256,7 +199,7 @@ {/if}
    - +
    diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 95611d486d..2e13e5997d 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -36,7 +36,7 @@ export default { danger: 'rgb(var(--immich-ui-danger) / )', warning: 'rgb(var(--immich-ui-warning) / )', info: 'rgb(var(--immich-ui-info) / )', - subtle: 'rgb(var(--immich-gray) / )', + subtle: 'rgb(var(--immich-ui-gray) / )', }, borderColor: ({ theme }) => ({ ...theme('colors'), From ea9f11bf3922907b509e233a015dd3ac247de090 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Sat, 3 May 2025 09:39:44 -0400 Subject: [PATCH 121/356] refactor: controller tests (#18035) * feat: controller unit tests * refactor: controller tests --- e2e/src/api/specs/activity.e2e-spec.ts | 70 ------ e2e/src/api/specs/album.e2e-spec.ts | 72 +------ e2e/src/api/specs/api-key.e2e-spec.ts | 57 +---- e2e/src/api/specs/asset.e2e-spec.ts | 161 -------------- e2e/src/api/specs/search.e2e-spec.ts | 99 --------- .../controllers/activity.controller.spec.ts | 81 +++++++ .../src/controllers/album.controller.spec.ts | 86 ++++++++ .../controllers/api-key.controller.spec.ts | 73 +++++++ server/src/controllers/app.controller.spec.ts | 49 +++++ .../asset-media.controller.spec.ts | 137 ++++++++++++ .../src/controllers/asset.controller.spec.ts | 118 ++++++++++ .../src/controllers/auth.controller.spec.ts | 60 ++++++ .../notification.controller.spec.ts | 64 ++++++ .../src/controllers/search.controller.spec.ts | 201 ++++++++++++++++++ .../src/controllers/server.controller.spec.ts | 28 +++ .../src/controllers/user.controller.spec.ts | 77 +++++++ server/test/medium/responses.ts | 1 - .../specs/controllers/auth.controller.spec.ts | 60 ------ .../notification.controller.spec.ts | 86 -------- .../specs/controllers/user.controller.spec.ts | 100 --------- server/test/medium/utils.ts | 100 --------- server/test/small.factory.ts | 7 + server/test/utils.ts | 53 ++++- 23 files changed, 1035 insertions(+), 805 deletions(-) create mode 100644 server/src/controllers/activity.controller.spec.ts create mode 100644 server/src/controllers/album.controller.spec.ts create mode 100644 server/src/controllers/api-key.controller.spec.ts create mode 100644 server/src/controllers/app.controller.spec.ts create mode 100644 server/src/controllers/asset-media.controller.spec.ts create mode 100644 server/src/controllers/asset.controller.spec.ts create mode 100644 server/src/controllers/auth.controller.spec.ts create mode 100644 server/src/controllers/notification.controller.spec.ts create mode 100644 server/src/controllers/search.controller.spec.ts create mode 100644 server/src/controllers/server.controller.spec.ts create mode 100644 server/src/controllers/user.controller.spec.ts delete mode 100644 server/test/medium/specs/controllers/auth.controller.spec.ts delete mode 100644 server/test/medium/specs/controllers/notification.controller.spec.ts delete mode 100644 server/test/medium/specs/controllers/user.controller.spec.ts delete mode 100644 server/test/medium/utils.ts diff --git a/e2e/src/api/specs/activity.e2e-spec.ts b/e2e/src/api/specs/activity.e2e-spec.ts index ee75d6070b..70c32313f1 100644 --- a/e2e/src/api/specs/activity.e2e-spec.ts +++ b/e2e/src/api/specs/activity.e2e-spec.ts @@ -46,38 +46,6 @@ describe('/activities', () => { }); describe('GET /activities', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/activities'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require an albumId', async () => { - const { status, body } = await request(app) - .get('/activities') - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))); - }); - - it('should reject an invalid albumId', async () => { - const { status, body } = await request(app) - .get('/activities') - .query({ albumId: uuidDto.invalid }) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))); - }); - - it('should reject an invalid assetId', async () => { - const { status, body } = await request(app) - .get('/activities') - .query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid }) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID']))); - }); - it('should start off empty', async () => { const { status, body } = await request(app) .get('/activities') @@ -192,30 +160,6 @@ describe('/activities', () => { }); describe('POST /activities', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/activities'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require an albumId', async () => { - const { status, body } = await request(app) - .post('/activities') - .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ albumId: uuidDto.invalid }); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))); - }); - - it('should require a comment when type is comment', async () => { - const { status, body } = await request(app) - .post('/activities') - .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ albumId: uuidDto.notFound, type: 'comment', comment: null }); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(['comment must be a string', 'comment should not be empty'])); - }); - it('should add a comment to an album', async () => { const { status, body } = await request(app) .post('/activities') @@ -330,20 +274,6 @@ describe('/activities', () => { }); describe('DELETE /activities/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/activities/${uuidDto.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid uuid', async () => { - const { status, body } = await request(app) - .delete(`/activities/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should remove a comment from an album', async () => { const reaction = await createActivity({ albumId: album.id, diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index cede49f469..65a94122fa 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -9,7 +9,7 @@ import { LoginResponseDto, SharedLinkType, } from '@immich/sdk'; -import { createUserDto, uuidDto } from 'src/fixtures'; +import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; @@ -128,28 +128,6 @@ describe('/albums', () => { }); describe('GET /albums', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/albums'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should reject an invalid shared param', async () => { - const { status, body } = await request(app) - .get('/albums?shared=invalid') - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value'])); - }); - - it('should reject an invalid assetId param', async () => { - const { status, body } = await request(app) - .get('/albums?assetId=invalid') - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID'])); - }); - it("should not show other users' favorites", async () => { const { status, body } = await request(app) .get(`/albums/${user1Albums[0].id}?withoutAssets=false`) @@ -323,12 +301,6 @@ describe('/albums', () => { }); describe('GET /albums/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/albums/${user1Albums[0].id}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should return album info for own album', async () => { const { status, body } = await request(app) .get(`/albums/${user1Albums[0].id}?withoutAssets=false`) @@ -421,12 +393,6 @@ describe('/albums', () => { }); describe('GET /albums/statistics', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/albums/statistics'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should return total count of albums the user has access to', async () => { const { status, body } = await request(app) .get('/albums/statistics') @@ -438,12 +404,6 @@ describe('/albums', () => { }); describe('POST /albums', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/albums').send({ albumName: 'New album' }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should create an album', async () => { const { status, body } = await request(app) .post('/albums') @@ -471,12 +431,6 @@ describe('/albums', () => { }); describe('PUT /albums/:id/assets', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/assets`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should be able to add own asset to own album', async () => { const asset = await utils.createAsset(user1.accessToken); const { status, body } = await request(app) @@ -526,14 +480,6 @@ describe('/albums', () => { }); describe('PATCH /albums/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .patch(`/albums/${uuidDto.notFound}`) - .send({ albumName: 'New album name' }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should update an album', async () => { const album = await utils.createAlbum(user1.accessToken, { albumName: 'New album', @@ -576,15 +522,6 @@ describe('/albums', () => { }); describe('DELETE /albums/:id/assets', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .delete(`/albums/${user1Albums[0].id}/assets`) - .send({ ids: [user1Asset1.id] }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { status, body } = await request(app) .delete(`/albums/${user1Albums[1].id}/assets`) @@ -679,13 +616,6 @@ describe('/albums', () => { }); }); - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/users`).send({ sharedUserIds: [] }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should be able to add user to own album', async () => { const { status, body } = await request(app) .put(`/albums/${album.id}/users`) diff --git a/e2e/src/api/specs/api-key.e2e-spec.ts b/e2e/src/api/specs/api-key.e2e-spec.ts index 1748276625..e86edddcdf 100644 --- a/e2e/src/api/specs/api-key.e2e-spec.ts +++ b/e2e/src/api/specs/api-key.e2e-spec.ts @@ -1,5 +1,5 @@ import { LoginResponseDto, Permission, createApiKey } from '@immich/sdk'; -import { createUserDto, uuidDto } from 'src/fixtures'; +import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; @@ -24,12 +24,6 @@ describe('/api-keys', () => { }); describe('POST /api-keys', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/api-keys').send({ name: 'API Key' }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should not work without permission', async () => { const { secret } = await create(user.accessToken, [Permission.ApiKeyRead]); const { status, body } = await request(app).post('/api-keys').set('x-api-key', secret).send({ name: 'API Key' }); @@ -99,12 +93,6 @@ describe('/api-keys', () => { }); describe('GET /api-keys', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/api-keys'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should start off empty', async () => { const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`); expect(body).toEqual([]); @@ -125,12 +113,6 @@ describe('/api-keys', () => { }); describe('GET /api-keys/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/api-keys/${uuidDto.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) @@ -140,14 +122,6 @@ describe('/api-keys', () => { expect(body).toEqual(errorDto.badRequest('API Key not found')); }); - it('should require a valid uuid', async () => { - const { status, body } = await request(app) - .get(`/api-keys/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should get api key details', async () => { const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) @@ -165,12 +139,6 @@ describe('/api-keys', () => { }); describe('PUT /api-keys/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/api-keys/${uuidDto.notFound}`).send({ name: 'new name' }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) @@ -181,15 +149,6 @@ describe('/api-keys', () => { expect(body).toEqual(errorDto.badRequest('API Key not found')); }); - it('should require a valid uuid', async () => { - const { status, body } = await request(app) - .put(`/api-keys/${uuidDto.invalid}`) - .send({ name: 'new name' }) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should update api key details', async () => { const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) @@ -208,12 +167,6 @@ describe('/api-keys', () => { }); describe('DELETE /api-keys/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/api-keys/${uuidDto.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) @@ -223,14 +176,6 @@ describe('/api-keys', () => { expect(body).toEqual(errorDto.badRequest('API Key not found')); }); - it('should require a valid uuid', async () => { - const { status, body } = await request(app) - .delete(`/api-keys/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should delete an api key', async () => { const { apiKey } = await create(user.accessToken, [Permission.All]); const { status } = await request(app) diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 01129b3299..f5cf78bb8a 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -22,24 +22,6 @@ import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/ import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -const makeUploadDto = (options?: { omit: string }): Record => { - const dto: Record = { - deviceAssetId: 'example-image', - deviceId: 'TEST', - fileCreatedAt: new Date().toISOString(), - fileModifiedAt: new Date().toISOString(), - isFavorite: 'testing', - duration: '0:00:00.000000', - }; - - const omit = options?.omit; - if (omit) { - delete dto[omit]; - } - - return dto; -}; - const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`; const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`; const facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`; @@ -160,13 +142,6 @@ describe('/asset', () => { }); describe('GET /assets/:id/original', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/assets/${uuidDto.notFound}/original`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should download the file', async () => { const response = await request(app) .get(`/assets/${user1Assets[0].id}/original`) @@ -178,20 +153,6 @@ describe('/asset', () => { }); describe('GET /assets/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/assets/${uuidDto.notFound}`); - expect(body).toEqual(errorDto.unauthorized); - expect(status).toBe(401); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .get(`/assets/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should require access', async () => { const { status, body } = await request(app) .get(`/assets/${user2Assets[0].id}`) @@ -354,13 +315,6 @@ describe('/asset', () => { }); describe('GET /assets/statistics', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/assets/statistics'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should return stats of all assets', async () => { const { status, body } = await request(app) .get('/assets/statistics') @@ -425,13 +379,6 @@ describe('/asset', () => { await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); }); - it('should require authentication', async () => { - const { status, body } = await request(app).get('/assets/random'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it.each(TEN_TIMES)('should return 1 random assets', async () => { const { status, body } = await request(app) .get('/assets/random') @@ -467,14 +414,6 @@ describe('/asset', () => { expect(status).toBe(200); expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]); }); - - it('should return error', async () => { - const { status } = await request(app) - .get('/assets/random?count=ABC') - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(400); - }); }); describe('PUT /assets/:id', () => { @@ -619,28 +558,6 @@ describe('/asset', () => { expect(status).toEqual(200); }); - it('should reject invalid gps coordinates', async () => { - for (const test of [ - { latitude: 12 }, - { longitude: 12 }, - { latitude: 12, longitude: 'abc' }, - { latitude: 'abc', longitude: 12 }, - { latitude: null, longitude: 12 }, - { latitude: 12, longitude: null }, - { latitude: 91, longitude: 12 }, - { latitude: -91, longitude: 12 }, - { latitude: 12, longitude: -181 }, - { latitude: 12, longitude: 181 }, - ]) { - const { status, body } = await request(app) - .put(`/assets/${user1Assets[0].id}`) - .send(test) - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest()); - } - }); - it('should update gps data', async () => { const { status, body } = await request(app) .put(`/assets/${user1Assets[0].id}`) @@ -712,17 +629,6 @@ describe('/asset', () => { expect(status).toEqual(200); }); - it('should reject invalid rating', async () => { - for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) { - const { status, body } = await request(app) - .put(`/assets/${user1Assets[0].id}`) - .send(test) - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest()); - } - }); - it('should return tagged people', async () => { const { status, body } = await request(app) .put(`/assets/${user1Assets[0].id}`) @@ -746,25 +652,6 @@ describe('/asset', () => { }); describe('DELETE /assets', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .delete(`/assets`) - .send({ ids: [uuidDto.notFound] }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid uuid', async () => { - const { status, body } = await request(app) - .delete(`/assets`) - .send({ ids: [uuidDto.invalid] }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); - }); - it('should throw an error when the id is not found', async () => { const { status, body } = await request(app) .delete(`/assets`) @@ -877,13 +764,6 @@ describe('/asset', () => { }); describe('GET /assets/:id/thumbnail', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/assets/${locationAsset.id}/thumbnail`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should not include gps data for webp thumbnails', async () => { await utils.waitForWebsocketEvent({ event: 'assetUpload', @@ -919,13 +799,6 @@ describe('/asset', () => { }); describe('GET /assets/:id/original', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/assets/${locationAsset.id}/original`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should download the original', async () => { const { status, body, type } = await request(app) .get(`/assets/${locationAsset.id}/original`) @@ -946,43 +819,9 @@ describe('/asset', () => { }); }); - describe('PUT /assets', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put('/assets'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - describe('POST /assets', () => { beforeAll(setupTests, 30_000); - it('should require authentication', async () => { - const { status, body } = await request(app).post(`/assets`); - expect(body).toEqual(errorDto.unauthorized); - expect(status).toBe(401); - }); - - it.each([ - { should: 'require `deviceAssetId`', dto: { ...makeUploadDto({ omit: 'deviceAssetId' }) } }, - { should: 'require `deviceId`', dto: { ...makeUploadDto({ omit: 'deviceId' }) } }, - { should: 'require `fileCreatedAt`', dto: { ...makeUploadDto({ omit: 'fileCreatedAt' }) } }, - { should: 'require `fileModifiedAt`', dto: { ...makeUploadDto({ omit: 'fileModifiedAt' }) } }, - { should: 'require `duration`', dto: { ...makeUploadDto({ omit: 'duration' }) } }, - { should: 'throw if `isFavorite` is not a boolean', dto: { ...makeUploadDto(), isFavorite: 'not-a-boolean' } }, - { should: 'throw if `isVisible` is not a boolean', dto: { ...makeUploadDto(), isVisible: 'not-a-boolean' } }, - { should: 'throw if `isArchived` is not a boolean', dto: { ...makeUploadDto(), isArchived: 'not-a-boolean' } }, - ])('should $should', async ({ dto }) => { - const { status, body } = await request(app) - .post('/assets') - .set('Authorization', `Bearer ${user1.accessToken}`) - .attach('assetData', makeRandomImage(), 'example.png') - .field(dto); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest()); - }); - const tests = [ { input: 'formats/avif/8bit-sRGB.avif', diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts index 1031390ee9..23f8c65fa8 100644 --- a/e2e/src/api/specs/search.e2e-spec.ts +++ b/e2e/src/api/specs/search.e2e-spec.ts @@ -3,7 +3,6 @@ import { DateTime } from 'luxon'; import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { Socket } from 'socket.io-client'; -import { errorDto } from 'src/responses'; import { app, asBearerAuth, TEN_TIMES, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -141,65 +140,6 @@ describe('/search', () => { }); describe('POST /search/metadata', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/search/metadata'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - const badTests = [ - { - should: 'should reject page as a string', - dto: { page: 'abc' }, - expected: ['page must not be less than 1', 'page must be an integer number'], - }, - { - should: 'should reject page as a decimal', - dto: { page: 1.5 }, - expected: ['page must be an integer number'], - }, - { - should: 'should reject page as a negative number', - dto: { page: -10 }, - expected: ['page must not be less than 1'], - }, - { - should: 'should reject page as 0', - dto: { page: 0 }, - expected: ['page must not be less than 1'], - }, - { - should: 'should reject size as a string', - dto: { size: 'abc' }, - expected: [ - 'size must not be greater than 1000', - 'size must not be less than 1', - 'size must be an integer number', - ], - }, - { - should: 'should reject an invalid size', - dto: { size: -1.5 }, - expected: ['size must not be less than 1', 'size must be an integer number'], - }, - ...['isArchived', 'isFavorite', 'isEncoded', 'isOffline', 'isMotion', 'isVisible'].map((value) => ({ - should: `should reject ${value} not a boolean`, - dto: { [value]: 'immich' }, - expected: [`${value} must be a boolean value`], - })), - ]; - - for (const { should, dto, expected } of badTests) { - it(should, async () => { - const { status, body } = await request(app) - .post('/search/metadata') - .set('Authorization', `Bearer ${admin.accessToken}`) - .send(dto); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(expected)); - }); - } - const searchTests = [ { should: 'should get my assets', @@ -454,14 +394,6 @@ describe('/search', () => { } }); - describe('POST /search/smart', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/search/smart'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - describe('POST /search/random', () => { beforeAll(async () => { await Promise.all([ @@ -476,13 +408,6 @@ describe('/search', () => { await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); }); - it('should require authentication', async () => { - const { status, body } = await request(app).post('/search/random').send({ size: 1 }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it.each(TEN_TIMES)('should return 1 random assets', async () => { const { status, body } = await request(app) .post('/search/random') @@ -512,12 +437,6 @@ describe('/search', () => { }); describe('GET /search/explore', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/search/explore'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get explore data', async () => { const { status, body } = await request(app) .get('/search/explore') @@ -528,12 +447,6 @@ describe('/search', () => { }); describe('GET /search/places', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/search/places'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get relevant places', async () => { const name = 'Paris'; @@ -552,12 +465,6 @@ describe('/search', () => { }); describe('GET /search/cities', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/search/cities'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get all cities', async () => { const { status, body } = await request(app) .get('/search/cities') @@ -576,12 +483,6 @@ describe('/search', () => { }); describe('GET /search/suggestions', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/search/suggestions'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get suggestions for country (including null)', async () => { const { status, body } = await request(app) .get('/search/suggestions?type=country&includeNull=true') diff --git a/server/src/controllers/activity.controller.spec.ts b/server/src/controllers/activity.controller.spec.ts new file mode 100644 index 0000000000..fdf1c34d06 --- /dev/null +++ b/server/src/controllers/activity.controller.spec.ts @@ -0,0 +1,81 @@ +import { ActivityController } from 'src/controllers/activity.controller'; +import { ActivityService } from 'src/services/activity.service'; +import request from 'supertest'; +import { factory } from 'test/small.factory'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(ActivityController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(ActivityController, [ + { provide: ActivityService, useValue: mockBaseService(ActivityService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('GET /activities', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/activities'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require an albumId', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/activities'); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + }); + + it('should reject an invalid albumId', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/activities').query({ albumId: '123' }); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + }); + + it('should reject an invalid assetId', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .get('/activities') + .query({ albumId: factory.uuid(), assetId: '123' }); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['assetId must be a UUID']))); + }); + }); + + describe('POST /activities', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/activities'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require an albumId', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/activities').send({ albumId: '123' }); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + }); + + it('should require a comment when type is comment', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/activities') + .send({ albumId: factory.uuid(), type: 'comment', comment: null }); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(['comment must be a string', 'comment should not be empty'])); + }); + }); + + describe('DELETE /activities/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/activities/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).delete(`/activities/123`); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + }); + }); +}); diff --git a/server/src/controllers/album.controller.spec.ts b/server/src/controllers/album.controller.spec.ts new file mode 100644 index 0000000000..5aee4dfdd5 --- /dev/null +++ b/server/src/controllers/album.controller.spec.ts @@ -0,0 +1,86 @@ +import { AlbumController } from 'src/controllers/album.controller'; +import { AlbumService } from 'src/services/album.service'; +import request from 'supertest'; +import { factory } from 'test/small.factory'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(AlbumController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(AlbumController, [{ provide: AlbumService, useValue: mockBaseService(AlbumService) }]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('GET /albums', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/albums'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should reject an invalid shared param', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/albums?shared=invalid'); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(['shared must be a boolean value'])); + }); + + it('should reject an invalid assetId param', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/albums?assetId=invalid'); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(['assetId must be a UUID'])); + }); + }); + + describe('GET /albums/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/albums/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /albums/statistics', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/albums/statistics'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('POST /albums', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/albums').send({ albumName: 'New album' }); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PUT /albums/:id/assets', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/albums/${factory.uuid()}/assets`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PATCH /albums/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).patch(`/albums/${factory.uuid()}`).send({ albumName: 'New album name' }); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('DELETE /albums/:id/assets', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/albums/${factory.uuid()}/assets`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PUT :id/users', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/albums/${factory.uuid()}/users`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/controllers/api-key.controller.spec.ts b/server/src/controllers/api-key.controller.spec.ts new file mode 100644 index 0000000000..b481d1e63d --- /dev/null +++ b/server/src/controllers/api-key.controller.spec.ts @@ -0,0 +1,73 @@ +import { APIKeyController } from 'src/controllers/api-key.controller'; +import { ApiKeyService } from 'src/services/api-key.service'; +import request from 'supertest'; +import { factory } from 'test/small.factory'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(APIKeyController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(APIKeyController, [ + { provide: ApiKeyService, useValue: mockBaseService(ApiKeyService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('POST /api-keys', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/api-keys').send({ name: 'API Key' }); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /api-keys', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/api-keys'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /api-keys/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/api-keys/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).get(`/api-keys/123`); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + }); + }); + + describe('PUT /api-keys/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/api-keys/${factory.uuid()}`).send({ name: 'new name' }); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).put(`/api-keys/123`).send({ name: 'new name' }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + }); + }); + + describe('DELETE /api-keys/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/api-keys/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).delete(`/api-keys/123`); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + }); + }); +}); diff --git a/server/src/controllers/app.controller.spec.ts b/server/src/controllers/app.controller.spec.ts new file mode 100644 index 0000000000..4cc00e78c5 --- /dev/null +++ b/server/src/controllers/app.controller.spec.ts @@ -0,0 +1,49 @@ +import { AppController } from 'src/controllers/app.controller'; +import { SystemConfigService } from 'src/services/system-config.service'; +import request from 'supertest'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(AppController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(AppController, [ + { provide: SystemConfigService, useValue: mockBaseService(SystemConfigService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('GET /.well-known/immich', () => { + it('should not be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/.well-known/immich'); + expect(ctx.authenticate).not.toHaveBeenCalled(); + }); + + it('should return a 200 status code', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/.well-known/immich'); + expect(status).toBe(200); + expect(body).toEqual({ + api: { + endpoint: '/api', + }, + }); + }); + }); + + describe('GET /custom.css', () => { + it('should not be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/custom.css'); + expect(ctx.authenticate).not.toHaveBeenCalled(); + }); + + it('should reply with text/css', async () => { + const { status, headers } = await request(ctx.getHttpServer()).get('/custom.css'); + expect(status).toBe(200); + expect(headers['content-type']).toEqual('text/css; charset=utf-8'); + }); + }); +}); diff --git a/server/src/controllers/asset-media.controller.spec.ts b/server/src/controllers/asset-media.controller.spec.ts new file mode 100644 index 0000000000..c674dc1f2c --- /dev/null +++ b/server/src/controllers/asset-media.controller.spec.ts @@ -0,0 +1,137 @@ +import { AssetMediaController } from 'src/controllers/asset-media.controller'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { AssetMediaService } from 'src/services/asset-media.service'; +import request from 'supertest'; +import { factory } from 'test/small.factory'; +import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +const makeUploadDto = (options?: { omit: string }): Record => { + const dto: Record = { + deviceAssetId: 'example-image', + deviceId: 'TEST', + fileCreatedAt: new Date().toISOString(), + fileModifiedAt: new Date().toISOString(), + isFavorite: 'testing', + duration: '0:00:00.000000', + }; + + const omit = options?.omit; + if (omit) { + delete dto[omit]; + } + + return dto; +}; + +describe(AssetMediaController.name, () => { + let ctx: ControllerContext; + const assetData = Buffer.from('123'); + const filename = 'example.png'; + + beforeAll(async () => { + ctx = await controllerSetup(AssetMediaController, [ + { provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) }, + { provide: AssetMediaService, useValue: mockBaseService(AssetMediaService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('POST /assets', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post(`/assets`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require `deviceAssetId`', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto({ omit: 'deviceAssetId' }) }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + + it('should require `deviceId`', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto({ omit: 'deviceId' }) }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + + it('should require `fileCreatedAt`', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto({ omit: 'fileCreatedAt' }) }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + + it('should require `fileModifiedAt`', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto({ omit: 'fileModifiedAt' }) }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + + it('should require `duration`', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto({ omit: 'duration' }) }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + + it('should throw if `isFavorite` is not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto(), isFavorite: 'not-a-boolean' }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + + it('should throw if `isVisible` is not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto(), isVisible: 'not-a-boolean' }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + + it('should throw if `isArchived` is not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto(), isArchived: 'not-a-boolean' }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + }); + + // TODO figure out how to deal with `sendFile` + describe.skip('GET /assets/:id/original', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/original`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + // TODO figure out how to deal with `sendFile` + describe.skip('GET /assets/:id/thumbnail', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/controllers/asset.controller.spec.ts b/server/src/controllers/asset.controller.spec.ts new file mode 100644 index 0000000000..66d2d7c206 --- /dev/null +++ b/server/src/controllers/asset.controller.spec.ts @@ -0,0 +1,118 @@ +import { AssetController } from 'src/controllers/asset.controller'; +import { AssetService } from 'src/services/asset.service'; +import request from 'supertest'; +import { factory } from 'test/small.factory'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(AssetController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(AssetController, [{ provide: AssetService, useValue: mockBaseService(AssetService) }]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('PUT /assets', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/assets`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('DELETE /assets', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()) + .delete(`/assets`) + .send({ ids: [factory.uuid()] }); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .delete(`/assets`) + .send({ ids: ['123'] }); + + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['each value in ids must be a UUID'])); + }); + }); + + describe('GET /assets/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123`); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + }); + }); + + describe('PUT /assets/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/assets/123`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123`); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + }); + + it('should reject invalid gps coordinates', async () => { + for (const test of [ + { latitude: 12 }, + { longitude: 12 }, + { latitude: 12, longitude: 'abc' }, + { latitude: 'abc', longitude: 12 }, + { latitude: null, longitude: 12 }, + { latitude: 12, longitude: null }, + { latitude: 91, longitude: 12 }, + { latitude: -91, longitude: 12 }, + { latitude: 12, longitude: -181 }, + { latitude: 12, longitude: 181 }, + ]) { + const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + } + }); + + it('should reject invalid rating', async () => { + for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) { + const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + } + }); + }); + + describe('GET /assets/statistics', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/assets/statistics`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /assets/random', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/assets/random`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should not allow count to be a string', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/assets/random?count=ABC'); + expect(status).toBe(400); + expect(body).toEqual( + factory.responses.badRequest(['count must be a positive number', 'count must be an integer number']), + ); + }); + }); +}); diff --git a/server/src/controllers/auth.controller.spec.ts b/server/src/controllers/auth.controller.spec.ts new file mode 100644 index 0000000000..d8ee5ab27d --- /dev/null +++ b/server/src/controllers/auth.controller.spec.ts @@ -0,0 +1,60 @@ +import { AuthController } from 'src/controllers/auth.controller'; +import { AuthService } from 'src/services/auth.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(AuthController.name, () => { + let ctx: ControllerContext; + const service = mockBaseService(AuthService); + + beforeAll(async () => { + ctx = await controllerSetup(AuthController, [{ provide: AuthService, useValue: service }]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('POST /auth/admin-sign-up', () => { + const name = 'admin'; + const email = 'admin@immich.cloud'; + const password = 'password'; + + it('should require an email address', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, password }); + expect(status).toEqual(400); + expect(body).toEqual(errorDto.badRequest()); + }); + + it('should require a password', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, email }); + expect(status).toEqual(400); + expect(body).toEqual(errorDto.badRequest()); + }); + + it('should require a name', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ email, password }); + expect(status).toEqual(400); + expect(body).toEqual(errorDto.badRequest()); + }); + + it('should require a valid email', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/auth/admin-sign-up') + .send({ name, email: 'immich', password }); + expect(status).toEqual(400); + expect(body).toEqual(errorDto.badRequest()); + }); + + it('should transform email to lower case', async () => { + service.adminSignUp.mockReset(); + const { status } = await request(ctx.getHttpServer()) + .post('/auth/admin-sign-up') + .send({ name: 'admin', password: 'password', email: 'aDmIn@IMMICH.cloud' }); + expect(status).toEqual(201); + expect(service.adminSignUp).toHaveBeenCalledWith(expect.objectContaining({ email: 'admin@immich.cloud' })); + }); + }); +}); diff --git a/server/src/controllers/notification.controller.spec.ts b/server/src/controllers/notification.controller.spec.ts new file mode 100644 index 0000000000..4eac7d3451 --- /dev/null +++ b/server/src/controllers/notification.controller.spec.ts @@ -0,0 +1,64 @@ +import { NotificationController } from 'src/controllers/notification.controller'; +import { NotificationService } from 'src/services/notification.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { factory } from 'test/small.factory'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(NotificationController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(NotificationController, [ + { provide: NotificationService, useValue: mockBaseService(NotificationService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('GET /notifications', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/notifications'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it(`should reject an invalid notification level`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .get(`/notifications`) + .query({ level: 'invalid' }) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('level must be one of the following values')])); + }); + }); + + describe('PUT /notifications', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/notifications'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /notifications/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/notifications/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).get(`/notifications/123`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('id must be a UUID')])); + }); + }); + + describe('PUT /notifications/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/notifications/${factory.uuid()}`).send({ readAt: factory.date() }); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/controllers/search.controller.spec.ts b/server/src/controllers/search.controller.spec.ts new file mode 100644 index 0000000000..2388f93e53 --- /dev/null +++ b/server/src/controllers/search.controller.spec.ts @@ -0,0 +1,201 @@ +import { SearchController } from 'src/controllers/search.controller'; +import { SearchService } from 'src/services/search.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(SearchController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(SearchController, [ + { provide: SearchService, useValue: mockBaseService(SearchService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('POST /search/metadata', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/search/metadata'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should reject page as a string', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: 'abc' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['page must not be less than 1', 'page must be an integer number'])); + }); + + it('should reject page as a negative number', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: -10 }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['page must not be less than 1'])); + }); + + it('should reject page as 0', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: 0 }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['page must not be less than 1'])); + }); + + it('should reject size as a string', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ size: 'abc' }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest([ + 'size must not be greater than 1000', + 'size must not be less than 1', + 'size must be an integer number', + ]), + ); + }); + + it('should reject an invalid size', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ size: -1.5 }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['size must not be less than 1', 'size must be an integer number'])); + }); + + it('should reject an isArchived as not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/search/metadata') + .send({ isArchived: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['isArchived must be a boolean value'])); + }); + + it('should reject an isFavorite as not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/search/metadata') + .send({ isFavorite: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['isFavorite must be a boolean value'])); + }); + + it('should reject an isEncoded as not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/search/metadata') + .send({ isEncoded: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['isEncoded must be a boolean value'])); + }); + + it('should reject an isOffline as not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/search/metadata') + .send({ isOffline: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['isOffline must be a boolean value'])); + }); + + it('should reject an isMotion as not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ isMotion: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['isMotion must be a boolean value'])); + }); + + it('should reject an isVisible as not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/search/metadata') + .send({ isVisible: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['isVisible must be a boolean value'])); + }); + }); + + describe('POST /search/random', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/search/random'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should reject if withStacked is not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/search/random') + .send({ withStacked: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['withStacked must be a boolean value'])); + }); + + it('should reject if withPeople is not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/random').send({ withPeople: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['withPeople must be a boolean value'])); + }); + }); + + describe('POST /search/smart', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/search/smart'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a query', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/smart').send({}); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['query should not be empty', 'query must be a string'])); + }); + }); + + describe('GET /search/explore', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/search/explore'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('POST /search/person', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/search/person'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a name', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({}); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string'])); + }); + }); + + describe('GET /search/places', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/search/places'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a name', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({}); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string'])); + }); + }); + + describe('GET /search/cities', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/search/cities'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /search/suggestions', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/search/suggestions'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a type', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/search/suggestions').send({}); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest([ + 'type should not be empty', + expect.stringContaining('type must be one of the following values:'), + ]), + ); + }); + }); +}); diff --git a/server/src/controllers/server.controller.spec.ts b/server/src/controllers/server.controller.spec.ts new file mode 100644 index 0000000000..49edee061a --- /dev/null +++ b/server/src/controllers/server.controller.spec.ts @@ -0,0 +1,28 @@ +import { ServerController } from 'src/controllers/server.controller'; +import { ServerService } from 'src/services/server.service'; +import { VersionService } from 'src/services/version.service'; +import request from 'supertest'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(ServerController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(ServerController, [ + { provide: ServerService, useValue: mockBaseService(ServerService) }, + { provide: VersionService, useValue: mockBaseService(VersionService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('GET /server/license', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/server/license'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/controllers/user.controller.spec.ts b/server/src/controllers/user.controller.spec.ts new file mode 100644 index 0000000000..bb3e709e68 --- /dev/null +++ b/server/src/controllers/user.controller.spec.ts @@ -0,0 +1,77 @@ +import { UserController } from 'src/controllers/user.controller'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { UserService } from 'src/services/user.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { factory } from 'test/small.factory'; +import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(UserController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(UserController, [ + { provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) }, + { provide: UserService, useValue: mockBaseService(UserService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('GET /users', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/users'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /users/me', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/users/me'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PUT /users/me', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put('/users/me'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + for (const key of ['email', 'name']) { + it(`should not allow null ${key}`, async () => { + const dto = { [key]: null }; + const { status, body } = await request(ctx.getHttpServer()) + .put(`/users/me`) + .set('Authorization', `Bearer token`) + .send(dto); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest()); + }); + } + }); + + describe('GET /users/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/users/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PUT /users/me/license', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put('/users/me/license'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('DELETE /users/me/license', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete('/users/me/license'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); +}); diff --git a/server/test/medium/responses.ts b/server/test/medium/responses.ts index 0148f2e1e9..15673ab54b 100644 --- a/server/test/medium/responses.ts +++ b/server/test/medium/responses.ts @@ -47,7 +47,6 @@ export const errorDto = { error: 'Bad Request', statusCode: 400, message: message ?? expect.anything(), - correlationId: expect.any(String), }), noPermission: { error: 'Bad Request', diff --git a/server/test/medium/specs/controllers/auth.controller.spec.ts b/server/test/medium/specs/controllers/auth.controller.spec.ts deleted file mode 100644 index ef2b904f48..0000000000 --- a/server/test/medium/specs/controllers/auth.controller.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { AuthController } from 'src/controllers/auth.controller'; -import { AuthService } from 'src/services/auth.service'; -import request from 'supertest'; -import { errorDto } from 'test/medium/responses'; -import { createControllerTestApp, TestControllerApp } from 'test/medium/utils'; - -describe(AuthController.name, () => { - let app: TestControllerApp; - - beforeAll(async () => { - app = await createControllerTestApp(); - }); - - describe('POST /auth/admin-sign-up', () => { - const name = 'admin'; - const email = 'admin@immich.cloud'; - const password = 'password'; - - const invalid = [ - { - should: 'require an email address', - data: { name, password }, - }, - { - should: 'require a password', - data: { name, email }, - }, - { - should: 'require a name', - data: { email, password }, - }, - { - should: 'require a valid email', - data: { name, email: 'immich', password }, - }, - ]; - - for (const { should, data } of invalid) { - it(`should ${should}`, async () => { - const { status, body } = await request(app.getHttpServer()).post('/auth/admin-sign-up').send(data); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest()); - }); - } - - it('should transform email to lower case', async () => { - const { status } = await request(app.getHttpServer()) - .post('/auth/admin-sign-up') - .send({ name: 'admin', password: 'password', email: 'aDmIn@IMMICH.cloud' }); - expect(status).toEqual(201); - expect(app.getMockedService(AuthService).adminSignUp).toHaveBeenCalledWith( - expect.objectContaining({ email: 'admin@immich.cloud' }), - ); - }); - }); - - afterAll(async () => { - await app.close(); - }); -}); diff --git a/server/test/medium/specs/controllers/notification.controller.spec.ts b/server/test/medium/specs/controllers/notification.controller.spec.ts deleted file mode 100644 index f4a0ec82d5..0000000000 --- a/server/test/medium/specs/controllers/notification.controller.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { NotificationController } from 'src/controllers/notification.controller'; -import { AuthService } from 'src/services/auth.service'; -import { NotificationService } from 'src/services/notification.service'; -import request from 'supertest'; -import { errorDto } from 'test/medium/responses'; -import { createControllerTestApp, TestControllerApp } from 'test/medium/utils'; -import { factory } from 'test/small.factory'; - -describe(NotificationController.name, () => { - let realApp: TestControllerApp; - let mockApp: TestControllerApp; - - beforeEach(async () => { - realApp = await createControllerTestApp({ authType: 'real' }); - mockApp = await createControllerTestApp({ authType: 'mock' }); - }); - - describe('GET /notifications', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()).get('/notifications'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should call the service with an auth dto', async () => { - const auth = factory.auth({ user: factory.user() }); - mockApp.getMockedService(AuthService).authenticate.mockResolvedValue(auth); - const service = mockApp.getMockedService(NotificationService); - - const { status } = await request(mockApp.getHttpServer()) - .get('/notifications') - .set('Authorization', `Bearer token`); - - expect(status).toBe(200); - expect(service.search).toHaveBeenCalledWith(auth, {}); - }); - - it(`should reject an invalid notification level`, async () => { - const auth = factory.auth({ user: factory.user() }); - mockApp.getMockedService(AuthService).authenticate.mockResolvedValue(auth); - const service = mockApp.getMockedService(NotificationService); - - const { status, body } = await request(mockApp.getHttpServer()) - .get(`/notifications`) - .query({ level: 'invalid' }) - .set('Authorization', `Bearer token`); - - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('level must be one of the following values')])); - expect(service.search).not.toHaveBeenCalled(); - }); - }); - - describe('PUT /notifications', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()) - .put(`/notifications`) - .send({ ids: [], readAt: new Date().toISOString() }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - - describe('GET /notifications/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()).get(`/notifications/${factory.uuid()}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - - describe('PUT /notifications/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()) - .put(`/notifications/${factory.uuid()}`) - .send({ readAt: factory.date() }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - - afterAll(async () => { - await realApp.close(); - await mockApp.close(); - }); -}); diff --git a/server/test/medium/specs/controllers/user.controller.spec.ts b/server/test/medium/specs/controllers/user.controller.spec.ts deleted file mode 100644 index f4d90d5469..0000000000 --- a/server/test/medium/specs/controllers/user.controller.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { UserController } from 'src/controllers/user.controller'; -import { AuthService } from 'src/services/auth.service'; -import { UserService } from 'src/services/user.service'; -import request from 'supertest'; -import { errorDto } from 'test/medium/responses'; -import { createControllerTestApp, TestControllerApp } from 'test/medium/utils'; -import { factory } from 'test/small.factory'; - -describe(UserController.name, () => { - let realApp: TestControllerApp; - let mockApp: TestControllerApp; - - beforeAll(async () => { - realApp = await createControllerTestApp({ authType: 'real' }); - mockApp = await createControllerTestApp({ authType: 'mock' }); - }); - - describe('GET /users', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()).get('/users'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should call the service with an auth dto', async () => { - const user = factory.user(); - const authService = mockApp.getMockedService(AuthService); - const auth = factory.auth({ user }); - authService.authenticate.mockResolvedValue(auth); - - const userService = mockApp.getMockedService(UserService); - const { status } = await request(mockApp.getHttpServer()).get('/users').set('Authorization', `Bearer token`); - - expect(status).toBe(200); - expect(userService.search).toHaveBeenCalledWith(auth); - }); - }); - - describe('GET /users/me', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()).get(`/users/me`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - - describe('PUT /users/me', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()).put(`/users/me`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - for (const key of ['email', 'name']) { - it(`should not allow null ${key}`, async () => { - const dto = { [key]: null }; - const { status, body } = await request(mockApp.getHttpServer()) - .put(`/users/me`) - .set('Authorization', `Bearer token`) - .send(dto); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest()); - }); - } - }); - - describe('GET /users/:id', () => { - it('should require authentication', async () => { - const { status } = await request(realApp.getHttpServer()).get(`/users/${factory.uuid()}`); - expect(status).toEqual(401); - }); - }); - - describe('GET /server/license', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()).get('/users/me/license'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - - describe('PUT /users/me/license', () => { - it('should require authentication', async () => { - const { status } = await request(realApp.getHttpServer()).put(`/users/me/license`); - expect(status).toEqual(401); - }); - }); - - describe('DELETE /users/me/license', () => { - it('should require authentication', async () => { - const { status } = await request(realApp.getHttpServer()).put(`/users/me/license`); - expect(status).toEqual(401); - }); - }); - - afterAll(async () => { - await realApp.close(); - await mockApp.close(); - }); -}); diff --git a/server/test/medium/utils.ts b/server/test/medium/utils.ts deleted file mode 100644 index 030780b35b..0000000000 --- a/server/test/medium/utils.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Provider } from '@nestjs/common'; -import { SchedulerRegistry } from '@nestjs/schedule'; -import { Test } from '@nestjs/testing'; -import { ClassConstructor } from 'class-transformer'; -import { ClsService } from 'nestjs-cls'; -import { middleware } from 'src/app.module'; -import { controllers } from 'src/controllers'; -import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter'; -import { LoggingRepository } from 'src/repositories/logging.repository'; -import { services } from 'src/services'; -import { ApiService } from 'src/services/api.service'; -import { AuthService } from 'src/services/auth.service'; -import { BaseService } from 'src/services/base.service'; -import { automock } from 'test/utils'; -import { Mocked } from 'vitest'; - -export const createControllerTestApp = async (options?: { authType?: 'mock' | 'real' }) => { - const { authType = 'mock' } = options || {}; - - const configMock = { getEnv: () => ({ noColor: true }) }; - const clsMock = { getId: vitest.fn().mockReturnValue('cls-id') }; - const loggerMock = automock(LoggingRepository, { args: [clsMock, configMock], strict: false }); - loggerMock.setContext.mockReturnValue(void 0); - loggerMock.error.mockImplementation((...args: any[]) => { - console.log('Logger.error was called with', ...args); - }); - - const mockBaseService = (service: ClassConstructor) => { - return automock(service, { args: [loggerMock], strict: false }); - }; - - const clsServiceMock = clsMock; - - const FAKE_MOCK = vitest.fn(); - - const providers: Provider[] = [ - ...middleware, - ...services.map((Service) => { - if ((authType === 'real' && Service === AuthService) || Service === ApiService) { - return Service; - } - return { provide: Service, useValue: mockBaseService(Service as ClassConstructor) }; - }), - GlobalExceptionFilter, - { provide: LoggingRepository, useValue: loggerMock }, - { provide: ClsService, useValue: clsServiceMock }, - ]; - - const moduleRef = await Test.createTestingModule({ - imports: [], - controllers: [...controllers], - providers, - }) - .useMocker((token) => { - if (token === LoggingRepository) { - return; - } - - if (token === SchedulerRegistry) { - return FAKE_MOCK; - } - - if (typeof token === 'function' && token.name.endsWith('Repository')) { - return FAKE_MOCK; - } - - if (typeof token === 'string' && token === 'KyselyModuleConnectionToken') { - return FAKE_MOCK; - } - }) - - .compile(); - - const app = moduleRef.createNestApplication(); - - await app.init(); - - const getMockedRepository = (token: ClassConstructor) => { - return app.get(token) as Mocked; - }; - - return { - getHttpServer: () => app.getHttpServer(), - getMockedService: (token: ClassConstructor) => { - if (authType === 'real' && token === AuthService) { - throw new Error('Auth type is real, cannot get mocked service'); - } - return app.get(token) as Mocked; - }, - getMockedRepository, - close: () => app.close(), - }; -}; - -export type TestControllerApp = { - getHttpServer: () => any; - getMockedService: (token: ClassConstructor) => Mocked; - getMockedRepository: (token: ClassConstructor) => Mocked; - close: () => Promise; -}; diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index d2742f7f80..81ada65b68 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -315,4 +315,11 @@ export const factory = { }, uuid: newUuid, date: newDate, + responses: { + badRequest: (message: any = null) => ({ + error: 'Bad Request', + statusCode: 400, + message: message ?? expect.anything(), + }), + }, }; diff --git a/server/test/utils.ts b/server/test/utils.ts index 2c444f491e..1f80c9bbe2 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -1,3 +1,6 @@ +import { CallHandler, Provider, ValidationPipe } from '@nestjs/common'; +import { APP_GUARD, APP_PIPE } from '@nestjs/core'; +import { Test } from '@nestjs/testing'; import { ClassConstructor } from 'class-transformer'; import { Kysely } from 'kysely'; import { ChildProcessWithoutNullStreams } from 'node:child_process'; @@ -5,6 +8,9 @@ import { Writable } from 'node:stream'; import { PNG } from 'pngjs'; import postgres from 'postgres'; import { DB } from 'src/db'; +import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; +import { AuthGuard } from 'src/middleware/auth.guard'; +import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumUserRepository } from 'src/repositories/album-user.repository'; @@ -48,6 +54,7 @@ import { TrashRepository } from 'src/repositories/trash.repository'; import { UserRepository } from 'src/repositories/user.repository'; import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { ViewRepository } from 'src/repositories/view-repository'; +import { AuthService } from 'src/services/auth.service'; import { BaseService } from 'src/services/base.service'; import { RepositoryInterface } from 'src/types'; import { asPostgresConnectionConfig, getKyselyConfig } from 'src/utils/database'; @@ -64,7 +71,47 @@ import { newStorageRepositoryMock } from 'test/repositories/storage.repository.m import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; import { ITelemetryRepositoryMock, newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock'; import { Readable } from 'typeorm/platform/PlatformTools'; -import { assert, Mocked, vitest } from 'vitest'; +import { assert, Mock, Mocked, vitest } from 'vitest'; + +export type ControllerContext = { + authenticate: Mock; + getHttpServer: () => any; + reset: () => void; + close: () => Promise; +}; + +export const controllerSetup = async (controller: ClassConstructor, providers: Provider[]) => { + const noopInterceptor = { intercept: (ctx: never, next: CallHandler) => next.handle() }; + const authenticate = vi.fn(); + const moduleRef = await Test.createTestingModule({ + controllers: [controller], + providers: [ + { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, + { provide: APP_GUARD, useClass: AuthGuard }, + { provide: LoggingRepository, useValue: LoggingRepository.create() }, + { provide: AuthService, useValue: { authenticate } }, + ...providers, + ], + }) + .overrideInterceptor(FileUploadInterceptor) + .useValue(noopInterceptor) + .overrideInterceptor(AssetUploadInterceptor) + .useValue(noopInterceptor) + .compile(); + const app = moduleRef.createNestApplication(); + await app.init(); + + return { + authenticate, + getHttpServer: () => app.getHttpServer(), + reset: () => { + authenticate.mockReset(); + }, + close: async () => { + await app.close(); + }, + }; +}; const mockFn = (label: string, { strict }: { strict: boolean }) => { const message = `Called a mock function without a mock implementation (${label})`; @@ -77,6 +124,10 @@ const mockFn = (label: string, { strict }: { strict: boolean }) => { }); }; +export const mockBaseService = (service: ClassConstructor) => { + return automock(service, { args: [{ setContext: () => {} }], strict: false }); +}; + export const automock = ( Dependency: ClassConstructor, options?: { From 8801ae58212dff91f97071b371d8ac45304e3d68 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 4 May 2025 07:30:21 -0500 Subject: [PATCH 122/356] fix(web): text dim in darkmode (#18072) --- .../shared-components/side-bar/server-status.svelte | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/src/lib/components/shared-components/side-bar/server-status.svelte b/web/src/lib/components/shared-components/side-bar/server-status.svelte index 500bce524e..49006dfe5a 100644 --- a/web/src/lib/components/shared-components/side-bar/server-status.svelte +++ b/web/src/lib/components/shared-components/side-bar/server-status.svelte @@ -1,18 +1,18 @@ + + + +{#if albumMapViewManager.isInMapView} +
    + +
    +
    + {#await import('../shared-components/map/map.svelte')} + {#await delay(timeToLoadTheMap) then} + +
    + +
    + {/await} + {:then { default: Map }} + + {/await} +
    +
    +
    +
    + + + {#if $showAssetViewer} + {#await import('../../../lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }} + 1} + onNext={navigateNext} + onPrevious={navigatePrevious} + onRandom={navigateRandom} + onClose={() => { + assetViewingStore.showAssetViewer(false); + handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); + }} + isShared={false} + /> + {/await} + {/if} + +{/if} diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index 09ec67e92b..1f15e22d9e 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -20,6 +20,7 @@ import { t } from 'svelte-i18n'; import { onDestroy } from 'svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; + import AlbumMap from '$lib/components/album-page/album-map.svelte'; interface Props { sharedLink: SharedLinkResponseDto; @@ -91,7 +92,9 @@ icon={mdiFolderDownloadOutline} /> {/if} - + {#if sharedLink.showMetadata} + + {/if} {/snippet} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 7d496b7e3f..b454d89217 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -27,6 +27,7 @@ import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { mobileDevice } from '$lib/stores/mobile-device.svelte'; import { focusNext } from '$lib/utils/focus-util'; + import { albumMapViewManager } from '$lib/managers/album-view-map.manager.svelte'; interface Props { isSelectionMode?: boolean; @@ -382,7 +383,6 @@ const handleNext = async () => { const nextAsset = await assetStore.getNextAsset($viewingAsset); - if (nextAsset) { const preloadAsset = await assetStore.getNextAsset(nextAsset); assetViewingStore.setAsset(nextAsset, preloadAsset ? [preloadAsset] : []); @@ -802,26 +802,28 @@ - - {#if $showAssetViewer} - {#await import('../asset-viewer/asset-viewer.svelte') then { default: AssetViewer }} - - {/await} - {/if} - +{#if !albumMapViewManager.isInMapView} + + {#if $showAssetViewer} + {#await import('../asset-viewer/asset-viewer.svelte') then { default: AssetViewer }} + + {/await} + {/if} + +{/if} diff --git a/web/src/lib/components/shared-components/search-bar/search-bar.svelte b/web/src/lib/components/shared-components/search-bar/search-bar.svelte index 29a63fe049..1670f823cc 100644 --- a/web/src/lib/components/shared-components/search-bar/search-bar.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-bar.svelte @@ -282,7 +282,7 @@ class:end-28={isFocus && value.length > 0} >

    {getSearchTypeText()}

    diff --git a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte index aafb430046..d247c9448d 100644 --- a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte +++ b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte @@ -119,7 +119,7 @@ {#if showMessage}
    - + diff --git a/web/src/routes/admin/users/[id]/+page.svelte b/web/src/routes/admin/users/[id]/+page.svelte index 03f5e64963..6b0b0234fb 100644 --- a/web/src/routes/admin/users/[id]/+page.svelte +++ b/web/src/routes/admin/users/[id]/+page.svelte @@ -1,6 +1,6 @@ - + {#snippet buttons()} {#if canResetPassword} @@ -365,4 +365,4 @@ - + diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 2e13e5997d..bd6a834427 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -40,7 +40,7 @@ export default { }, borderColor: ({ theme }) => ({ ...theme('colors'), - DEFAULT: 'rgb(var(--immich-ui-default-border) / )', + DEFAULT: 'rgb(var(--immich-ui-gray) / )', }), fontFamily: { 'immich-mono': ['Overpass Mono', 'monospace'], From 3944f5d73bb94740ceb00a315c3aa57c9ab977cd Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Wed, 14 May 2025 18:02:25 +0200 Subject: [PATCH 214/356] fix: mobile sidebar (#18286) --- .../shared-components/side-bar/side-bar-section.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte b/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte index bf87ba1465..cf8e569e7e 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte @@ -35,7 +35,7 @@ id="sidebar" aria-label={ariaLabel} tabindex="-1" - class="immich-scrollbar relative z-[1] w-0 sidebar:w-[16rem] overflow-y-auto overflow-x-hidden pt-8 transition-all duration-200" + class="immich-scrollbar relative z-[1] w-0 sidebar:w-[16rem] overflow-y-auto overflow-x-hidden pt-8 transition-all duration-200 bg-light" class:shadow-2xl={isExpanded} class:dark:border-e-immich-dark-gray={isExpanded} class:border-r={isExpanded} From fac1beb7d830b59a9e7bcf6dc604060d45730185 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 14 May 2025 12:09:10 -0400 Subject: [PATCH 215/356] refactor: buy immich (#18289) * refactor: buy container * refactor: buy immich --- .../purchasing/purchase-content.svelte | 87 +++++++++---------- web/src/routes/(user)/buy/+page.svelte | 24 ++--- 2 files changed, 52 insertions(+), 59 deletions(-) diff --git a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte index b46bdcb5e3..637ed18869 100644 --- a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte +++ b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte @@ -4,6 +4,7 @@ import { purchaseStore } from '$lib/stores/purchase.store'; import { handleError } from '$lib/utils/handle-error'; import { activateProduct, getActivationKey } from '$lib/utils/license-utils'; + import { Heading } from '@immich/ui'; import { t } from 'svelte-i18n'; import UserPurchaseOptionCard from './individual-purchase-option-card.svelte'; import ServerPurchaseOptionCard from './server-purchase-option-card.svelte'; @@ -36,52 +37,50 @@ }; -
    -
    - {#if showTitle} -

    - {$t('purchase_option_title')} -

    - {/if} +
    + {#if showTitle} + + {$t('purchase_option_title')} + + {/if} - {#if showMessage} -
    -

    - {$t('purchase_panel_info_1')} -

    -
    -

    - {$t('purchase_panel_info_2')} -

    -
    -
    - {/if} - -
    - - + {#if showMessage} +
    +

    + {$t('purchase_panel_info_1')} +

    +
    +

    + {$t('purchase_panel_info_2')} +

    +
    + {/if} -
    -

    {$t('purchase_input_suggestion')}

    -
    - - -
    -
    +
    + + +
    + +
    +

    {$t('purchase_input_suggestion')}

    +
    + + +
    diff --git a/web/src/routes/(user)/buy/+page.svelte b/web/src/routes/(user)/buy/+page.svelte index eb0194c447..e8bfd5451b 100644 --- a/web/src/routes/(user)/buy/+page.svelte +++ b/web/src/routes/(user)/buy/+page.svelte @@ -3,13 +3,13 @@ import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import LicenseActivationSuccess from '$lib/components/shared-components/purchasing/purchase-activation-success.svelte'; import LicenseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte'; + import SupporterBadge from '$lib/components/shared-components/side-bar/supporter-badge.svelte'; import { AppRoute } from '$lib/constants'; + import { purchaseStore } from '$lib/stores/purchase.store'; + import { Alert, Container, Stack } from '@immich/ui'; + import { mdiAlertCircleOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; - import Icon from '$lib/components/elements/icon.svelte'; - import { mdiAlertCircleOutline } from '@mdi/js'; - import { purchaseStore } from '$lib/stores/purchase.store'; - import SupporterBadge from '$lib/components/shared-components/side-bar/supporter-badge.svelte'; interface Props { data: PageData; @@ -21,16 +21,10 @@ -
    -
    + + {#if data.isActivated === false} - + {/if} {#if $isPurchased} @@ -46,6 +40,6 @@ }} /> {/if} -
    -
    + +
    From 77b0505006e1b6c6b3abe43e050af58dc187deeb Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 14 May 2025 12:30:47 -0400 Subject: [PATCH 216/356] refactor: layout components (#18290) --- .../components/layouts/AdminPageLayout.svelte | 37 +++++++------------ .../lib/components/layouts/PageContent.svelte | 24 +++--------- .../lib/components/layouts/TitleLayout.svelte | 27 ++++++++++++++ web/src/lib/sidebars/AdminSidebar.svelte | 21 +++++++++++ 4 files changed, 67 insertions(+), 42 deletions(-) create mode 100644 web/src/lib/components/layouts/TitleLayout.svelte create mode 100644 web/src/lib/sidebars/AdminSidebar.svelte diff --git a/web/src/lib/components/layouts/AdminPageLayout.svelte b/web/src/lib/components/layouts/AdminPageLayout.svelte index 4693035a43..5a580dbde8 100644 --- a/web/src/lib/components/layouts/AdminPageLayout.svelte +++ b/web/src/lib/components/layouts/AdminPageLayout.svelte @@ -1,22 +1,19 @@ @@ -24,20 +21,14 @@ -
    -
    - - - - - -
    - -
    - -
    -
    +
    - + + + + {@render children?.()} + + +
    diff --git a/web/src/lib/components/layouts/PageContent.svelte b/web/src/lib/components/layouts/PageContent.svelte index bfd291b074..150aaecf43 100644 --- a/web/src/lib/components/layouts/PageContent.svelte +++ b/web/src/lib/components/layouts/PageContent.svelte @@ -1,26 +1,12 @@ -
    -
    -
    {title}
    - {@render buttons?.()} -
    - - - {@render children?.()} - - -
    + diff --git a/web/src/lib/components/layouts/TitleLayout.svelte b/web/src/lib/components/layouts/TitleLayout.svelte new file mode 100644 index 0000000000..1beab45586 --- /dev/null +++ b/web/src/lib/components/layouts/TitleLayout.svelte @@ -0,0 +1,27 @@ + + +
    +
    +
    +
    {title}
    + {#if description} + {description} + {/if} +
    + {@render buttons?.()} +
    + {@render children?.()} +
    diff --git a/web/src/lib/sidebars/AdminSidebar.svelte b/web/src/lib/sidebars/AdminSidebar.svelte new file mode 100644 index 0000000000..2fecaebf49 --- /dev/null +++ b/web/src/lib/sidebars/AdminSidebar.svelte @@ -0,0 +1,21 @@ + + +
    +
    + + + + + +
    + +
    + +
    +
    From 7d95bad5cb2187dea4b432661791732e1ad9cb90 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 14 May 2025 12:30:55 -0400 Subject: [PATCH 217/356] refactor: user settings container (#18291) --- web/src/routes/(user)/user-settings/+page.svelte | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/web/src/routes/(user)/user-settings/+page.svelte b/web/src/routes/(user)/user-settings/+page.svelte index 028941cdd6..c434bc7de6 100644 --- a/web/src/routes/(user)/user-settings/+page.svelte +++ b/web/src/routes/(user)/user-settings/+page.svelte @@ -4,6 +4,7 @@ import UserSettingsList from '$lib/components/user-settings-page/user-settings-list.svelte'; import { modalManager } from '$lib/managers/modal-manager.svelte'; import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte'; + import { Container } from '@immich/ui'; import { mdiKeyboard } from '@mdi/js'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; @@ -23,9 +24,7 @@ onclick={() => modalManager.show(ShortcutsModal, {})} /> {/snippet} -
    -
    - -
    -
    + + + From f357f3324f967c952bb1b37af2e32d4076f5067d Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 14 May 2025 14:12:57 -0400 Subject: [PATCH 218/356] refactor: default border color (#18292) --- web/src/app.css | 4 ++++ web/src/lib/components/layouts/user-page-layout.svelte | 4 +--- .../navigation-bar/navigation-bar.svelte | 2 +- .../shared-components/settings/setting-accordion.svelte | 8 ++++---- web/tailwind.config.js | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/web/src/app.css b/web/src/app.css index 329d9ce82d..211d34bb6c 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -32,6 +32,8 @@ --immich-ui-warning: 216 143 64; --immich-ui-info: 8 111 230; --immich-ui-gray: 246 246 246; + + --immich-ui-default-border: 209 213 219; } .dark { @@ -44,6 +46,8 @@ --immich-ui-warning: 254 197 132; --immich-ui-info: 121 183 254; --immich-ui-gray: 33 33 33; + + --immich-ui-default-border: 55 65 81; } } diff --git a/web/src/lib/components/layouts/user-page-layout.svelte b/web/src/lib/components/layouts/user-page-layout.svelte index 8ecddaab78..d5e3811ca5 100644 --- a/web/src/lib/components/layouts/user-page-layout.svelte +++ b/web/src/lib/components/layouts/user-page-layout.svelte @@ -65,9 +65,7 @@
    {#if title || buttons} -
    +
    {#if title}
    {title}
    diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index b0b3c1f31e..3b6caf8668 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -57,7 +57,7 @@ >
    diff --git a/web/src/lib/components/shared-components/settings/setting-accordion.svelte b/web/src/lib/components/shared-components/settings/setting-accordion.svelte index 5ae41c0551..f48d14ea30 100755 --- a/web/src/lib/components/shared-components/settings/setting-accordion.svelte +++ b/web/src/lib/components/shared-components/settings/setting-accordion.svelte @@ -1,8 +1,8 @@ - + - + diff --git a/web/src/lib/components/shared-components/side-bar/side-bar-section.spec.ts b/web/src/lib/components/sidebar/sidebar.spec.ts similarity index 93% rename from web/src/lib/components/shared-components/side-bar/side-bar-section.spec.ts rename to web/src/lib/components/sidebar/sidebar.spec.ts index 16c985ce35..cf9ecabada 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar-section.spec.ts +++ b/web/src/lib/components/sidebar/sidebar.spec.ts @@ -1,4 +1,4 @@ -import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte'; +import SideBarSection from '$lib/components/sidebar/sidebar.svelte'; import { sidebarStore } from '$lib/stores/sidebar.svelte'; import { render, screen } from '@testing-library/svelte'; import { vi } from 'vitest'; @@ -22,7 +22,7 @@ vi.mock('$lib/stores/sidebar.svelte', () => ({ }, })); -describe('SideBarSection component', () => { +describe('Sidebar component', () => { beforeEach(() => { vi.resetAllMocks(); mocks.mobileDevice.isFullSidebar = false; diff --git a/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte b/web/src/lib/components/sidebar/sidebar.svelte similarity index 100% rename from web/src/lib/components/shared-components/side-bar/side-bar-section.svelte rename to web/src/lib/components/sidebar/sidebar.svelte diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index 0f0f194a57..2daf63b9e3 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -17,10 +17,10 @@ import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; - import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte'; import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte'; import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte'; import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; + import Sidebar from '$lib/components/sidebar/sidebar.svelte'; import { AppRoute, QueryParameter } from '$lib/constants'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import type { Viewport } from '$lib/stores/assets-store.svelte'; @@ -130,7 +130,7 @@ {#snippet sidebar()} - +
    {$t('explorer').toUpperCase()}
    @@ -143,7 +143,7 @@ />
    - + {/snippet} diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte index 52667abc94..3825268950 100644 --- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -10,10 +10,10 @@ NotificationType, } from '$lib/components/shared-components/notification/notification'; import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte'; - import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte'; import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte'; import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte'; import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; + import Sidebar from '$lib/components/sidebar/sidebar.svelte'; import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants'; import { modalManager } from '$lib/managers/modal-manager.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; @@ -142,7 +142,7 @@ {#snippet sidebar()} - +
    {$t('explorer').toUpperCase()}
    @@ -156,7 +156,7 @@ />
    -
    + {/snippet} {#snippet buttons()} From cd03d0c0f24cfe3951fbee8513e1795a7cb673ec Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Thu, 15 May 2025 02:30:24 +0200 Subject: [PATCH 220/356] refactor: person merge suggestion modal (#18287) --- .../faces-page/merge-suggestion-modal.svelte | 126 --------------- web/src/lib/constants.ts | 1 - .../modals/PersonMergeSuggestionModal.svelte | 147 ++++++++++++++++++ web/src/routes/(user)/people/+page.svelte | 105 +++++-------- .../[[assetId=id]]/+page.svelte | 73 ++++----- 5 files changed, 214 insertions(+), 238 deletions(-) delete mode 100644 web/src/lib/components/faces-page/merge-suggestion-modal.svelte create mode 100644 web/src/lib/modals/PersonMergeSuggestionModal.svelte diff --git a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte deleted file mode 100644 index 3aedfd3450..0000000000 --- a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte +++ /dev/null @@ -1,126 +0,0 @@ - - - -
    - {#if !choosePersonToMerge} -
    - -
    -
    - ([personMerge1, personMerge2] = [personMerge2, personMerge1])} - /> -
    - - - {:else} -
    -
    - -
    -
    -
    - {#each potentialMergePeople as person (person.id)} -
    - -
    - {/each} -
    -
    -
    - {/if} -
    - -
    -

    {$t('are_these_the_same_person')}

    -
    -
    -

    {$t('they_will_be_merged_together')}

    -
    - - {#snippet stickyBottom()} - - - {/snippet} -
    diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index bec3b0ceaa..e4603217e0 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -383,7 +383,6 @@ export enum PersonPageViewMode { VIEW_ASSETS = 'view-assets', SELECT_PERSON = 'select-person', MERGE_PEOPLE = 'merge-people', - SUGGEST_MERGE = 'suggest-merge', UNASSIGN_ASSETS = 'unassign-faces', } diff --git a/web/src/lib/modals/PersonMergeSuggestionModal.svelte b/web/src/lib/modals/PersonMergeSuggestionModal.svelte new file mode 100644 index 0000000000..e762b30c03 --- /dev/null +++ b/web/src/lib/modals/PersonMergeSuggestionModal.svelte @@ -0,0 +1,147 @@ + + + + +
    + {#if !choosePersonToMerge} +
    + +
    +
    + ([personToMerge, personToBeMergedInto] = [personToBeMergedInto, personToMerge])} + /> +
    + + + {:else} +
    +
    + +
    +
    +
    + {#each potentialMergePeople as person (person.id)} +
    + +
    + {/each} +
    +
    +
    + {/if} +
    + +
    +

    {$t('are_these_the_same_person')}

    +
    +
    +

    {$t('they_will_be_merged_together')}

    +
    +
    + + +
    + + +
    +
    +
    diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index f53d364611..7ee95a5f7d 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -3,9 +3,9 @@ import { page } from '$app/stores'; import { focusTrap } from '$lib/actions/focus-trap'; import { scrollMemory } from '$lib/actions/scroll-memory'; + import { shortcut } from '$lib/actions/shortcut'; import Icon from '$lib/components/elements/icon.svelte'; import ManagePeopleVisibility from '$lib/components/faces-page/manage-people-visibility.svelte'; - import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte'; import PeopleCard from '$lib/components/faces-page/people-card.svelte'; import PeopleInfiniteScroll from '$lib/components/faces-page/people-infinite-scroll.svelte'; import SearchPeople from '$lib/components/faces-page/people-search.svelte'; @@ -17,19 +17,13 @@ import { ActionQueryParameterValue, AppRoute, QueryParameter, SessionStorageKey } from '$lib/constants'; import { modalManager } from '$lib/managers/modal-manager.svelte'; import PersonEditBirthDateModal from '$lib/modals/PersonEditBirthDateModal.svelte'; + import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte'; import { locale } from '$lib/stores/preferences.store'; import { websocketEvents } from '$lib/stores/websocket'; import { handlePromiseError } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; import { clearQueryParam } from '$lib/utils/navigation'; - import { - getAllPeople, - getPerson, - mergePerson, - searchPerson, - updatePerson, - type PersonResponseDto, - } from '@immich/sdk'; + import { getAllPeople, getPerson, searchPerson, updatePerson, type PersonResponseDto } from '@immich/sdk'; import { Button } from '@immich/ui'; import { mdiAccountOff, mdiEyeOutline } from '@mdi/js'; import { onMount } from 'svelte'; @@ -46,7 +40,6 @@ let selectHidden = $state(false); let searchName = $state(''); - let showMergeModal = $state(false); let newName = $state(''); let currentPage = $state(1); let nextPage = $state(data.people.hasNextPage ? 2 : null); @@ -131,42 +124,41 @@ } }; - const handleMergeSamePerson = async (response: [PersonResponseDto, PersonResponseDto]) => { - const [personToMerge, personToBeMergedIn] = response; - showMergeModal = false; - - if (!editingPerson) { + const handleMerge = async () => { + if (!editingPerson || !personMerge1 || !personMerge2) { return; } - try { - await mergePerson({ - id: personToBeMergedIn.id, - mergePersonDto: { ids: [personToMerge.id] }, - }); - const mergedPerson = await getPerson({ id: personToBeMergedIn.id }); + const response = await modalManager.show(PersonMergeSuggestionModal, { + personToMerge: personMerge1, + personToBeMergedInto: personMerge2, + potentialMergePeople, + }); - people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id); - people = people.map((person: PersonResponseDto) => (person.id === personToBeMergedIn.id ? mergedPerson : person)); - notificationController.show({ - message: $t('merge_people_successfully'), - type: NotificationType.Info, - }); - } catch (error) { - handleError(error, $t('errors.unable_to_save_name')); + if (!response) { + await updateName(personMerge1.id, newName); + return; } - if (personToBeMergedIn.name !== newName && editingPerson.id === personToBeMergedIn.id) { + + const [personToMerge, personToBeMergedInto] = response; + + const mergedPerson = await getPerson({ id: personToBeMergedInto.id }); + + people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id); + people = people.map((person: PersonResponseDto) => (person.id === personToBeMergedInto.id ? mergedPerson : person)); + + if (personToBeMergedInto.name !== newName && editingPerson.id === personToBeMergedInto.id) { /* * - * If the user merges one of the suggested people into the person he's editing it, it's merging the suggested person AND renames + * If the user merges one of the suggested people into the person he's editing, it's merging the suggested person AND renames * the person he's editing * */ try { - await updatePerson({ id: personToBeMergedIn.id, personUpdateDto: { name: newName } }); + await updatePerson({ id: personToBeMergedInto.id, personUpdateDto: { name: newName } }); for (const person of people) { - if (person.id === personToBeMergedIn.id) { + if (person.id === personToBeMergedInto.id) { person.name = newName; break; } @@ -263,7 +255,7 @@ const onNameChangeSubmit = async (name: string, targetPerson: PersonResponseDto) => { try { - if (name == targetPerson.name || showMergeModal) { + if (name == targetPerson.name) { return; } @@ -285,7 +277,7 @@ !person.isHidden, ) .slice(0, 3); - showMergeModal = true; + await handleMerge(); return; } await updateName(targetPerson.id, name); @@ -315,32 +307,10 @@ (person) => person.name.toLowerCase() === name.toLowerCase() && person.id !== personId && person.name, ); }; - - const handleMergeCancel = async () => { - if (!personMerge1) { - return; - } - - await updateName(personMerge1.id, newName); - showMergeModal = false; - }; -{#if showMergeModal && personMerge1 && personMerge2} - { - showMergeModal = false; - }} - onReject={() => handleMergeCancel()} - onConfirm={handleMergeSamePerson} - /> -{/if} - handleToggleFavorite(person)} /> -
    onNameChangeSubmit(newName, person)}> - onNameChangeInputFocus(person)} - onfocusout={() => onNameChangeSubmit(newName, person)} - oninput={(event) => onNameChangeInputUpdate(event)} - /> -
    + e.currentTarget.blur() }} + onfocusin={() => onNameChangeInputFocus(person)} + onfocusout={() => onNameChangeSubmit(newName, person)} + oninput={(event) => onNameChangeInputUpdate(event)} + /> {/snippet} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 70500ca755..1c63cf8d05 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -7,7 +7,6 @@ import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte'; import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte'; - import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte'; import UnMergeFaceSelector from '$lib/components/faces-page/unmerge-face-selector.svelte'; import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; @@ -32,6 +31,7 @@ import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants'; import { modalManager } from '$lib/managers/modal-manager.svelte'; import PersonEditBirthDateModal from '$lib/modals/PersonEditBirthDateModal.svelte'; + import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetStore } from '$lib/stores/assets-store.svelte'; @@ -44,7 +44,6 @@ import { AssetVisibility, getPersonStatistics, - mergePerson, searchPerson, updatePerson, type AssetResponseDto, @@ -122,7 +121,7 @@ }); const handleEscape = async () => { - if ($showAssetViewer || viewMode === PersonPageViewMode.SUGGEST_MERGE) { + if ($showAssetViewer) { return; } if (assetInteraction.selectionActive) { @@ -220,31 +219,32 @@ viewMode = PersonPageViewMode.VIEW_ASSETS; }; - const handleMergeSamePerson = async (response: [PersonResponseDto, PersonResponseDto]) => { - const [personToMerge, personToBeMergedIn] = response; - viewMode = PersonPageViewMode.VIEW_ASSETS; - isEditingName = false; - try { - await mergePerson({ - id: personToBeMergedIn.id, - mergePersonDto: { ids: [personToMerge.id] }, - }); - notificationController.show({ - message: $t('merge_people_successfully'), - type: NotificationType.Info, - }); - people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id); - if (personToBeMergedIn.name != personName && person.id === personToBeMergedIn.id) { - await updateAssetCount(); - return; - } - await goto(`${AppRoute.PEOPLE}/${personToBeMergedIn.id}`, { replaceState: true }); - } catch (error) { - handleError(error, $t('errors.unable_to_save_name')); + const handleMergeSuggestion = async () => { + if (!personMerge1 || !personMerge2) { + return; } + + const result = await modalManager.show(PersonMergeSuggestionModal, { + personToMerge: personMerge1, + personToBeMergedInto: personMerge2, + potentialMergePeople, + }); + + if (!result) { + return; + } + + const [personToMerge, personToBeMergedInto] = result; + + people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id); + if (personToBeMergedInto.name != personName && person.id === personToBeMergedInto.id) { + await updateAssetCount(); + return; + } + await goto(`${AppRoute.PEOPLE}/${personToBeMergedInto.id}`, { replaceState: true }); }; - const handleSuggestPeople = (person2: PersonResponseDto) => { + const handleSuggestPeople = async (person2: PersonResponseDto) => { isEditingName = false; if (person.id !== person2.id) { potentialMergePeople = []; @@ -252,7 +252,8 @@ personMerge1 = person; personMerge2 = person2; isSuggestionSelectedByUser = true; - viewMode = PersonPageViewMode.SUGGEST_MERGE; + + await handleMergeSuggestion(); } }; @@ -280,9 +281,6 @@ }; const handleCancelEditName = () => { - if (viewMode === PersonPageViewMode.SUGGEST_MERGE) { - return; - } isSearchingPeople = false; isEditingName = false; }; @@ -317,7 +315,7 @@ !person.isHidden, ) .slice(0, 3); - viewMode = PersonPageViewMode.SUGGEST_MERGE; + await handleMergeSuggestion(); return; } await changeName(); @@ -382,7 +380,7 @@ onSelect={handleSelectFeaturePhoto} onEscape={handleEscape} > - {#if viewMode === PersonPageViewMode.VIEW_ASSETS || viewMode === PersonPageViewMode.SUGGEST_MERGE} + {#if viewMode === PersonPageViewMode.VIEW_ASSETS}
    {/if} -{#if viewMode === PersonPageViewMode.SUGGEST_MERGE && personMerge1 && personMerge2} - (viewMode = PersonPageViewMode.VIEW_ASSETS)} - onReject={changeName} - onConfirm={handleMergeSamePerson} - /> -{/if} - {#if viewMode === PersonPageViewMode.MERGE_PEOPLE} {/if} @@ -553,7 +540,7 @@ {:else} - {#if viewMode === PersonPageViewMode.VIEW_ASSETS || viewMode === PersonPageViewMode.SUGGEST_MERGE} + {#if viewMode === PersonPageViewMode.VIEW_ASSETS} goto(previousRoute)}> {#snippet trailing()} From 3a0ddfb92daee9a55d569c59abc0427a16d626bf Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Wed, 14 May 2025 23:13:13 -0400 Subject: [PATCH 221/356] fix(server): vacuum after deleting people (#18299) * vacuum after deleting people * update sql --- server/src/queries/person.repository.sql | 12 ------------ server/src/repositories/person.repository.ts | 6 +----- server/src/services/person.service.spec.ts | 16 ++++++++++++++-- server/src/services/person.service.ts | 2 ++ .../test/repositories/person.repository.mock.ts | 1 + 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index c77d9835fa..2ab0045e32 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -13,12 +13,6 @@ set "personId" = $1 where "asset_faces"."sourceType" = $2 -VACUUM -ANALYZE asset_faces, -face_search, -person -REINDEX TABLE asset_faces -REINDEX TABLE person -- PersonRepository.delete delete from "person" @@ -29,12 +23,6 @@ where delete from "asset_faces" where "asset_faces"."sourceType" = $1 -VACUUM -ANALYZE asset_faces, -face_search, -person -REINDEX TABLE asset_faces -REINDEX TABLE person -- PersonRepository.getAllWithoutFaces select diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 789c47ccaf..478ff15d53 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -105,8 +105,6 @@ export class PersonRepository { .set({ personId: null }) .where('asset_faces.sourceType', '=', sourceType) .execute(); - - await this.vacuum({ reindexVectors: false }); } @GenerateSql({ params: [DummyValue.UUID] }) @@ -121,8 +119,6 @@ export class PersonRepository { @GenerateSql({ params: [{ sourceType: SourceType.EXIF }] }) async deleteFaces({ sourceType }: DeleteFacesOptions): Promise { await this.db.deleteFrom('asset_faces').where('asset_faces.sourceType', '=', sourceType).execute(); - - await this.vacuum({ reindexVectors: sourceType === SourceType.MACHINE_LEARNING }); } getAllFaces(options: GetAllFacesOptions = {}) { @@ -519,7 +515,7 @@ export class PersonRepository { await this.db.updateTable('asset_faces').set({ deletedAt: new Date() }).where('asset_faces.id', '=', id).execute(); } - private async vacuum({ reindexVectors }: { reindexVectors: boolean }): Promise { + async vacuum({ reindexVectors }: { reindexVectors: boolean }): Promise { await sql`VACUUM ANALYZE asset_faces, face_search, person`.execute(this.db); await sql`REINDEX TABLE asset_faces`.execute(this.db); await sql`REINDEX TABLE person`.execute(this.db); diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 52e5ff03ee..d9df2225f4 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -459,6 +459,7 @@ describe(PersonService.name, () => { await sut.handleQueueDetectFaces({ force: false }); expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(false); + expect(mocks.person.vacuum).not.toHaveBeenCalled(); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FACE_DETECTION, @@ -475,6 +476,7 @@ describe(PersonService.name, () => { expect(mocks.person.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING }); expect(mocks.person.delete).toHaveBeenCalledWith([personStub.withName.id]); + expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: true }); expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.withName.thumbnailPath); expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(true); expect(mocks.job.queueAll).toHaveBeenCalledWith([ @@ -492,6 +494,7 @@ describe(PersonService.name, () => { expect(mocks.person.delete).not.toHaveBeenCalled(); expect(mocks.person.deleteFaces).not.toHaveBeenCalled(); + expect(mocks.person.vacuum).not.toHaveBeenCalled(); expect(mocks.storage.unlink).not.toHaveBeenCalled(); expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(undefined); expect(mocks.job.queueAll).toHaveBeenCalledWith([ @@ -521,6 +524,7 @@ describe(PersonService.name, () => { ]); expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]); expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath); + expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: true }); }); }); @@ -584,6 +588,7 @@ describe(PersonService.name, () => { expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, { lastRun: expect.any(String), }); + expect(mocks.person.vacuum).not.toHaveBeenCalled(); }); it('should queue all assets', async () => { @@ -611,6 +616,7 @@ describe(PersonService.name, () => { expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, { lastRun: expect.any(String), }); + expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: false }); }); it('should run nightly if new face has been added since last run', async () => { @@ -629,11 +635,14 @@ describe(PersonService.name, () => { mocks.person.getAllWithoutFaces.mockResolvedValue([]); mocks.person.unassignFaces.mockResolvedValue(); - await sut.handleQueueRecognizeFaces({ force: true, nightly: true }); + await sut.handleQueueRecognizeFaces({ force: false, nightly: true }); expect(mocks.systemMetadata.get).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE); expect(mocks.person.getLatestFaceDate).toHaveBeenCalledOnce(); - expect(mocks.person.getAllFaces).toHaveBeenCalledWith(undefined); + expect(mocks.person.getAllFaces).toHaveBeenCalledWith({ + personId: null, + sourceType: SourceType.MACHINE_LEARNING, + }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FACIAL_RECOGNITION, @@ -643,6 +652,7 @@ describe(PersonService.name, () => { expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, { lastRun: expect.any(String), }); + expect(mocks.person.vacuum).not.toHaveBeenCalled(); }); it('should skip nightly if no new face has been added since last run', async () => { @@ -660,6 +670,7 @@ describe(PersonService.name, () => { expect(mocks.person.getAllFaces).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.systemMetadata.set).not.toHaveBeenCalled(); + expect(mocks.person.vacuum).not.toHaveBeenCalled(); }); it('should delete existing people if forced', async () => { @@ -688,6 +699,7 @@ describe(PersonService.name, () => { ]); expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]); expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath); + expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: false }); }); }); diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index e6161b8f9c..23ba562ba6 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -259,6 +259,7 @@ export class PersonService extends BaseService { if (force) { await this.personRepository.deleteFaces({ sourceType: SourceType.MACHINE_LEARNING }); await this.handlePersonCleanup(); + await this.personRepository.vacuum({ reindexVectors: true }); } let jobs: JobItem[] = []; @@ -409,6 +410,7 @@ export class PersonService extends BaseService { if (force) { await this.personRepository.unassignFaces({ sourceType: SourceType.MACHINE_LEARNING }); await this.handlePersonCleanup(); + await this.personRepository.vacuum({ reindexVectors: false }); } else if (waiting) { this.logger.debug( `Skipping facial recognition queueing because ${waiting} job${waiting > 1 ? 's are' : ' is'} already queued`, diff --git a/server/test/repositories/person.repository.mock.ts b/server/test/repositories/person.repository.mock.ts index 59377576b1..2875c9ada5 100644 --- a/server/test/repositories/person.repository.mock.ts +++ b/server/test/repositories/person.repository.mock.ts @@ -33,5 +33,6 @@ export const newPersonRepositoryMock = (): Mocked Date: Wed, 14 May 2025 23:23:34 -0400 Subject: [PATCH 222/356] fix(server): do not filter out assets without preview path for person thumbnail generation (#18300) * allow assets without preview path * update sql * Update person.repository.ts Co-authored-by: Jason Rasmussen * update sql, e2e --------- Co-authored-by: Jason Rasmussen --- e2e/src/api/specs/asset.e2e-spec.ts | 2 -- server/src/queries/person.repository.sql | 14 ++++++++++---- server/src/repositories/person.repository.ts | 14 +++++++++----- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 8c203860df..4673db5426 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -202,7 +202,6 @@ describe('/asset', () => { { name: 'Marie Curie', birthDate: null, - thumbnailPath: '', isHidden: false, faces: [ { @@ -219,7 +218,6 @@ describe('/asset', () => { { name: 'Pierre Curie', birthDate: null, - thumbnailPath: '', isHidden: false, faces: [ { diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 2ab0045e32..659abbde03 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -133,18 +133,24 @@ select "asset_faces"."imageHeight" as "oldHeight", "assets"."type", "assets"."originalPath", - "asset_files"."path" as "previewPath", - "exif"."orientation" as "exifOrientation" + "exif"."orientation" as "exifOrientation", + ( + select + "asset_files"."path" + from + "asset_files" + where + "asset_files"."assetId" = "assets"."id" + and "asset_files"."type" = 'preview' + ) as "previewPath" from "person" inner join "asset_faces" on "asset_faces"."id" = "person"."faceAssetId" inner join "assets" on "asset_faces"."assetId" = "assets"."id" left join "exif" on "exif"."assetId" = "assets"."id" - left join "asset_files" on "asset_files"."assetId" = "assets"."id" where "person"."id" = $1 and "asset_faces"."deletedAt" is null - and "asset_files"."type" = $2 -- PersonRepository.reassignFace update "asset_faces" diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 478ff15d53..0b48e57f7a 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { ExpressionBuilder, Insertable, Kysely, NotNull, Selectable, sql, Updateable } from 'kysely'; +import { ExpressionBuilder, Insertable, Kysely, Selectable, sql, Updateable } from 'kysely'; import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { InjectKysely } from 'nestjs-kysely'; import { AssetFaces, DB, FaceSearch, Person } from 'src/db'; @@ -261,7 +261,6 @@ export class PersonRepository { .innerJoin('asset_faces', 'asset_faces.id', 'person.faceAssetId') .innerJoin('assets', 'asset_faces.assetId', 'assets.id') .leftJoin('exif', 'exif.assetId', 'assets.id') - .leftJoin('asset_files', 'asset_files.assetId', 'assets.id') .select([ 'person.ownerId', 'asset_faces.boundingBoxX1 as x1', @@ -272,13 +271,18 @@ export class PersonRepository { 'asset_faces.imageHeight as oldHeight', 'assets.type', 'assets.originalPath', - 'asset_files.path as previewPath', 'exif.orientation as exifOrientation', ]) + .select((eb) => + eb + .selectFrom('asset_files') + .select('asset_files.path') + .whereRef('asset_files.assetId', '=', 'assets.id') + .where('asset_files.type', '=', sql.lit(AssetFileType.PREVIEW)) + .as('previewPath'), + ) .where('person.id', '=', id) .where('asset_faces.deletedAt', 'is', null) - .where('asset_files.type', '=', AssetFileType.PREVIEW) - .$narrowType<{ exifImageWidth: NotNull; exifImageHeight: NotNull }>() .executeTakeFirst(); } From 709a7b70aa10805c6eeb0fb82bc6ed0e485a7019 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Wed, 14 May 2025 23:34:22 -0400 Subject: [PATCH 223/356] chore: no sql generation for queries with side effects (#18301) no sql generation for queries with side effects --- server/src/queries/asset.repository.sql | 31 +++++++++++++ server/src/queries/audit.repository.sql | 5 --- server/src/queries/memory.repository.sql | 6 --- server/src/queries/move.repository.sql | 13 ------ .../src/queries/notification.repository.sql | 18 -------- server/src/queries/partner.repository.sql | 44 ------------------- server/src/queries/person.repository.sql | 29 +----------- .../queries/system.metadata.repository.sql | 9 ---- server/src/queries/tag.repository.sql | 25 +++++------ .../queries/version.history.repository.sql | 8 ---- server/src/repositories/asset.repository.ts | 8 +--- server/src/repositories/audit.repository.ts | 1 - server/src/repositories/memory.repository.ts | 1 - server/src/repositories/move.repository.ts | 3 +- .../repositories/notification.repository.ts | 1 - server/src/repositories/partner.repository.ts | 1 - server/src/repositories/person.repository.ts | 5 +-- .../system-metadata.repository.ts | 1 - server/src/repositories/tag.repository.ts | 5 +-- .../version-history.repository.ts | 1 - 20 files changed, 48 insertions(+), 167 deletions(-) diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 4a3fbf0e39..4564971ac2 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -432,3 +432,34 @@ where and "assets"."updatedAt" > $3 limit $4 + +-- AssetRepository.detectOfflineExternalAssets +update "assets" +set + "isOffline" = $1, + "deletedAt" = $2 +where + "isOffline" = $3 + and "isExternal" = $4 + and "libraryId" = $5::uuid + and ( + not "originalPath" like $6 + or "originalPath" like $7 + ) + +-- AssetRepository.filterNewExternalAssetPaths +select + "path" +from + unnest(array[$1]::text[]) as "path" +where + not exists ( + select + "originalPath" + from + "assets" + where + "assets"."originalPath" = "path" + and "libraryId" = $2::uuid + and "isExternal" = $3 + ) diff --git a/server/src/queries/audit.repository.sql b/server/src/queries/audit.repository.sql index 3c83d2d3e8..b1a10abf48 100644 --- a/server/src/queries/audit.repository.sql +++ b/server/src/queries/audit.repository.sql @@ -14,8 +14,3 @@ order by "audit"."entityId" desc, "audit"."entityType" desc, "audit"."createdAt" desc - --- AuditRepository.removeBefore -delete from "audit" -where - "createdAt" < $1 diff --git a/server/src/queries/memory.repository.sql b/server/src/queries/memory.repository.sql index d44d017045..e9e7340bf6 100644 --- a/server/src/queries/memory.repository.sql +++ b/server/src/queries/memory.repository.sql @@ -1,11 +1,5 @@ -- NOTE: This file is auto generated by ./sql-generator --- MemoryRepository.cleanup -delete from "memories" -where - "createdAt" < $1 - and "isSaved" = $2 - -- MemoryRepository.search select "memories".*, diff --git a/server/src/queries/move.repository.sql b/server/src/queries/move.repository.sql index a65c7a8b85..50c9ad7dd9 100644 --- a/server/src/queries/move.repository.sql +++ b/server/src/queries/move.repository.sql @@ -16,19 +16,6 @@ where returning * --- MoveRepository.cleanMoveHistory -delete from "move_history" -where - "move_history"."entityId" not in ( - select - "id" - from - "assets" - where - "assets"."id" = "move_history"."entityId" - ) - and "move_history"."pathType" = 'original' - -- MoveRepository.cleanMoveHistorySingle delete from "move_history" where diff --git a/server/src/queries/notification.repository.sql b/server/src/queries/notification.repository.sql index c55e00d226..f7e211d80a 100644 --- a/server/src/queries/notification.repository.sql +++ b/server/src/queries/notification.repository.sql @@ -1,23 +1,5 @@ -- NOTE: This file is auto generated by ./sql-generator --- NotificationRepository.cleanup -delete from "notifications" -where - ( - ( - "deletedAt" is not null - and "deletedAt" < $1 - ) - or ( - "readAt" > $2 - and "createdAt" < $3 - ) - or ( - "readAt" = $4 - and "createdAt" < $5 - ) - ) - -- NotificationRepository.search select "id", diff --git a/server/src/queries/partner.repository.sql b/server/src/queries/partner.repository.sql index e7170f367e..100f1bc638 100644 --- a/server/src/queries/partner.repository.sql +++ b/server/src/queries/partner.repository.sql @@ -100,50 +100,6 @@ where "sharedWithId" = $1 and "sharedById" = $2 --- PartnerRepository.create -insert into - "partners" ("sharedWithId", "sharedById") -values - ($1, $2) -returning - *, - ( - select - to_json(obj) - from - ( - select - "id", - "name", - "email", - "avatarColor", - "profileImagePath", - "profileChangedAt" - from - "users" as "sharedBy" - where - "sharedBy"."id" = "partners"."sharedById" - ) as obj - ) as "sharedBy", - ( - select - to_json(obj) - from - ( - select - "id", - "name", - "email", - "avatarColor", - "profileImagePath", - "profileChangedAt" - from - "users" as "sharedWith" - where - "sharedWith"."id" = "partners"."sharedWithId" - ) as obj - ) as "sharedWith" - -- PartnerRepository.update update "partners" set diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 659abbde03..fefc25ee6a 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -7,22 +7,10 @@ set where "asset_faces"."personId" = $2 --- PersonRepository.unassignFaces -update "asset_faces" -set - "personId" = $1 -where - "asset_faces"."sourceType" = $2 - -- PersonRepository.delete delete from "person" where - "person"."id" in $1 - --- PersonRepository.deleteFaces -delete from "asset_faces" -where - "asset_faces"."sourceType" = $1 + "person"."id" in ($1) -- PersonRepository.getAllWithoutFaces select @@ -216,21 +204,6 @@ where "person"."ownerId" = $3 and "asset_faces"."deletedAt" is null --- PersonRepository.refreshFaces -with - "added_embeddings" as ( - insert into - "face_search" ("faceId", "embedding") - values - ($1, $2) - ) -select -from - ( - select - 1 - ) as "dummy" - -- PersonRepository.getFacesByIds select "asset_faces".*, diff --git a/server/src/queries/system.metadata.repository.sql b/server/src/queries/system.metadata.repository.sql index c4fd7b96f8..8bdf1b3ad7 100644 --- a/server/src/queries/system.metadata.repository.sql +++ b/server/src/queries/system.metadata.repository.sql @@ -8,15 +8,6 @@ from where "key" = $1 --- SystemMetadataRepository.set -insert into - "system_metadata" ("key", "value") -values - ($1, $2) -on conflict ("key") do update -set - "value" = $3 - -- SystemMetadataRepository.delete delete from "system_metadata" where diff --git a/server/src/queries/tag.repository.sql b/server/src/queries/tag.repository.sql index d728d3af88..af757d96b7 100644 --- a/server/src/queries/tag.repository.sql +++ b/server/src/queries/tag.repository.sql @@ -58,7 +58,7 @@ from where "userId" = $1 order by - "value" asc + "value" -- TagRepository.create insert into @@ -94,6 +94,15 @@ where "tagsId" = $1 and "assetsId" in ($2) +-- TagRepository.upsertAssetIds +insert into + "tag_asset" ("assetId", "tagsIds") +values + ($1, $2) +on conflict do nothing +returning + * + -- TagRepository.replaceAssetTags begin delete from "tag_asset" @@ -107,17 +116,3 @@ on conflict do nothing returning * rollback - --- TagRepository.deleteEmptyTags -begin -select - "tags"."id", - count("assets"."id") as "count" -from - "assets" - inner join "tag_asset" on "tag_asset"."assetsId" = "assets"."id" - inner join "tags_closure" on "tags_closure"."id_descendant" = "tag_asset"."tagsId" - inner join "tags" on "tags"."id" = "tags_closure"."id_descendant" -group by - "tags"."id" -commit diff --git a/server/src/queries/version.history.repository.sql b/server/src/queries/version.history.repository.sql index a9805e8c25..2e898cac31 100644 --- a/server/src/queries/version.history.repository.sql +++ b/server/src/queries/version.history.repository.sql @@ -15,11 +15,3 @@ from "version_history" order by "createdAt" desc - --- VersionHistoryRepository.create -insert into - "version_history" ("version") -values - ($1) -returning - * diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 9bd115089f..d49124b04b 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -817,9 +817,7 @@ export class AssetRepository { .execute(); } - @GenerateSql({ - params: [{ libraryId: DummyValue.UUID, importPaths: [DummyValue.STRING], exclusionPatterns: [DummyValue.STRING] }], - }) + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING], [DummyValue.STRING]] }) async detectOfflineExternalAssets( libraryId: string, importPaths: string[], @@ -846,9 +844,7 @@ export class AssetRepository { .executeTakeFirstOrThrow(); } - @GenerateSql({ - params: [{ libraryId: DummyValue.UUID, paths: [DummyValue.STRING] }], - }) + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING]] }) async filterNewExternalAssetPaths(libraryId: string, paths: string[]): Promise { const result = await this.db .selectFrom(unnest(paths).as('path')) diff --git a/server/src/repositories/audit.repository.ts b/server/src/repositories/audit.repository.ts index 48d7f28d12..1193e26ebe 100644 --- a/server/src/repositories/audit.repository.ts +++ b/server/src/repositories/audit.repository.ts @@ -38,7 +38,6 @@ export class AuditRepository { return records.map(({ entityId }) => entityId); } - @GenerateSql({ params: [DummyValue.DATE] }) async removeBefore(before: Date): Promise { await this.db.deleteFrom('audit').where('createdAt', '<', before).execute(); } diff --git a/server/src/repositories/memory.repository.ts b/server/src/repositories/memory.repository.ts index 44c7c30857..1a1ea2827b 100644 --- a/server/src/repositories/memory.repository.ts +++ b/server/src/repositories/memory.repository.ts @@ -12,7 +12,6 @@ import { IBulkAsset } from 'src/types'; export class MemoryRepository implements IBulkAsset { constructor(@InjectKysely() private db: Kysely) {} - @GenerateSql({ params: [DummyValue.UUID] }) cleanup() { return this.db .deleteFrom('memories') diff --git a/server/src/repositories/move.repository.ts b/server/src/repositories/move.repository.ts index 21c52aec65..a21167fffd 100644 --- a/server/src/repositories/move.repository.ts +++ b/server/src/repositories/move.repository.ts @@ -37,7 +37,6 @@ export class MoveRepository { return this.db.deleteFrom('move_history').where('id', '=', id).returningAll().executeTakeFirstOrThrow(); } - @GenerateSql() async cleanMoveHistory(): Promise { await this.db .deleteFrom('move_history') @@ -52,7 +51,7 @@ export class MoveRepository { .execute(); } - @GenerateSql() + @GenerateSql({ params: [DummyValue.UUID] }) async cleanMoveHistorySingle(assetId: string): Promise { await this.db .deleteFrom('move_history') diff --git a/server/src/repositories/notification.repository.ts b/server/src/repositories/notification.repository.ts index 112bb97e60..b35f532094 100644 --- a/server/src/repositories/notification.repository.ts +++ b/server/src/repositories/notification.repository.ts @@ -9,7 +9,6 @@ import { NotificationSearchDto } from 'src/dtos/notification.dto'; export class NotificationRepository { constructor(@InjectKysely() private db: Kysely) {} - @GenerateSql({ params: [DummyValue.UUID] }) cleanup() { return this.db .deleteFrom('notifications') diff --git a/server/src/repositories/partner.repository.ts b/server/src/repositories/partner.repository.ts index ea762d0aaf..31350541ca 100644 --- a/server/src/repositories/partner.repository.ts +++ b/server/src/repositories/partner.repository.ts @@ -47,7 +47,6 @@ export class PartnerRepository { .executeTakeFirst(); } - @GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }] }) create(values: Insertable) { return this.db .insertInto('partners') diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 0b48e57f7a..ad18d7ed67 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -98,7 +98,6 @@ export class PersonRepository { return Number(result.numChangedRows ?? 0); } - @GenerateSql({ params: [{ sourceType: SourceType.EXIF }] }) async unassignFaces({ sourceType }: UnassignFacesOptions): Promise { await this.db .updateTable('asset_faces') @@ -107,7 +106,7 @@ export class PersonRepository { .execute(); } - @GenerateSql({ params: [DummyValue.UUID] }) + @GenerateSql({ params: [[DummyValue.UUID]] }) async delete(ids: string[]): Promise { if (ids.length === 0) { return; @@ -116,7 +115,6 @@ export class PersonRepository { await this.db.deleteFrom('person').where('person.id', 'in', ids).execute(); } - @GenerateSql({ params: [{ sourceType: SourceType.EXIF }] }) async deleteFaces({ sourceType }: DeleteFacesOptions): Promise { await this.db.deleteFrom('asset_faces').where('asset_faces.sourceType', '=', sourceType).execute(); } @@ -400,7 +398,6 @@ export class PersonRepository { return results.map(({ id }) => id); } - @GenerateSql({ params: [[], [], [{ faceId: DummyValue.UUID, embedding: DummyValue.VECTOR }]] }) async refreshFaces( facesToAdd: (Insertable & { assetId: string })[], faceIdsToRemove: string[], diff --git a/server/src/repositories/system-metadata.repository.ts b/server/src/repositories/system-metadata.repository.ts index 2038f204f7..fcccde6a5c 100644 --- a/server/src/repositories/system-metadata.repository.ts +++ b/server/src/repositories/system-metadata.repository.ts @@ -26,7 +26,6 @@ export class SystemMetadataRepository { return metadata.value as SystemMetadata[T]; } - @GenerateSql({ params: ['metadata_key', { foo: 'bar' }] }) async set(key: T, value: SystemMetadata[T]): Promise { await this.db .insertInto('system_metadata') diff --git a/server/src/repositories/tag.repository.ts b/server/src/repositories/tag.repository.ts index 9a3b33188f..a7cdc9554c 100644 --- a/server/src/repositories/tag.repository.ts +++ b/server/src/repositories/tag.repository.ts @@ -68,7 +68,7 @@ export class TagRepository { @GenerateSql({ params: [DummyValue.UUID] }) getAll(userId: string) { - return this.db.selectFrom('tags').select(columns.tag).where('userId', '=', userId).orderBy('value asc').execute(); + return this.db.selectFrom('tags').select(columns.tag).where('userId', '=', userId).orderBy('value').execute(); } @GenerateSql({ params: [{ userId: DummyValue.UUID, color: DummyValue.STRING, value: DummyValue.STRING }] }) @@ -126,7 +126,7 @@ export class TagRepository { await this.db.deleteFrom('tag_asset').where('tagsId', '=', tagId).where('assetsId', 'in', assetIds).execute(); } - @GenerateSql({ params: [{ assetId: DummyValue.UUID, tagsIds: [DummyValue.UUID] }] }) + @GenerateSql({ params: [[{ assetId: DummyValue.UUID, tagsIds: [DummyValue.UUID] }]] }) @Chunked() upsertAssetIds(items: Insertable[]) { if (items.length === 0) { @@ -160,7 +160,6 @@ export class TagRepository { }); } - @GenerateSql() async deleteEmptyTags() { // TODO rewrite as a single statement await this.db.transaction().execute(async (tx) => { diff --git a/server/src/repositories/version-history.repository.ts b/server/src/repositories/version-history.repository.ts index 063ee0da84..b1d2696164 100644 --- a/server/src/repositories/version-history.repository.ts +++ b/server/src/repositories/version-history.repository.ts @@ -18,7 +18,6 @@ export class VersionHistoryRepository { return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').executeTakeFirst(); } - @GenerateSql({ params: [{ version: 'v1.123.0' }] }) create(version: Insertable) { return this.db.insertInto('version_history').values(version).returningAll().executeTakeFirstOrThrow(); } From 4935f3e0bbf699ef83f219a9f32d53b4cbe7822e Mon Sep 17 00:00:00 2001 From: Ruslan Date: Thu, 15 May 2025 18:32:31 +0300 Subject: [PATCH 224/356] fix(docs): Update old jellyfin docs links (#18311) Update old jellyfin docs links Updated old links to jellyfin docs --- docs/docs/features/hardware-transcoding.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/features/hardware-transcoding.md b/docs/docs/features/hardware-transcoding.md index 18c7f6b298..d28cd97de0 100644 --- a/docs/docs/features/hardware-transcoding.md +++ b/docs/docs/features/hardware-transcoding.md @@ -121,6 +121,6 @@ Once this is done, you can continue to step 3 of "Basic Setup". [hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml [nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html -[jellyfin-lp]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#configure-and-verify-lp-mode-on-linux -[jellyfin-kernel-bug]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#known-issues-and-limitations +[jellyfin-lp]: https://jellyfin.org/docs/general/post-install/transcoding/hardware-acceleration/intel#low-power-encoding +[jellyfin-kernel-bug]: https://jellyfin.org/docs/general/post-install/transcoding/hardware-acceleration/intel#known-issues-and-limitations-on-linux [libmali-rockchip]: https://github.com/tsukumijima/libmali-rockchip/releases From b7b0b9b6d8d7ed1ca03ad6fdccb1b1a7d499af44 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 15 May 2025 09:35:21 -0600 Subject: [PATCH 225/356] feat: locked/private view (#18268) * feat: locked/private view * feat: locked/private view * pr feedback * fix: redirect loop * pr feedback --- i18n/en.json | 16 +++ mobile/lib/utils/openapi_patching.dart | 1 + mobile/openapi/README.md | 1 + .../openapi/lib/api/authentication_api.dart | 39 ++++++ .../openapi/lib/model/asset_response_dto.dart | 94 +++++++++++++- .../openapi/lib/model/asset_visibility.dart | 3 + .../lib/model/auth_status_response_dto.dart | 10 +- mobile/openapi/lib/model/sync_asset_v1.dart | 3 + open-api/immich-openapi-specs.json | 57 ++++++++- open-api/typescript-sdk/src/fetch-client.ts | 20 ++- server/src/controllers/auth.controller.ts | 7 ++ .../src/controllers/search.controller.spec.ts | 2 +- server/src/database.ts | 4 +- server/src/db.d.ts | 1 + server/src/dtos/asset-response.dto.ts | 2 + server/src/dtos/auth.dto.ts | 1 + server/src/enum.ts | 1 + server/src/queries/access.repository.sql | 1 + server/src/queries/album.repository.sql | 5 + server/src/queries/session.repository.sql | 1 + server/src/repositories/access.repository.ts | 3 +- server/src/repositories/album.repository.ts | 6 +- .../1746844028242-AddLockedVisibilityEnum.ts | 9 ++ .../1746987967923-AddPinExpiresAtColumn.ts | 9 ++ server/src/schema/tables/session.table.ts | 3 + server/src/services/album.service.spec.ts | 9 +- .../src/services/asset-media.service.spec.ts | 10 +- server/src/services/asset.service.spec.ts | 1 + server/src/services/asset.service.ts | 6 +- server/src/services/auth.service.spec.ts | 12 +- server/src/services/auth.service.ts | 37 ++++++ server/src/services/metadata.service.spec.ts | 6 +- server/src/services/metadata.service.ts | 2 +- server/src/services/session.service.spec.ts | 1 + .../src/services/shared-link.service.spec.ts | 2 + server/src/utils/access.ts | 14 +-- server/test/fixtures/auth.stub.ts | 6 +- server/test/fixtures/shared-link.stub.ts | 1 + server/test/small.factory.ts | 3 +- .../components/asset-viewer/actions/action.ts | 2 + .../actions/set-visibility-action.svelte | 60 +++++++++ .../asset-viewer/asset-viewer-nav-bar.svelte | 49 +++++--- .../components/layouts/AuthPageLayout.svelte | 19 +-- .../photos-page/actions/delete-assets.svelte | 8 +- .../actions/select-all-assets.svelte | 30 +++-- .../actions/set-visibility-action.svelte | 72 +++++++++++ .../components/photos-page/asset-grid.svelte | 13 +- .../empty-placeholder.svelte | 9 +- .../side-bar/user-sidebar.svelte | 10 ++ .../PinCodeChangeForm.svelte | 79 ++++++++++++ .../PinCodeCreateForm.svelte | 72 +++++++++++ .../user-settings-page/PinCodeInput.svelte | 29 ++++- .../user-settings-page/PinCodeSettings.svelte | 118 +++--------------- web/src/lib/constants.ts | 4 + web/src/lib/utils/actions.ts | 1 + .../[[assetId=id]]/+page.svelte | 76 +++++++++++ .../[[photos=photos]]/[[assetId=id]]/+page.ts | 28 +++++ .../(user)/photos/[[assetId=id]]/+page.svelte | 7 ++ web/src/routes/auth/pin-prompt/+page.svelte | 84 +++++++++++++ web/src/routes/auth/pin-prompt/+page.ts | 22 ++++ web/src/test-data/factories/asset-factory.ts | 3 +- 61 files changed, 1018 insertions(+), 186 deletions(-) create mode 100644 server/src/schema/migrations/1746844028242-AddLockedVisibilityEnum.ts create mode 100644 server/src/schema/migrations/1746987967923-AddPinExpiresAtColumn.ts create mode 100644 web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte create mode 100644 web/src/lib/components/photos-page/actions/set-visibility-action.svelte create mode 100644 web/src/lib/components/user-settings-page/PinCodeChangeForm.svelte create mode 100644 web/src/lib/components/user-settings-page/PinCodeCreateForm.svelte create mode 100644 web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte create mode 100644 web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts create mode 100644 web/src/routes/auth/pin-prompt/+page.svelte create mode 100644 web/src/routes/auth/pin-prompt/+page.ts diff --git a/i18n/en.json b/i18n/en.json index b712faa3c2..05b236b33a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1,4 +1,19 @@ { + "new_pin_code_subtitle": "This is your first time accessing the locked folder. Create a PIN code to securely access this page", + "enter_your_pin_code": "Enter your PIN code", + "enter_your_pin_code_subtitle": "Enter your PIN code to access the locked folder", + "pin_verification": "PIN code verification", + "wrong_pin_code": "Wrong PIN code", + "nothing_here_yet": "Nothing here yet", + "move_to_locked_folder": "Move to Locked Folder", + "remove_from_locked_folder": "Remove from Locked Folder", + "move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the Locked Folder", + "remove_from_locked_folder_confirmation": "Are you sure you want to move these photos and videos out of Locked Folder? They will be visible in your library", + "move": "Move", + "no_locked_photos_message": "Photos and videos in Locked Folder are hidden and won't show up as you browser your library.", + "locked_folder": "Locked Folder", + "add_to_locked_folder": "Add to Locked Folder", + "move_off_locked_folder": "Move out of Locked Folder", "user_pin_code_settings": "PIN Code", "user_pin_code_settings_description": "Manage your PIN code", "current_pin_code": "Current PIN code", @@ -837,6 +852,7 @@ "error_saving_image": "Error: {error}", "error_title": "Error - Something went wrong", "errors": { + "unable_to_move_to_locked_folder": "Unable to move to locked folder", "cannot_navigate_next_asset": "Cannot navigate to the next asset", "cannot_navigate_previous_asset": "Cannot navigate to previous asset", "cant_apply_changes": "Can't apply changes", diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 708aec603f..d054749b1e 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -29,6 +29,7 @@ dynamic upgradeDto(dynamic value, String targetType) { case 'UserResponseDto': if (value is Map) { addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String()); + addDefault(value, 'visibility', AssetVisibility.timeline); } break; case 'UserAdminResponseDto': diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 9a3055911d..3aed98adf1 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -117,6 +117,7 @@ Class | Method | HTTP request | Description *AuthenticationApi* | [**setupPinCode**](doc//AuthenticationApi.md#setuppincode) | **POST** /auth/pin-code | *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | +*AuthenticationApi* | [**verifyPinCode**](doc//AuthenticationApi.md#verifypincode) | **POST** /auth/pin-code/verify | *DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | diff --git a/mobile/openapi/lib/api/authentication_api.dart b/mobile/openapi/lib/api/authentication_api.dart index f850bdf403..446a0616ed 100644 --- a/mobile/openapi/lib/api/authentication_api.dart +++ b/mobile/openapi/lib/api/authentication_api.dart @@ -396,4 +396,43 @@ class AuthenticationApi { } return null; } + + /// Performs an HTTP 'POST /auth/pin-code/verify' operation and returns the [Response]. + /// Parameters: + /// + /// * [PinCodeSetupDto] pinCodeSetupDto (required): + Future verifyPinCodeWithHttpInfo(PinCodeSetupDto pinCodeSetupDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/auth/pin-code/verify'; + + // ignore: prefer_final_locals + Object? postBody = pinCodeSetupDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [PinCodeSetupDto] pinCodeSetupDto (required): + Future verifyPinCode(PinCodeSetupDto pinCodeSetupDto,) async { + final response = await verifyPinCodeWithHttpInfo(pinCodeSetupDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } } diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 5f01f84419..74af8bd1eb 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -43,6 +43,7 @@ class AssetResponseDto { required this.type, this.unassignedFaces = const [], required this.updatedAt, + required this.visibility, }); /// base64 encoded sha1 hash @@ -132,6 +133,8 @@ class AssetResponseDto { DateTime updatedAt; + AssetResponseDtoVisibilityEnum visibility; + @override bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto && other.checksum == checksum && @@ -163,7 +166,8 @@ class AssetResponseDto { other.thumbhash == thumbhash && other.type == type && _deepEquality.equals(other.unassignedFaces, unassignedFaces) && - other.updatedAt == updatedAt; + other.updatedAt == updatedAt && + other.visibility == visibility; @override int get hashCode => @@ -197,10 +201,11 @@ class AssetResponseDto { (thumbhash == null ? 0 : thumbhash!.hashCode) + (type.hashCode) + (unassignedFaces.hashCode) + - (updatedAt.hashCode); + (updatedAt.hashCode) + + (visibility.hashCode); @override - String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]'; + String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility]'; Map toJson() { final json = {}; @@ -270,6 +275,7 @@ class AssetResponseDto { json[r'type'] = this.type; json[r'unassignedFaces'] = this.unassignedFaces; json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'visibility'] = this.visibility; return json; } @@ -312,6 +318,7 @@ class AssetResponseDto { type: AssetTypeEnum.fromJson(json[r'type'])!, unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']), updatedAt: mapDateTime(json, r'updatedAt', r'')!, + visibility: AssetResponseDtoVisibilityEnum.fromJson(json[r'visibility'])!, ); } return null; @@ -378,6 +385,87 @@ class AssetResponseDto { 'thumbhash', 'type', 'updatedAt', + 'visibility', }; } + +class AssetResponseDtoVisibilityEnum { + /// Instantiate a new enum with the provided [value]. + const AssetResponseDtoVisibilityEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const archive = AssetResponseDtoVisibilityEnum._(r'archive'); + static const timeline = AssetResponseDtoVisibilityEnum._(r'timeline'); + static const hidden = AssetResponseDtoVisibilityEnum._(r'hidden'); + static const locked = AssetResponseDtoVisibilityEnum._(r'locked'); + + /// List of all possible values in this [enum][AssetResponseDtoVisibilityEnum]. + static const values = [ + archive, + timeline, + hidden, + locked, + ]; + + static AssetResponseDtoVisibilityEnum? fromJson(dynamic value) => AssetResponseDtoVisibilityEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetResponseDtoVisibilityEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetResponseDtoVisibilityEnum] to String, +/// and [decode] dynamic data back to [AssetResponseDtoVisibilityEnum]. +class AssetResponseDtoVisibilityEnumTypeTransformer { + factory AssetResponseDtoVisibilityEnumTypeTransformer() => _instance ??= const AssetResponseDtoVisibilityEnumTypeTransformer._(); + + const AssetResponseDtoVisibilityEnumTypeTransformer._(); + + String encode(AssetResponseDtoVisibilityEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetResponseDtoVisibilityEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetResponseDtoVisibilityEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'archive': return AssetResponseDtoVisibilityEnum.archive; + case r'timeline': return AssetResponseDtoVisibilityEnum.timeline; + case r'hidden': return AssetResponseDtoVisibilityEnum.hidden; + case r'locked': return AssetResponseDtoVisibilityEnum.locked; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetResponseDtoVisibilityEnumTypeTransformer] instance. + static AssetResponseDtoVisibilityEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/asset_visibility.dart b/mobile/openapi/lib/model/asset_visibility.dart index 4d0c7ee8d3..498bf17c38 100644 --- a/mobile/openapi/lib/model/asset_visibility.dart +++ b/mobile/openapi/lib/model/asset_visibility.dart @@ -26,12 +26,14 @@ class AssetVisibility { static const archive = AssetVisibility._(r'archive'); static const timeline = AssetVisibility._(r'timeline'); static const hidden = AssetVisibility._(r'hidden'); + static const locked = AssetVisibility._(r'locked'); /// List of all possible values in this [enum][AssetVisibility]. static const values = [ archive, timeline, hidden, + locked, ]; static AssetVisibility? fromJson(dynamic value) => AssetVisibilityTypeTransformer().decode(value); @@ -73,6 +75,7 @@ class AssetVisibilityTypeTransformer { case r'archive': return AssetVisibility.archive; case r'timeline': return AssetVisibility.timeline; case r'hidden': return AssetVisibility.hidden; + case r'locked': return AssetVisibility.locked; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/auth_status_response_dto.dart b/mobile/openapi/lib/model/auth_status_response_dto.dart index 203923164f..0ccd87114e 100644 --- a/mobile/openapi/lib/model/auth_status_response_dto.dart +++ b/mobile/openapi/lib/model/auth_status_response_dto.dart @@ -13,30 +13,36 @@ part of openapi.api; class AuthStatusResponseDto { /// Returns a new [AuthStatusResponseDto] instance. AuthStatusResponseDto({ + required this.isElevated, required this.password, required this.pinCode, }); + bool isElevated; + bool password; bool pinCode; @override bool operator ==(Object other) => identical(this, other) || other is AuthStatusResponseDto && + other.isElevated == isElevated && other.password == password && other.pinCode == pinCode; @override int get hashCode => // ignore: unnecessary_parenthesis + (isElevated.hashCode) + (password.hashCode) + (pinCode.hashCode); @override - String toString() => 'AuthStatusResponseDto[password=$password, pinCode=$pinCode]'; + String toString() => 'AuthStatusResponseDto[isElevated=$isElevated, password=$password, pinCode=$pinCode]'; Map toJson() { final json = {}; + json[r'isElevated'] = this.isElevated; json[r'password'] = this.password; json[r'pinCode'] = this.pinCode; return json; @@ -51,6 +57,7 @@ class AuthStatusResponseDto { final json = value.cast(); return AuthStatusResponseDto( + isElevated: mapValueOfType(json, r'isElevated')!, password: mapValueOfType(json, r'password')!, pinCode: mapValueOfType(json, r'pinCode')!, ); @@ -100,6 +107,7 @@ class AuthStatusResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'isElevated', 'password', 'pinCode', }; diff --git a/mobile/openapi/lib/model/sync_asset_v1.dart b/mobile/openapi/lib/model/sync_asset_v1.dart index e1d3199428..f5d59b6ae9 100644 --- a/mobile/openapi/lib/model/sync_asset_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_v1.dart @@ -293,12 +293,14 @@ class SyncAssetV1VisibilityEnum { static const archive = SyncAssetV1VisibilityEnum._(r'archive'); static const timeline = SyncAssetV1VisibilityEnum._(r'timeline'); static const hidden = SyncAssetV1VisibilityEnum._(r'hidden'); + static const locked = SyncAssetV1VisibilityEnum._(r'locked'); /// List of all possible values in this [enum][SyncAssetV1VisibilityEnum]. static const values = [ archive, timeline, hidden, + locked, ]; static SyncAssetV1VisibilityEnum? fromJson(dynamic value) => SyncAssetV1VisibilityEnumTypeTransformer().decode(value); @@ -340,6 +342,7 @@ class SyncAssetV1VisibilityEnumTypeTransformer { case r'archive': return SyncAssetV1VisibilityEnum.archive; case r'timeline': return SyncAssetV1VisibilityEnum.timeline; case r'hidden': return SyncAssetV1VisibilityEnum.hidden; + case r'locked': return SyncAssetV1VisibilityEnum.locked; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 3c0dc09953..2dbec35079 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2470,6 +2470,41 @@ ] } }, + "/auth/pin-code/verify": { + "post": { + "operationId": "verifyPinCode", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PinCodeSetupDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Authentication" + ] + } + }, "/auth/status": { "get": { "operationId": "getAuthStatus", @@ -9150,6 +9185,15 @@ "updatedAt": { "format": "date-time", "type": "string" + }, + "visibility": { + "enum": [ + "archive", + "timeline", + "hidden", + "locked" + ], + "type": "string" } }, "required": [ @@ -9171,7 +9215,8 @@ "ownerId", "thumbhash", "type", - "updatedAt" + "updatedAt", + "visibility" ], "type": "object" }, @@ -9226,7 +9271,8 @@ "enum": [ "archive", "timeline", - "hidden" + "hidden", + "locked" ], "type": "string" }, @@ -9241,6 +9287,9 @@ }, "AuthStatusResponseDto": { "properties": { + "isElevated": { + "type": "boolean" + }, "password": { "type": "boolean" }, @@ -9249,6 +9298,7 @@ } }, "required": [ + "isElevated", "password", "pinCode" ], @@ -12664,7 +12714,8 @@ "enum": [ "archive", "timeline", - "hidden" + "hidden", + "locked" ], "type": "string" } diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 144e7f8ac1..ad7413e6fd 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -329,6 +329,7 @@ export type AssetResponseDto = { "type": AssetTypeEnum; unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; updatedAt: string; + visibility: Visibility; }; export type AlbumResponseDto = { albumName: string; @@ -520,6 +521,7 @@ export type PinCodeSetupDto = { pinCode: string; }; export type AuthStatusResponseDto = { + isElevated: boolean; password: boolean; pinCode: boolean; }; @@ -2076,6 +2078,15 @@ export function changePinCode({ pinCodeChangeDto }: { body: pinCodeChangeDto }))); } +export function verifyPinCode({ pinCodeSetupDto }: { + pinCodeSetupDto: PinCodeSetupDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/auth/pin-code/verify", oazapfts.json({ + ...opts, + method: "POST", + body: pinCodeSetupDto + }))); +} export function getAuthStatus(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -3574,7 +3585,8 @@ export enum UserStatus { export enum AssetVisibility { Archive = "archive", Timeline = "timeline", - Hidden = "hidden" + Hidden = "hidden", + Locked = "locked" } export enum AlbumUserRole { Editor = "editor", @@ -3591,6 +3603,12 @@ export enum AssetTypeEnum { Audio = "AUDIO", Other = "OTHER" } +export enum Visibility { + Archive = "archive", + Timeline = "timeline", + Hidden = "hidden", + Locked = "locked" +} export enum AssetOrder { Asc = "asc", Desc = "desc" diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index 56acaa5c6d..5d3ba8be95 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -101,4 +101,11 @@ export class AuthController { async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeChangeDto): Promise { return this.service.resetPinCode(auth, dto); } + + @Post('pin-code/verify') + @HttpCode(HttpStatus.OK) + @Authenticated() + async verifyPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeSetupDto): Promise { + return this.service.verifyPinCode(auth, dto); + } } diff --git a/server/src/controllers/search.controller.spec.ts b/server/src/controllers/search.controller.spec.ts index 14130fabcb..39d2cb8fcd 100644 --- a/server/src/controllers/search.controller.spec.ts +++ b/server/src/controllers/search.controller.spec.ts @@ -66,7 +66,7 @@ describe(SearchController.name, () => { .send({ visibility: 'immich' }); expect(status).toBe(400); expect(body).toEqual( - errorDto.badRequest(['visibility must be one of the following values: archive, timeline, hidden']), + errorDto.badRequest(['visibility must be one of the following values: archive, timeline, hidden, locked']), ); }); diff --git a/server/src/database.ts b/server/src/database.ts index a13b074448..29c746aa1f 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -200,6 +200,7 @@ export type Album = Selectable & { export type AuthSession = { id: string; + hasElevatedPermission: boolean; }; export type Partner = { @@ -233,6 +234,7 @@ export type Session = { updatedAt: Date; deviceOS: string; deviceType: string; + pinExpiresAt: Date | null; }; export type Exif = Omit, 'updatedAt' | 'updateId'>; @@ -306,7 +308,7 @@ export const columns = { 'users.quotaSizeInBytes', ], authApiKey: ['api_keys.id', 'api_keys.permissions'], - authSession: ['sessions.id', 'sessions.updatedAt'], + authSession: ['sessions.id', 'sessions.updatedAt', 'sessions.pinExpiresAt'], authSharedLink: [ 'shared_links.id', 'shared_links.userId', diff --git a/server/src/db.d.ts b/server/src/db.d.ts index 1b039f9982..1fd7fdc22b 100644 --- a/server/src/db.d.ts +++ b/server/src/db.d.ts @@ -347,6 +347,7 @@ export interface Sessions { updatedAt: Generated; updateId: Generated; userId: string; + pinExpiresAt: Timestamp | null; } export interface SessionSyncCheckpoints { diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 480ad0b9b9..2a44a34b58 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -43,6 +43,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto { isArchived!: boolean; isTrashed!: boolean; isOffline!: boolean; + visibility!: AssetVisibility; exifInfo?: ExifResponseDto; tags?: TagResponseDto[]; people?: PersonWithFacesResponseDto[]; @@ -184,6 +185,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset isFavorite: options.auth?.user.id === entity.ownerId ? entity.isFavorite : false, isArchived: entity.visibility === AssetVisibility.ARCHIVE, isTrashed: !!entity.deletedAt, + visibility: entity.visibility, duration: entity.duration ?? '0:00:00.00000', exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined, livePhotoVideoId: entity.livePhotoVideoId, diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index cc05d2d860..8644426ab2 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -138,4 +138,5 @@ export class OAuthAuthorizeResponseDto { export class AuthStatusResponseDto { pinCode!: boolean; password!: boolean; + isElevated!: boolean; } diff --git a/server/src/enum.ts b/server/src/enum.ts index f214593975..fedfaa6b79 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -627,4 +627,5 @@ export enum AssetVisibility { * Video part of the LivePhotos and MotionPhotos */ HIDDEN = 'hidden', + LOCKED = 'locked', } diff --git a/server/src/queries/access.repository.sql b/server/src/queries/access.repository.sql index f550c5b0c1..c73f44c19d 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -98,6 +98,7 @@ from where "assets"."id" in ($1) and "assets"."ownerId" = $2 + and "assets"."visibility" != $3 -- AccessRepository.asset.checkPartnerAccess select diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index f4eb6a9929..2b351368ef 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -392,6 +392,11 @@ where order by "albums"."createdAt" desc +-- AlbumRepository.removeAssetsFromAll +delete from "albums_assets_assets" +where + "albums_assets_assets"."assetsId" in ($1) + -- AlbumRepository.getAssetIds select * diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql index eea2356897..c2daa2a49c 100644 --- a/server/src/queries/session.repository.sql +++ b/server/src/queries/session.repository.sql @@ -12,6 +12,7 @@ where select "sessions"."id", "sessions"."updatedAt", + "sessions"."pinExpiresAt", ( select to_json(obj) diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index 5680ce2c64..b25007c4ea 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -168,7 +168,7 @@ class AssetAccess { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkOwnerAccess(userId: string, assetIds: Set) { + async checkOwnerAccess(userId: string, assetIds: Set, hasElevatedPermission: boolean | undefined) { if (assetIds.size === 0) { return new Set(); } @@ -178,6 +178,7 @@ class AssetAccess { .select('assets.id') .where('assets.id', 'in', [...assetIds]) .where('assets.ownerId', '=', userId) + .$if(!hasElevatedPermission, (eb) => eb.where('assets.visibility', '!=', AssetVisibility.LOCKED)) .execute() .then((assets) => new Set(assets.map((asset) => asset.id))); } diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index 1768135210..c8bdae6d31 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -220,8 +220,10 @@ export class AlbumRepository { await this.db.deleteFrom('albums').where('ownerId', '=', userId).execute(); } - async removeAsset(assetId: string): Promise { - await this.db.deleteFrom('albums_assets_assets').where('albums_assets_assets.assetsId', '=', assetId).execute(); + @GenerateSql({ params: [[DummyValue.UUID]] }) + @Chunked() + async removeAssetsFromAll(assetIds: string[]): Promise { + await this.db.deleteFrom('albums_assets_assets').where('albums_assets_assets.assetsId', 'in', assetIds).execute(); } @Chunked({ paramIndex: 1 }) diff --git a/server/src/schema/migrations/1746844028242-AddLockedVisibilityEnum.ts b/server/src/schema/migrations/1746844028242-AddLockedVisibilityEnum.ts new file mode 100644 index 0000000000..9a344be66d --- /dev/null +++ b/server/src/schema/migrations/1746844028242-AddLockedVisibilityEnum.ts @@ -0,0 +1,9 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TYPE "asset_visibility_enum" ADD VALUE IF NOT EXISTS 'locked';`.execute(db); +} + +export async function down(): Promise { + // noop +} diff --git a/server/src/schema/migrations/1746987967923-AddPinExpiresAtColumn.ts b/server/src/schema/migrations/1746987967923-AddPinExpiresAtColumn.ts new file mode 100644 index 0000000000..b0f7d072d5 --- /dev/null +++ b/server/src/schema/migrations/1746987967923-AddPinExpiresAtColumn.ts @@ -0,0 +1,9 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "sessions" ADD "pinExpiresAt" timestamp with time zone;`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "sessions" DROP COLUMN "pinExpiresAt";`.execute(db); +} diff --git a/server/src/schema/tables/session.table.ts b/server/src/schema/tables/session.table.ts index ad43d0d6e4..090b469b54 100644 --- a/server/src/schema/tables/session.table.ts +++ b/server/src/schema/tables/session.table.ts @@ -36,4 +36,7 @@ export class SessionTable { @UpdateIdColumn({ indexName: 'IDX_sessions_update_id' }) updateId!: string; + + @Column({ type: 'timestamp with time zone', nullable: true }) + pinExpiresAt!: Date | null; } diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 9a3bb605f7..c2b792d091 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -163,7 +163,7 @@ describe(AlbumService.name, () => { ); expect(mocks.user.get).toHaveBeenCalledWith('user-id', {}); - expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123'])); + expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123']), false); expect(mocks.event.emit).toHaveBeenCalledWith('album.invite', { id: albumStub.empty.id, userId: 'user-id', @@ -207,6 +207,7 @@ describe(AlbumService.name, () => { expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( authStub.admin.user.id, new Set(['asset-1', 'asset-2']), + false, ); }); }); @@ -688,7 +689,11 @@ describe(AlbumService.name, () => { { success: false, id: 'asset-1', error: BulkIdErrorReason.NO_PERMISSION }, ]); - expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1'])); + expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( + authStub.admin.user.id, + new Set(['asset-1']), + false, + ); expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1'])); }); diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 8490e8aaea..bb8f7115b8 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -481,7 +481,11 @@ describe(AssetMediaService.name, () => { it('should require the asset.download permission', async () => { await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(BadRequestException); - expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1'])); + expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( + authStub.admin.user.id, + new Set(['asset-1']), + undefined, + ); expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1'])); expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1'])); }); @@ -512,7 +516,7 @@ describe(AssetMediaService.name, () => { it('should require asset.view permissions', async () => { await expect(sut.viewThumbnail(authStub.admin, 'id', {})).rejects.toBeInstanceOf(BadRequestException); - expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id'])); + expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']), undefined); expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id'])); expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id'])); }); @@ -611,7 +615,7 @@ describe(AssetMediaService.name, () => { it('should require asset.view permissions', async () => { await expect(sut.playbackVideo(authStub.admin, 'id')).rejects.toBeInstanceOf(BadRequestException); - expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id'])); + expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']), undefined); expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id'])); expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id'])); }); diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 1e4cfddcf5..333f4530de 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -122,6 +122,7 @@ describe(AssetService.name, () => { expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( authStub.admin.user.id, new Set([assetStub.image.id]), + undefined, ); }); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 3ab6fcb8a7..556641fdb0 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -14,7 +14,7 @@ import { mapStats, } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetStatus, JobName, JobStatus, Permission, QueueName } from 'src/enum'; +import { AssetStatus, AssetVisibility, JobName, JobStatus, Permission, QueueName } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { ISidecarWriteJob, JobItem, JobOf } from 'src/types'; import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util'; @@ -125,6 +125,10 @@ export class AssetService extends BaseService { options.rating !== undefined ) { await this.assetRepository.updateAll(ids, options); + + if (options.visibility === AssetVisibility.LOCKED) { + await this.albumRepository.removeAssetsFromAll(ids); + } } } diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 82172d6b95..fb1a5ae042 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -253,6 +253,7 @@ describe(AuthService.name, () => { id: session.id, updatedAt: session.updatedAt, user: factory.authUser(), + pinExpiresAt: null, }; mocks.session.getByToken.mockResolvedValue(sessionWithToken); @@ -265,7 +266,7 @@ describe(AuthService.name, () => { }), ).resolves.toEqual({ user: sessionWithToken.user, - session: { id: session.id }, + session: { id: session.id, hasElevatedPermission: false }, }); }); }); @@ -376,6 +377,7 @@ describe(AuthService.name, () => { id: session.id, updatedAt: session.updatedAt, user: factory.authUser(), + pinExpiresAt: null, }; mocks.session.getByToken.mockResolvedValue(sessionWithToken); @@ -388,7 +390,7 @@ describe(AuthService.name, () => { }), ).resolves.toEqual({ user: sessionWithToken.user, - session: { id: session.id }, + session: { id: session.id, hasElevatedPermission: false }, }); }); @@ -398,6 +400,7 @@ describe(AuthService.name, () => { id: session.id, updatedAt: session.updatedAt, user: factory.authUser(), + pinExpiresAt: null, }; mocks.session.getByToken.mockResolvedValue(sessionWithToken); @@ -417,6 +420,7 @@ describe(AuthService.name, () => { id: session.id, updatedAt: session.updatedAt, user: factory.authUser(), + pinExpiresAt: null, }; mocks.session.getByToken.mockResolvedValue(sessionWithToken); @@ -916,13 +920,17 @@ describe(AuthService.name, () => { describe('resetPinCode', () => { it('should reset the PIN code', async () => { + const currentSession = factory.session(); const user = factory.userAdmin(); mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' }); mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b); + mocks.session.getByUserId.mockResolvedValue([currentSession]); + mocks.session.update.mockResolvedValue(currentSession); await sut.resetPinCode(factory.auth({ user }), { pinCode: '123456' }); expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pinCode: null }); + expect(mocks.session.update).toHaveBeenCalledWith(currentSession.id, { pinExpiresAt: null }); }); it('should throw if the PIN code does not match', async () => { diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 65dd84693b..496c252643 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -126,6 +126,10 @@ export class AuthService extends BaseService { this.resetPinChecks(user, dto); await this.userRepository.update(auth.user.id, { pinCode: null }); + const sessions = await this.sessionRepository.getByUserId(auth.user.id); + for (const session of sessions) { + await this.sessionRepository.update(session.id, { pinExpiresAt: null }); + } } async changePinCode(auth: AuthDto, dto: PinCodeChangeDto) { @@ -444,10 +448,25 @@ export class AuthService extends BaseService { await this.sessionRepository.update(session.id, { id: session.id, updatedAt: new Date() }); } + // Pin check + let hasElevatedPermission = false; + + if (session.pinExpiresAt) { + const pinExpiresAt = DateTime.fromJSDate(session.pinExpiresAt); + hasElevatedPermission = pinExpiresAt > now; + + if (hasElevatedPermission && now.plus({ minutes: 5 }) > pinExpiresAt) { + await this.sessionRepository.update(session.id, { + pinExpiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate(), + }); + } + } + return { user: session.user, session: { id: session.id, + hasElevatedPermission, }, }; } @@ -455,6 +474,23 @@ export class AuthService extends BaseService { throw new UnauthorizedException('Invalid user token'); } + async verifyPinCode(auth: AuthDto, dto: PinCodeSetupDto): Promise { + const user = await this.userRepository.getForPinCode(auth.user.id); + if (!user) { + throw new UnauthorizedException(); + } + + this.resetPinChecks(user, { pinCode: dto.pinCode }); + + if (!auth.session) { + throw new BadRequestException('Session is missing'); + } + + await this.sessionRepository.update(auth.session.id, { + pinExpiresAt: new Date(DateTime.now().plus({ minutes: 15 }).toJSDate()), + }); + } + private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) { const key = this.cryptoRepository.newPassword(32); const token = this.cryptoRepository.hashSha256(key); @@ -493,6 +529,7 @@ export class AuthService extends BaseService { return { pinCode: !!user.pinCode, password: !!user.password, + isElevated: !!auth.session?.hasElevatedPermission, }; } } diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 28cb42a16b..7b2cba1250 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -1310,7 +1310,7 @@ describe(MetadataService.name, () => { expect(mocks.asset.update).not.toHaveBeenCalledWith( expect.objectContaining({ visibility: AssetVisibility.HIDDEN }), ); - expect(mocks.album.removeAsset).not.toHaveBeenCalled(); + expect(mocks.album.removeAssetsFromAll).not.toHaveBeenCalled(); }); it('should handle not finding a match', async () => { @@ -1331,7 +1331,7 @@ describe(MetadataService.name, () => { expect(mocks.asset.update).not.toHaveBeenCalledWith( expect.objectContaining({ visibility: AssetVisibility.HIDDEN }), ); - expect(mocks.album.removeAsset).not.toHaveBeenCalled(); + expect(mocks.album.removeAssetsFromAll).not.toHaveBeenCalled(); }); it('should link photo and video', async () => { @@ -1356,7 +1356,7 @@ describe(MetadataService.name, () => { id: assetStub.livePhotoMotionAsset.id, visibility: AssetVisibility.HIDDEN, }); - expect(mocks.album.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id); + expect(mocks.album.removeAssetsFromAll).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]); }); it('should notify clients on live photo link', async () => { diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 3497b808da..109f5f6936 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -158,7 +158,7 @@ export class MetadataService extends BaseService { await Promise.all([ this.assetRepository.update({ id: photoAsset.id, livePhotoVideoId: motionAsset.id }), this.assetRepository.update({ id: motionAsset.id, visibility: AssetVisibility.HIDDEN }), - this.albumRepository.removeAsset(motionAsset.id), + this.albumRepository.removeAssetsFromAll([motionAsset.id]), ]); await this.eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId: motionAsset.ownerId }); diff --git a/server/src/services/session.service.spec.ts b/server/src/services/session.service.spec.ts index c3ab5619be..6e26b26407 100644 --- a/server/src/services/session.service.spec.ts +++ b/server/src/services/session.service.spec.ts @@ -34,6 +34,7 @@ describe('SessionService', () => { token: '420', userId: '42', updateId: 'uuid-v7', + pinExpiresAt: null, }, ]); mocks.session.delete.mockResolvedValue(); diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts index 66a0a925c7..b3b4c4b1cf 100644 --- a/server/src/services/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -156,6 +156,7 @@ describe(SharedLinkService.name, () => { expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( authStub.admin.user.id, new Set([assetStub.image.id]), + false, ); expect(mocks.sharedLink.create).toHaveBeenCalledWith({ type: SharedLinkType.INDIVIDUAL, @@ -186,6 +187,7 @@ describe(SharedLinkService.name, () => { expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( authStub.admin.user.id, new Set([assetStub.image.id]), + false, ); expect(mocks.sharedLink.create).toHaveBeenCalledWith({ type: SharedLinkType.INDIVIDUAL, diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index b04d23f114..e2fe7429f3 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -81,7 +81,7 @@ const checkSharedLinkAccess = async ( case Permission.ASSET_SHARE: { // TODO: fix this to not use sharedLink.userId for access control - return await access.asset.checkOwnerAccess(sharedLink.userId, ids); + return await access.asset.checkOwnerAccess(sharedLink.userId, ids, false); } case Permission.ALBUM_READ: { @@ -119,38 +119,38 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe } case Permission.ASSET_READ: { - const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission); const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); return setUnion(isOwner, isAlbum, isPartner); } case Permission.ASSET_SHARE: { - const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, false); const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); return setUnion(isOwner, isPartner); } case Permission.ASSET_VIEW: { - const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission); const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); return setUnion(isOwner, isAlbum, isPartner); } case Permission.ASSET_DOWNLOAD: { - const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission); const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); return setUnion(isOwner, isAlbum, isPartner); } case Permission.ASSET_UPDATE: { - return await access.asset.checkOwnerAccess(auth.user.id, ids); + return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission); } case Permission.ASSET_DELETE: { - return await access.asset.checkOwnerAccess(auth.user.id, ids); + return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission); } case Permission.ALBUM_READ: { diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts index 9ef55398d3..3e5825c0cc 100644 --- a/server/test/fixtures/auth.stub.ts +++ b/server/test/fixtures/auth.stub.ts @@ -1,4 +1,4 @@ -import { Session } from 'src/database'; +import { AuthSession } from 'src/database'; import { AuthDto } from 'src/dtos/auth.dto'; const authUser = { @@ -26,7 +26,7 @@ export const authStub = { user: authUser.user1, session: { id: 'token-id', - } as Session, + } as AuthSession, }), user2: Object.freeze({ user: { @@ -39,7 +39,7 @@ export const authStub = { }, session: { id: 'token-id', - } as Session, + } as AuthSession, }), adminSharedLink: Object.freeze({ user: authUser.admin, diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index fc4b74ba2d..f3096280d9 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -70,6 +70,7 @@ const assetResponse: AssetResponseDto = { isTrashed: false, libraryId: 'library-id', hasMetadata: true, + visibility: AssetVisibility.TIMELINE, }; const assetResponseWithoutMetadata = { diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 94ae3b74aa..01091854fa 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -58,7 +58,7 @@ const authFactory = ({ } if (session) { - auth.session = { id: session.id }; + auth.session = { id: session.id, hasElevatedPermission: false }; } if (sharedLink) { @@ -127,6 +127,7 @@ const sessionFactory = (session: Partial = {}) => ({ deviceType: 'mobile', token: 'abc123', userId: newUuid(), + pinExpiresAt: newDate(), ...session, }); diff --git a/web/src/lib/components/asset-viewer/actions/action.ts b/web/src/lib/components/asset-viewer/actions/action.ts index 40b189080f..d85325b59a 100644 --- a/web/src/lib/components/asset-viewer/actions/action.ts +++ b/web/src/lib/components/asset-viewer/actions/action.ts @@ -13,6 +13,8 @@ type ActionMap = { [AssetAction.ADD_TO_ALBUM]: { asset: AssetResponseDto; album: AlbumResponseDto }; [AssetAction.UNSTACK]: { assets: AssetResponseDto[] }; [AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: AssetResponseDto }; + [AssetAction.SET_VISIBILITY_LOCKED]: { asset: AssetResponseDto }; + [AssetAction.SET_VISIBILITY_TIMELINE]: { asset: AssetResponseDto }; }; export type Action = { diff --git a/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte b/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte new file mode 100644 index 0000000000..6a7f6d3078 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte @@ -0,0 +1,60 @@ + + + toggleLockedVisibility()} + text={isLocked ? $t('move_off_locked_folder') : $t('add_to_locked_folder')} + icon={isLocked ? mdiFolderMoveOutline : mdiEyeOffOutline} +/> diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index b0ac455bc8..9436dc13c8 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -12,6 +12,7 @@ import SetAlbumCoverAction from '$lib/components/asset-viewer/actions/set-album-cover-action.svelte'; import SetFeaturedPhotoAction from '$lib/components/asset-viewer/actions/set-person-featured-action.svelte'; import SetProfilePictureAction from '$lib/components/asset-viewer/actions/set-profile-picture-action.svelte'; + import SetVisibilityAction from '$lib/components/asset-viewer/actions/set-visibility-action.svelte'; import ShareAction from '$lib/components/asset-viewer/actions/share-action.svelte'; import ShowDetailAction from '$lib/components/asset-viewer/actions/show-detail-action.svelte'; import UnstackAction from '$lib/components/asset-viewer/actions/unstack-action.svelte'; @@ -27,6 +28,7 @@ import { AssetJobName, AssetTypeEnum, + Visibility, type AlbumResponseDto, type AssetResponseDto, type PersonResponseDto, @@ -91,6 +93,7 @@ const sharedLink = getSharedLink(); let isOwner = $derived($user && asset.ownerId === $user?.id); let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline); + let isLocked = $derived(asset.visibility === Visibility.Locked); // $: showEditorButton = // isOwner && @@ -112,7 +115,7 @@ {/if}
    - {#if !asset.isTrashed && $user} + {#if !asset.isTrashed && $user && !isLocked} {/if} {#if asset.isOffline} @@ -159,17 +162,20 @@ - {#if showSlideshow} + {#if showSlideshow && !isLocked} {/if} {#if showDownloadButton} {/if} - {#if asset.isTrashed} - - {:else} - - + + {#if !isLocked} + {#if asset.isTrashed} + + {:else} + + + {/if} {/if} {#if isOwner} @@ -183,21 +189,28 @@ {#if person} {/if} - {#if asset.type === AssetTypeEnum.Image} + {#if asset.type === AssetTypeEnum.Image && !isLocked} {/if} - - openFileUploadDialog({ multiple: false, assetId: asset.id })} - text={$t('replace_with_upload')} - /> - {#if !asset.isArchived && !asset.isTrashed} + + {#if !isLocked} + goto(`${AppRoute.PHOTOS}?at=${stack?.primaryAssetId ?? asset.id}`)} - text={$t('view_in_timeline')} + icon={mdiUpload} + onClick={() => openFileUploadDialog({ multiple: false, assetId: asset.id })} + text={$t('replace_with_upload')} /> + {#if !asset.isArchived && !asset.isTrashed} + goto(`${AppRoute.PHOTOS}?at=${stack?.primaryAssetId ?? asset.id}`)} + text={$t('view_in_timeline')} + /> + {/if} + {/if} + + {#if !asset.isTrashed} + {/if}
    @@ -18,12 +19,14 @@
    - - - - {title} - - + {#if withHeader} + + + + {title} + + + {/if} {@render children?.()} diff --git a/web/src/lib/components/photos-page/actions/delete-assets.svelte b/web/src/lib/components/photos-page/actions/delete-assets.svelte index 75bdc0f8a6..5cdcffb937 100644 --- a/web/src/lib/components/photos-page/actions/delete-assets.svelte +++ b/web/src/lib/components/photos-page/actions/delete-assets.svelte @@ -1,12 +1,12 @@ -{#if $isSelectingAllAssets} - +{#if withText} + {:else} - + {/if} diff --git a/web/src/lib/components/photos-page/actions/set-visibility-action.svelte b/web/src/lib/components/photos-page/actions/set-visibility-action.svelte new file mode 100644 index 0000000000..c11ba114ce --- /dev/null +++ b/web/src/lib/components/photos-page/actions/set-visibility-action.svelte @@ -0,0 +1,72 @@ + + +{#if menuItem} + +{:else} + +{/if} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index dd17874a61..508e3dea6c 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -39,7 +39,13 @@ enableRouting: boolean; assetStore: AssetStore; assetInteraction: AssetInteraction; - removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.FAVORITE | AssetAction.UNFAVORITE | null; + removeAction?: + | AssetAction.UNARCHIVE + | AssetAction.ARCHIVE + | AssetAction.FAVORITE + | AssetAction.UNFAVORITE + | AssetAction.SET_VISIBILITY_TIMELINE + | null; withStacked?: boolean; showArchiveIcon?: boolean; isShared?: boolean; @@ -417,7 +423,9 @@ case AssetAction.TRASH: case AssetAction.RESTORE: case AssetAction.DELETE: - case AssetAction.ARCHIVE: { + case AssetAction.ARCHIVE: + case AssetAction.SET_VISIBILITY_LOCKED: + case AssetAction.SET_VISIBILITY_TIMELINE: { // find the next asset to show or close the viewer // eslint-disable-next-line @typescript-eslint/no-unused-expressions (await handleNext()) || (await handlePrevious()) || (await handleClose({ asset: action.asset })); @@ -445,6 +453,7 @@ case AssetAction.UNSTACK: { updateUnstackedAssetInTimeline(assetStore, action.assets); + break; } } }; diff --git a/web/src/lib/components/shared-components/empty-placeholder.svelte b/web/src/lib/components/shared-components/empty-placeholder.svelte index 922d7ad92f..63c30a0c4a 100644 --- a/web/src/lib/components/shared-components/empty-placeholder.svelte +++ b/web/src/lib/components/shared-components/empty-placeholder.svelte @@ -6,9 +6,10 @@ text: string; fullWidth?: boolean; src?: string; + title?: string; } - let { onClick = undefined, text, fullWidth = false, src = empty1Url }: Props = $props(); + let { onClick = undefined, text, fullWidth = false, src = empty1Url, title }: Props = $props(); let width = $derived(fullWidth ? 'w-full' : 'w-1/2'); @@ -24,5 +25,9 @@ class="{width} m-auto mt-10 flex flex-col place-content-center place-items-center rounded-3xl bg-gray-50 p-5 dark:bg-immich-dark-gray {hoverClasses}" > -

    {text}

    + + {#if title} +

    {title}

    + {/if} +

    {text}

    diff --git a/web/src/lib/components/shared-components/side-bar/user-sidebar.svelte b/web/src/lib/components/shared-components/side-bar/user-sidebar.svelte index 08911b4ef5..74cf69b08e 100644 --- a/web/src/lib/components/shared-components/side-bar/user-sidebar.svelte +++ b/web/src/lib/components/shared-components/side-bar/user-sidebar.svelte @@ -19,6 +19,8 @@ mdiImageMultiple, mdiImageMultipleOutline, mdiLink, + mdiLock, + mdiLockOutline, mdiMagnify, mdiMap, mdiMapOutline, @@ -40,6 +42,7 @@ let isSharingSelected: boolean = $state(false); let isTrashSelected: boolean = $state(false); let isUtilitiesSelected: boolean = $state(false); + let isLockedFolderSelected: boolean = $state(false); @@ -128,6 +131,13 @@ icon={isArchiveSelected ? mdiArchiveArrowDown : mdiArchiveArrowDownOutline} > + + {#if $featureFlags.trash} + import { + notificationController, + NotificationType, + } from '$lib/components/shared-components/notification/notification'; + import PinCodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte'; + import { handleError } from '$lib/utils/handle-error'; + import { changePinCode } from '@immich/sdk'; + import { Button } from '@immich/ui'; + import { t } from 'svelte-i18n'; + import { fade } from 'svelte/transition'; + + let currentPinCode = $state(''); + let newPinCode = $state(''); + let confirmPinCode = $state(''); + let isLoading = $state(false); + let canSubmit = $derived(currentPinCode.length === 6 && confirmPinCode.length === 6 && newPinCode === confirmPinCode); + + interface Props { + onChanged?: () => void; + } + + let { onChanged }: Props = $props(); + + const handleSubmit = async (event: Event) => { + event.preventDefault(); + await handleChangePinCode(); + }; + + const handleChangePinCode = async () => { + isLoading = true; + try { + await changePinCode({ pinCodeChangeDto: { pinCode: currentPinCode, newPinCode } }); + + resetForm(); + + notificationController.show({ + message: $t('pin_code_changed_successfully'), + type: NotificationType.Info, + }); + + onChanged?.(); + } catch (error) { + handleError(error, $t('unable_to_change_pin_code')); + } finally { + isLoading = false; + } + }; + + const resetForm = () => { + currentPinCode = ''; + newPinCode = ''; + confirmPinCode = ''; + }; + + +
    +
    +
    +
    +

    {$t('change_pin_code')}

    + + + + + +
    + +
    + + +
    +
    +
    +
    diff --git a/web/src/lib/components/user-settings-page/PinCodeCreateForm.svelte b/web/src/lib/components/user-settings-page/PinCodeCreateForm.svelte new file mode 100644 index 0000000000..ae07e976b7 --- /dev/null +++ b/web/src/lib/components/user-settings-page/PinCodeCreateForm.svelte @@ -0,0 +1,72 @@ + + +
    +
    + {#if showLabel} +

    {$t('setup_pin_code')}

    + {/if} + + + +
    + +
    + + +
    +
    diff --git a/web/src/lib/components/user-settings-page/PinCodeInput.svelte b/web/src/lib/components/user-settings-page/PinCodeInput.svelte index e149f26851..01de7b3563 100644 --- a/web/src/lib/components/user-settings-page/PinCodeInput.svelte +++ b/web/src/lib/components/user-settings-page/PinCodeInput.svelte @@ -1,12 +1,25 @@
    -
    -
    -
    - {#if hasPinCode} -

    {$t('change_pin_code')}

    - - - - - - {:else} -

    {$t('setup_pin_code')}

    - - - - {/if} -
    - -
    - - -
    -
    -
    + {#if hasPinCode} +
    + +
    + {:else} +
    + (hasPinCode = true)} /> +
    + {/if}
    diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index e4603217e0..167c976eeb 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -10,6 +10,8 @@ export enum AssetAction { ADD_TO_ALBUM = 'add-to-album', UNSTACK = 'unstack', KEEP_THIS_DELETE_OTHERS = 'keep-this-delete-others', + SET_VISIBILITY_LOCKED = 'set-visibility-locked', + SET_VISIBILITY_TIMELINE = 'set-visibility-timeline', } export enum AppRoute { @@ -43,12 +45,14 @@ export enum AppRoute { AUTH_REGISTER = '/auth/register', AUTH_CHANGE_PASSWORD = '/auth/change-password', AUTH_ONBOARDING = '/auth/onboarding', + AUTH_PIN_PROMPT = '/auth/pin-prompt', UTILITIES = '/utilities', DUPLICATES = '/utilities/duplicates', FOLDERS = '/folders', TAGS = '/tags', + LOCKED = '/locked', } export enum ProjectionType { diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts index 472f55cbca..45fc21a7d9 100644 --- a/web/src/lib/utils/actions.ts +++ b/web/src/lib/utils/actions.ts @@ -15,6 +15,7 @@ export type OnArchive = (ids: string[], isArchived: boolean) => void; export type OnFavorite = (ids: string[], favorite: boolean) => void; export type OnStack = (result: StackResponse) => void; export type OnUnstack = (assets: AssetResponseDto[]) => void; +export type OnSetVisibility = (ids: string[]) => void; export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => { const $t = get(t); diff --git a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte new file mode 100644 index 0000000000..49b40866dd --- /dev/null +++ b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -0,0 +1,76 @@ + + + +{#if assetInteraction.selectionActive} + assetInteraction.clearMultiselect()} + > + + + + + + + assetStore.removeAssets(assetIds)} /> + + +{/if} + + + + {#snippet empty()} + + {/snippet} + + diff --git a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts new file mode 100644 index 0000000000..9b9d86a4b3 --- /dev/null +++ b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -0,0 +1,28 @@ +import { AppRoute } from '$lib/constants'; +import { authenticate } from '$lib/utils/auth'; +import { getFormatter } from '$lib/utils/i18n'; +import { getAssetInfoFromParam } from '$lib/utils/navigation'; +import { getAuthStatus } from '@immich/sdk'; +import { redirect } from '@sveltejs/kit'; +import type { PageLoad } from './$types'; + +export const load = (async ({ params, url }) => { + await authenticate(); + const { isElevated, pinCode } = await getAuthStatus(); + + if (!isElevated || !pinCode) { + const continuePath = encodeURIComponent(url.pathname); + const redirectPath = `${AppRoute.AUTH_PIN_PROMPT}?continue=${continuePath}`; + + redirect(302, redirectPath); + } + const asset = await getAssetInfoFromParam(params); + const $t = await getFormatter(); + + return { + asset, + meta: { + title: $t('locked_folder'), + }, + }; +}) satisfies PageLoad; diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index 73f04380a5..20f4ca0abc 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -12,6 +12,7 @@ import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; import LinkLivePhotoAction from '$lib/components/photos-page/actions/link-live-photo-action.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; + import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte'; import StackAction from '$lib/components/photos-page/actions/stack-action.svelte'; import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; @@ -75,6 +76,11 @@ assetStore.updateAssets([still]); }; + const handleSetVisibility = (assetIds: string[]) => { + assetStore.removeAssets(assetIds); + assetInteraction.clearMultiselect(); + }; + beforeNavigate(() => { isFaceEditMode.value = false; }); @@ -142,6 +148,7 @@ {/if} assetStore.removeAssets(assetIds)} /> +
    diff --git a/web/src/routes/auth/pin-prompt/+page.svelte b/web/src/routes/auth/pin-prompt/+page.svelte new file mode 100644 index 0000000000..91480cd35c --- /dev/null +++ b/web/src/routes/auth/pin-prompt/+page.svelte @@ -0,0 +1,84 @@ + + + + {#if hasPinCode} +
    +
    + {#if isVerified} +
    + +
    + {:else} +
    + +
    + {/if} + +

    {$t('enter_your_pin_code_subtitle')}

    + + onPinFilled(pinCode, true)} + /> +
    +
    + {:else} +
    +
    +
    + +
    +

    + {$t('new_pin_code_subtitle')} +

    + (hasPinCode = true)} /> +
    +
    + {/if} +
    diff --git a/web/src/routes/auth/pin-prompt/+page.ts b/web/src/routes/auth/pin-prompt/+page.ts new file mode 100644 index 0000000000..e2b79605d8 --- /dev/null +++ b/web/src/routes/auth/pin-prompt/+page.ts @@ -0,0 +1,22 @@ +import { authenticate } from '$lib/utils/auth'; +import { getFormatter } from '$lib/utils/i18n'; +import { getAuthStatus } from '@immich/sdk'; +import type { PageLoad } from './$types'; + +export const load = (async ({ url }) => { + await authenticate(); + + const { pinCode } = await getAuthStatus(); + + const continuePath = url.searchParams.get('continue'); + + const $t = await getFormatter(); + + return { + meta: { + title: $t('pin_verification'), + }, + hasPinCode: !!pinCode, + continuePath, + }; +}) satisfies PageLoad; diff --git a/web/src/test-data/factories/asset-factory.ts b/web/src/test-data/factories/asset-factory.ts index 656c4143a7..b727286590 100644 --- a/web/src/test-data/factories/asset-factory.ts +++ b/web/src/test-data/factories/asset-factory.ts @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker'; -import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk'; +import { AssetTypeEnum, Visibility, type AssetResponseDto } from '@immich/sdk'; import { Sync } from 'factory.ts'; export const assetFactory = Sync.makeFactory({ @@ -24,4 +24,5 @@ export const assetFactory = Sync.makeFactory({ checksum: Sync.each(() => faker.string.alphanumeric(28)), isOffline: Sync.each(() => faker.datatype.boolean()), hasMetadata: Sync.each(() => faker.datatype.boolean()), + visibility: Visibility.Timeline, }); From 7146ec99b121b950aec30b24bf876c33152040f7 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 15 May 2025 11:44:10 -0400 Subject: [PATCH 226/356] chore: use default theme config (#18314) --- web/package-lock.json | 8 ++++---- web/package.json | 2 +- web/src/app.css | 28 ---------------------------- web/src/routes/+layout.svelte | 1 + web/tailwind.config.js | 17 +++++------------ 5 files changed, 11 insertions(+), 45 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 76278058f1..12d65473c9 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.20.0", + "@immich/ui": "^0.21.1", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", @@ -1337,9 +1337,9 @@ "link": true }, "node_modules/@immich/ui": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.20.0.tgz", - "integrity": "sha512-euK3N0AhQLB28qFteorRKyDUdet3UpA9MEAd8eBLbTtTFZKvZismBGa4J7pHbQrSkuOlbmJD5LJuM575q8zigQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.21.1.tgz", + "integrity": "sha512-ofDbLMYgM3Bnrv1nCbyPV5Gw9PdWvyhTAJPtojw4C3r2m7CbRW1kJDHt5M79n6xAVgjMOFyre1lOE5cwSSvRQA==", "license": "GNU Affero General Public License version 3", "dependencies": { "@mdi/js": "^7.4.47", diff --git a/web/package.json b/web/package.json index 8a9f6472b6..7bf5e36189 100644 --- a/web/package.json +++ b/web/package.json @@ -27,7 +27,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.20.0", + "@immich/ui": "^0.21.1", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", diff --git a/web/src/app.css b/web/src/app.css index 211d34bb6c..1693aacab8 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -21,34 +21,6 @@ --immich-dark-success: 56 142 60; --immich-dark-warning: 245 124 0; } - - :root { - /* light */ - --immich-ui-primary: 66 80 175; - --immich-ui-dark: 58 58 58; - --immich-ui-light: 255 255 255; - --immich-ui-success: 16 188 99; - --immich-ui-danger: 200 60 60; - --immich-ui-warning: 216 143 64; - --immich-ui-info: 8 111 230; - --immich-ui-gray: 246 246 246; - - --immich-ui-default-border: 209 213 219; - } - - .dark { - /* dark */ - --immich-ui-primary: 172 203 250; - --immich-ui-light: 0 0 0; - --immich-ui-dark: 229 231 235; - --immich-ui-danger: 246 125 125; - --immich-ui-success: 72 237 152; - --immich-ui-warning: 254 197 132; - --immich-ui-info: 121 183 254; - --immich-ui-gray: 33 33 33; - - --immich-ui-default-border: 55 65 81; - } } @font-face { diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 3a6320a265..fe0c680ec3 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -16,6 +16,7 @@ import { copyToClipboard } from '$lib/utils'; import { isAssetViewerRoute } from '$lib/utils/navigation'; import { setTranslations } from '@immich/ui'; + import '@immich/ui/theme/default.css'; import { onMount, type Snippet } from 'svelte'; import { t } from 'svelte-i18n'; import { run } from 'svelte/legacy'; diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 2e13e5997d..ae241a44bb 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -1,5 +1,8 @@ +import { tailwindConfig } from '@immich/ui/theme/default.js'; import plugin from 'tailwindcss/plugin'; +const { colors, borderColor } = tailwindConfig(); + /** @type {import('tailwindcss').Config} */ export default { content: [ @@ -29,19 +32,9 @@ export default { 'immich-dark-success': 'rgb(var(--immich-dark-success) / )', 'immich-dark-warning': 'rgb(var(--immich-dark-warning) / )', - primary: 'rgb(var(--immich-ui-primary) / )', - light: 'rgb(var(--immich-ui-light) / )', - dark: 'rgb(var(--immich-ui-dark) / )', - success: 'rgb(var(--immich-ui-success) / )', - danger: 'rgb(var(--immich-ui-danger) / )', - warning: 'rgb(var(--immich-ui-warning) / )', - info: 'rgb(var(--immich-ui-info) / )', - subtle: 'rgb(var(--immich-ui-gray) / )', + ...colors, }, - borderColor: ({ theme }) => ({ - ...theme('colors'), - DEFAULT: 'rgb(var(--immich-ui-default-border) / )', - }), + borderColor, fontFamily: { 'immich-mono': ['Overpass Mono', 'monospace'], }, From 585997d46f688b21ae88d6f6a0a3c04082973927 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Thu, 15 May 2025 20:28:20 +0200 Subject: [PATCH 227/356] fix: person edit sidebar cursedness (#18318) --- web/src/lib/components/faces-page/assign-face-side-panel.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte index e8a774a364..d45a8d2320 100644 --- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte +++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte @@ -74,7 +74,7 @@
    {#if !searchFaces} From 61173290579226e5790e64dfafec7c87701ebb1d Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Thu, 15 May 2025 13:34:33 -0500 Subject: [PATCH 228/356] feat: add session creation endpoint (#18295) --- mobile/openapi/README.md | 3 + mobile/openapi/lib/api.dart | 2 + mobile/openapi/lib/api/sessions_api.dart | 47 ++++++ mobile/openapi/lib/api_client.dart | 4 + mobile/openapi/lib/model/permission.dart | 3 + .../openapi/lib/model/session_create_dto.dart | 145 +++++++++++++++++ .../model/session_create_response_dto.dart | 147 ++++++++++++++++++ open-api/immich-openapi-specs.json | 92 +++++++++++ open-api/typescript-sdk/src/fetch-client.ts | 28 ++++ server/src/controllers/session.controller.ts | 10 +- server/src/db.d.ts | 2 + server/src/dtos/session.dto.ts | 24 +++ server/src/enum.ts | 1 + server/src/queries/session.repository.sql | 4 + server/src/repositories/crypto.repository.ts | 2 +- server/src/repositories/session.repository.ts | 17 ++ .../1747329504572-AddNewSessionColumns.ts | 15 ++ server/src/schema/tables/session.table.ts | 6 + server/src/services/api-key.service.spec.ts | 8 +- server/src/services/api-key.service.ts | 7 +- server/src/services/auth.service.ts | 8 +- server/src/services/cli.service.ts | 2 +- server/src/services/session.service.spec.ts | 25 +-- server/src/services/session.service.ts | 33 ++-- .../repositories/crypto.repository.mock.ts | 2 +- server/test/small.factory.ts | 2 + .../user-settings-page/device-card.svelte | 3 + 27 files changed, 592 insertions(+), 50 deletions(-) create mode 100644 mobile/openapi/lib/model/session_create_dto.dart create mode 100644 mobile/openapi/lib/model/session_create_response_dto.dart create mode 100644 server/src/schema/migrations/1747329504572-AddNewSessionColumns.ts diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 3aed98adf1..9544b2ddab 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -194,6 +194,7 @@ Class | Method | HTTP request | Description *ServerApi* | [**getVersionHistory**](doc//ServerApi.md#getversionhistory) | **GET** /server/version-history | *ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping | *ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license | +*SessionsApi* | [**createSession**](doc//SessionsApi.md#createsession) | **POST** /sessions | *SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions | *SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} | *SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions | @@ -420,6 +421,8 @@ Class | Method | HTTP request | Description - [ServerThemeDto](doc//ServerThemeDto.md) - [ServerVersionHistoryResponseDto](doc//ServerVersionHistoryResponseDto.md) - [ServerVersionResponseDto](doc//ServerVersionResponseDto.md) + - [SessionCreateDto](doc//SessionCreateDto.md) + - [SessionCreateResponseDto](doc//SessionCreateResponseDto.md) - [SessionResponseDto](doc//SessionResponseDto.md) - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) - [SharedLinkEditDto](doc//SharedLinkEditDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index b2cbe222e8..d0e39e0965 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -218,6 +218,8 @@ part 'model/server_storage_response_dto.dart'; part 'model/server_theme_dto.dart'; part 'model/server_version_history_response_dto.dart'; part 'model/server_version_response_dto.dart'; +part 'model/session_create_dto.dart'; +part 'model/session_create_response_dto.dart'; part 'model/session_response_dto.dart'; part 'model/shared_link_create_dto.dart'; part 'model/shared_link_edit_dto.dart'; diff --git a/mobile/openapi/lib/api/sessions_api.dart b/mobile/openapi/lib/api/sessions_api.dart index 203f801b72..9f850fb4c8 100644 --- a/mobile/openapi/lib/api/sessions_api.dart +++ b/mobile/openapi/lib/api/sessions_api.dart @@ -16,6 +16,53 @@ class SessionsApi { final ApiClient apiClient; + /// Performs an HTTP 'POST /sessions' operation and returns the [Response]. + /// Parameters: + /// + /// * [SessionCreateDto] sessionCreateDto (required): + Future createSessionWithHttpInfo(SessionCreateDto sessionCreateDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/sessions'; + + // ignore: prefer_final_locals + Object? postBody = sessionCreateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [SessionCreateDto] sessionCreateDto (required): + Future createSession(SessionCreateDto sessionCreateDto,) async { + final response = await createSessionWithHttpInfo(sessionCreateDto,); + 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), 'SessionCreateResponseDto',) as SessionCreateResponseDto; + + } + return null; + } + /// Performs an HTTP 'DELETE /sessions' operation and returns the [Response]. Future deleteAllSessionsWithHttpInfo() async { // ignore: prefer_const_declarations diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index cdd69307ad..f40d09ecc3 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -492,6 +492,10 @@ class ApiClient { return ServerVersionHistoryResponseDto.fromJson(value); case 'ServerVersionResponseDto': return ServerVersionResponseDto.fromJson(value); + case 'SessionCreateDto': + return SessionCreateDto.fromJson(value); + case 'SessionCreateResponseDto': + return SessionCreateResponseDto.fromJson(value); case 'SessionResponseDto': return SessionResponseDto.fromJson(value); case 'SharedLinkCreateDto': diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index 1735bc2eb5..73ecbd5868 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -81,6 +81,7 @@ class Permission { static const personPeriodStatistics = Permission._(r'person.statistics'); static const personPeriodMerge = Permission._(r'person.merge'); static const personPeriodReassign = Permission._(r'person.reassign'); + static const sessionPeriodCreate = Permission._(r'session.create'); static const sessionPeriodRead = Permission._(r'session.read'); static const sessionPeriodUpdate = Permission._(r'session.update'); static const sessionPeriodDelete = Permission._(r'session.delete'); @@ -166,6 +167,7 @@ class Permission { personPeriodStatistics, personPeriodMerge, personPeriodReassign, + sessionPeriodCreate, sessionPeriodRead, sessionPeriodUpdate, sessionPeriodDelete, @@ -286,6 +288,7 @@ class PermissionTypeTransformer { case r'person.statistics': return Permission.personPeriodStatistics; case r'person.merge': return Permission.personPeriodMerge; case r'person.reassign': return Permission.personPeriodReassign; + case r'session.create': return Permission.sessionPeriodCreate; case r'session.read': return Permission.sessionPeriodRead; case r'session.update': return Permission.sessionPeriodUpdate; case r'session.delete': return Permission.sessionPeriodDelete; diff --git a/mobile/openapi/lib/model/session_create_dto.dart b/mobile/openapi/lib/model/session_create_dto.dart new file mode 100644 index 0000000000..aacf1150a5 --- /dev/null +++ b/mobile/openapi/lib/model/session_create_dto.dart @@ -0,0 +1,145 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SessionCreateDto { + /// Returns a new [SessionCreateDto] instance. + SessionCreateDto({ + this.deviceOS, + this.deviceType, + this.duration, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? deviceOS; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? deviceType; + + /// session duration, in seconds + /// + /// Minimum value: 1 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// 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; + + @override + bool operator ==(Object other) => identical(this, other) || other is SessionCreateDto && + other.deviceOS == deviceOS && + other.deviceType == deviceType && + other.duration == duration; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (deviceOS == null ? 0 : deviceOS!.hashCode) + + (deviceType == null ? 0 : deviceType!.hashCode) + + (duration == null ? 0 : duration!.hashCode); + + @override + String toString() => 'SessionCreateDto[deviceOS=$deviceOS, deviceType=$deviceType, duration=$duration]'; + + Map toJson() { + final json = {}; + if (this.deviceOS != null) { + json[r'deviceOS'] = this.deviceOS; + } else { + // json[r'deviceOS'] = null; + } + if (this.deviceType != null) { + json[r'deviceType'] = this.deviceType; + } else { + // json[r'deviceType'] = null; + } + if (this.duration != null) { + json[r'duration'] = this.duration; + } else { + // json[r'duration'] = null; + } + return json; + } + + /// Returns a new [SessionCreateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SessionCreateDto? fromJson(dynamic value) { + upgradeDto(value, "SessionCreateDto"); + if (value is Map) { + final json = value.cast(); + + return SessionCreateDto( + deviceOS: mapValueOfType(json, r'deviceOS'), + deviceType: mapValueOfType(json, r'deviceType'), + duration: num.parse('${json[r'duration']}'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SessionCreateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SessionCreateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SessionCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SessionCreateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/session_create_response_dto.dart b/mobile/openapi/lib/model/session_create_response_dto.dart new file mode 100644 index 0000000000..1ef346c96a --- /dev/null +++ b/mobile/openapi/lib/model/session_create_response_dto.dart @@ -0,0 +1,147 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SessionCreateResponseDto { + /// Returns a new [SessionCreateResponseDto] instance. + SessionCreateResponseDto({ + required this.createdAt, + required this.current, + required this.deviceOS, + required this.deviceType, + required this.id, + required this.token, + required this.updatedAt, + }); + + String createdAt; + + bool current; + + String deviceOS; + + String deviceType; + + String id; + + String token; + + String updatedAt; + + @override + bool operator ==(Object other) => identical(this, other) || other is SessionCreateResponseDto && + other.createdAt == createdAt && + other.current == current && + other.deviceOS == deviceOS && + other.deviceType == deviceType && + other.id == id && + other.token == token && + other.updatedAt == updatedAt; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (createdAt.hashCode) + + (current.hashCode) + + (deviceOS.hashCode) + + (deviceType.hashCode) + + (id.hashCode) + + (token.hashCode) + + (updatedAt.hashCode); + + @override + String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, id=$id, token=$token, updatedAt=$updatedAt]'; + + Map toJson() { + final json = {}; + json[r'createdAt'] = this.createdAt; + json[r'current'] = this.current; + json[r'deviceOS'] = this.deviceOS; + json[r'deviceType'] = this.deviceType; + json[r'id'] = this.id; + json[r'token'] = this.token; + json[r'updatedAt'] = this.updatedAt; + return json; + } + + /// Returns a new [SessionCreateResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SessionCreateResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SessionCreateResponseDto"); + if (value is Map) { + final json = value.cast(); + + return SessionCreateResponseDto( + createdAt: mapValueOfType(json, r'createdAt')!, + current: mapValueOfType(json, r'current')!, + deviceOS: mapValueOfType(json, r'deviceOS')!, + deviceType: mapValueOfType(json, r'deviceType')!, + id: mapValueOfType(json, r'id')!, + token: mapValueOfType(json, r'token')!, + updatedAt: mapValueOfType(json, r'updatedAt')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SessionCreateResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SessionCreateResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SessionCreateResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SessionCreateResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'createdAt', + 'current', + 'deviceOS', + 'deviceType', + 'id', + 'token', + 'updatedAt', + }; +} + diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 2dbec35079..d4a1e219c9 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -5618,6 +5618,46 @@ "tags": [ "Sessions" ] + }, + "post": { + "operationId": "createSession", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionCreateDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionCreateResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Sessions" + ] } }, "/sessions/{id}": { @@ -11052,6 +11092,7 @@ "person.statistics", "person.merge", "person.reassign", + "session.create", "session.read", "session.update", "session.delete", @@ -12038,6 +12079,57 @@ ], "type": "object" }, + "SessionCreateDto": { + "properties": { + "deviceOS": { + "type": "string" + }, + "deviceType": { + "type": "string" + }, + "duration": { + "description": "session duration, in seconds", + "minimum": 1, + "type": "number" + } + }, + "type": "object" + }, + "SessionCreateResponseDto": { + "properties": { + "createdAt": { + "type": "string" + }, + "current": { + "type": "boolean" + }, + "deviceOS": { + "type": "string" + }, + "deviceType": { + "type": "string" + }, + "id": { + "type": "string" + }, + "token": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": [ + "createdAt", + "current", + "deviceOS", + "deviceType", + "id", + "token", + "updatedAt" + ], + "type": "object" + }, "SessionResponseDto": { "properties": { "createdAt": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index ad7413e6fd..de0a723ffa 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1078,6 +1078,21 @@ export type SessionResponseDto = { id: string; updatedAt: string; }; +export type SessionCreateDto = { + deviceOS?: string; + deviceType?: string; + /** session duration, in seconds */ + duration?: number; +}; +export type SessionCreateResponseDto = { + createdAt: string; + current: boolean; + deviceOS: string; + deviceType: string; + id: string; + token: string; + updatedAt: string; +}; export type SharedLinkResponseDto = { album?: AlbumResponseDto; allowDownload: boolean; @@ -2917,6 +2932,18 @@ export function getSessions(opts?: Oazapfts.RequestOpts) { ...opts })); } +export function createSession({ sessionCreateDto }: { + sessionCreateDto: SessionCreateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: SessionCreateResponseDto; + }>("/sessions", oazapfts.json({ + ...opts, + method: "POST", + body: sessionCreateDto + }))); +} export function deleteSession({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { @@ -3678,6 +3705,7 @@ export enum Permission { PersonStatistics = "person.statistics", PersonMerge = "person.merge", PersonReassign = "person.reassign", + SessionCreate = "session.create", SessionRead = "session.read", SessionUpdate = "session.update", SessionDelete = "session.delete", diff --git a/server/src/controllers/session.controller.ts b/server/src/controllers/session.controller.ts index d526c2e599..addcfd8fe9 100644 --- a/server/src/controllers/session.controller.ts +++ b/server/src/controllers/session.controller.ts @@ -1,7 +1,7 @@ -import { Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; -import { SessionResponseDto } from 'src/dtos/session.dto'; +import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto } from 'src/dtos/session.dto'; import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { SessionService } from 'src/services/session.service'; @@ -12,6 +12,12 @@ import { UUIDParamDto } from 'src/validation'; export class SessionController { constructor(private service: SessionService) {} + @Post() + @Authenticated({ permission: Permission.SESSION_CREATE }) + createSession(@Auth() auth: AuthDto, @Body() dto: SessionCreateDto): Promise { + return this.service.create(auth, dto); + } + @Get() @Authenticated({ permission: Permission.SESSION_READ }) getSessions(@Auth() auth: AuthDto): Promise { diff --git a/server/src/db.d.ts b/server/src/db.d.ts index 1fd7fdc22b..6efbd5f7d7 100644 --- a/server/src/db.d.ts +++ b/server/src/db.d.ts @@ -343,6 +343,8 @@ export interface Sessions { deviceOS: Generated; deviceType: Generated; id: Generated; + parentId: string | null; + expiredAt: Date | null; token: string; updatedAt: Generated; updateId: Generated; diff --git a/server/src/dtos/session.dto.ts b/server/src/dtos/session.dto.ts index b54264a5b4..f109e44fa0 100644 --- a/server/src/dtos/session.dto.ts +++ b/server/src/dtos/session.dto.ts @@ -1,4 +1,24 @@ +import { IsInt, IsPositive, IsString } from 'class-validator'; import { Session } from 'src/database'; +import { Optional } from 'src/validation'; + +export class SessionCreateDto { + /** + * session duration, in seconds + */ + @IsInt() + @IsPositive() + @Optional() + duration?: number; + + @IsString() + @Optional() + deviceType?: string; + + @IsString() + @Optional() + deviceOS?: string; +} export class SessionResponseDto { id!: string; @@ -9,6 +29,10 @@ export class SessionResponseDto { deviceOS!: string; } +export class SessionCreateResponseDto extends SessionResponseDto { + token!: string; +} + export const mapSession = (entity: Session, currentId?: string): SessionResponseDto => ({ id: entity.id, createdAt: entity.createdAt.toISOString(), diff --git a/server/src/enum.ts b/server/src/enum.ts index fedfaa6b79..c6feb27dcc 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -144,6 +144,7 @@ export enum Permission { PERSON_MERGE = 'person.merge', PERSON_REASSIGN = 'person.reassign', + SESSION_CREATE = 'session.create', SESSION_READ = 'session.read', SESSION_UPDATE = 'session.update', SESSION_DELETE = 'session.delete', diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql index c2daa2a49c..b265380a1f 100644 --- a/server/src/queries/session.repository.sql +++ b/server/src/queries/session.repository.sql @@ -36,6 +36,10 @@ from "sessions" where "sessions"."token" = $1 + and ( + "sessions"."expiredAt" is null + or "sessions"."expiredAt" > $2 + ) -- SessionRepository.getByUserId select diff --git a/server/src/repositories/crypto.repository.ts b/server/src/repositories/crypto.repository.ts index e471ccb031..c3136db456 100644 --- a/server/src/repositories/crypto.repository.ts +++ b/server/src/repositories/crypto.repository.ts @@ -54,7 +54,7 @@ export class CryptoRepository { }); } - newPassword(bytes: number) { + randomBytesAsText(bytes: number) { return randomBytes(bytes).toString('base64').replaceAll(/\W/g, ''); } } diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts index 742807dc9c..ce819470c7 100644 --- a/server/src/repositories/session.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Insertable, Kysely, Updateable } from 'kysely'; import { jsonObjectFrom } from 'kysely/helpers/postgres'; +import { DateTime } from 'luxon'; import { InjectKysely } from 'nestjs-kysely'; import { columns } from 'src/database'; import { DB, Sessions } from 'src/db'; @@ -13,6 +14,19 @@ export type SessionSearchOptions = { updatedBefore: Date }; export class SessionRepository { constructor(@InjectKysely() private db: Kysely) {} + cleanup() { + return this.db + .deleteFrom('sessions') + .where((eb) => + eb.or([ + eb('updatedAt', '<=', DateTime.now().minus({ days: 90 }).toJSDate()), + eb.and([eb('expiredAt', 'is not', null), eb('expiredAt', '<=', DateTime.now().toJSDate())]), + ]), + ) + .returning(['id', 'deviceOS', 'deviceType']) + .execute(); + } + @GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] }) search(options: SessionSearchOptions) { return this.db @@ -37,6 +51,9 @@ export class SessionRepository { ).as('user'), ]) .where('sessions.token', '=', token) + .where((eb) => + eb.or([eb('sessions.expiredAt', 'is', null), eb('sessions.expiredAt', '>', DateTime.now().toJSDate())]), + ) .executeTakeFirst(); } diff --git a/server/src/schema/migrations/1747329504572-AddNewSessionColumns.ts b/server/src/schema/migrations/1747329504572-AddNewSessionColumns.ts new file mode 100644 index 0000000000..d3cf8de173 --- /dev/null +++ b/server/src/schema/migrations/1747329504572-AddNewSessionColumns.ts @@ -0,0 +1,15 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "sessions" ADD "expiredAt" timestamp with time zone;`.execute(db); + await sql`ALTER TABLE "sessions" ADD "parentId" uuid;`.execute(db); + await sql`ALTER TABLE "sessions" ADD CONSTRAINT "FK_afbbabbd7daf5b91de4dca84de8" FOREIGN KEY ("parentId") REFERENCES "sessions" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`CREATE INDEX "IDX_afbbabbd7daf5b91de4dca84de" ON "sessions" ("parentId")`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP INDEX "IDX_afbbabbd7daf5b91de4dca84de";`.execute(db); + await sql`ALTER TABLE "sessions" DROP CONSTRAINT "FK_afbbabbd7daf5b91de4dca84de8";`.execute(db); + await sql`ALTER TABLE "sessions" DROP COLUMN "expiredAt";`.execute(db); + await sql`ALTER TABLE "sessions" DROP COLUMN "parentId";`.execute(db); +} diff --git a/server/src/schema/tables/session.table.ts b/server/src/schema/tables/session.table.ts index 090b469b54..9cc41c5bba 100644 --- a/server/src/schema/tables/session.table.ts +++ b/server/src/schema/tables/session.table.ts @@ -25,9 +25,15 @@ export class SessionTable { @UpdateDateColumn() updatedAt!: Date; + @Column({ type: 'timestamp with time zone', nullable: true }) + expiredAt!: Date | null; + @ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) userId!: string; + @ForeignKeyColumn(() => SessionTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', nullable: true }) + parentId!: string | null; + @Column({ default: '' }) deviceType!: string; diff --git a/server/src/services/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts index 680cd38f1e..784c944146 100644 --- a/server/src/services/api-key.service.spec.ts +++ b/server/src/services/api-key.service.spec.ts @@ -18,7 +18,7 @@ describe(ApiKeyService.name, () => { const apiKey = factory.apiKey({ userId: auth.user.id, permissions: [Permission.ALL] }); const key = 'super-secret'; - mocks.crypto.newPassword.mockReturnValue(key); + mocks.crypto.randomBytesAsText.mockReturnValue(key); mocks.apiKey.create.mockResolvedValue(apiKey); await sut.create(auth, { name: apiKey.name, permissions: apiKey.permissions }); @@ -29,7 +29,7 @@ describe(ApiKeyService.name, () => { permissions: apiKey.permissions, userId: apiKey.userId, }); - expect(mocks.crypto.newPassword).toHaveBeenCalled(); + expect(mocks.crypto.randomBytesAsText).toHaveBeenCalled(); expect(mocks.crypto.hashSha256).toHaveBeenCalled(); }); @@ -38,7 +38,7 @@ describe(ApiKeyService.name, () => { const apiKey = factory.apiKey({ userId: auth.user.id }); const key = 'super-secret'; - mocks.crypto.newPassword.mockReturnValue(key); + mocks.crypto.randomBytesAsText.mockReturnValue(key); mocks.apiKey.create.mockResolvedValue(apiKey); await sut.create(auth, { permissions: [Permission.ALL] }); @@ -49,7 +49,7 @@ describe(ApiKeyService.name, () => { permissions: [Permission.ALL], userId: auth.user.id, }); - expect(mocks.crypto.newPassword).toHaveBeenCalled(); + expect(mocks.crypto.randomBytesAsText).toHaveBeenCalled(); expect(mocks.crypto.hashSha256).toHaveBeenCalled(); }); diff --git a/server/src/services/api-key.service.ts b/server/src/services/api-key.service.ts index 33861d82cd..49d4183b01 100644 --- a/server/src/services/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -9,20 +9,21 @@ import { isGranted } from 'src/utils/access'; @Injectable() export class ApiKeyService extends BaseService { async create(auth: AuthDto, dto: APIKeyCreateDto): Promise { - const secret = this.cryptoRepository.newPassword(32); + const token = this.cryptoRepository.randomBytesAsText(32); + const tokenHashed = this.cryptoRepository.hashSha256(token); if (auth.apiKey && !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })) { throw new BadRequestException('Cannot grant permissions you do not have'); } const entity = await this.apiKeyRepository.create({ - key: this.cryptoRepository.hashSha256(secret), + key: tokenHashed, name: dto.name || 'API Key', userId: auth.user.id, permissions: dto.permissions, }); - return { secret, apiKey: this.map(entity) }; + return { secret: token, apiKey: this.map(entity) }; } async update(auth: AuthDto, id: string, dto: APIKeyUpdateDto): Promise { diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 496c252643..7bda2eeb98 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -492,17 +492,17 @@ export class AuthService extends BaseService { } private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) { - const key = this.cryptoRepository.newPassword(32); - const token = this.cryptoRepository.hashSha256(key); + const token = this.cryptoRepository.randomBytesAsText(32); + const tokenHashed = this.cryptoRepository.hashSha256(token); await this.sessionRepository.create({ - token, + token: tokenHashed, deviceOS: loginDetails.deviceOS, deviceType: loginDetails.deviceType, userId: user.id, }); - return mapLoginResponse(user, key); + return mapLoginResponse(user, token); } private getClaim(profile: OAuthProfile, options: ClaimOptions): T { diff --git a/server/src/services/cli.service.ts b/server/src/services/cli.service.ts index 87e004845d..f6173c69f7 100644 --- a/server/src/services/cli.service.ts +++ b/server/src/services/cli.service.ts @@ -17,7 +17,7 @@ export class CliService extends BaseService { } const providedPassword = await ask(mapUserAdmin(admin)); - const password = providedPassword || this.cryptoRepository.newPassword(24); + const password = providedPassword || this.cryptoRepository.randomBytesAsText(24); const hashedPassword = await this.cryptoRepository.hashBcrypt(password, SALT_ROUNDS); await this.userRepository.update(admin.id, { password: hashedPassword }); diff --git a/server/src/services/session.service.spec.ts b/server/src/services/session.service.spec.ts index 6e26b26407..7ac338da80 100644 --- a/server/src/services/session.service.spec.ts +++ b/server/src/services/session.service.spec.ts @@ -17,30 +17,9 @@ describe('SessionService', () => { }); describe('handleCleanup', () => { - it('should return skipped if nothing is to be deleted', async () => { - mocks.session.search.mockResolvedValue([]); - await expect(sut.handleCleanup()).resolves.toEqual(JobStatus.SKIPPED); - expect(mocks.session.search).toHaveBeenCalled(); - }); - - it('should delete sessions', async () => { - mocks.session.search.mockResolvedValue([ - { - createdAt: new Date('1970-01-01T00:00:00.00Z'), - updatedAt: new Date('1970-01-02T00:00:00.00Z'), - deviceOS: '', - deviceType: '', - id: '123', - token: '420', - userId: '42', - updateId: 'uuid-v7', - pinExpiresAt: null, - }, - ]); - mocks.session.delete.mockResolvedValue(); - + it('should clean sessions', async () => { + mocks.session.cleanup.mockResolvedValue([]); await expect(sut.handleCleanup()).resolves.toEqual(JobStatus.SUCCESS); - expect(mocks.session.delete).toHaveBeenCalledWith('123'); }); }); diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts index 6b0632cd44..9f49cda07f 100644 --- a/server/src/services/session.service.ts +++ b/server/src/services/session.service.ts @@ -1,8 +1,8 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { OnJob } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; -import { SessionResponseDto, mapSession } from 'src/dtos/session.dto'; +import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto, mapSession } from 'src/dtos/session.dto'; import { JobName, JobStatus, Permission, QueueName } from 'src/enum'; import { BaseService } from 'src/services/base.service'; @@ -10,16 +10,8 @@ import { BaseService } from 'src/services/base.service'; export class SessionService extends BaseService { @OnJob({ name: JobName.CLEAN_OLD_SESSION_TOKENS, queue: QueueName.BACKGROUND_TASK }) async handleCleanup(): Promise { - const sessions = await this.sessionRepository.search({ - updatedBefore: DateTime.now().minus({ days: 90 }).toJSDate(), - }); - - if (sessions.length === 0) { - return JobStatus.SKIPPED; - } - + const sessions = await this.sessionRepository.cleanup(); for (const session of sessions) { - await this.sessionRepository.delete(session.id); this.logger.verbose(`Deleted expired session token: ${session.deviceOS}/${session.deviceType}`); } @@ -28,6 +20,25 @@ export class SessionService extends BaseService { return JobStatus.SUCCESS; } + async create(auth: AuthDto, dto: SessionCreateDto): Promise { + if (!auth.session) { + throw new BadRequestException('This endpoint can only be used with a session token'); + } + + const token = this.cryptoRepository.randomBytesAsText(32); + const tokenHashed = this.cryptoRepository.hashSha256(token); + const session = await this.sessionRepository.create({ + parentId: auth.session.id, + userId: auth.user.id, + expiredAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null, + deviceType: dto.deviceType, + deviceOS: dto.deviceOS, + token: tokenHashed, + }); + + return { ...mapSession(session), token }; + } + async getAll(auth: AuthDto): Promise { const sessions = await this.sessionRepository.getByUserId(auth.user.id); return sessions.map((session) => mapSession(session, auth.session?.id)); diff --git a/server/test/repositories/crypto.repository.mock.ts b/server/test/repositories/crypto.repository.mock.ts index 9d32a88987..1167923c0c 100644 --- a/server/test/repositories/crypto.repository.mock.ts +++ b/server/test/repositories/crypto.repository.mock.ts @@ -12,6 +12,6 @@ export const newCryptoRepositoryMock = (): Mocked true), hashSha1: vitest.fn().mockImplementation((input) => Buffer.from(`${input.toString()} (hashed)`)), hashFile: vitest.fn().mockImplementation((input) => `${input} (file-hashed)`), - newPassword: vitest.fn().mockReturnValue(Buffer.from('random-bytes').toString('base64')), + randomBytesAsText: vitest.fn().mockReturnValue(Buffer.from('random-bytes').toString('base64')), }; }; diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 01091854fa..231deeba83 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -126,6 +126,8 @@ const sessionFactory = (session: Partial = {}) => ({ deviceOS: 'android', deviceType: 'mobile', token: 'abc123', + parentId: null, + expiredAt: null, userId: newUuid(), pinExpiresAt: newDate(), ...session, diff --git a/web/src/lib/components/user-settings-page/device-card.svelte b/web/src/lib/components/user-settings-page/device-card.svelte index ad0b621921..47636fe4bf 100644 --- a/web/src/lib/components/user-settings-page/device-card.svelte +++ b/web/src/lib/components/user-settings-page/device-card.svelte @@ -7,6 +7,7 @@ mdiAndroid, mdiApple, mdiAppleSafari, + mdiCast, mdiGoogleChrome, mdiHelp, mdiLinux, @@ -46,6 +47,8 @@ {:else if device.deviceOS === 'Chrome OS' || device.deviceType === 'Chrome' || device.deviceType === 'Chromium' || device.deviceType === 'Mobile Chrome'} + {:else if device.deviceOS === 'Google Cast'} + {:else} {/if} From c046651f234d09bac3fa0369eb4c06f3598638a6 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 15 May 2025 14:45:23 -0400 Subject: [PATCH 229/356] feat(web): continue after login (#18302) --- web/src/lib/utils/auth.ts | 4 ++-- web/src/routes/(user)/albums/+page.ts | 4 ++-- .../[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts | 4 ++-- .../(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.ts | 4 ++-- web/src/routes/(user)/buy/+page.ts | 2 +- web/src/routes/(user)/explore/+page.ts | 4 ++-- .../favorites/[[photos=photos]]/[[assetId=id]]/+page.ts | 4 ++-- .../(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts | 2 +- .../(user)/map/[[photos=photos]]/[[assetId=id]]/+page.ts | 4 ++-- .../(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.ts | 4 ++-- .../[userId]/[[photos=photos]]/[[assetId=id]]/+page.ts | 4 ++-- web/src/routes/(user)/people/+page.ts | 4 ++-- .../[personId]/[[photos=photos]]/[[assetId=id]]/+page.ts | 4 ++-- web/src/routes/(user)/photos/[[assetId=id]]/+page.ts | 4 ++-- web/src/routes/(user)/places/+page.ts | 4 ++-- .../(user)/search/[[photos=photos]]/[[assetId=id]]/+page.ts | 4 ++-- .../share/[key]/[[photos=photos]]/[[assetId=id]]/+page.ts | 4 ++-- web/src/routes/(user)/shared-links/[[id=id]]/+page.ts | 4 ++-- web/src/routes/(user)/sharing/+page.ts | 4 ++-- .../(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts | 2 +- .../(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.ts | 4 ++-- web/src/routes/(user)/user-settings/+page.ts | 4 ++-- web/src/routes/(user)/utilities/+page.ts | 4 ++-- .../duplicates/[[photos=photos]]/[[assetId=id]]/+page.ts | 4 ++-- web/src/routes/admin/jobs-status/+page.ts | 4 ++-- web/src/routes/admin/library-management/+page.ts | 4 ++-- web/src/routes/admin/server-status/+page.ts | 4 ++-- web/src/routes/admin/system-settings/+page.ts | 4 ++-- web/src/routes/admin/users/+page.ts | 4 ++-- web/src/routes/admin/users/[id]/+page.ts | 4 ++-- web/src/routes/auth/change-password/+page.ts | 4 ++-- web/src/routes/auth/login/+page.svelte | 3 ++- web/src/routes/auth/login/+page.ts | 3 ++- web/src/routes/auth/onboarding/+page.ts | 4 ++-- 34 files changed, 65 insertions(+), 63 deletions(-) diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts index 9b78c345e2..c22b706631 100644 --- a/web/src/lib/utils/auth.ts +++ b/web/src/lib/utils/auth.ts @@ -50,7 +50,7 @@ const hasAuthCookie = (): boolean => { return false; }; -export const authenticate = async (options?: AuthOptions) => { +export const authenticate = async (url: URL, options?: AuthOptions) => { const { public: publicRoute, admin: adminRoute } = options || {}; const user = await loadUser(); @@ -59,7 +59,7 @@ export const authenticate = async (options?: AuthOptions) => { } if (!user) { - redirect(302, AppRoute.AUTH_LOGIN); + redirect(302, `${AppRoute.AUTH_LOGIN}?continue=${encodeURIComponent(url.pathname + url.search)}`); } if (adminRoute && !user.isAdmin) { diff --git a/web/src/routes/(user)/albums/+page.ts b/web/src/routes/(user)/albums/+page.ts index e56d0f06b7..f4527d56d2 100644 --- a/web/src/routes/(user)/albums/+page.ts +++ b/web/src/routes/(user)/albums/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getAllAlbums } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async () => { - await authenticate(); +export const load = (async ({ url }) => { + await authenticate(url); const sharedAlbums = await getAllAlbums({ shared: true }); const albums = await getAllAlbums({}); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts index 0143390974..f8691b5fd1 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -3,8 +3,8 @@ import { getAssetInfoFromParam } from '$lib/utils/navigation'; import { getAlbumInfo } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async ({ params }) => { - await authenticate(); +export const load = (async ({ params, url }) => { + await authenticate(url); const [album, asset] = await Promise.all([ getAlbumInfo({ id: params.albumId, withoutAssets: true }), getAssetInfoFromParam(params), diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.ts index c44ba64d5b..f5d4560505 100644 --- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getAssetInfoFromParam } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; -export const load = (async ({ params }) => { - await authenticate(); +export const load = (async ({ params, url }) => { + await authenticate(url); const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/buy/+page.ts b/web/src/routes/(user)/buy/+page.ts index ba55948b1e..d0180b39ff 100644 --- a/web/src/routes/(user)/buy/+page.ts +++ b/web/src/routes/(user)/buy/+page.ts @@ -5,7 +5,7 @@ import { activateProduct, getActivationKey } from '$lib/utils/license-utils'; import type { PageLoad } from './$types'; export const load = (async ({ url }) => { - await authenticate(); + await authenticate(url); const $t = await getFormatter(); const licenseKey = url.searchParams.get('licenseKey'); diff --git a/web/src/routes/(user)/explore/+page.ts b/web/src/routes/(user)/explore/+page.ts index 84ec944efe..9005f7dced 100644 --- a/web/src/routes/(user)/explore/+page.ts +++ b/web/src/routes/(user)/explore/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getAllPeople, getExploreData } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async () => { - await authenticate(); +export const load = (async ({ url }) => { + await authenticate(url); const [items, response] = await Promise.all([getExploreData(), getAllPeople({ withHidden: false })]); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.ts index be828b69dd..0d9fe7a203 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getAssetInfoFromParam } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; -export const load = (async ({ params }) => { - await authenticate(); +export const load = (async ({ params, url }) => { + await authenticate(url); const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts index d00ba238ef..7fd0a749c0 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -7,7 +7,7 @@ import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; import type { PageLoad } from './$types'; export const load = (async ({ params, url }) => { - await authenticate(); + await authenticate(url); const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.ts index 490e1430e6..add9882bcd 100644 --- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getAssetInfoFromParam } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; -export const load = (async ({ params }) => { - await authenticate(); +export const load = (async ({ params, url }) => { + await authenticate(url); const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.ts index e323fca182..5c030da72f 100644 --- a/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getAssetInfoFromParam } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; -export const load = (async ({ params }) => { - const user = await authenticate(); +export const load = (async ({ params, url }) => { + const user = await authenticate(url); const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.ts index 1395a3e8d3..1977d9a095 100644 --- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -4,8 +4,8 @@ import { getAssetInfoFromParam } from '$lib/utils/navigation'; import { getUser } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async ({ params }) => { - await authenticate(); +export const load = (async ({ params, url }) => { + await authenticate(url); const partner = await getUser({ id: params.userId }); const asset = await getAssetInfoFromParam(params); diff --git a/web/src/routes/(user)/people/+page.ts b/web/src/routes/(user)/people/+page.ts index 305ba31da6..35ed6c06c4 100644 --- a/web/src/routes/(user)/people/+page.ts +++ b/web/src/routes/(user)/people/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getAllPeople } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async () => { - await authenticate(); +export const load = (async ({ url }) => { + await authenticate(url); const people = await getAllPeople({ withHidden: true }); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.ts index 88e223640f..92371bd34e 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -4,8 +4,8 @@ import { getAssetInfoFromParam } from '$lib/utils/navigation'; import { getPerson, getPersonStatistics } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async ({ params }) => { - await authenticate(); +export const load = (async ({ params, url }) => { + await authenticate(url); const [person, statistics, asset] = await Promise.all([ getPerson({ id: params.personId }), diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.ts b/web/src/routes/(user)/photos/[[assetId=id]]/+page.ts index 6e9384f853..209b5483a8 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getAssetInfoFromParam } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; -export const load = (async ({ params }) => { - await authenticate(); +export const load = (async ({ params, url }) => { + await authenticate(url); const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/places/+page.ts b/web/src/routes/(user)/places/+page.ts index a0c421ef3a..9449f416be 100644 --- a/web/src/routes/(user)/places/+page.ts +++ b/web/src/routes/(user)/places/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getAssetsByCity } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async () => { - await authenticate(); +export const load = (async ({ url }) => { + await authenticate(url); const items = await getAssetsByCity(); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.ts index 23871d8bdf..82dd18acaa 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getAssetInfoFromParam } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; -export const load = (async ({ params }) => { - await authenticate(); +export const load = (async ({ params, url }) => { + await authenticate(url); const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.ts index 66fc3552c7..c0edb5e669 100644 --- a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -5,9 +5,9 @@ import { getAssetInfoFromParam } from '$lib/utils/navigation'; import { getMySharedLink, isHttpError } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async ({ params }) => { +export const load = (async ({ params, url }) => { const { key } = params; - await authenticate({ public: true }); + await authenticate(url, { public: true }); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/shared-links/[[id=id]]/+page.ts b/web/src/routes/(user)/shared-links/[[id=id]]/+page.ts index 920e5bdba4..f61484a910 100644 --- a/web/src/routes/(user)/shared-links/[[id=id]]/+page.ts +++ b/web/src/routes/(user)/shared-links/[[id=id]]/+page.ts @@ -2,8 +2,8 @@ import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; import type { PageLoad } from './$types'; -export const load = (async () => { - await authenticate(); +export const load = (async ({ url }) => { + await authenticate(url); const $t = await getFormatter(); return { diff --git a/web/src/routes/(user)/sharing/+page.ts b/web/src/routes/(user)/sharing/+page.ts index b1872ca9f2..2bf737dfc7 100644 --- a/web/src/routes/(user)/sharing/+page.ts +++ b/web/src/routes/(user)/sharing/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { PartnerDirection, getAllAlbums, getPartners } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async () => { - await authenticate(); +export const load = (async ({ url }) => { + await authenticate(url); const sharedAlbums = await getAllAlbums({ shared: true }); const partners = await getPartners({ direction: PartnerDirection.SharedWith }); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts index 23846e57c4..6e92eda7d3 100644 --- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -7,7 +7,7 @@ import { getAllTags } from '@immich/sdk'; import type { PageLoad } from './$types'; export const load = (async ({ params, url }) => { - await authenticate(); + await authenticate(url); const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.ts index 926af322ca..79c41892c7 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getAssetInfoFromParam } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; -export const load = (async ({ params }) => { - await authenticate(); +export const load = (async ({ params, url }) => { + await authenticate(url); const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/user-settings/+page.ts b/web/src/routes/(user)/user-settings/+page.ts index 15b8d8125c..bf36eeefb5 100644 --- a/web/src/routes/(user)/user-settings/+page.ts +++ b/web/src/routes/(user)/user-settings/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getApiKeys, getSessions } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async () => { - await authenticate(); +export const load = (async ({ url }) => { + await authenticate(url); const keys = await getApiKeys(); const sessions = await getSessions(); diff --git a/web/src/routes/(user)/utilities/+page.ts b/web/src/routes/(user)/utilities/+page.ts index a0420a575b..af241d0fd7 100644 --- a/web/src/routes/(user)/utilities/+page.ts +++ b/web/src/routes/(user)/utilities/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getAssetInfoFromParam } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; -export const load = (async ({ params }) => { - await authenticate(); +export const load = (async ({ params, url }) => { + await authenticate(url); const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.ts index a7faaed3c3..978f50830e 100644 --- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -4,8 +4,8 @@ import { getAssetInfoFromParam } from '$lib/utils/navigation'; import { getAssetDuplicates } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async ({ params }) => { - await authenticate(); +export const load = (async ({ params, url }) => { + await authenticate(url); const asset = await getAssetInfoFromParam(params); const duplicates = await getAssetDuplicates(); const $t = await getFormatter(); diff --git a/web/src/routes/admin/jobs-status/+page.ts b/web/src/routes/admin/jobs-status/+page.ts index 8044b61861..0d4ec8b41f 100644 --- a/web/src/routes/admin/jobs-status/+page.ts +++ b/web/src/routes/admin/jobs-status/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getAllJobsStatus } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async () => { - await authenticate({ admin: true }); +export const load = (async ({ url }) => { + await authenticate(url, { admin: true }); const jobs = await getAllJobsStatus(); const $t = await getFormatter(); diff --git a/web/src/routes/admin/library-management/+page.ts b/web/src/routes/admin/library-management/+page.ts index 71bc835a6f..735c7fac92 100644 --- a/web/src/routes/admin/library-management/+page.ts +++ b/web/src/routes/admin/library-management/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { searchUsersAdmin } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async () => { - await authenticate({ admin: true }); +export const load = (async ({ url }) => { + await authenticate(url, { admin: true }); await requestServerInfo(); const allUsers = await searchUsersAdmin({ withDeleted: false }); const $t = await getFormatter(); diff --git a/web/src/routes/admin/server-status/+page.ts b/web/src/routes/admin/server-status/+page.ts index 39ce96ae41..7450550737 100644 --- a/web/src/routes/admin/server-status/+page.ts +++ b/web/src/routes/admin/server-status/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getServerStatistics } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async () => { - await authenticate({ admin: true }); +export const load = (async ({ url }) => { + await authenticate(url, { admin: true }); const stats = await getServerStatistics(); const $t = await getFormatter(); diff --git a/web/src/routes/admin/system-settings/+page.ts b/web/src/routes/admin/system-settings/+page.ts index 555835e017..294096a4be 100644 --- a/web/src/routes/admin/system-settings/+page.ts +++ b/web/src/routes/admin/system-settings/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { getConfig } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async () => { - await authenticate({ admin: true }); +export const load = (async ({ url }) => { + await authenticate(url, { admin: true }); const configs = await getConfig(); const $t = await getFormatter(); diff --git a/web/src/routes/admin/users/+page.ts b/web/src/routes/admin/users/+page.ts index 0a6af40c69..521f8573e1 100644 --- a/web/src/routes/admin/users/+page.ts +++ b/web/src/routes/admin/users/+page.ts @@ -3,8 +3,8 @@ import { getFormatter } from '$lib/utils/i18n'; import { searchUsersAdmin } from '@immich/sdk'; import type { PageLoad } from './$types'; -export const load = (async () => { - await authenticate({ admin: true }); +export const load = (async ({ url }) => { + await authenticate(url, { admin: true }); await requestServerInfo(); const allUsers = await searchUsersAdmin({ withDeleted: true }); const $t = await getFormatter(); diff --git a/web/src/routes/admin/users/[id]/+page.ts b/web/src/routes/admin/users/[id]/+page.ts index 7e2930c46a..c6e918d648 100644 --- a/web/src/routes/admin/users/[id]/+page.ts +++ b/web/src/routes/admin/users/[id]/+page.ts @@ -5,8 +5,8 @@ import { getUserPreferencesAdmin, getUserStatisticsAdmin, searchUsersAdmin } fro import { redirect } from '@sveltejs/kit'; import type { PageLoad } from './$types'; -export const load = (async ({ params }) => { - await authenticate({ admin: true }); +export const load = (async ({ params, url }) => { + await authenticate(url, { admin: true }); await requestServerInfo(); const [user] = await searchUsersAdmin({ id: params.id, withDeleted: true }).catch(() => []); if (!user) { diff --git a/web/src/routes/auth/change-password/+page.ts b/web/src/routes/auth/change-password/+page.ts index 19abb2e832..c4331b73cc 100644 --- a/web/src/routes/auth/change-password/+page.ts +++ b/web/src/routes/auth/change-password/+page.ts @@ -6,8 +6,8 @@ import { redirect } from '@sveltejs/kit'; import { get } from 'svelte/store'; import type { PageLoad } from './$types'; -export const load = (async () => { - await authenticate(); +export const load = (async ({ url }) => { + await authenticate(url); if (!get(user).shouldChangePassword) { redirect(302, AppRoute.PHOTOS); } diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index fdad88e1ff..5cce88ae2c 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -26,7 +26,8 @@ let oauthLoading = $state(true); const onSuccess = async (user: LoginResponseDto) => { - await goto(AppRoute.PHOTOS, { invalidateAll: true }); + console.log(data.continueUrl); + await goto(data.continueUrl, { invalidateAll: true }); eventManager.emit('auth.login', user); }; diff --git a/web/src/routes/auth/login/+page.ts b/web/src/routes/auth/login/+page.ts index 847992ab20..54c5da716a 100644 --- a/web/src/routes/auth/login/+page.ts +++ b/web/src/routes/auth/login/+page.ts @@ -6,7 +6,7 @@ import { redirect } from '@sveltejs/kit'; import { get } from 'svelte/store'; import type { PageLoad } from './$types'; -export const load = (async ({ parent }) => { +export const load = (async ({ parent, url }) => { await parent(); const { isInitialized } = get(serverConfig); @@ -20,5 +20,6 @@ export const load = (async ({ parent }) => { meta: { title: $t('login'), }, + continueUrl: url.searchParams.get('continue') || AppRoute.PHOTOS, }; }) satisfies PageLoad; diff --git a/web/src/routes/auth/onboarding/+page.ts b/web/src/routes/auth/onboarding/+page.ts index db16c8e514..86c19c10a8 100644 --- a/web/src/routes/auth/onboarding/+page.ts +++ b/web/src/routes/auth/onboarding/+page.ts @@ -2,8 +2,8 @@ import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; import type { PageLoad } from './$types'; -export const load = (async () => { - await authenticate({ admin: true }); +export const load = (async ({ url }) => { + await authenticate(url, { admin: true }); const $t = await getFormatter(); From ecb66fdb2c3aa0858dc92fadbff2946b3574c091 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 15 May 2025 17:55:16 -0400 Subject: [PATCH 230/356] fix: check i18n are sorted (#18324) --- .github/workflows/test.yml | 43 +++++++++++ i18n/en.json | 72 +++++++++---------- web/package.json | 3 +- .../[[photos=photos]]/[[assetId=id]]/+page.ts | 2 +- web/src/routes/auth/pin-prompt/+page.ts | 2 +- 5 files changed, 83 insertions(+), 39 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f36b01518e..91f4ffce4f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,6 +17,7 @@ jobs: permissions: contents: read outputs: + should_run_i18n: ${{ steps.found_paths.outputs.i18n == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_cli: ${{ steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }} @@ -36,6 +37,8 @@ jobs: uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 with: filters: | + i18n: + - 'i18n/**' web: - 'web/**' - 'i18n/**' @@ -262,6 +265,46 @@ jobs: run: npm run test:cov if: ${{ !cancelled() }} + i18n-tests: + name: Test i18n + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_i18n == 'true' }} + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: './web/.nvmrc' + + - name: Install dependencies + run: npm --prefix=web ci + + - name: Format + run: npm --prefix=web run format:i18n + + - name: Find file changes + uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 + id: verify-changed-files + with: + files: | + i18n/** + + - name: Verify files have not changed + if: steps.verify-changed-files.outputs.files_changed == 'true' + env: + CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }} + run: | + echo "ERROR: i18n files not up to date!" + echo "Changed files: ${CHANGED_FILES}" + exit 1 + e2e-tests-lint: name: End-to-End Lint needs: pre-job diff --git a/i18n/en.json b/i18n/en.json index 05b236b33a..e4fc825cda 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1,32 +1,4 @@ { - "new_pin_code_subtitle": "This is your first time accessing the locked folder. Create a PIN code to securely access this page", - "enter_your_pin_code": "Enter your PIN code", - "enter_your_pin_code_subtitle": "Enter your PIN code to access the locked folder", - "pin_verification": "PIN code verification", - "wrong_pin_code": "Wrong PIN code", - "nothing_here_yet": "Nothing here yet", - "move_to_locked_folder": "Move to Locked Folder", - "remove_from_locked_folder": "Remove from Locked Folder", - "move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the Locked Folder", - "remove_from_locked_folder_confirmation": "Are you sure you want to move these photos and videos out of Locked Folder? They will be visible in your library", - "move": "Move", - "no_locked_photos_message": "Photos and videos in Locked Folder are hidden and won't show up as you browser your library.", - "locked_folder": "Locked Folder", - "add_to_locked_folder": "Add to Locked Folder", - "move_off_locked_folder": "Move out of Locked Folder", - "user_pin_code_settings": "PIN Code", - "user_pin_code_settings_description": "Manage your PIN code", - "current_pin_code": "Current PIN code", - "new_pin_code": "New PIN code", - "setup_pin_code": "Setup a PIN code", - "confirm_new_pin_code": "Confirm new PIN code", - "change_pin_code": "Change PIN code", - "unable_to_change_pin_code": "Unable to change PIN code", - "unable_to_setup_pin_code": "Unable to setup PIN code", - "pin_code_changed_successfully": "Successfully changed PIN code", - "pin_code_setup_successfully": "Successfully setup a PIN code", - "pin_code_reset_successfully": "Successfully reset PIN code", - "reset_pin_code": "Reset PIN code", "about": "About", "account": "Account", "account_settings": "Account Settings", @@ -54,6 +26,7 @@ "add_to_album": "Add to album", "add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_already_exists": "Already in {album}", + "add_to_locked_folder": "Add to Locked Folder", "add_to_shared_album": "Add to shared album", "add_url": "Add URL", "added_to_archive": "Added to archive", @@ -640,6 +613,7 @@ "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", + "change_pin_code": "Change PIN code", "change_your_password": "Change your password", "changed_visibility_successfully": "Changed visibility successfully", "check_all": "Check All", @@ -680,6 +654,7 @@ "confirm_delete_face": "Are you sure you want to delete {name} face from the asset?", "confirm_delete_shared_link": "Are you sure you want to delete this shared link?", "confirm_keep_this_delete_others": "All other assets in the stack will be deleted except for this asset. Are you sure you want to continue?", + "confirm_new_pin_code": "Confirm new PIN code", "confirm_password": "Confirm password", "contain": "Contain", "context": "Context", @@ -722,9 +697,11 @@ "create_tag_description": "Create a new tag. For nested tags, please enter the full path of the tag including forward slashes.", "create_user": "Create user", "created": "Created", + "created_at": "Created", "crop": "Crop", "curated_object_page_title": "Things", "current_device": "Current device", + "current_pin_code": "Current PIN code", "current_server_address": "Current server address", "custom_locale": "Custom Locale", "custom_locale_description": "Format dates and numbers based on the language and the region", @@ -837,6 +814,7 @@ "editor_crop_tool_h2_aspect_ratios": "Aspect ratios", "editor_crop_tool_h2_rotation": "Rotation", "email": "Email", + "email_notifications": "Email notifications", "empty_folder": "This folder is empty", "empty_trash": "Empty trash", "empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!", @@ -845,6 +823,8 @@ "end_date": "End date", "enqueued": "Enqueued", "enter_wifi_name": "Enter Wi-Fi name", + "enter_your_pin_code": "Enter your PIN code", + "enter_your_pin_code_subtitle": "Enter your PIN code to access the locked folder", "error": "Error", "error_change_sort_album": "Failed to change album sort order", "error_delete_face": "Error deleting face from asset", @@ -852,7 +832,6 @@ "error_saving_image": "Error: {error}", "error_title": "Error - Something went wrong", "errors": { - "unable_to_move_to_locked_folder": "Unable to move to locked folder", "cannot_navigate_next_asset": "Cannot navigate to the next asset", "cannot_navigate_previous_asset": "Cannot navigate to previous asset", "cant_apply_changes": "Can't apply changes", @@ -940,6 +919,7 @@ "unable_to_log_out_all_devices": "Unable to log out all devices", "unable_to_log_out_device": "Unable to log out device", "unable_to_login_with_oauth": "Unable to login with OAuth", + "unable_to_move_to_locked_folder": "Unable to move to locked folder", "unable_to_play_video": "Unable to play video", "unable_to_reassign_assets_existing_person": "Unable to reassign assets to {name, select, null {an existing person} other {{name}}}", "unable_to_reassign_assets_new_person": "Unable to reassign assets to a new person", @@ -1080,6 +1060,7 @@ "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "host": "Host", "hour": "Hour", + "id": "ID", "ignore_icloud_photos": "Ignore iCloud photos", "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", "image": "Image", @@ -1161,6 +1142,7 @@ "location_picker_latitude_hint": "Enter your latitude here", "location_picker_longitude_error": "Enter a valid longitude", "location_picker_longitude_hint": "Enter your longitude here", + "locked_folder": "Locked Folder", "log_out": "Log out", "log_out_all_devices": "Log Out All Devices", "logged_out_all_devices": "Logged out all devices", @@ -1229,8 +1211,8 @@ "map_settings_only_show_favorites": "Show Favorite Only", "map_settings_theme_settings": "Map Theme", "map_zoom_to_see_photos": "Zoom out to see photos", - "mark_as_read": "Mark as read", "mark_all_as_read": "Mark all as read", + "mark_as_read": "Mark as read", "marked_all_as_read": "Marked all as read", "matches": "Matches", "media_type": "Media type", @@ -1258,6 +1240,10 @@ "month": "Month", "monthly_title_text_date_format": "MMMM y", "more": "More", + "move": "Move", + "move_off_locked_folder": "Move out of Locked Folder", + "move_to_locked_folder": "Move to Locked Folder", + "move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the Locked Folder", "moved_to_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive", "moved_to_library": "Moved {count, plural, one {# asset} other {# assets}} to library", "moved_to_trash": "Moved to trash", @@ -1274,6 +1260,8 @@ "new_api_key": "New API Key", "new_password": "New password", "new_person": "New person", + "new_pin_code": "New PIN code", + "new_pin_code_subtitle": "This is your first time accessing the locked folder. Create a PIN code to securely access this page", "new_user_created": "New user created", "new_version_available": "NEW VERSION AVAILABLE", "newest_first": "Newest first", @@ -1291,23 +1279,24 @@ "no_explore_results_message": "Upload more photos to explore your collection.", "no_favorites_message": "Add favorites to quickly find your best pictures and videos", "no_libraries_message": "Create an external library to view your photos and videos", + "no_locked_photos_message": "Photos and videos in Locked Folder are hidden and won't show up as you browser your library.", "no_name": "No Name", + "no_notifications": "No notifications", "no_people_found": "No matching people found", "no_places": "No places", "no_results": "No results", "no_results_description": "Try a synonym or more general keyword", - "no_notifications": "No notifications", "no_shared_albums_message": "Create an album to share photos and videos with people in your network", "not_in_any_album": "Not in any album", "not_selected": "Not selected", "note_apply_storage_label_to_previously_uploaded assets": "Note: To apply the Storage Label to previously uploaded assets, run the", "notes": "Notes", + "nothing_here_yet": "Nothing here yet", "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", "notification_permission_list_tile_content": "Grant permission to enable notifications.", "notification_permission_list_tile_enable_button": "Enable Notifications", "notification_permission_list_tile_title": "Notification Permission", "notification_toggle_setting_description": "Enable email notifications", - "email_notifications": "Email notifications", "notifications": "Notifications", "notifications_setting_description": "Manage notifications", "oauth": "OAuth", @@ -1395,6 +1384,10 @@ "photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} Photos}}", "photos_from_previous_years": "Photos from previous years", "pick_a_location": "Pick a location", + "pin_code_changed_successfully": "Successfully changed PIN code", + "pin_code_reset_successfully": "Successfully reset PIN code", + "pin_code_setup_successfully": "Successfully setup a PIN code", + "pin_verification": "PIN code verification", "place": "Place", "places": "Places", "places_count": "{count, plural, one {{count, number} Place} other {{count, number} Places}}", @@ -1492,6 +1485,8 @@ "remove_deleted_assets": "Remove Deleted Assets", "remove_from_album": "Remove from album", "remove_from_favorites": "Remove from favorites", + "remove_from_locked_folder": "Remove from Locked Folder", + "remove_from_locked_folder_confirmation": "Are you sure you want to move these photos and videos out of Locked Folder? They will be visible in your library", "remove_from_shared_link": "Remove from shared link", "remove_memory": "Remove memory", "remove_photo_from_memory": "Remove photo from this memory", @@ -1515,6 +1510,7 @@ "reset": "Reset", "reset_password": "Reset password", "reset_people_visibility": "Reset people visibility", + "reset_pin_code": "Reset PIN code", "reset_to_default": "Reset to default", "resolve_duplicates": "Resolve duplicates", "resolved_all_duplicates": "Resolved all duplicates", @@ -1655,6 +1651,7 @@ "settings": "Settings", "settings_require_restart": "Please restart Immich to apply this setting", "settings_saved": "Settings saved", + "setup_pin_code": "Setup a PIN code", "share": "Share", "share_add_photos": "Add photos", "share_assets_selected": "{count} selected", @@ -1771,8 +1768,8 @@ "stop_sharing_photos_with_user": "Stop sharing your photos with this user", "storage": "Storage space", "storage_label": "Storage label", - "storage_usage": "{used} of {available} used", "storage_quota": "Storage Quota", + "storage_usage": "{used} of {available} used", "submit": "Submit", "suggestions": "Suggestions", "sunrise_on_the_beach": "Sunrise on the beach", @@ -1840,6 +1837,8 @@ "trash_page_title": "Trash ({count})", "trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.", "type": "Type", + "unable_to_change_pin_code": "Unable to change PIN code", + "unable_to_setup_pin_code": "Unable to setup PIN code", "unarchive": "Unarchive", "unarchived_count": "{count, plural, other {Unarchived #}}", "unfavorite": "Unfavorite", @@ -1863,6 +1862,7 @@ "untracked_files": "Untracked files", "untracked_files_decription": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug", "up_next": "Up next", + "updated_at": "Updated", "updated_password": "Updated password", "upload": "Upload", "upload_concurrency": "Upload concurrency", @@ -1877,7 +1877,6 @@ "upload_success": "Upload success, refresh the page to see new upload assets.", "upload_to_immich": "Upload to Immich ({count})", "uploading": "Uploading", - "id": "ID", "url": "URL", "usage": "Usage", "use_current_connection": "use current connection", @@ -1886,8 +1885,8 @@ "user_has_been_deleted": "This user has been deleted.", "user_id": "User ID", "user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", - "created_at": "Created", - "updated_at": "Updated", + "user_pin_code_settings": "PIN Code", + "user_pin_code_settings_description": "Manage your PIN code", "user_purchase_settings": "Purchase", "user_purchase_settings_description": "Manage your purchase", "user_role_set": "Set {user} as {role}", @@ -1937,6 +1936,7 @@ "welcome": "Welcome", "welcome_to_immich": "Welcome to Immich", "wifi_name": "Wi-Fi Name", + "wrong_pin_code": "Wrong PIN code", "year": "Year", "years_ago": "{years, plural, one {# year} other {# years}} ago", "yes": "Yes", diff --git a/web/package.json b/web/package.json index 7bf5e36189..94f48a7d97 100644 --- a/web/package.json +++ b/web/package.json @@ -18,7 +18,8 @@ "lint:p": "eslint-p . --max-warnings 0 --concurrency=4", "lint:fix": "npm run lint -- --fix", "format": "prettier --check .", - "format:fix": "prettier --write .", + "format:fix": "prettier --write . && npm run format:i18n", + "format:i18n": "npx --yes sort-json ../i18n/*.json", "test": "vitest --run", "test:cov": "vitest --coverage", "test:watch": "vitest dev", diff --git a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts index 9b9d86a4b3..445917f0d0 100644 --- a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -7,7 +7,7 @@ import { redirect } from '@sveltejs/kit'; import type { PageLoad } from './$types'; export const load = (async ({ params, url }) => { - await authenticate(); + await authenticate(url); const { isElevated, pinCode } = await getAuthStatus(); if (!isElevated || !pinCode) { diff --git a/web/src/routes/auth/pin-prompt/+page.ts b/web/src/routes/auth/pin-prompt/+page.ts index e2b79605d8..b0d248ebe6 100644 --- a/web/src/routes/auth/pin-prompt/+page.ts +++ b/web/src/routes/auth/pin-prompt/+page.ts @@ -4,7 +4,7 @@ import { getAuthStatus } from '@immich/sdk'; import type { PageLoad } from './$types'; export const load = (async ({ url }) => { - await authenticate(); + await authenticate(url); const { pinCode } = await getAuthStatus(); From c1150fe7e3cbf8fa2c2a7e41613dbeffac1cae9c Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 15 May 2025 18:08:31 -0400 Subject: [PATCH 231/356] feat: lock auth session (#18322) --- i18n/en.json | 1 + mobile/openapi/README.md | 6 +- mobile/openapi/lib/api.dart | 2 + .../openapi/lib/api/authentication_api.dart | 123 ++++++++++------- mobile/openapi/lib/api/sessions_api.dart | 40 ++++++ mobile/openapi/lib/api_client.dart | 4 + .../lib/model/auth_status_response_dto.dart | 40 +++++- mobile/openapi/lib/model/permission.dart | 3 + .../openapi/lib/model/pin_code_reset_dto.dart | 125 ++++++++++++++++++ .../model/session_create_response_dto.dart | 19 ++- .../lib/model/session_response_dto.dart | 19 ++- .../openapi/lib/model/session_unlock_dto.dart | 125 ++++++++++++++++++ open-api/immich-openapi-specs.json | 105 ++++++++++++++- open-api/typescript-sdk/src/fetch-client.ts | 45 +++++-- server/src/controllers/auth.controller.ts | 17 ++- server/src/controllers/session.controller.ts | 7 + server/src/database.ts | 1 + server/src/db.d.ts | 2 +- server/src/dtos/auth.dto.ts | 4 + server/src/dtos/session.dto.ts | 2 + server/src/enum.ts | 1 + server/src/queries/access.repository.sql | 9 ++ server/src/queries/session.repository.sql | 23 +++- server/src/repositories/access.repository.ts | 21 +++ server/src/repositories/session.repository.ts | 22 ++- .../migrations/1747338664832-SessionRename.ts | 9 ++ server/src/schema/tables/session.table.ts | 2 +- server/src/services/auth.service.spec.ts | 4 +- server/src/services/auth.service.ts | 40 +++--- server/src/services/session.service.ts | 7 +- server/src/utils/access.ts | 7 + .../repositories/access.repository.mock.ts | 4 + server/test/small.factory.ts | 2 +- .../[[assetId=id]]/+page.svelte | 19 ++- .../[[photos=photos]]/[[assetId=id]]/+page.ts | 8 +- web/src/routes/auth/pin-prompt/+page.svelte | 15 +-- web/src/routes/auth/pin-prompt/+page.ts | 5 +- 37 files changed, 765 insertions(+), 123 deletions(-) create mode 100644 mobile/openapi/lib/model/pin_code_reset_dto.dart create mode 100644 mobile/openapi/lib/model/session_unlock_dto.dart create mode 100644 server/src/schema/migrations/1747338664832-SessionRename.ts diff --git a/i18n/en.json b/i18n/en.json index e4fc825cda..578fe9a115 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1142,6 +1142,7 @@ "location_picker_latitude_hint": "Enter your latitude here", "location_picker_longitude_error": "Enter a valid longitude", "location_picker_longitude_hint": "Enter your longitude here", + "lock": "Lock", "locked_folder": "Locked Folder", "log_out": "Log out", "log_out_all_devices": "Log Out All Devices", diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 9544b2ddab..620fc97664 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -111,13 +111,14 @@ Class | Method | HTTP request | Description *AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password | *AuthenticationApi* | [**changePinCode**](doc//AuthenticationApi.md#changepincode) | **PUT** /auth/pin-code | *AuthenticationApi* | [**getAuthStatus**](doc//AuthenticationApi.md#getauthstatus) | **GET** /auth/status | +*AuthenticationApi* | [**lockAuthSession**](doc//AuthenticationApi.md#lockauthsession) | **POST** /auth/session/lock | *AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login | *AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout | *AuthenticationApi* | [**resetPinCode**](doc//AuthenticationApi.md#resetpincode) | **DELETE** /auth/pin-code | *AuthenticationApi* | [**setupPinCode**](doc//AuthenticationApi.md#setuppincode) | **POST** /auth/pin-code | *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | +*AuthenticationApi* | [**unlockAuthSession**](doc//AuthenticationApi.md#unlockauthsession) | **POST** /auth/session/unlock | *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | -*AuthenticationApi* | [**verifyPinCode**](doc//AuthenticationApi.md#verifypincode) | **POST** /auth/pin-code/verify | *DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | @@ -198,6 +199,7 @@ Class | Method | HTTP request | Description *SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions | *SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} | *SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions | +*SessionsApi* | [**lockSession**](doc//SessionsApi.md#locksession) | **POST** /sessions/{id}/lock | *SharedLinksApi* | [**addSharedLinkAssets**](doc//SharedLinksApi.md#addsharedlinkassets) | **PUT** /shared-links/{id}/assets | *SharedLinksApi* | [**createSharedLink**](doc//SharedLinksApi.md#createsharedlink) | **POST** /shared-links | *SharedLinksApi* | [**getAllSharedLinks**](doc//SharedLinksApi.md#getallsharedlinks) | **GET** /shared-links | @@ -392,6 +394,7 @@ Class | Method | HTTP request | Description - [PersonUpdateDto](doc//PersonUpdateDto.md) - [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md) - [PinCodeChangeDto](doc//PinCodeChangeDto.md) + - [PinCodeResetDto](doc//PinCodeResetDto.md) - [PinCodeSetupDto](doc//PinCodeSetupDto.md) - [PlacesResponseDto](doc//PlacesResponseDto.md) - [PurchaseResponse](doc//PurchaseResponse.md) @@ -424,6 +427,7 @@ Class | Method | HTTP request | Description - [SessionCreateDto](doc//SessionCreateDto.md) - [SessionCreateResponseDto](doc//SessionCreateResponseDto.md) - [SessionResponseDto](doc//SessionResponseDto.md) + - [SessionUnlockDto](doc//SessionUnlockDto.md) - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) - [SharedLinkEditDto](doc//SharedLinkEditDto.md) - [SharedLinkResponseDto](doc//SharedLinkResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index d0e39e0965..8710298d7d 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -189,6 +189,7 @@ part 'model/person_statistics_response_dto.dart'; part 'model/person_update_dto.dart'; part 'model/person_with_faces_response_dto.dart'; part 'model/pin_code_change_dto.dart'; +part 'model/pin_code_reset_dto.dart'; part 'model/pin_code_setup_dto.dart'; part 'model/places_response_dto.dart'; part 'model/purchase_response.dart'; @@ -221,6 +222,7 @@ part 'model/server_version_response_dto.dart'; part 'model/session_create_dto.dart'; part 'model/session_create_response_dto.dart'; part 'model/session_response_dto.dart'; +part 'model/session_unlock_dto.dart'; part 'model/shared_link_create_dto.dart'; part 'model/shared_link_edit_dto.dart'; part 'model/shared_link_response_dto.dart'; diff --git a/mobile/openapi/lib/api/authentication_api.dart b/mobile/openapi/lib/api/authentication_api.dart index 446a0616ed..5482a9fc51 100644 --- a/mobile/openapi/lib/api/authentication_api.dart +++ b/mobile/openapi/lib/api/authentication_api.dart @@ -143,6 +143,39 @@ class AuthenticationApi { return null; } + /// Performs an HTTP 'POST /auth/session/lock' operation and returns the [Response]. + Future lockAuthSessionWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/auth/session/lock'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future lockAuthSession() async { + final response = await lockAuthSessionWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Performs an HTTP 'POST /auth/login' operation and returns the [Response]. /// Parameters: /// @@ -234,13 +267,13 @@ class AuthenticationApi { /// Performs an HTTP 'DELETE /auth/pin-code' operation and returns the [Response]. /// Parameters: /// - /// * [PinCodeChangeDto] pinCodeChangeDto (required): - Future resetPinCodeWithHttpInfo(PinCodeChangeDto pinCodeChangeDto,) async { + /// * [PinCodeResetDto] pinCodeResetDto (required): + Future resetPinCodeWithHttpInfo(PinCodeResetDto pinCodeResetDto,) async { // ignore: prefer_const_declarations final apiPath = r'/auth/pin-code'; // ignore: prefer_final_locals - Object? postBody = pinCodeChangeDto; + Object? postBody = pinCodeResetDto; final queryParams = []; final headerParams = {}; @@ -262,9 +295,9 @@ class AuthenticationApi { /// Parameters: /// - /// * [PinCodeChangeDto] pinCodeChangeDto (required): - Future resetPinCode(PinCodeChangeDto pinCodeChangeDto,) async { - final response = await resetPinCodeWithHttpInfo(pinCodeChangeDto,); + /// * [PinCodeResetDto] pinCodeResetDto (required): + Future resetPinCode(PinCodeResetDto pinCodeResetDto,) async { + final response = await resetPinCodeWithHttpInfo(pinCodeResetDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -356,6 +389,45 @@ class AuthenticationApi { return null; } + /// Performs an HTTP 'POST /auth/session/unlock' operation and returns the [Response]. + /// Parameters: + /// + /// * [SessionUnlockDto] sessionUnlockDto (required): + Future unlockAuthSessionWithHttpInfo(SessionUnlockDto sessionUnlockDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/auth/session/unlock'; + + // ignore: prefer_final_locals + Object? postBody = sessionUnlockDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [SessionUnlockDto] sessionUnlockDto (required): + Future unlockAuthSession(SessionUnlockDto sessionUnlockDto,) async { + final response = await unlockAuthSessionWithHttpInfo(sessionUnlockDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Performs an HTTP 'POST /auth/validateToken' operation and returns the [Response]. Future validateAccessTokenWithHttpInfo() async { // ignore: prefer_const_declarations @@ -396,43 +468,4 @@ class AuthenticationApi { } return null; } - - /// Performs an HTTP 'POST /auth/pin-code/verify' operation and returns the [Response]. - /// Parameters: - /// - /// * [PinCodeSetupDto] pinCodeSetupDto (required): - Future verifyPinCodeWithHttpInfo(PinCodeSetupDto pinCodeSetupDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/auth/pin-code/verify'; - - // ignore: prefer_final_locals - Object? postBody = pinCodeSetupDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [PinCodeSetupDto] pinCodeSetupDto (required): - Future verifyPinCode(PinCodeSetupDto pinCodeSetupDto,) async { - final response = await verifyPinCodeWithHttpInfo(pinCodeSetupDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } } diff --git a/mobile/openapi/lib/api/sessions_api.dart b/mobile/openapi/lib/api/sessions_api.dart index 9f850fb4c8..3228d31e91 100644 --- a/mobile/openapi/lib/api/sessions_api.dart +++ b/mobile/openapi/lib/api/sessions_api.dart @@ -179,4 +179,44 @@ class SessionsApi { } return null; } + + /// Performs an HTTP 'POST /sessions/{id}/lock' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future lockSessionWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final apiPath = r'/sessions/{id}/lock' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future lockSession(String id,) async { + final response = await lockSessionWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index f40d09ecc3..a3b1c41ca6 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -434,6 +434,8 @@ class ApiClient { return PersonWithFacesResponseDto.fromJson(value); case 'PinCodeChangeDto': return PinCodeChangeDto.fromJson(value); + case 'PinCodeResetDto': + return PinCodeResetDto.fromJson(value); case 'PinCodeSetupDto': return PinCodeSetupDto.fromJson(value); case 'PlacesResponseDto': @@ -498,6 +500,8 @@ class ApiClient { return SessionCreateResponseDto.fromJson(value); case 'SessionResponseDto': return SessionResponseDto.fromJson(value); + case 'SessionUnlockDto': + return SessionUnlockDto.fromJson(value); case 'SharedLinkCreateDto': return SharedLinkCreateDto.fromJson(value); case 'SharedLinkEditDto': diff --git a/mobile/openapi/lib/model/auth_status_response_dto.dart b/mobile/openapi/lib/model/auth_status_response_dto.dart index 0ccd87114e..4e823506ee 100644 --- a/mobile/openapi/lib/model/auth_status_response_dto.dart +++ b/mobile/openapi/lib/model/auth_status_response_dto.dart @@ -13,38 +13,70 @@ part of openapi.api; class AuthStatusResponseDto { /// Returns a new [AuthStatusResponseDto] instance. AuthStatusResponseDto({ + this.expiresAt, required this.isElevated, required this.password, required this.pinCode, + this.pinExpiresAt, }); + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? expiresAt; + bool isElevated; bool password; bool pinCode; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? pinExpiresAt; + @override bool operator ==(Object other) => identical(this, other) || other is AuthStatusResponseDto && + other.expiresAt == expiresAt && other.isElevated == isElevated && other.password == password && - other.pinCode == pinCode; + other.pinCode == pinCode && + other.pinExpiresAt == pinExpiresAt; @override int get hashCode => // ignore: unnecessary_parenthesis + (expiresAt == null ? 0 : expiresAt!.hashCode) + (isElevated.hashCode) + (password.hashCode) + - (pinCode.hashCode); + (pinCode.hashCode) + + (pinExpiresAt == null ? 0 : pinExpiresAt!.hashCode); @override - String toString() => 'AuthStatusResponseDto[isElevated=$isElevated, password=$password, pinCode=$pinCode]'; + String toString() => 'AuthStatusResponseDto[expiresAt=$expiresAt, isElevated=$isElevated, password=$password, pinCode=$pinCode, pinExpiresAt=$pinExpiresAt]'; Map toJson() { final json = {}; + if (this.expiresAt != null) { + json[r'expiresAt'] = this.expiresAt; + } else { + // json[r'expiresAt'] = null; + } json[r'isElevated'] = this.isElevated; json[r'password'] = this.password; json[r'pinCode'] = this.pinCode; + if (this.pinExpiresAt != null) { + json[r'pinExpiresAt'] = this.pinExpiresAt; + } else { + // json[r'pinExpiresAt'] = null; + } return json; } @@ -57,9 +89,11 @@ class AuthStatusResponseDto { final json = value.cast(); return AuthStatusResponseDto( + expiresAt: mapValueOfType(json, r'expiresAt'), isElevated: mapValueOfType(json, r'isElevated')!, password: mapValueOfType(json, r'password')!, pinCode: mapValueOfType(json, r'pinCode')!, + pinExpiresAt: mapValueOfType(json, r'pinExpiresAt'), ); } return null; diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index 73ecbd5868..a85b5002bf 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -85,6 +85,7 @@ class Permission { static const sessionPeriodRead = Permission._(r'session.read'); static const sessionPeriodUpdate = Permission._(r'session.update'); static const sessionPeriodDelete = Permission._(r'session.delete'); + static const sessionPeriodLock = Permission._(r'session.lock'); static const sharedLinkPeriodCreate = Permission._(r'sharedLink.create'); static const sharedLinkPeriodRead = Permission._(r'sharedLink.read'); static const sharedLinkPeriodUpdate = Permission._(r'sharedLink.update'); @@ -171,6 +172,7 @@ class Permission { sessionPeriodRead, sessionPeriodUpdate, sessionPeriodDelete, + sessionPeriodLock, sharedLinkPeriodCreate, sharedLinkPeriodRead, sharedLinkPeriodUpdate, @@ -292,6 +294,7 @@ class PermissionTypeTransformer { case r'session.read': return Permission.sessionPeriodRead; case r'session.update': return Permission.sessionPeriodUpdate; case r'session.delete': return Permission.sessionPeriodDelete; + case r'session.lock': return Permission.sessionPeriodLock; case r'sharedLink.create': return Permission.sharedLinkPeriodCreate; case r'sharedLink.read': return Permission.sharedLinkPeriodRead; case r'sharedLink.update': return Permission.sharedLinkPeriodUpdate; diff --git a/mobile/openapi/lib/model/pin_code_reset_dto.dart b/mobile/openapi/lib/model/pin_code_reset_dto.dart new file mode 100644 index 0000000000..3585348675 --- /dev/null +++ b/mobile/openapi/lib/model/pin_code_reset_dto.dart @@ -0,0 +1,125 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PinCodeResetDto { + /// Returns a new [PinCodeResetDto] instance. + PinCodeResetDto({ + this.password, + this.pinCode, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? password; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? pinCode; + + @override + bool operator ==(Object other) => identical(this, other) || other is PinCodeResetDto && + other.password == password && + other.pinCode == pinCode; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (password == null ? 0 : password!.hashCode) + + (pinCode == null ? 0 : pinCode!.hashCode); + + @override + String toString() => 'PinCodeResetDto[password=$password, pinCode=$pinCode]'; + + Map toJson() { + final json = {}; + if (this.password != null) { + json[r'password'] = this.password; + } else { + // json[r'password'] = null; + } + if (this.pinCode != null) { + json[r'pinCode'] = this.pinCode; + } else { + // json[r'pinCode'] = null; + } + return json; + } + + /// Returns a new [PinCodeResetDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PinCodeResetDto? fromJson(dynamic value) { + upgradeDto(value, "PinCodeResetDto"); + if (value is Map) { + final json = value.cast(); + + return PinCodeResetDto( + password: mapValueOfType(json, r'password'), + pinCode: mapValueOfType(json, r'pinCode'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PinCodeResetDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PinCodeResetDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PinCodeResetDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PinCodeResetDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/session_create_response_dto.dart b/mobile/openapi/lib/model/session_create_response_dto.dart index 1ef346c96a..ab1c4ca2d8 100644 --- a/mobile/openapi/lib/model/session_create_response_dto.dart +++ b/mobile/openapi/lib/model/session_create_response_dto.dart @@ -17,6 +17,7 @@ class SessionCreateResponseDto { required this.current, required this.deviceOS, required this.deviceType, + this.expiresAt, required this.id, required this.token, required this.updatedAt, @@ -30,6 +31,14 @@ class SessionCreateResponseDto { String deviceType; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? expiresAt; + String id; String token; @@ -42,6 +51,7 @@ class SessionCreateResponseDto { other.current == current && other.deviceOS == deviceOS && other.deviceType == deviceType && + other.expiresAt == expiresAt && other.id == id && other.token == token && other.updatedAt == updatedAt; @@ -53,12 +63,13 @@ class SessionCreateResponseDto { (current.hashCode) + (deviceOS.hashCode) + (deviceType.hashCode) + + (expiresAt == null ? 0 : expiresAt!.hashCode) + (id.hashCode) + (token.hashCode) + (updatedAt.hashCode); @override - String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, id=$id, token=$token, updatedAt=$updatedAt]'; + String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, token=$token, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -66,6 +77,11 @@ class SessionCreateResponseDto { json[r'current'] = this.current; json[r'deviceOS'] = this.deviceOS; json[r'deviceType'] = this.deviceType; + if (this.expiresAt != null) { + json[r'expiresAt'] = this.expiresAt; + } else { + // json[r'expiresAt'] = null; + } json[r'id'] = this.id; json[r'token'] = this.token; json[r'updatedAt'] = this.updatedAt; @@ -85,6 +101,7 @@ class SessionCreateResponseDto { current: mapValueOfType(json, r'current')!, deviceOS: mapValueOfType(json, r'deviceOS')!, deviceType: mapValueOfType(json, r'deviceType')!, + expiresAt: mapValueOfType(json, r'expiresAt'), id: mapValueOfType(json, r'id')!, token: mapValueOfType(json, r'token')!, updatedAt: mapValueOfType(json, r'updatedAt')!, diff --git a/mobile/openapi/lib/model/session_response_dto.dart b/mobile/openapi/lib/model/session_response_dto.dart index 92e2dc6067..cf9eb08a78 100644 --- a/mobile/openapi/lib/model/session_response_dto.dart +++ b/mobile/openapi/lib/model/session_response_dto.dart @@ -17,6 +17,7 @@ class SessionResponseDto { required this.current, required this.deviceOS, required this.deviceType, + this.expiresAt, required this.id, required this.updatedAt, }); @@ -29,6 +30,14 @@ class SessionResponseDto { String deviceType; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? expiresAt; + String id; String updatedAt; @@ -39,6 +48,7 @@ class SessionResponseDto { other.current == current && other.deviceOS == deviceOS && other.deviceType == deviceType && + other.expiresAt == expiresAt && other.id == id && other.updatedAt == updatedAt; @@ -49,11 +59,12 @@ class SessionResponseDto { (current.hashCode) + (deviceOS.hashCode) + (deviceType.hashCode) + + (expiresAt == null ? 0 : expiresAt!.hashCode) + (id.hashCode) + (updatedAt.hashCode); @override - String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, id=$id, updatedAt=$updatedAt]'; + String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -61,6 +72,11 @@ class SessionResponseDto { json[r'current'] = this.current; json[r'deviceOS'] = this.deviceOS; json[r'deviceType'] = this.deviceType; + if (this.expiresAt != null) { + json[r'expiresAt'] = this.expiresAt; + } else { + // json[r'expiresAt'] = null; + } json[r'id'] = this.id; json[r'updatedAt'] = this.updatedAt; return json; @@ -79,6 +95,7 @@ class SessionResponseDto { current: mapValueOfType(json, r'current')!, deviceOS: mapValueOfType(json, r'deviceOS')!, deviceType: mapValueOfType(json, r'deviceType')!, + expiresAt: mapValueOfType(json, r'expiresAt'), id: mapValueOfType(json, r'id')!, updatedAt: mapValueOfType(json, r'updatedAt')!, ); diff --git a/mobile/openapi/lib/model/session_unlock_dto.dart b/mobile/openapi/lib/model/session_unlock_dto.dart new file mode 100644 index 0000000000..4cfeb14385 --- /dev/null +++ b/mobile/openapi/lib/model/session_unlock_dto.dart @@ -0,0 +1,125 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SessionUnlockDto { + /// Returns a new [SessionUnlockDto] instance. + SessionUnlockDto({ + this.password, + this.pinCode, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? password; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? pinCode; + + @override + bool operator ==(Object other) => identical(this, other) || other is SessionUnlockDto && + other.password == password && + other.pinCode == pinCode; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (password == null ? 0 : password!.hashCode) + + (pinCode == null ? 0 : pinCode!.hashCode); + + @override + String toString() => 'SessionUnlockDto[password=$password, pinCode=$pinCode]'; + + Map toJson() { + final json = {}; + if (this.password != null) { + json[r'password'] = this.password; + } else { + // json[r'password'] = null; + } + if (this.pinCode != null) { + json[r'pinCode'] = this.pinCode; + } else { + // json[r'pinCode'] = null; + } + return json; + } + + /// Returns a new [SessionUnlockDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SessionUnlockDto? fromJson(dynamic value) { + upgradeDto(value, "SessionUnlockDto"); + if (value is Map) { + final json = value.cast(); + + return SessionUnlockDto( + password: mapValueOfType(json, r'password'), + pinCode: mapValueOfType(json, r'pinCode'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SessionUnlockDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SessionUnlockDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SessionUnlockDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SessionUnlockDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index d4a1e219c9..89bdfef45e 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2377,7 +2377,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PinCodeChangeDto" + "$ref": "#/components/schemas/PinCodeResetDto" } } }, @@ -2470,15 +2470,40 @@ ] } }, - "/auth/pin-code/verify": { + "/auth/session/lock": { "post": { - "operationId": "verifyPinCode", + "operationId": "lockAuthSession", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Authentication" + ] + } + }, + "/auth/session/unlock": { + "post": { + "operationId": "unlockAuthSession", "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PinCodeSetupDto" + "$ref": "#/components/schemas/SessionUnlockDto" } } }, @@ -5695,6 +5720,41 @@ ] } }, + "/sessions/{id}/lock": { + "post": { + "operationId": "lockSession", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Sessions" + ] + } + }, "/shared-links": { "get": { "operationId": "getAllSharedLinks", @@ -9327,6 +9387,9 @@ }, "AuthStatusResponseDto": { "properties": { + "expiresAt": { + "type": "string" + }, "isElevated": { "type": "boolean" }, @@ -9335,6 +9398,9 @@ }, "pinCode": { "type": "boolean" + }, + "pinExpiresAt": { + "type": "string" } }, "required": [ @@ -11096,6 +11162,7 @@ "session.read", "session.update", "session.delete", + "session.lock", "sharedLink.create", "sharedLink.read", "sharedLink.update", @@ -11297,6 +11364,18 @@ ], "type": "object" }, + "PinCodeResetDto": { + "properties": { + "password": { + "type": "string" + }, + "pinCode": { + "example": "123456", + "type": "string" + } + }, + "type": "object" + }, "PinCodeSetupDto": { "properties": { "pinCode": { @@ -12109,6 +12188,9 @@ "deviceType": { "type": "string" }, + "expiresAt": { + "type": "string" + }, "id": { "type": "string" }, @@ -12144,6 +12226,9 @@ "deviceType": { "type": "string" }, + "expiresAt": { + "type": "string" + }, "id": { "type": "string" }, @@ -12161,6 +12246,18 @@ ], "type": "object" }, + "SessionUnlockDto": { + "properties": { + "password": { + "type": "string" + }, + "pinCode": { + "example": "123456", + "type": "string" + } + }, + "type": "object" + }, "SharedLinkCreateDto": { "properties": { "albumId": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index de0a723ffa..1d3a04da44 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -512,18 +512,28 @@ export type LogoutResponseDto = { redirectUri: string; successful: boolean; }; -export type PinCodeChangeDto = { - newPinCode: string; +export type PinCodeResetDto = { password?: string; pinCode?: string; }; export type PinCodeSetupDto = { pinCode: string; }; +export type PinCodeChangeDto = { + newPinCode: string; + password?: string; + pinCode?: string; +}; +export type SessionUnlockDto = { + password?: string; + pinCode?: string; +}; export type AuthStatusResponseDto = { + expiresAt?: string; isElevated: boolean; password: boolean; pinCode: boolean; + pinExpiresAt?: string; }; export type ValidateAccessTokenResponseDto = { authStatus: boolean; @@ -1075,6 +1085,7 @@ export type SessionResponseDto = { current: boolean; deviceOS: string; deviceType: string; + expiresAt?: string; id: string; updatedAt: string; }; @@ -1089,6 +1100,7 @@ export type SessionCreateResponseDto = { current: boolean; deviceOS: string; deviceType: string; + expiresAt?: string; id: string; token: string; updatedAt: string; @@ -2066,13 +2078,13 @@ export function logout(opts?: Oazapfts.RequestOpts) { method: "POST" })); } -export function resetPinCode({ pinCodeChangeDto }: { - pinCodeChangeDto: PinCodeChangeDto; +export function resetPinCode({ pinCodeResetDto }: { + pinCodeResetDto: PinCodeResetDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchText("/auth/pin-code", oazapfts.json({ ...opts, method: "DELETE", - body: pinCodeChangeDto + body: pinCodeResetDto }))); } export function setupPinCode({ pinCodeSetupDto }: { @@ -2093,13 +2105,19 @@ export function changePinCode({ pinCodeChangeDto }: { body: pinCodeChangeDto }))); } -export function verifyPinCode({ pinCodeSetupDto }: { - pinCodeSetupDto: PinCodeSetupDto; +export function lockAuthSession(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/auth/session/lock", { + ...opts, + method: "POST" + })); +} +export function unlockAuthSession({ sessionUnlockDto }: { + sessionUnlockDto: SessionUnlockDto; }, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/auth/pin-code/verify", oazapfts.json({ + return oazapfts.ok(oazapfts.fetchText("/auth/session/unlock", oazapfts.json({ ...opts, method: "POST", - body: pinCodeSetupDto + body: sessionUnlockDto }))); } export function getAuthStatus(opts?: Oazapfts.RequestOpts) { @@ -2952,6 +2970,14 @@ export function deleteSession({ id }: { method: "DELETE" })); } +export function lockSession({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/sessions/${encodeURIComponent(id)}/lock`, { + ...opts, + method: "POST" + })); +} export function getAllSharedLinks({ albumId }: { albumId?: string; }, opts?: Oazapfts.RequestOpts) { @@ -3709,6 +3735,7 @@ export enum Permission { SessionRead = "session.read", SessionUpdate = "session.update", SessionDelete = "session.delete", + SessionLock = "session.lock", SharedLinkCreate = "sharedLink.create", SharedLinkRead = "sharedLink.read", SharedLinkUpdate = "sharedLink.update", diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index 5d3ba8be95..78c611d761 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -9,7 +9,9 @@ import { LoginResponseDto, LogoutResponseDto, PinCodeChangeDto, + PinCodeResetDto, PinCodeSetupDto, + SessionUnlockDto, SignUpDto, ValidateAccessTokenResponseDto, } from 'src/dtos/auth.dto'; @@ -98,14 +100,21 @@ export class AuthController { @Delete('pin-code') @Authenticated() - async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeChangeDto): Promise { + async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeResetDto): Promise { return this.service.resetPinCode(auth, dto); } - @Post('pin-code/verify') + @Post('session/unlock') @HttpCode(HttpStatus.OK) @Authenticated() - async verifyPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeSetupDto): Promise { - return this.service.verifyPinCode(auth, dto); + async unlockAuthSession(@Auth() auth: AuthDto, @Body() dto: SessionUnlockDto): Promise { + return this.service.unlockSession(auth, dto); + } + + @Post('session/lock') + @HttpCode(HttpStatus.OK) + @Authenticated() + async lockAuthSession(@Auth() auth: AuthDto): Promise { + return this.service.lockSession(auth); } } diff --git a/server/src/controllers/session.controller.ts b/server/src/controllers/session.controller.ts index addcfd8fe9..3838d5af80 100644 --- a/server/src/controllers/session.controller.ts +++ b/server/src/controllers/session.controller.ts @@ -37,4 +37,11 @@ export class SessionController { deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } + + @Post(':id/lock') + @Authenticated({ permission: Permission.SESSION_LOCK }) + @HttpCode(HttpStatus.NO_CONTENT) + lockSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.lock(auth, id); + } } diff --git a/server/src/database.ts b/server/src/database.ts index 29c746aa1f..cfccd70b75 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -232,6 +232,7 @@ export type Session = { id: string; createdAt: Date; updatedAt: Date; + expiresAt: Date | null; deviceOS: string; deviceType: string; pinExpiresAt: Date | null; diff --git a/server/src/db.d.ts b/server/src/db.d.ts index 6efbd5f7d7..943c9ddfa0 100644 --- a/server/src/db.d.ts +++ b/server/src/db.d.ts @@ -344,7 +344,7 @@ export interface Sessions { deviceType: Generated; id: Generated; parentId: string | null; - expiredAt: Date | null; + expiresAt: Date | null; token: string; updatedAt: Generated; updateId: Generated; diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index 8644426ab2..2f3ae5c14b 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -93,6 +93,8 @@ export class PinCodeResetDto { password?: string; } +export class SessionUnlockDto extends PinCodeResetDto {} + export class PinCodeChangeDto extends PinCodeResetDto { @PinCode() newPinCode!: string; @@ -139,4 +141,6 @@ export class AuthStatusResponseDto { pinCode!: boolean; password!: boolean; isElevated!: boolean; + expiresAt?: string; + pinExpiresAt?: string; } diff --git a/server/src/dtos/session.dto.ts b/server/src/dtos/session.dto.ts index f109e44fa0..f15166fbf5 100644 --- a/server/src/dtos/session.dto.ts +++ b/server/src/dtos/session.dto.ts @@ -24,6 +24,7 @@ export class SessionResponseDto { id!: string; createdAt!: string; updatedAt!: string; + expiresAt?: string; current!: boolean; deviceType!: string; deviceOS!: string; @@ -37,6 +38,7 @@ export const mapSession = (entity: Session, currentId?: string): SessionResponse id: entity.id, createdAt: entity.createdAt.toISOString(), updatedAt: entity.updatedAt.toISOString(), + expiresAt: entity.expiresAt?.toISOString(), current: currentId === entity.id, deviceOS: entity.deviceOS, deviceType: entity.deviceType, diff --git a/server/src/enum.ts b/server/src/enum.ts index c6feb27dcc..a4d2d21274 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -148,6 +148,7 @@ export enum Permission { SESSION_READ = 'session.read', SESSION_UPDATE = 'session.update', SESSION_DELETE = 'session.delete', + SESSION_LOCK = 'session.lock', SHARED_LINK_CREATE = 'sharedLink.create', SHARED_LINK_READ = 'sharedLink.read', diff --git a/server/src/queries/access.repository.sql b/server/src/queries/access.repository.sql index c73f44c19d..402bbdcfaf 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -199,6 +199,15 @@ where "partners"."sharedById" in ($1) and "partners"."sharedWithId" = $2 +-- AccessRepository.session.checkOwnerAccess +select + "sessions"."id" +from + "sessions" +where + "sessions"."id" in ($1) + and "sessions"."userId" = $2 + -- AccessRepository.stack.checkOwnerAccess select "stacks"."id" diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql index b265380a1f..6a9b69c2e3 100644 --- a/server/src/queries/session.repository.sql +++ b/server/src/queries/session.repository.sql @@ -1,12 +1,14 @@ -- NOTE: This file is auto generated by ./sql-generator --- SessionRepository.search +-- SessionRepository.get select - * + "id", + "expiresAt", + "pinExpiresAt" from "sessions" where - "sessions"."updatedAt" <= $1 + "id" = $1 -- SessionRepository.getByToken select @@ -37,8 +39,8 @@ from where "sessions"."token" = $1 and ( - "sessions"."expiredAt" is null - or "sessions"."expiredAt" > $2 + "sessions"."expiresAt" is null + or "sessions"."expiresAt" > $2 ) -- SessionRepository.getByUserId @@ -50,6 +52,10 @@ from and "users"."deletedAt" is null where "sessions"."userId" = $1 + and ( + "sessions"."expiresAt" is null + or "sessions"."expiresAt" > $2 + ) order by "sessions"."updatedAt" desc, "sessions"."createdAt" desc @@ -58,3 +64,10 @@ order by delete from "sessions" where "id" = $1::uuid + +-- SessionRepository.lockAll +update "sessions" +set + "pinExpiresAt" = $1 +where + "userId" = $2 diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index b25007c4ea..17f69c0e52 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -306,6 +306,25 @@ class NotificationAccess { } } +class SessionAccess { + constructor(private db: Kysely) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) + async checkOwnerAccess(userId: string, sessionIds: Set) { + if (sessionIds.size === 0) { + return new Set(); + } + + return this.db + .selectFrom('sessions') + .select('sessions.id') + .where('sessions.id', 'in', [...sessionIds]) + .where('sessions.userId', '=', userId) + .execute() + .then((sessions) => new Set(sessions.map((session) => session.id))); + } +} class StackAccess { constructor(private db: Kysely) {} @@ -456,6 +475,7 @@ export class AccessRepository { notification: NotificationAccess; person: PersonAccess; partner: PartnerAccess; + session: SessionAccess; stack: StackAccess; tag: TagAccess; timeline: TimelineAccess; @@ -469,6 +489,7 @@ export class AccessRepository { this.notification = new NotificationAccess(db); this.person = new PersonAccess(db); this.partner = new PartnerAccess(db); + this.session = new SessionAccess(db); this.stack = new StackAccess(db); this.tag = new TagAccess(db); this.timeline = new TimelineAccess(db); diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts index ce819470c7..6c3d10cb9a 100644 --- a/server/src/repositories/session.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -20,20 +20,20 @@ export class SessionRepository { .where((eb) => eb.or([ eb('updatedAt', '<=', DateTime.now().minus({ days: 90 }).toJSDate()), - eb.and([eb('expiredAt', 'is not', null), eb('expiredAt', '<=', DateTime.now().toJSDate())]), + eb.and([eb('expiresAt', 'is not', null), eb('expiresAt', '<=', DateTime.now().toJSDate())]), ]), ) .returning(['id', 'deviceOS', 'deviceType']) .execute(); } - @GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] }) - search(options: SessionSearchOptions) { + @GenerateSql({ params: [DummyValue.UUID] }) + get(id: string) { return this.db .selectFrom('sessions') - .selectAll() - .where('sessions.updatedAt', '<=', options.updatedBefore) - .execute(); + .select(['id', 'expiresAt', 'pinExpiresAt']) + .where('id', '=', id) + .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.STRING] }) @@ -52,7 +52,7 @@ export class SessionRepository { ]) .where('sessions.token', '=', token) .where((eb) => - eb.or([eb('sessions.expiredAt', 'is', null), eb('sessions.expiredAt', '>', DateTime.now().toJSDate())]), + eb.or([eb('sessions.expiresAt', 'is', null), eb('sessions.expiresAt', '>', DateTime.now().toJSDate())]), ) .executeTakeFirst(); } @@ -64,6 +64,9 @@ export class SessionRepository { .innerJoin('users', (join) => join.onRef('users.id', '=', 'sessions.userId').on('users.deletedAt', 'is', null)) .selectAll('sessions') .where('sessions.userId', '=', userId) + .where((eb) => + eb.or([eb('sessions.expiresAt', 'is', null), eb('sessions.expiresAt', '>', DateTime.now().toJSDate())]), + ) .orderBy('sessions.updatedAt', 'desc') .orderBy('sessions.createdAt', 'desc') .execute(); @@ -86,4 +89,9 @@ export class SessionRepository { async delete(id: string) { await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute(); } + + @GenerateSql({ params: [DummyValue.UUID] }) + async lockAll(userId: string) { + await this.db.updateTable('sessions').set({ pinExpiresAt: null }).where('userId', '=', userId).execute(); + } } diff --git a/server/src/schema/migrations/1747338664832-SessionRename.ts b/server/src/schema/migrations/1747338664832-SessionRename.ts new file mode 100644 index 0000000000..5ba532d136 --- /dev/null +++ b/server/src/schema/migrations/1747338664832-SessionRename.ts @@ -0,0 +1,9 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "sessions" RENAME "expiredAt" TO "expiresAt";`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "sessions" RENAME "expiresAt" TO "expiredAt";`.execute(db); +} diff --git a/server/src/schema/tables/session.table.ts b/server/src/schema/tables/session.table.ts index 9cc41c5bba..6bd5d84cb2 100644 --- a/server/src/schema/tables/session.table.ts +++ b/server/src/schema/tables/session.table.ts @@ -26,7 +26,7 @@ export class SessionTable { updatedAt!: Date; @Column({ type: 'timestamp with time zone', nullable: true }) - expiredAt!: Date | null; + expiresAt!: Date | null; @ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) userId!: string; diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index fb1a5ae042..4bc5f1ce0b 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -924,13 +924,13 @@ describe(AuthService.name, () => { const user = factory.userAdmin(); mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' }); mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b); - mocks.session.getByUserId.mockResolvedValue([currentSession]); + mocks.session.lockAll.mockResolvedValue(void 0); mocks.session.update.mockResolvedValue(currentSession); await sut.resetPinCode(factory.auth({ user }), { pinCode: '123456' }); expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pinCode: null }); - expect(mocks.session.update).toHaveBeenCalledWith(currentSession.id, { pinExpiresAt: null }); + expect(mocks.session.lockAll).toHaveBeenCalledWith(user.id); }); it('should throw if the PIN code does not match', async () => { diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 7bda2eeb98..e6c541a624 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -18,6 +18,7 @@ import { PinCodeChangeDto, PinCodeResetDto, PinCodeSetupDto, + SessionUnlockDto, SignUpDto, mapLoginResponse, } from 'src/dtos/auth.dto'; @@ -123,24 +124,21 @@ export class AuthService extends BaseService { async resetPinCode(auth: AuthDto, dto: PinCodeResetDto) { const user = await this.userRepository.getForPinCode(auth.user.id); - this.resetPinChecks(user, dto); + this.validatePinCode(user, dto); await this.userRepository.update(auth.user.id, { pinCode: null }); - const sessions = await this.sessionRepository.getByUserId(auth.user.id); - for (const session of sessions) { - await this.sessionRepository.update(session.id, { pinExpiresAt: null }); - } + await this.sessionRepository.lockAll(auth.user.id); } async changePinCode(auth: AuthDto, dto: PinCodeChangeDto) { const user = await this.userRepository.getForPinCode(auth.user.id); - this.resetPinChecks(user, dto); + this.validatePinCode(user, dto); const hashed = await this.cryptoRepository.hashBcrypt(dto.newPinCode, SALT_ROUNDS); await this.userRepository.update(auth.user.id, { pinCode: hashed }); } - private resetPinChecks( + private validatePinCode( user: { pinCode: string | null; password: string | null }, dto: { pinCode?: string; password?: string }, ) { @@ -474,23 +472,27 @@ export class AuthService extends BaseService { throw new UnauthorizedException('Invalid user token'); } - async verifyPinCode(auth: AuthDto, dto: PinCodeSetupDto): Promise { - const user = await this.userRepository.getForPinCode(auth.user.id); - if (!user) { - throw new UnauthorizedException(); - } - - this.resetPinChecks(user, { pinCode: dto.pinCode }); - + async unlockSession(auth: AuthDto, dto: SessionUnlockDto): Promise { if (!auth.session) { - throw new BadRequestException('Session is missing'); + throw new BadRequestException('This endpoint can only be used with a session token'); } + const user = await this.userRepository.getForPinCode(auth.user.id); + this.validatePinCode(user, { pinCode: dto.pinCode }); + await this.sessionRepository.update(auth.session.id, { - pinExpiresAt: new Date(DateTime.now().plus({ minutes: 15 }).toJSDate()), + pinExpiresAt: DateTime.now().plus({ minutes: 15 }).toJSDate(), }); } + async lockSession(auth: AuthDto): Promise { + if (!auth.session) { + throw new BadRequestException('This endpoint can only be used with a session token'); + } + + await this.sessionRepository.update(auth.session.id, { pinExpiresAt: null }); + } + private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) { const token = this.cryptoRepository.randomBytesAsText(32); const tokenHashed = this.cryptoRepository.hashSha256(token); @@ -526,10 +528,14 @@ export class AuthService extends BaseService { throw new UnauthorizedException(); } + const session = auth.session ? await this.sessionRepository.get(auth.session.id) : undefined; + return { pinCode: !!user.pinCode, password: !!user.password, isElevated: !!auth.session?.hasElevatedPermission, + expiresAt: session?.expiresAt?.toISOString(), + pinExpiresAt: session?.pinExpiresAt?.toISOString(), }; } } diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts index 9f49cda07f..059ff00e16 100644 --- a/server/src/services/session.service.ts +++ b/server/src/services/session.service.ts @@ -30,7 +30,7 @@ export class SessionService extends BaseService { const session = await this.sessionRepository.create({ parentId: auth.session.id, userId: auth.user.id, - expiredAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null, + expiresAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null, deviceType: dto.deviceType, deviceOS: dto.deviceOS, token: tokenHashed, @@ -49,6 +49,11 @@ export class SessionService extends BaseService { await this.sessionRepository.delete(id); } + async lock(auth: AuthDto, id: string): Promise { + await this.requireAccess({ auth, permission: Permission.SESSION_LOCK, ids: [id] }); + await this.sessionRepository.update(id, { pinExpiresAt: null }); + } + async deleteAll(auth: AuthDto): Promise { const sessions = await this.sessionRepository.getByUserId(auth.user.id); for (const session of sessions) { diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index e2fe7429f3..38697a654b 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -280,6 +280,13 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe return await access.partner.checkUpdateAccess(auth.user.id, ids); } + case Permission.SESSION_READ: + case Permission.SESSION_UPDATE: + case Permission.SESSION_DELETE: + case Permission.SESSION_LOCK: { + return access.session.checkOwnerAccess(auth.user.id, ids); + } + case Permission.STACK_READ: { return access.stack.checkOwnerAccess(auth.user.id, ids); } diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index 5b98b95e27..50db983cba 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -50,6 +50,10 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => { checkUpdateAccess: vitest.fn().mockResolvedValue(new Set()), }, + session: { + checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), + }, + stack: { checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), }, diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 231deeba83..75e36c1da2 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -127,7 +127,7 @@ const sessionFactory = (session: Partial = {}) => ({ deviceType: 'mobile', token: 'abc123', parentId: null, - expiredAt: null, + expiresAt: null, userId: newUuid(), pinExpiresAt: newDate(), ...session, diff --git a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte index 49b40866dd..9c41a7fe59 100644 --- a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -1,4 +1,5 @@ @@ -62,6 +69,12 @@ {/if} + {#snippet buttons()} + + {/snippet} + { await authenticate(url); + const { isElevated, pinCode } = await getAuthStatus(); - if (!isElevated || !pinCode) { - const continuePath = encodeURIComponent(url.pathname); - const redirectPath = `${AppRoute.AUTH_PIN_PROMPT}?continue=${continuePath}`; - - redirect(302, redirectPath); + redirect(302, `${AppRoute.AUTH_PIN_PROMPT}?continue=${encodeURIComponent(url.pathname + url.search)}`); } + const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); diff --git a/web/src/routes/auth/pin-prompt/+page.svelte b/web/src/routes/auth/pin-prompt/+page.svelte index 91480cd35c..ffed9d5de0 100644 --- a/web/src/routes/auth/pin-prompt/+page.svelte +++ b/web/src/routes/auth/pin-prompt/+page.svelte @@ -3,9 +3,8 @@ import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte'; import PinCodeCreateForm from '$lib/components/user-settings-page/PinCodeCreateForm.svelte'; import PincodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte'; - import { AppRoute } from '$lib/constants'; import { handleError } from '$lib/utils/handle-error'; - import { verifyPinCode } from '@immich/sdk'; + import { unlockAuthSession } from '@immich/sdk'; import { Icon } from '@immich/ui'; import { mdiLockOpenVariantOutline, mdiLockOutline, mdiLockSmart } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -23,17 +22,15 @@ let hasPinCode = $derived(data.hasPinCode); let pinCode = $state(''); - const onPinFilled = async (code: string, withDelay = false) => { + const handleUnlockSession = async (code: string) => { try { - await verifyPinCode({ pinCodeSetupDto: { pinCode: code } }); + await unlockAuthSession({ sessionUnlockDto: { pinCode: code } }); isVerified = true; - if (withDelay) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - } + await new Promise((resolve) => setTimeout(resolve, 1000)); - void goto(data.continuePath ?? AppRoute.LOCKED); + await goto(data.continueUrl); } catch (error) { handleError(error, $t('wrong_pin_code')); isBadPinCode = true; @@ -64,7 +61,7 @@ bind:value={pinCode} tabindexStart={1} pinLength={6} - onFilled={(pinCode) => onPinFilled(pinCode, true)} + onFilled={handleUnlockSession} />
    diff --git a/web/src/routes/auth/pin-prompt/+page.ts b/web/src/routes/auth/pin-prompt/+page.ts index b0d248ebe6..89d59a3127 100644 --- a/web/src/routes/auth/pin-prompt/+page.ts +++ b/web/src/routes/auth/pin-prompt/+page.ts @@ -1,3 +1,4 @@ +import { AppRoute } from '$lib/constants'; import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; import { getAuthStatus } from '@immich/sdk'; @@ -8,8 +9,6 @@ export const load = (async ({ url }) => { const { pinCode } = await getAuthStatus(); - const continuePath = url.searchParams.get('continue'); - const $t = await getFormatter(); return { @@ -17,6 +16,6 @@ export const load = (async ({ url }) => { title: $t('pin_verification'), }, hasPinCode: !!pinCode, - continuePath, + continueUrl: url.searchParams.get('continue') || AppRoute.LOCKED, }; }) satisfies PageLoad; From 86d64f34833718dbf24ce67e234dc0b0dc8b2b3b Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 15 May 2025 18:31:33 -0400 Subject: [PATCH 232/356] refactor: buttons (#18317) * refactor: buttons * fix: woopsie --------- Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> --- .../elements/buttons/__test__/button.spec.ts | 20 --- .../components/elements/buttons/button.svelte | 123 ------------------ .../elements/buttons/skip-link.svelte | 5 +- .../faces-page/edit-name-input.svelte | 8 +- .../manage-people-visibility.svelte | 9 +- .../faces-page/merge-face-selector.svelte | 9 +- .../faces-page/unmerge-face-selector.svelte | 32 ++--- .../forms/library-scan-settings-form.svelte | 21 +-- .../components/forms/tag-asset-form.svelte | 14 +- .../onboarding-page/onboarding-hello.svelte | 10 +- .../onboarding-page/onboarding-privacy.svelte | 20 +-- .../onboarding-storage-template.svelte | 22 ++-- .../onboarding-page/onboarding-theme.svelte | 12 +- .../navigation-bar/account-info-panel.svelte | 21 ++- .../profile-image-cropper.svelte | 10 +- .../individual-purchase-option-card.svelte | 4 +- .../purchase-activation-success.svelte | 4 +- .../purchasing/purchase-content.svelte | 5 +- .../server-purchase-option-card.svelte | 4 +- .../search-bar/search-people-section.svelte | 17 +-- .../version-announcement-box.svelte | 8 +- .../lib/components/slideshow-settings.svelte | 8 +- .../duplicates-compare-control.svelte | 22 +++- .../modals/PersonEditBirthDateModal.svelte | 11 +- .../[[assetId=id]]/+page.svelte | 6 +- .../[[assetId=id]]/+page.svelte | 2 +- 26 files changed, 148 insertions(+), 279 deletions(-) delete mode 100644 web/src/lib/components/elements/buttons/__test__/button.spec.ts delete mode 100644 web/src/lib/components/elements/buttons/button.svelte diff --git a/web/src/lib/components/elements/buttons/__test__/button.spec.ts b/web/src/lib/components/elements/buttons/__test__/button.spec.ts deleted file mode 100644 index 0539315c57..0000000000 --- a/web/src/lib/components/elements/buttons/__test__/button.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Button from '$lib/components/elements/buttons/button.svelte'; -import { render, screen } from '@testing-library/svelte'; - -describe('Button component', () => { - it('should render as a button', () => { - render(Button); - const button = screen.getByRole('button'); - expect(button).toBeInTheDocument(); - expect(button).toHaveAttribute('type', 'button'); - expect(button).not.toHaveAttribute('href'); - }); - - it('should render as a link if href prop is set', () => { - render(Button, { props: { href: '/test' } }); - const link = screen.getByRole('link'); - expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute('href', '/test'); - expect(link).not.toHaveAttribute('type'); - }); -}); diff --git a/web/src/lib/components/elements/buttons/button.svelte b/web/src/lib/components/elements/buttons/button.svelte deleted file mode 100644 index ac7d9808f3..0000000000 --- a/web/src/lib/components/elements/buttons/button.svelte +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - {@render children?.()} - diff --git a/web/src/lib/components/elements/buttons/skip-link.svelte b/web/src/lib/components/elements/buttons/skip-link.svelte index b8f8fcd483..65e5001f8a 100644 --- a/web/src/lib/components/elements/buttons/skip-link.svelte +++ b/web/src/lib/components/elements/buttons/skip-link.svelte @@ -1,7 +1,7 @@ @@ -39,6 +39,6 @@ - + diff --git a/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte index 1b1a91d163..387f01395d 100644 --- a/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte +++ b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte @@ -1,9 +1,9 @@ @@ -39,6 +39,6 @@ - + diff --git a/web/src/lib/components/shared-components/search-bar/search-people-section.svelte b/web/src/lib/components/shared-components/search-bar/search-people-section.svelte index e4b6ae7c3b..270be62527 100644 --- a/web/src/lib/components/shared-components/search-bar/search-people-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-people-section.svelte @@ -1,13 +1,12 @@
    { @@ -486,19 +486,6 @@ {/key}
    -{#if viewMode === PersonPageViewMode.UNASSIGN_ASSETS} - a.id)} - personAssets={person} - onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)} - onConfirm={handleUnmerge} - /> -{/if} - -{#if viewMode === PersonPageViewMode.MERGE_PEOPLE} - -{/if} -
    {#if assetInteraction.selectionActive} + +{#if viewMode === PersonPageViewMode.UNASSIGN_ASSETS} + a.id)} + personAssets={person} + onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)} + onConfirm={handleUnmerge} + /> +{/if} + +{#if viewMode === PersonPageViewMode.MERGE_PEOPLE} + +{/if} From 28d8357cc50b01b01a1533baf903a8c5c7d2b383 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 16 May 2025 11:56:25 -0400 Subject: [PATCH 234/356] feat(web): clear person birthdate (#18330) --- e2e/src/api/specs/person.e2e-spec.ts | 94 ---------- open-api/immich-openapi-specs.json | 2 + .../src/controllers/person.controller.spec.ts | 172 ++++++++++++++++++ server/src/controllers/person.controller.ts | 4 +- server/src/dtos/person.dto.ts | 5 +- .../modals/PersonEditBirthDateModal.svelte | 13 +- .../[[assetId=id]]/+page.svelte | 1 + 7 files changed, 190 insertions(+), 101 deletions(-) create mode 100644 server/src/controllers/person.controller.spec.ts diff --git a/e2e/src/api/specs/person.e2e-spec.ts b/e2e/src/api/specs/person.e2e-spec.ts index 6e7eba74ba..1826002af6 100644 --- a/e2e/src/api/specs/person.e2e-spec.ts +++ b/e2e/src/api/specs/person.e2e-spec.ts @@ -5,22 +5,6 @@ import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; -const invalidBirthday = [ - { - birthDate: 'false', - response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'], - }, - { - birthDate: '123567', - response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'], - }, - { - birthDate: 123_567, - response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'], - }, - { birthDate: '9999-01-01', response: ['Birth date cannot be in the future'] }, -]; - describe('/people', () => { let admin: LoginResponseDto; let visiblePerson: PersonResponseDto; @@ -58,14 +42,6 @@ describe('/people', () => { describe('GET /people', () => { beforeEach(async () => {}); - - it('should require authentication', async () => { - const { status, body } = await request(app).get('/people'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should return all people (including hidden)', async () => { const { status, body } = await request(app) .get('/people') @@ -117,13 +93,6 @@ describe('/people', () => { }); describe('GET /people/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/people/${uuidDto.notFound}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should throw error if person with id does not exist', async () => { const { status, body } = await request(app) .get(`/people/${uuidDto.notFound}`) @@ -144,13 +113,6 @@ describe('/people', () => { }); describe('GET /people/:id/statistics', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/people/${multipleAssetsPerson.id}/statistics`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should throw error if person with id does not exist', async () => { const { status, body } = await request(app) .get(`/people/${uuidDto.notFound}/statistics`) @@ -171,23 +133,6 @@ describe('/people', () => { }); describe('POST /people', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post(`/people`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - for (const { birthDate, response } of invalidBirthday) { - it(`should not accept an invalid birth date [${birthDate}]`, async () => { - const { status, body } = await request(app) - .post(`/people`) - .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ birthDate }); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(response)); - }); - } - it('should create a person', async () => { const { status, body } = await request(app) .post(`/people`) @@ -223,39 +168,6 @@ describe('/people', () => { }); describe('PUT /people/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/people/${uuidDto.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - for (const { key, type } of [ - { key: 'name', type: 'string' }, - { key: 'featureFaceAssetId', type: 'string' }, - { key: 'isHidden', type: 'boolean value' }, - { key: 'isFavorite', type: 'boolean value' }, - ]) { - it(`should not allow null ${key}`, async () => { - const { status, body } = await request(app) - .put(`/people/${visiblePerson.id}`) - .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ [key]: null }); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([`${key} must be a ${type}`])); - }); - } - - for (const { birthDate, response } of invalidBirthday) { - it(`should not accept an invalid birth date [${birthDate}]`, async () => { - const { status, body } = await request(app) - .put(`/people/${visiblePerson.id}`) - .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ birthDate }); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(response)); - }); - } - it('should update a date of birth', async () => { const { status, body } = await request(app) .put(`/people/${visiblePerson.id}`) @@ -312,12 +224,6 @@ describe('/people', () => { }); describe('POST /people/:id/merge', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post(`/people/${uuidDto.notFound}/merge`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should not supporting merging a person into themselves', async () => { const { status, body } = await request(app) .post(`/people/${visiblePerson.id}/merge`) diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 89bdfef45e..e7bf81ce3e 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -11075,6 +11075,7 @@ }, "featureFaceAssetId": { "description": "Asset is used to get the feature face thumbnail.", + "format": "uuid", "type": "string" }, "id": { @@ -11280,6 +11281,7 @@ }, "featureFaceAssetId": { "description": "Asset is used to get the feature face thumbnail.", + "format": "uuid", "type": "string" }, "isFavorite": { diff --git a/server/src/controllers/person.controller.spec.ts b/server/src/controllers/person.controller.spec.ts new file mode 100644 index 0000000000..0366829336 --- /dev/null +++ b/server/src/controllers/person.controller.spec.ts @@ -0,0 +1,172 @@ +import { PersonController } from 'src/controllers/person.controller'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { PersonService } from 'src/services/person.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { factory } from 'test/small.factory'; +import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(PersonController.name, () => { + let ctx: ControllerContext; + const service = mockBaseService(PersonService); + + beforeAll(async () => { + ctx = await controllerSetup(PersonController, [ + { provide: PersonService, useValue: service }, + { provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + ctx.reset(); + }); + + describe('GET /people', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/people'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it(`should require closestPersonId to be a uuid`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .get(`/people`) + .query({ closestPersonId: 'invalid' }) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + }); + + it(`should require closestAssetId to be a uuid`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .get(`/people`) + .query({ closestAssetId: 'invalid' }) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + }); + }); + + describe('POST /people', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/people'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should map an empty birthDate to null', async () => { + await request(ctx.getHttpServer()).post('/people').send({ birthDate: '' }); + expect(service.create).toHaveBeenCalledWith(undefined, { birthDate: null }); + }); + }); + + describe('GET /people/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PUT /people/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).put(`/people/123`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('id must be a UUID')])); + }); + + it(`should not allow a null name`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post(`/people`) + .send({ name: null }) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['name must be a string'])); + }); + + it(`should require featureFaceAssetId to be a uuid`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/people/${factory.uuid()}`) + .send({ featureFaceAssetId: 'invalid' }) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['featureFaceAssetId must be a UUID'])); + }); + + it(`should require isFavorite to be a boolean`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/people/${factory.uuid()}`) + .send({ isFavorite: 'invalid' }) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['isFavorite must be a boolean value'])); + }); + + it(`should require isHidden to be a boolean`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/people/${factory.uuid()}`) + .send({ isHidden: 'invalid' }) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['isHidden must be a boolean value'])); + }); + + it('should map an empty birthDate to null', async () => { + const id = factory.uuid(); + await request(ctx.getHttpServer()).put(`/people/${id}`).send({ birthDate: '' }); + expect(service.update).toHaveBeenCalledWith(undefined, id, { birthDate: null }); + }); + + it('should not accept an invalid birth date (false)', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/people/${factory.uuid()}`) + .send({ birthDate: false }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest([ + 'birthDate must be a string in the format yyyy-MM-dd', + 'Birth date cannot be in the future', + ]), + ); + }); + + it('should not accept an invalid birth date (number)', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/people/${factory.uuid()}`) + .send({ birthDate: 123_456 }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest([ + 'birthDate must be a string in the format yyyy-MM-dd', + 'Birth date cannot be in the future', + ]), + ); + }); + + it('should not accept a birth date in the future)', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/people/${factory.uuid()}`) + .send({ birthDate: '9999-01-01' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['Birth date cannot be in the future'])); + }); + }); + + describe('POST /people/:id/merge', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post(`/people/${factory.uuid()}/merge`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /people/:id/statistics', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}/statistics`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/controllers/person.controller.ts b/server/src/controllers/person.controller.ts index e98dd6a002..3440042eda 100644 --- a/server/src/controllers/person.controller.ts +++ b/server/src/controllers/person.controller.ts @@ -27,7 +27,9 @@ export class PersonController { constructor( private service: PersonService, private logger: LoggingRepository, - ) {} + ) { + this.logger.setContext(PersonController.name); + } @Get() @Authenticated({ permission: Permission.PERSON_READ }) diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index 90490715ef..c59ab905bd 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -33,7 +33,7 @@ export class PersonCreateDto { @ApiProperty({ format: 'date' }) @MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' }) @IsDateStringFormat('yyyy-MM-dd') - @Optional({ nullable: true }) + @Optional({ nullable: true, emptyToNull: true }) birthDate?: Date | null; /** @@ -54,8 +54,7 @@ export class PersonUpdateDto extends PersonCreateDto { /** * Asset is used to get the feature face thumbnail. */ - @Optional() - @IsString() + @ValidateUUID({ optional: true }) featureFaceAssetId?: string; } diff --git a/web/src/lib/modals/PersonEditBirthDateModal.svelte b/web/src/lib/modals/PersonEditBirthDateModal.svelte index 52d23f4075..d79b716364 100644 --- a/web/src/lib/modals/PersonEditBirthDateModal.svelte +++ b/web/src/lib/modals/PersonEditBirthDateModal.svelte @@ -24,7 +24,7 @@ try { const updatedPerson = await updatePerson({ id: person.id, - personUpdateDto: { birthDate: birthDate.length > 0 ? birthDate : null }, + personUpdateDto: { birthDate }, }); notificationController.show({ message: $t('date_of_birth_saved'), type: NotificationType.Info }); @@ -53,6 +53,13 @@ bind:value={birthDate} max={todayFormatted} /> + {#if person.birthDate} +
    + +
    + {/if} @@ -62,8 +69,8 @@ - diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 50dc8f8166..1dc213729d 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -328,6 +328,7 @@ return; } + person = updatedPerson; people = people.map((person: PersonResponseDto) => { if (person.id === updatedPerson.id) { return updatedPerson; From 1219fd82a038016240e92e73aab2acdb3274f713 Mon Sep 17 00:00:00 2001 From: Sebastian Schneider Date: Fri, 16 May 2025 18:03:54 +0200 Subject: [PATCH 235/356] fix(web): format dates with the locale preference (#18259) fix: Format dates in settings according to user setting --- .../user-settings-page/user-api-key-list.svelte | 9 ++------- .../user-settings-page/user-purchase-settings.svelte | 10 ++++++++-- web/src/lib/constants.ts | 5 +++++ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/web/src/lib/components/user-settings-page/user-api-key-list.svelte b/web/src/lib/components/user-settings-page/user-api-key-list.svelte index 6aebab282c..ccc1bdfe92 100644 --- a/web/src/lib/components/user-settings-page/user-api-key-list.svelte +++ b/web/src/lib/components/user-settings-page/user-api-key-list.svelte @@ -18,6 +18,7 @@ import { fade } from 'svelte/transition'; import { handleError } from '../../utils/handle-error'; import { notificationController, NotificationType } from '../shared-components/notification/notification'; + import { dateFormats } from '$lib/constants'; interface Props { keys: ApiKeyResponseDto[]; @@ -25,12 +26,6 @@ let { keys = $bindable() }: Props = $props(); - const format: Intl.DateTimeFormatOptions = { - month: 'short', - day: 'numeric', - year: 'numeric', - }; - async function refreshKeys() { keys = await getApiKeys(); } @@ -130,7 +125,7 @@ >
    {key.name} {new Date(key.createdAt).toLocaleDateString($locale, format)} + >{new Date(key.createdAt).toLocaleDateString($locale, dateFormats.settings)} {$t('purchase_activated_time', { - values: { date: new Date(serverPurchaseInfo.activatedAt) }, + values: { + date: new Date(serverPurchaseInfo.activatedAt).toLocaleString($locale, dateFormats.settings), + }, })}

    {:else} @@ -161,7 +165,9 @@ {#if $user.license?.activatedAt}

    {$t('purchase_activated_time', { - values: { date: new Date($user.license?.activatedAt) }, + values: { + date: new Date($user.license?.activatedAt).toLocaleString($locale, dateFormats.settings), + }, })}

    {/if} diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 167c976eeb..fdb18b3978 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -72,6 +72,11 @@ export const dateFormats = { day: 'numeric', year: 'numeric', }, + settings: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, }; export enum QueryParameter { From 8ab50403510ddf914319e29983664ce3753925c8 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 16 May 2025 12:58:17 -0400 Subject: [PATCH 236/356] fix(web): modal colors (#18332) * feat(web): clear person birthdate * fix(web): modal colors --- web/src/lib/modals/ConfirmModal.svelte | 2 +- web/src/lib/modals/PasswordResetSuccessModal.svelte | 8 +------- web/src/lib/modals/UserCreateModal.svelte | 2 +- web/src/lib/modals/UserEditModal.svelte | 2 +- web/src/lib/modals/UserRestoreConfirmModal.svelte | 2 +- 5 files changed, 5 insertions(+), 11 deletions(-) diff --git a/web/src/lib/modals/ConfirmModal.svelte b/web/src/lib/modals/ConfirmModal.svelte index 9726a1d9cf..327d13c355 100644 --- a/web/src/lib/modals/ConfirmModal.svelte +++ b/web/src/lib/modals/ConfirmModal.svelte @@ -30,7 +30,7 @@ }; - onClose(false)} {size} class="bg-light text-dark"> + onClose(false)} {size}> {#if promptSnippet}{@render promptSnippet()}{:else}

    {prompt}

    diff --git a/web/src/lib/modals/PasswordResetSuccessModal.svelte b/web/src/lib/modals/PasswordResetSuccessModal.svelte index 74e035b93b..9f8dc9d668 100644 --- a/web/src/lib/modals/PasswordResetSuccessModal.svelte +++ b/web/src/lib/modals/PasswordResetSuccessModal.svelte @@ -12,13 +12,7 @@ const { onClose, newPassword }: Props = $props(); - onClose()} - size="small" - class="bg-light text-dark" -> + onClose()} size="small">
    {$t('admin.user_password_has_been_reset')} diff --git a/web/src/lib/modals/UserCreateModal.svelte b/web/src/lib/modals/UserCreateModal.svelte index 34e498ce1c..f40a709215 100644 --- a/web/src/lib/modals/UserCreateModal.svelte +++ b/web/src/lib/modals/UserCreateModal.svelte @@ -81,7 +81,7 @@ }; - +
    {#if error} diff --git a/web/src/lib/modals/UserEditModal.svelte b/web/src/lib/modals/UserEditModal.svelte index a54dd90590..0bb018721b 100644 --- a/web/src/lib/modals/UserEditModal.svelte +++ b/web/src/lib/modals/UserEditModal.svelte @@ -51,7 +51,7 @@ }; - +
    diff --git a/web/src/lib/modals/UserRestoreConfirmModal.svelte b/web/src/lib/modals/UserRestoreConfirmModal.svelte index c8fde89f36..5cf9c1c91b 100644 --- a/web/src/lib/modals/UserRestoreConfirmModal.svelte +++ b/web/src/lib/modals/UserRestoreConfirmModal.svelte @@ -23,7 +23,7 @@ }; - +

    From 48d746d9d55d9675238e07668015f7a9fcfed812 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Fri, 16 May 2025 13:16:27 -0400 Subject: [PATCH 237/356] refactor(server): "on this day" memory creation (#18333) * refactor memory creation * always update system metadata * maybe fix medium tests --- server/src/enum.ts | 1 + server/src/services/memory.service.ts | 99 ++++++++++--------- .../specs/services/memory.service.spec.ts | 1 + 3 files changed, 54 insertions(+), 47 deletions(-) diff --git a/server/src/enum.ts b/server/src/enum.ts index a4d2d21274..e49f1636a0 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -567,6 +567,7 @@ export enum DatabaseLock { Library = 1337, GetSystemConfig = 69, BackupDatabase = 42, + MemoryCreation = 777, } export enum SyncRequestType { diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 3d3d10540b..1ccd311790 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -4,9 +4,8 @@ import { OnJob } from 'src/decorators'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryCreateDto, MemoryResponseDto, MemorySearchDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; -import { JobName, MemoryType, Permission, QueueName, SystemMetadataKey } from 'src/enum'; +import { DatabaseLock, JobName, MemoryType, Permission, QueueName, SystemMetadataKey } from 'src/enum'; import { BaseService } from 'src/services/base.service'; -import { OnThisDayData } from 'src/types'; import { addAssets, getMyPartnerIds, removeAssets } from 'src/utils/asset.util'; const DAYS = 3; @@ -16,55 +15,61 @@ export class MemoryService extends BaseService { @OnJob({ name: JobName.MEMORIES_CREATE, queue: QueueName.BACKGROUND_TASK }) async onMemoriesCreate() { const users = await this.userRepository.getList({ withDeleted: false }); - const userMap: Record = {}; - for (const user of users) { - const partnerIds = await getMyPartnerIds({ - userId: user.id, - repository: this.partnerRepository, - timelineEnabled: true, - }); - userMap[user.id] = [user.id, ...partnerIds]; - } + const usersIds = await Promise.all( + users.map((user) => + getMyPartnerIds({ + userId: user.id, + repository: this.partnerRepository, + timelineEnabled: true, + }), + ), + ); - const start = DateTime.utc().startOf('day').minus({ days: DAYS }); + await this.databaseRepository.withLock(DatabaseLock.MemoryCreation, async () => { + const state = await this.systemMetadataRepository.get(SystemMetadataKey.MEMORIES_STATE); + const start = DateTime.utc().startOf('day').minus({ days: DAYS }); + const lastOnThisDayDate = state?.lastOnThisDayDate ? DateTime.fromISO(state.lastOnThisDayDate) : start; - const state = await this.systemMetadataRepository.get(SystemMetadataKey.MEMORIES_STATE); - const lastOnThisDayDate = state?.lastOnThisDayDate ? DateTime.fromISO(state.lastOnThisDayDate) : start; - - // generate a memory +/- X days from today - for (let i = 0; i <= DAYS * 2; i++) { - const target = start.plus({ days: i }); - if (lastOnThisDayDate >= target) { - continue; - } - - const showAt = target.startOf('day').toISO(); - const hideAt = target.endOf('day').toISO(); - - for (const [userId, userIds] of Object.entries(userMap)) { - const memories = await this.assetRepository.getByDayOfYear(userIds, target); - - for (const { year, assets } of memories) { - const data: OnThisDayData = { year }; - await this.memoryRepository.create( - { - ownerId: userId, - type: MemoryType.ON_THIS_DAY, - data, - memoryAt: target.set({ year }).toISO(), - showAt, - hideAt, - }, - new Set(assets.map(({ id }) => id)), - ); + // generate a memory +/- X days from today + for (let i = 0; i <= DAYS * 2; i++) { + const target = start.plus({ days: i }); + if (lastOnThisDayDate >= target) { + continue; } - } - await this.systemMetadataRepository.set(SystemMetadataKey.MEMORIES_STATE, { - ...state, - lastOnThisDayDate: target.toISO(), - }); - } + try { + await Promise.all(users.map((owner, i) => this.createOnThisDayMemories(owner.id, usersIds[i], target))); + } catch (error) { + this.logger.error(`Failed to create memories for ${target.toISO()}`, error); + } + // update system metadata even when there is an error to minimize the chance of duplicates + await this.systemMetadataRepository.set(SystemMetadataKey.MEMORIES_STATE, { + ...state, + lastOnThisDayDate: target.toISO(), + }); + } + }); + } + + private async createOnThisDayMemories(ownerId: string, userIds: string[], target: DateTime) { + const showAt = target.startOf('day').toISO(); + const hideAt = target.endOf('day').toISO(); + const memories = await this.assetRepository.getByDayOfYear([ownerId, ...userIds], target); + await Promise.all( + memories.map(({ year, assets }) => + this.memoryRepository.create( + { + ownerId, + type: MemoryType.ON_THIS_DAY, + data: { year }, + memoryAt: target.set({ year }).toISO()!, + showAt, + hideAt, + }, + new Set(assets.map(({ id }) => id)), + ), + ), + ); } @OnJob({ name: JobName.MEMORIES_CLEANUP, queue: QueueName.BACKGROUND_TASK }) diff --git a/server/test/medium/specs/services/memory.service.spec.ts b/server/test/medium/specs/services/memory.service.spec.ts index 445434d60a..8489e6bcc9 100644 --- a/server/test/medium/specs/services/memory.service.spec.ts +++ b/server/test/medium/specs/services/memory.service.spec.ts @@ -15,6 +15,7 @@ describe(MemoryService.name, () => { database: db || defaultDatabase, repos: { asset: 'real', + database: 'real', memory: 'real', user: 'real', systemMetadata: 'real', From 21880aec14270c54b26e930c016b7dd991679d90 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Fri, 16 May 2025 19:54:37 +0200 Subject: [PATCH 238/356] fix: z-index issues on search page (#18336) --- .../[[assetId=id]]/+page.svelte | 102 +++++++++--------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index d35e2697c1..5f995b9a7a 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -249,57 +249,6 @@ -

    - {#if assetInteraction.selectionActive} -
    - cancelMultiselect(assetInteraction)} - > - - - - - - - { - for (const id of ids) { - const asset = searchResultAssets.find((asset) => asset.id === id); - if (asset) { - asset.isFavorite = isFavorite; - } - } - }} - /> - - - - - - - {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} - - {/if} - -
    - -
    -
    -
    - {:else} -
    - goto(previousRoute)} backIcon={mdiArrowLeft}> -
    -
    - -
    -
    -
    - {/if} -
    - {#if terms}
    {/if}
    + +
    + {#if assetInteraction.selectionActive} +
    + cancelMultiselect(assetInteraction)} + > + + + + + + + { + for (const id of ids) { + const asset = searchResultAssets.find((asset) => asset.id === id); + if (asset) { + asset.isFavorite = isFavorite; + } + } + }} + /> + + + + + + + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} + + {/if} + +
    + +
    +
    +
    + {:else} +
    + goto(previousRoute)} backIcon={mdiArrowLeft}> +
    +
    + +
    +
    +
    + {/if} +
    From 53536581143dd41c345c922f2ec2d8d8a5b74986 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 16 May 2025 13:59:47 -0400 Subject: [PATCH 239/356] refactor: convert slider to switch (#18334) --- web/src/lib/components/elements/slider.svelte | 94 ------------------- .../settings/setting-switch.svelte | 10 +- 2 files changed, 5 insertions(+), 99 deletions(-) delete mode 100644 web/src/lib/components/elements/slider.svelte diff --git a/web/src/lib/components/elements/slider.svelte b/web/src/lib/components/elements/slider.svelte deleted file mode 100644 index 5c80eb2a9e..0000000000 --- a/web/src/lib/components/elements/slider.svelte +++ /dev/null @@ -1,94 +0,0 @@ - - - - - diff --git a/web/src/lib/components/shared-components/settings/setting-switch.svelte b/web/src/lib/components/shared-components/settings/setting-switch.svelte index aa165cfaaa..58e1649bb8 100644 --- a/web/src/lib/components/shared-components/settings/setting-switch.svelte +++ b/web/src/lib/components/shared-components/settings/setting-switch.svelte @@ -1,10 +1,10 @@ - -
    - - -
    diff --git a/web/src/lib/components/photos-page/delete-asset-dialog.svelte b/web/src/lib/components/photos-page/delete-asset-dialog.svelte index 336a0fd78a..4862e072b8 100644 --- a/web/src/lib/components/photos-page/delete-asset-dialog.svelte +++ b/web/src/lib/components/photos-page/delete-asset-dialog.svelte @@ -1,8 +1,8 @@ + +{#if menuItem} + handleUpdateDescription()} /> +{/if} diff --git a/web/src/lib/modals/AssetUpdateDecriptionConfirmModal.svelte b/web/src/lib/modals/AssetUpdateDecriptionConfirmModal.svelte new file mode 100644 index 0000000000..4d5a81f5fa --- /dev/null +++ b/web/src/lib/modals/AssetUpdateDecriptionConfirmModal.svelte @@ -0,0 +1,29 @@ + + + (confirmed ? onClose(description) : onClose())} +> + {#snippet promptSnippet()} +
    +
    + + +
    +
    + {/snippet} +
    diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 088d3dae97..e46ad0fc77 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -13,6 +13,7 @@ import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte'; + import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte'; import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte'; import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; @@ -478,6 +479,7 @@ {#if assetInteraction.isAllUserOwned} + {#if assetInteraction.selectedAssets.length === 1} + + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 1dc213729d..ea726d783a 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -11,6 +11,7 @@ import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte'; + import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte'; import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte'; import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; @@ -515,6 +516,7 @@ onClick={handleReassignAssets} /> + {/if} + assetStore.removeAssets(assetIds)} /> {#if $preferences.tags.enabled} diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index 5f995b9a7a..813683244e 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -9,6 +9,7 @@ import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte'; import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte'; + import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte'; import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte'; import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; @@ -358,6 +359,7 @@ + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} From 61d784f4e79af7c413f602c6a300407aa88f44ab Mon Sep 17 00:00:00 2001 From: Snowknight26 Date: Sat, 17 May 2025 08:05:23 -0500 Subject: [PATCH 243/356] fix(web): Make QR code colors solid (#18340) --- web/src/lib/components/shared-components/qrcode.svelte | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/web/src/lib/components/shared-components/qrcode.svelte b/web/src/lib/components/shared-components/qrcode.svelte index 3940975fba..5fa83e880c 100644 --- a/web/src/lib/components/shared-components/qrcode.svelte +++ b/web/src/lib/components/shared-components/qrcode.svelte @@ -1,6 +1,4 @@
    From a65c905621603fbfb8148df7ae3f9d923a587a41 Mon Sep 17 00:00:00 2001 From: Dhaval Javia <31767853+dj0024javia@users.noreply.github.com> Date: Sun, 18 May 2025 02:39:15 +0530 Subject: [PATCH 244/356] fix: delay settings apply for slideshow popup (#18028) * fix: fixed slideshow values to apply on done. * chore: linting error fixes * feat: added cancel button and changed text from done to confirm --- .../lib/components/slideshow-settings.svelte | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/web/src/lib/components/slideshow-settings.svelte b/web/src/lib/components/slideshow-settings.svelte index 4af6cdc5e7..c30d2cfb09 100644 --- a/web/src/lib/components/slideshow-settings.svelte +++ b/web/src/lib/components/slideshow-settings.svelte @@ -25,6 +25,13 @@ let { onClose = () => {} }: Props = $props(); + // Temporary variables to hold the settings - marked as reactive with $state() but initialized with store values + let tempSlideshowDelay = $state($slideshowDelay); + let tempShowProgressBar = $state($showProgressBar); + let tempSlideshowNavigation = $state($slideshowNavigation); + let tempSlideshowLook = $state($slideshowLook); + let tempSlideshowTransition = $state($slideshowTransition); + const navigationOptions: Record = { [SlideshowNavigation.Shuffle]: { icon: mdiShuffle, title: $t('shuffle') }, [SlideshowNavigation.AscendingOrder]: { icon: mdiArrowUpThin, title: $t('backward') }, @@ -47,6 +54,15 @@ } } }; + + const applyChanges = () => { + $slideshowDelay = tempSlideshowDelay; + $showProgressBar = tempShowProgressBar; + $slideshowNavigation = tempSlideshowNavigation; + $slideshowLook = tempSlideshowLook; + $slideshowTransition = tempSlideshowTransition; + onClose(); + }; onClose()}> @@ -54,31 +70,32 @@ { - $slideshowNavigation = handleToggle(option, navigationOptions) || $slideshowNavigation; + tempSlideshowNavigation = handleToggle(option, navigationOptions) || tempSlideshowNavigation; }} /> { - $slideshowLook = handleToggle(option, lookOptions) || $slideshowLook; + tempSlideshowLook = handleToggle(option, lookOptions) || tempSlideshowLook; }} /> - - + +
    {#snippet stickyBottom()} - + + {/snippet} From 0bbe70e6a347d3a4c74bc92701043d9d84a4b3fa Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Sat, 17 May 2025 22:57:08 -0400 Subject: [PATCH 245/356] feat(web): lighter timeline buckets (#17719) * feat(web): lighter timeline buckets * GalleryViewer * weird ssr * Remove generics from AssetInteraction * ensure keys on getAssetInfo, alt-text * empty - trigger ci * re-add alt-text * test fix * update tests * tests * missing import * fix: flappy e2e test * lint * revert settings * unneeded cast * fix after merge * missing import * lint * review * lint * avoid abbreviations * review comment - type safety in test * merge conflicts * lint * lint/abbreviations * fix: left-over migration --------- Co-authored-by: Alex --- .../components/album-page/album-viewer.svelte | 2 +- .../components/asset-viewer/actions/action.ts | 29 +-- .../actions/add-to-album-action.svelte | 5 +- .../actions/archive-action.svelte | 5 +- .../asset-viewer/actions/delete-action.svelte | 7 +- .../actions/download-action.svelte | 8 +- .../actions/favorite-action.svelte | 6 +- .../actions/keep-this-delete-others.svelte | 3 +- .../actions/restore-action.svelte | 3 +- .../actions/set-visibility-action.svelte | 5 +- .../actions/unstack-action.svelte | 3 +- .../asset-viewer/asset-viewer-nav-bar.spec.ts | 1 + .../asset-viewer/asset-viewer-nav-bar.svelte | 7 +- .../asset-viewer/asset-viewer.svelte | 25 +- .../editor/crop-tool/crop-area.svelte | 3 +- .../asset-viewer/photo-viewer.svelte | 22 +- .../assets/thumbnail/thumbnail.svelte | 31 +-- .../memory-page/memory-viewer.svelte | 40 +-- .../photos-page/actions/archive-action.svelte | 11 +- .../actions/asset-job-actions.svelte | 6 +- .../actions/change-date-action.svelte | 4 +- .../actions/download-action.svelte | 14 +- .../actions/link-live-photo-action.svelte | 23 +- .../photos-page/actions/stack-action.svelte | 9 +- .../photos-page/asset-date-group.svelte | 19 +- .../components/photos-page/asset-grid.svelte | 46 ++-- .../asset-select-control-bar.svelte | 10 +- .../components/photos-page/memory-lane.svelte | 3 +- .../individual-shared-viewer.svelte | 27 ++- .../gallery-viewer/gallery-viewer.svelte | 118 ++++----- .../duplicates/duplicate-asset.svelte | 3 +- .../stores/asset-interaction.svelte.spec.ts | 13 +- .../lib/stores/asset-interaction.svelte.ts | 19 +- web/src/lib/stores/asset-viewing.store.ts | 5 +- web/src/lib/stores/assets-store.spec.ts | 55 ++--- web/src/lib/stores/assets-store.svelte.ts | 229 +++++++++++------- web/src/lib/stores/memory.store.svelte.ts | 15 +- web/src/lib/utils/actions.ts | 16 +- web/src/lib/utils/asset-utils.ts | 36 +-- web/src/lib/utils/layout-utils.ts | 11 +- web/src/lib/utils/slideshow-history.ts | 8 +- web/src/lib/utils/thumbnail-util.spec.ts | 64 +++-- web/src/lib/utils/thumbnail-util.ts | 19 +- web/src/lib/utils/timeline-util.ts | 40 +++ web/src/lib/utils/tunables.ts | 17 +- .../[[assetId=id]]/+page.svelte | 5 +- .../[[assetId=id]]/+page.svelte | 4 +- .../[[assetId=id]]/+page.svelte | 3 +- .../[[assetId=id]]/+page.svelte | 29 ++- .../[[assetId=id]]/+page.svelte | 5 +- .../(user)/photos/[[assetId=id]]/+page.svelte | 7 +- .../[[assetId=id]]/+page.svelte | 75 +++++- web/src/test-data/factories/asset-factory.ts | 23 ++ 53 files changed, 725 insertions(+), 471 deletions(-) diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index deeb89c5c3..62216a750c 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -2,6 +2,7 @@ import { shortcut } from '$lib/actions/shortcut'; import AlbumMap from '$lib/components/album-page/album-map.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; + import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetStore } from '$lib/stores/assets-store.svelte'; @@ -16,7 +17,6 @@ import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import DownloadAction from '../photos-page/actions/download-action.svelte'; import AssetGrid from '../photos-page/asset-grid.svelte'; - import AssetSelectControlBar from '../photos-page/asset-select-control-bar.svelte'; import ControlAppBar from '../shared-components/control-app-bar.svelte'; import ImmichLogoSmallLink from '../shared-components/immich-logo-small-link.svelte'; import ThemeButton from '../shared-components/theme-button.svelte'; diff --git a/web/src/lib/components/asset-viewer/actions/action.ts b/web/src/lib/components/asset-viewer/actions/action.ts index d85325b59a..0918c86bfe 100644 --- a/web/src/lib/components/asset-viewer/actions/action.ts +++ b/web/src/lib/components/asset-viewer/actions/action.ts @@ -1,20 +1,21 @@ import type { AssetAction } from '$lib/constants'; -import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk'; +import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; +import type { AlbumResponseDto } from '@immich/sdk'; type ActionMap = { - [AssetAction.ARCHIVE]: { asset: AssetResponseDto }; - [AssetAction.UNARCHIVE]: { asset: AssetResponseDto }; - [AssetAction.FAVORITE]: { asset: AssetResponseDto }; - [AssetAction.UNFAVORITE]: { asset: AssetResponseDto }; - [AssetAction.TRASH]: { asset: AssetResponseDto }; - [AssetAction.DELETE]: { asset: AssetResponseDto }; - [AssetAction.RESTORE]: { asset: AssetResponseDto }; - [AssetAction.ADD]: { asset: AssetResponseDto }; - [AssetAction.ADD_TO_ALBUM]: { asset: AssetResponseDto; album: AlbumResponseDto }; - [AssetAction.UNSTACK]: { assets: AssetResponseDto[] }; - [AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: AssetResponseDto }; - [AssetAction.SET_VISIBILITY_LOCKED]: { asset: AssetResponseDto }; - [AssetAction.SET_VISIBILITY_TIMELINE]: { asset: AssetResponseDto }; + [AssetAction.ARCHIVE]: { asset: TimelineAsset }; + [AssetAction.UNARCHIVE]: { asset: TimelineAsset }; + [AssetAction.FAVORITE]: { asset: TimelineAsset }; + [AssetAction.UNFAVORITE]: { asset: TimelineAsset }; + [AssetAction.TRASH]: { asset: TimelineAsset }; + [AssetAction.DELETE]: { asset: TimelineAsset }; + [AssetAction.RESTORE]: { asset: TimelineAsset }; + [AssetAction.ADD]: { asset: TimelineAsset }; + [AssetAction.ADD_TO_ALBUM]: { asset: TimelineAsset; album: AlbumResponseDto }; + [AssetAction.UNSTACK]: { assets: TimelineAsset[] }; + [AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: TimelineAsset }; + [AssetAction.SET_VISIBILITY_LOCKED]: { asset: TimelineAsset }; + [AssetAction.SET_VISIBILITY_TIMELINE]: { asset: TimelineAsset }; }; export type Action = { diff --git a/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte b/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte index 202f0e4593..4ebe9d002a 100644 --- a/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte @@ -6,6 +6,7 @@ import Portal from '$lib/components/shared-components/portal/portal.svelte'; import { AssetAction } from '$lib/constants'; import { addAssetsToAlbum, addAssetsToNewAlbum } from '$lib/utils/asset-utils'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk'; import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -24,14 +25,14 @@ showSelectionModal = false; const album = await addAssetsToNewAlbum(albumName, [asset.id]); if (album) { - onAction({ type: AssetAction.ADD_TO_ALBUM, asset, album }); + onAction({ type: AssetAction.ADD_TO_ALBUM, asset: toTimelineAsset(asset), album }); } }; const handleAddToAlbum = async (album: AlbumResponseDto) => { showSelectionModal = false; await addAssetsToAlbum(album.id, [asset.id]); - onAction({ type: AssetAction.ADD_TO_ALBUM, asset, album }); + onAction({ type: AssetAction.ADD_TO_ALBUM, asset: toTimelineAsset(asset), album }); }; diff --git a/web/src/lib/components/asset-viewer/actions/archive-action.svelte b/web/src/lib/components/asset-viewer/actions/archive-action.svelte index ed19dff864..362a0a693a 100644 --- a/web/src/lib/components/asset-viewer/actions/archive-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/archive-action.svelte @@ -4,6 +4,7 @@ import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import { AssetAction } from '$lib/constants'; import { toggleArchive } from '$lib/utils/asset-utils'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import type { AssetResponseDto } from '@immich/sdk'; import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -18,11 +19,11 @@ const onArchive = async () => { if (!asset.isArchived) { - preAction({ type: AssetAction.ARCHIVE, asset }); + preAction({ type: AssetAction.ARCHIVE, asset: toTimelineAsset(asset) }); } const updatedAsset = await toggleArchive(asset); if (updatedAsset) { - onAction({ type: asset.isArchived ? AssetAction.ARCHIVE : AssetAction.UNARCHIVE, asset }); + onAction({ type: asset.isArchived ? AssetAction.ARCHIVE : AssetAction.UNARCHIVE, asset: toTimelineAsset(asset) }); } }; diff --git a/web/src/lib/components/asset-viewer/actions/delete-action.svelte b/web/src/lib/components/asset-viewer/actions/delete-action.svelte index 24ba2c845d..90322c00f0 100644 --- a/web/src/lib/components/asset-viewer/actions/delete-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/delete-action.svelte @@ -11,6 +11,7 @@ import { showDeleteModal } from '$lib/stores/preferences.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { handleError } from '$lib/utils/handle-error'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import { deleteAssets, type AssetResponseDto } from '@immich/sdk'; import { mdiDeleteForeverOutline, mdiDeleteOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -42,9 +43,9 @@ const trashAsset = async () => { try { - preAction({ type: AssetAction.TRASH, asset }); + preAction({ type: AssetAction.TRASH, asset: toTimelineAsset(asset) }); await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id] } }); - onAction({ type: AssetAction.TRASH, asset }); + onAction({ type: AssetAction.TRASH, asset: toTimelineAsset(asset) }); notificationController.show({ message: $t('moved_to_trash'), @@ -58,7 +59,7 @@ const deleteAsset = async () => { try { await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id], force: true } }); - onAction({ type: AssetAction.DELETE, asset }); + onAction({ type: AssetAction.DELETE, asset: toTimelineAsset(asset) }); notificationController.show({ message: $t('permanently_deleted_asset'), diff --git a/web/src/lib/components/asset-viewer/actions/download-action.svelte b/web/src/lib/components/asset-viewer/actions/download-action.svelte index d7f4f56352..c32766a725 100644 --- a/web/src/lib/components/asset-viewer/actions/download-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/download-action.svelte @@ -2,19 +2,21 @@ import { shortcut } from '$lib/actions/shortcut'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; + import { authManager } from '$lib/managers/auth-manager.svelte'; + import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; import { downloadFile } from '$lib/utils/asset-utils'; - import type { AssetResponseDto } from '@immich/sdk'; + import { getAssetInfo } from '@immich/sdk'; import { mdiFolderDownloadOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; interface Props { - asset: AssetResponseDto; + asset: TimelineAsset; menuItem?: boolean; } let { asset, menuItem = false }: Props = $props(); - const onDownloadFile = () => downloadFile(asset); + const onDownloadFile = async () => downloadFile(await getAssetInfo({ id: asset.id, key: authManager.key })); diff --git a/web/src/lib/components/asset-viewer/actions/favorite-action.svelte b/web/src/lib/components/asset-viewer/actions/favorite-action.svelte index 0cc3188d51..bb1a9343d9 100644 --- a/web/src/lib/components/asset-viewer/actions/favorite-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/favorite-action.svelte @@ -7,6 +7,7 @@ } from '$lib/components/shared-components/notification/notification'; import { AssetAction } from '$lib/constants'; import { handleError } from '$lib/utils/handle-error'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import { updateAsset, type AssetResponseDto } from '@immich/sdk'; import { mdiHeart, mdiHeartOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -30,7 +31,10 @@ asset = { ...asset, isFavorite: data.isFavorite }; - onAction({ type: asset.isFavorite ? AssetAction.FAVORITE : AssetAction.UNFAVORITE, asset }); + onAction({ + type: asset.isFavorite ? AssetAction.FAVORITE : AssetAction.UNFAVORITE, + asset: toTimelineAsset(asset), + }); notificationController.show({ type: NotificationType.Info, diff --git a/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte b/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte index 090e87f4a9..80dfb35067 100644 --- a/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte +++ b/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte @@ -3,6 +3,7 @@ import { AssetAction } from '$lib/constants'; import { modalManager } from '$lib/managers/modal-manager.svelte'; import { keepThisDeleteOthers } from '$lib/utils/asset-utils'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import type { AssetResponseDto, StackResponseDto } from '@immich/sdk'; import { mdiPinOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -29,7 +30,7 @@ const keptAsset = await keepThisDeleteOthers(asset, stack); if (keptAsset) { - onAction({ type: AssetAction.UNSTACK, assets: [keptAsset] }); + onAction({ type: AssetAction.UNSTACK, assets: [toTimelineAsset(keptAsset)] }); } }; diff --git a/web/src/lib/components/asset-viewer/actions/restore-action.svelte b/web/src/lib/components/asset-viewer/actions/restore-action.svelte index abcae5c4c9..c790dab853 100644 --- a/web/src/lib/components/asset-viewer/actions/restore-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/restore-action.svelte @@ -6,6 +6,7 @@ } from '$lib/components/shared-components/notification/notification'; import { AssetAction } from '$lib/constants'; import { handleError } from '$lib/utils/handle-error'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import { restoreAssets, type AssetResponseDto } from '@immich/sdk'; import { mdiHistory } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -23,7 +24,7 @@ await restoreAssets({ bulkIdsDto: { ids: [asset.id] } }); asset.isTrashed = false; - onAction({ type: AssetAction.RESTORE, asset }); + onAction({ type: AssetAction.RESTORE, asset: toTimelineAsset(asset) }); notificationController.show({ type: NotificationType.Info, diff --git a/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte b/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte index 6a7f6d3078..d133010af7 100644 --- a/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte @@ -3,14 +3,15 @@ import { AssetAction } from '$lib/constants'; import { modalManager } from '$lib/managers/modal-manager.svelte'; + import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; import { handleError } from '$lib/utils/handle-error'; - import { AssetVisibility, updateAssets, Visibility, type AssetResponseDto } from '@immich/sdk'; + import { AssetVisibility, updateAssets, Visibility } from '@immich/sdk'; import { mdiEyeOffOutline, mdiFolderMoveOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; import type { OnAction, PreAction } from './action'; interface Props { - asset: AssetResponseDto; + asset: TimelineAsset; onAction: OnAction; preAction: PreAction; } diff --git a/web/src/lib/components/asset-viewer/actions/unstack-action.svelte b/web/src/lib/components/asset-viewer/actions/unstack-action.svelte index f2a50cce13..1adeead05f 100644 --- a/web/src/lib/components/asset-viewer/actions/unstack-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/unstack-action.svelte @@ -2,6 +2,7 @@ import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import { AssetAction } from '$lib/constants'; import { deleteStack } from '$lib/utils/asset-utils'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import type { StackResponseDto } from '@immich/sdk'; import { mdiImageMinusOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -17,7 +18,7 @@ const handleUnstack = async () => { const unstackedAssets = await deleteStack([stack.id]); if (unstackedAssets) { - onAction({ type: AssetAction.UNSTACK, assets: unstackedAssets }); + onAction({ type: AssetAction.UNSTACK, assets: unstackedAssets.map((asset) => toTimelineAsset(asset)) }); } }; diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts index a25ea6bf90..f77fbc7f20 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts @@ -13,6 +13,7 @@ describe('AssetViewerNavBar component', () => { showDownloadButton: false, showMotionPlayButton: false, showShareButton: false, + preAction: () => {}, onZoomImage: () => {}, onCopyImage: () => {}, onAction: () => {}, diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 9436dc13c8..9a52067feb 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -25,6 +25,7 @@ import { getAssetJobName, getSharedLink } from '$lib/utils'; import { canCopyImageToClipboard } from '$lib/utils/asset-utils'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import { AssetJobName, AssetTypeEnum, @@ -138,7 +139,7 @@ {/if} {#if !isOwner && showDownloadButton} - + {/if} {#if showDetailButton} @@ -166,7 +167,7 @@ {/if} {#if showDownloadButton} - + {/if} {#if !isLocked} @@ -210,7 +211,7 @@ {/if} {#if !asset.isTrashed} - + {/if}
    void; + onClose: (asset: AssetResponseDto) => void; onNext: () => Promise; onPrevious: () => Promise; - onRandom: () => Promise; + onRandom: () => Promise<{ id: string } | undefined>; copyImage?: () => Promise; } @@ -81,7 +83,7 @@ copyImage = $bindable(), }: Props = $props(); - const { setAsset } = assetViewingStore; + const { setAssetId } = assetViewingStore; const { restartProgress: restartSlideshowProgress, stopProgress: stopSlideshowProgress, @@ -121,7 +123,7 @@ untrack(() => { if (stack && stack?.assets.length > 1) { - preloadAssets.push(stack.assets[1]); + preloadAssets.push(toTimelineAsset(stack.assets[1])); } }); }; @@ -161,7 +163,7 @@ slideshowStateUnsubscribe = slideshowState.subscribe((value) => { if (value === SlideshowState.PlaySlideshow) { slideshowHistory.reset(); - slideshowHistory.queue(asset); + slideshowHistory.queue(toTimelineAsset(asset)); handlePromiseError(handlePlaySlideshow()); } else if (value === SlideshowState.StopSlideshow) { handlePromiseError(handleStopSlideshow()); @@ -171,7 +173,7 @@ shuffleSlideshowUnsubscribe = slideshowNavigation.subscribe((value) => { if (value === SlideshowNavigation.Shuffle) { slideshowHistory.reset(); - slideshowHistory.queue(asset); + slideshowHistory.queue(toTimelineAsset(asset)); } }); @@ -225,7 +227,7 @@ }; const closeViewer = () => { - onClose({ asset }); + onClose(asset); }; const closeEditor = () => { @@ -292,8 +294,7 @@ let assetViewerHtmlElement = $state(); const slideshowHistory = new SlideshowHistory((asset) => { - setAsset(asset); - $restartSlideshowProgress = true; + handlePromiseError(setAssetId(asset.id).then(() => ($restartSlideshowProgress = true))); }); const handleVideoStarted = () => { @@ -563,8 +564,8 @@ imageClass={{ 'border-2 border-white': stackedAsset.id === asset.id }} brokenAssetClass="text-xs" dimmed={stackedAsset.id !== asset.id} - asset={stackedAsset} - onClick={(stackedAsset) => { + asset={toTimelineAsset(stackedAsset)} + onClick={() => { asset = stackedAsset; }} onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)} diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte index 9c4b0bcaa4..a264ad8ddd 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte @@ -12,6 +12,7 @@ resetGlobalCropStore, rotateDegrees, } from '$lib/stores/asset-editor.store'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import type { AssetResponseDto } from '@immich/sdk'; import { animateCropChange, recalculateCrop } from './crop-settings'; import { cropAreaEl, cropFrame, imgElement, isResizingOrDragging, overlayEl, resetCropStore } from './crop-store'; @@ -81,7 +82,7 @@ aria-label="Crop area" type="button" > - {$getAltText(asset)} + {$getAltText(toTimelineAsset(asset))}
    diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 6711d126ca..564cef5308 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -3,7 +3,7 @@ import { zoomImageAction, zoomed } from '$lib/actions/zoom-image'; import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte'; import BrokenAsset from '$lib/components/assets/broken-asset.svelte'; - import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; + import { photoViewerImgElement, type TimelineAsset } from '$lib/stores/assets-store.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { boundingBoxesArray } from '$lib/stores/people.store'; import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store'; @@ -13,9 +13,10 @@ import { canCopyImageToClipboard, copyImageToClipboard, isWebCompatibleImage } from '$lib/utils/asset-utils'; import { handleError } from '$lib/utils/handle-error'; import { getBoundingBox } from '$lib/utils/people-utils'; - import { cancelImageUrl, preloadImageUrl } from '$lib/utils/sw-messaging'; + import { cancelImageUrl } from '$lib/utils/sw-messaging'; import { getAltText } from '$lib/utils/thumbnail-util'; - import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; + import { AssetMediaSize, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk'; import { onDestroy, onMount } from 'svelte'; import { swipe, type SwipeCustomEvent } from 'svelte-gestures'; import { t } from 'svelte-i18n'; @@ -25,7 +26,7 @@ interface Props { asset: AssetResponseDto; - preloadAssets?: AssetResponseDto[] | undefined; + preloadAssets?: TimelineAsset[] | undefined; element?: HTMLDivElement | undefined; haveFadeTransition?: boolean; sharedLink?: SharedLinkResponseDto | undefined; @@ -69,10 +70,11 @@ $boundingBoxesArray = []; }); - const preload = (targetSize: AssetMediaSize | 'original', preloadAssets?: AssetResponseDto[]) => { + const preload = (targetSize: AssetMediaSize | 'original', preloadAssets?: TimelineAsset[]) => { for (const preloadAsset of preloadAssets || []) { - if (preloadAsset.type === AssetTypeEnum.Image) { - preloadImageUrl(getAssetUrl(preloadAsset.id, targetSize, preloadAsset.thumbhash)); + if (preloadAsset.isImage) { + let img = new Image(); + img.src = getAssetUrl(preloadAsset.id, targetSize, preloadAsset.thumbhash); } } }; @@ -197,7 +199,7 @@ bind:clientWidth={containerWidth} bind:clientHeight={containerHeight} > - {$getAltText(asset)} + {#if !imageLoaded}
    @@ -213,7 +215,7 @@ {#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground} {$getAltText(asset)} @@ -221,7 +223,7 @@ {$getAltText(asset)} void) | undefined; - onSelect?: ((asset: AssetResponseDto) => void) | undefined; - onMouseEvent?: ((event: { isMouseOver: boolean; selectedGroupIndex: number }) => void) | undefined; - handleFocus?: (() => void) | undefined; + onClick?: (asset: TimelineAsset) => void; + onSelect?: (asset: TimelineAsset) => void; + onMouseEvent?: (event: { isMouseOver: boolean; selectedGroupIndex: number }) => void; + handleFocus?: () => void; } let { @@ -290,13 +291,13 @@
    {/if} - {#if !authManager.key && showArchiveIcon && asset.isArchived} + {#if !authManager.key && showArchiveIcon && asset.visibility === Visibility.Archive}
    {/if} - {#if asset.type === AssetTypeEnum.Image && asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR} + {#if asset.isImage && asset.projectionType === ProjectionType.EQUIRECTANGULAR}
    @@ -309,7 +310,7 @@
    @@ -329,17 +330,17 @@ curve={selected} onComplete={(errored) => ((loaded = true), (thumbError = errored))} /> - {#if asset.type === AssetTypeEnum.Video} + {#if asset.isVideo}
    - {:else if asset.type === AssetTypeEnum.Image && asset.livePhotoVideoId} + {:else if asset.isImage && asset.livePhotoVideoId}
    (undefined); + let currentMemoryAssetFull = $derived.by(async () => + current?.asset ? await getAssetInfo({ id: current.asset.id, key: authManager.key }) : undefined, + ); + let currentTimelineAssets = $derived(current?.memory.assets.map((asset) => toTimelineAsset(asset)) || []); + let isSaved = $derived(current?.memory.isSaved); let viewerHeight = $state(0); @@ -77,8 +84,8 @@ const assetInteraction = new AssetInteraction(); let progressBarController: Tween | undefined = $state(undefined); let videoPlayer: HTMLVideoElement | undefined = $state(); - const asHref = (asset: AssetResponseDto) => `?${QueryParameter.ID}=${asset.id}`; - const handleNavigate = async (asset?: AssetResponseDto) => { + const asHref = (asset: { id: string }) => `?${QueryParameter.ID}=${asset.id}`; + const handleNavigate = async (asset?: { id: string }) => { if ($isViewing) { return asset; } @@ -89,9 +96,9 @@ await goto(asHref(asset)); }; - const setProgressDuration = (asset: AssetResponseDto) => { - if (asset.type === AssetTypeEnum.Video) { - const timeParts = asset.duration.split(':').map(Number); + const setProgressDuration = (asset: TimelineAsset) => { + if (asset.isVideo) { + const timeParts = asset.duration!.split(':').map(Number); const durationInMilliseconds = (timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2]) * 1000; progressBarController = new Tween(0, { duration: (from: number, to: number) => (to ? durationInMilliseconds * (to - from) : 0), @@ -107,7 +114,8 @@ const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]); const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]); const handleEscape = async () => goto(AppRoute.PHOTOS); - const handleSelectAll = () => assetInteraction.selectAssets(current?.memory.assets || []); + const handleSelectAll = () => + assetInteraction.selectAssets(current?.memory.assets.map((a) => toTimelineAsset(a)) || []); const handleAction = async (callingContext: string, action: 'reset' | 'pause' | 'play') => { // leaving these log statements here as comments. Very useful to figure out what's going on during dev! // console.log(`handleAction[${callingContext}] called with: ${action}`); @@ -240,7 +248,7 @@ }; const initPlayer = () => { - const isVideoAssetButPlayerHasNotLoadedYet = current && current.asset.type === AssetTypeEnum.Video && !videoPlayer; + const isVideoAssetButPlayerHasNotLoadedYet = current && current.asset.isVideo && !videoPlayer; if (playerInitialized || isVideoAssetButPlayerHasNotLoadedYet) { return; } @@ -441,7 +449,7 @@
    {#key current.asset.id}
    - {#if current.asset.type === AssetTypeEnum.Video} + {#if current.asset.isVideo}
    @@ -623,7 +633,7 @@ import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import type { OnArchive } from '$lib/utils/actions'; + import { archiveAssets } from '$lib/utils/asset-utils'; + import { AssetVisibility, Visibility } from '@immich/sdk'; import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiTimerSand } from '@mdi/js'; + import { t } from 'svelte-i18n'; import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte'; - import { archiveAssets } from '$lib/utils/asset-utils'; - import { t } from 'svelte-i18n'; interface Props { onArchive?: OnArchive; @@ -23,10 +24,10 @@ const { clearSelect, getOwnedAssets } = getAssetControlContext(); const handleArchive = async () => { - const isArchived = !unarchive; - const assets = [...getOwnedAssets()].filter((asset) => asset.isArchived !== isArchived); + const isArchived = unarchive ? Visibility.Timeline : Visibility.Archive; + const assets = [...getOwnedAssets()].filter((asset) => asset.visibility !== isArchived); loading = true; - const ids = await archiveAssets(assets, isArchived); + const ids = await archiveAssets(assets, isArchived as unknown as AssetVisibility); if (ids) { onArchive?.(ids, isArchived); clearSelect(); diff --git a/web/src/lib/components/photos-page/actions/asset-job-actions.svelte b/web/src/lib/components/photos-page/actions/asset-job-actions.svelte index 89c0b42165..5676ad5fbf 100644 --- a/web/src/lib/components/photos-page/actions/asset-job-actions.svelte +++ b/web/src/lib/components/photos-page/actions/asset-job-actions.svelte @@ -6,9 +6,9 @@ } from '$lib/components/shared-components/notification/notification'; import { getAssetJobIcon, getAssetJobMessage, getAssetJobName } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; - import { AssetJobName, AssetTypeEnum, runAssetJobs } from '@immich/sdk'; - import { getAssetControlContext } from '../asset-select-control-bar.svelte'; + import { AssetJobName, runAssetJobs } from '@immich/sdk'; import { t } from 'svelte-i18n'; + import { getAssetControlContext } from '../asset-select-control-bar.svelte'; interface Props { jobs?: AssetJobName[]; @@ -19,7 +19,7 @@ const { clearSelect, getOwnedAssets } = getAssetControlContext(); - let isAllVideos = $derived([...getOwnedAssets()].every((asset) => asset.type === AssetTypeEnum.Video)); + const isAllVideos = $derived([...getOwnedAssets()].every((asset) => asset.isVideo)); const handleRunJob = async (name: AssetJobName) => { try { diff --git a/web/src/lib/components/photos-page/actions/change-date-action.svelte b/web/src/lib/components/photos-page/actions/change-date-action.svelte index 3232cbd2b4..5f65fdd744 100644 --- a/web/src/lib/components/photos-page/actions/change-date-action.svelte +++ b/web/src/lib/components/photos-page/actions/change-date-action.svelte @@ -4,11 +4,11 @@ import { getSelectedAssets } from '$lib/utils/asset-utils'; import { handleError } from '$lib/utils/handle-error'; import { updateAssets } from '@immich/sdk'; + import { mdiCalendarEditOutline } from '@mdi/js'; import { DateTime } from 'luxon'; + import { t } from 'svelte-i18n'; import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte'; - import { mdiCalendarEditOutline } from '@mdi/js'; - import { t } from 'svelte-i18n'; interface Props { menuItem?: boolean; } diff --git a/web/src/lib/components/photos-page/actions/download-action.svelte b/web/src/lib/components/photos-page/actions/download-action.svelte index 1651936c08..df079e45b2 100644 --- a/web/src/lib/components/photos-page/actions/download-action.svelte +++ b/web/src/lib/components/photos-page/actions/download-action.svelte @@ -1,11 +1,14 @@ @@ -94,14 +97,14 @@ clearSelect={() => cancelMultiselect(assetInteraction)} > - + cancelMultiselect(assetInteraction)} /> cancelMultiselect(assetInteraction)} shared /> { + onFavorite={function handleFavoriteUpdate(ids, isFavorite) { if (data.pathAssets && data.pathAssets.length > 0) { for (const id of ids) { const asset = data.pathAssets.find((asset) => asset.id === id); @@ -141,17 +144,17 @@ icons={{ default: mdiFolderOutline, active: mdiFolder }} items={tree} active={currentPath} - {getLink} + getLink={getLinkForPath} />
    {/snippet} - +
    - + {#if data.pathAssets && data.pathAssets.length > 0} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index ea726d783a..da64314ecf 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -35,7 +35,7 @@ import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import { AssetStore } from '$lib/stores/assets-store.svelte'; + import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte'; import { locale } from '$lib/stores/preferences.store'; import { preferences } from '$lib/stores/user.store'; import { websocketEvents } from '$lib/stores/websocket'; @@ -47,7 +47,6 @@ getPersonStatistics, searchPerson, updatePerson, - type AssetResponseDto, type PersonResponseDto, } from '@immich/sdk'; import { @@ -204,7 +203,7 @@ data = { ...data, person }; }; - const handleSelectFeaturePhoto = async (asset: AssetResponseDto) => { + const handleSelectFeaturePhoto = async (asset: TimelineAsset) => { if (viewMode !== PersonPageViewMode.SELECT_PERSON) { return; } diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index 162beaf8f5..82816b36b4 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -34,7 +34,8 @@ type OnUnlink, } from '$lib/utils/actions'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; - import { AssetTypeEnum, AssetVisibility } from '@immich/sdk'; + import { AssetVisibility } from '@immich/sdk'; + import { mdiDotsVertical, mdiPlus } from '@mdi/js'; import { onDestroy } from 'svelte'; import { t } from 'svelte-i18n'; @@ -52,8 +53,8 @@ const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId; const isLivePhotoCandidate = selectedAssets.length === 2 && - selectedAssets.some((asset) => asset.type === AssetTypeEnum.Image) && - selectedAssets.some((asset) => asset.type === AssetTypeEnum.Video); + selectedAssets.some((asset) => asset.isImage) && + selectedAssets.some((asset) => asset.isVideo); return assetInteraction.isAllUserOwned && (isLivePhoto || isLivePhotoCandidate); }); diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index 813683244e..8c8036903f 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -25,7 +25,7 @@ import { AppRoute, QueryParameter } from '$lib/constants'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import type { Viewport } from '$lib/stores/assets-store.svelte'; + import type { TimelineAsset, Viewport } from '$lib/stores/assets-store.svelte'; import { lang, locale } from '$lib/stores/preferences.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { preferences } from '$lib/stores/user.store'; @@ -34,9 +34,9 @@ import { parseUtcDate } from '$lib/utils/date-time'; import { handleError } from '$lib/utils/handle-error'; import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import { type AlbumResponseDto, - type AssetResponseDto, getPerson, getTagById, type MetadataSearchDto, @@ -59,7 +59,7 @@ let nextPage = $state(1); let searchResultAlbums: AlbumResponseDto[] = $state([]); - let searchResultAssets: AssetResponseDto[] = $state([]); + let searchResultAssets: TimelineAsset[] = $state([]); let isLoading = $state(true); let scrollY = $state(0); let scrollYHistory = 0; @@ -123,7 +123,7 @@ const onAssetDelete = (assetIds: string[]) => { const assetIdSet = new Set(assetIds); - searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id)); + searchResultAssets = searchResultAssets.filter((asset: TimelineAsset) => !assetIdSet.has(asset.id)); }; const handleSelectAll = () => { assetInteraction.selectAssets(searchResultAssets); @@ -161,7 +161,7 @@ : await searchAssets({ metadataSearchDto: searchDto }); searchResultAlbums.push(...albums.items); - searchResultAssets.push(...assets.items); + searchResultAssets.push(...assets.items.map((asset) => toTimelineAsset(asset))); nextPage = Number(assets.nextPage) || 0; } catch (error) { @@ -239,7 +239,7 @@ if (terms.isNotInAlbum.toString() == 'true') { const assetIdSet = new Set(assetIds); - searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id)); + searchResultAssets = searchResultAssets.filter((asset) => !assetIdSet.has(asset.id)); } }; @@ -250,30 +250,81 @@ +
    + {#if assetInteraction.selectionActive} +
    + cancelMultiselect(assetInteraction)} + > + + + + + + + { + for (const assetId of assetIds) { + const asset = searchResultAssets.find((searchAsset) => searchAsset.id === assetId); + if (asset) { + asset.isFavorite = isFavorite; + } + } + }} + /> + + + + + + + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} + + {/if} + +
    + +
    +
    +
    + {:else} +
    + goto(previousRoute)} backIcon={mdiArrowLeft}> +
    +
    + +
    +
    +
    + {/if} +
    + {#if terms}
    - {#each getObjectKeys(terms) as key (key)} - {@const value = terms[key]} + {#each getObjectKeys(terms) as searchKey (searchKey)} + {@const value = terms[searchKey]}
    - {getHumanReadableSearchKey(key as keyof SearchTerms)} + {getHumanReadableSearchKey(searchKey as keyof SearchTerms)}
    {#if value !== true}
    - {#if (key === 'takenAfter' || key === 'takenBefore') && typeof value === 'string'} + {#if (searchKey === 'takenAfter' || searchKey === 'takenBefore') && typeof value === 'string'} {getHumanReadableDate(value)} - {:else if key === 'personIds' && Array.isArray(value)} + {:else if searchKey === 'personIds' && Array.isArray(value)} {#await getPersonName(value) then personName} {personName} {/await} - {:else if key === 'tagIds' && Array.isArray(value)} + {:else if searchKey === 'tagIds' && Array.isArray(value)} {#await getTagNames(value) then tagNames} {tagNames} {/await} diff --git a/web/src/test-data/factories/asset-factory.ts b/web/src/test-data/factories/asset-factory.ts index b727286590..bdffecc8bc 100644 --- a/web/src/test-data/factories/asset-factory.ts +++ b/web/src/test-data/factories/asset-factory.ts @@ -1,3 +1,4 @@ +import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; import { faker } from '@faker-js/faker'; import { AssetTypeEnum, Visibility, type AssetResponseDto } from '@immich/sdk'; import { Sync } from 'factory.ts'; @@ -26,3 +27,25 @@ export const assetFactory = Sync.makeFactory({ hasMetadata: Sync.each(() => faker.datatype.boolean()), visibility: Visibility.Timeline, }); + +export const timelineAssetFactory = Sync.makeFactory({ + id: Sync.each(() => faker.string.uuid()), + ratio: Sync.each(() => faker.number.int()), + ownerId: Sync.each(() => faker.string.uuid()), + thumbhash: Sync.each(() => faker.string.alphanumeric(28)), + localDateTime: Sync.each(() => faker.date.past().toISOString()), + isFavorite: Sync.each(() => faker.datatype.boolean()), + visibility: Visibility.Timeline, + isTrashed: false, + isImage: true, + isVideo: false, + duration: '0:00:00.00000', + stack: null, + projectionType: null, + livePhotoVideoId: Sync.each(() => faker.string.uuid()), + text: Sync.each(() => ({ + city: faker.location.city(), + country: faker.location.country(), + people: [faker.person.fullName()], + })), +}); From c411c1472a68257e2a730844e191309be8856109 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sun, 18 May 2025 13:05:16 +0200 Subject: [PATCH 246/356] chore(web): update translations (#18083) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: -J- Co-authored-by: Adam Tahri Co-authored-by: Andreas Johansen Co-authored-by: Antonio Vazquez Co-authored-by: Ash Mad Co-authored-by: Asier Zunzunegui Co-authored-by: Badri Isiani Co-authored-by: Bezruchenko Simon Co-authored-by: Bonov Co-authored-by: Denis Pacquier Co-authored-by: Dunya Cengiz Co-authored-by: Edi Hamiti Co-authored-by: FarSniper Co-authored-by: Florian Ostertag Co-authored-by: Hurricane-32 Co-authored-by: Imjustjokingwithya Co-authored-by: Indrek Haav Co-authored-by: JB Co-authored-by: Jan Hepaslimin Co-authored-by: Javier Villanueva García Co-authored-by: Jaymi Lai Co-authored-by: Jordy H Co-authored-by: JuanLu323 Co-authored-by: Junghyuk Kwon Co-authored-by: Leo Bottaro Co-authored-by: M Co-authored-by: Marc Casillas Co-authored-by: MarcusKLY <62999998a@gmail.com> Co-authored-by: Matjaž T Co-authored-by: Matthew Momjian Co-authored-by: Miki Mrvos Co-authored-by: Mārtiņš Bruņenieks Co-authored-by: Radovan Draskovic Co-authored-by: Remco Co-authored-by: Sebastian Schneider Co-authored-by: Serhii Co-authored-by: Shawn Co-authored-by: Simone Pagano Co-authored-by: Stan P Co-authored-by: Stefan Taiguara Co-authored-by: Sylvain Pichon Co-authored-by: Taiki M Co-authored-by: Tomi Pöyskö Co-authored-by: User 123456789 Co-authored-by: Vytautas Krivickas Co-authored-by: Väino Daum Co-authored-by: Waqas Ali Co-authored-by: Yago Raña Gayoso Co-authored-by: Z T Co-authored-by: anton garcias Co-authored-by: cherbib mehdi Co-authored-by: eav5jhl0 Co-authored-by: mehrdad Co-authored-by: millallo Co-authored-by: protonchang Co-authored-by: pyccl Co-authored-by: qtm Co-authored-by: taninme Co-authored-by: thehijacker Co-authored-by: theminer3746 Co-authored-by: timmy61109 Co-authored-by: tsengyuchen Co-authored-by: waclaw66 Co-authored-by: Вячеслав Лукьяненко Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> --- i18n/ar.json | 14 +- i18n/bg.json | 2 +- i18n/bn.json | 18 +- i18n/ca.json | 210 +++-- i18n/cs.json | 151 ++-- i18n/da.json | 2 +- i18n/de.json | 147 ++-- i18n/el.json | 20 +- i18n/en.json | 4 +- i18n/es.json | 157 ++-- i18n/et.json | 335 ++++++-- i18n/eu.json | 19 +- i18n/fa.json | 1 + i18n/fi.json | 185 ++-- i18n/fr.json | 151 ++-- i18n/gl.json | 154 ++-- i18n/he.json | 15 + i18n/hr.json | 2 +- i18n/hu.json | 34 +- i18n/hy.json | 2 +- i18n/id.json | 2 +- i18n/it.json | 153 ++-- i18n/ja.json | 45 +- i18n/ka.json | 18 +- i18n/ko.json | 147 ++-- i18n/lt.json | 34 +- i18n/lv.json | 223 ++--- i18n/nb_NO.json | 13 +- i18n/nl.json | 188 +++-- i18n/pl.json | 143 ++-- i18n/pt.json | 205 +++-- i18n/pt_BR.json | 160 ++-- i18n/ro.json | 2 +- i18n/ru.json | 151 ++-- i18n/sk.json | 2 +- i18n/sl.json | 174 ++-- i18n/sr_Cyrl.json | 1759 ++++++++++++++++++++------------------- i18n/sr_Latn.json | 729 ++++++++-------- i18n/sv.json | 2 +- i18n/ta.json | 2 +- i18n/te.json | 2 +- i18n/th.json | 15 +- i18n/tr.json | 16 +- i18n/uk.json | 164 ++-- i18n/ur.json | 36 +- i18n/vi.json | 2 +- i18n/zh_Hant.json | 508 +++++------ i18n/zh_SIMPLIFIED.json | 157 ++-- 48 files changed, 3834 insertions(+), 2841 deletions(-) diff --git a/i18n/ar.json b/i18n/ar.json index 815d870b08..a181a8375b 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -598,6 +598,7 @@ "change_password_form_new_password": "كلمة المرور الجديدة", "change_password_form_password_mismatch": "كلمة المرور غير مطابقة", "change_password_form_reenter_new_password": "أعد إدخال كلمة مرور جديدة", + "change_pin_code": "تغيير الرقم السري", "change_your_password": "غير كلمة المرور الخاصة بك", "changed_visibility_successfully": "تم تغيير الرؤية بنجاح", "check_all": "تحقق من الكل", @@ -638,6 +639,7 @@ "confirm_delete_face": "هل أنت متأكد من حذف وجه {name} من الأصول؟", "confirm_delete_shared_link": "هل أنت متأكد أنك تريد حذف هذا الرابط المشترك؟", "confirm_keep_this_delete_others": "سيتم حذف جميع الأصول الأخرى في المجموعة باستثناء هذا الأصل. هل أنت متأكد من أنك تريد المتابعة؟", + "confirm_new_pin_code": "ثبت الرقم السري الجديد", "confirm_password": "تأكيد كلمة المرور", "contain": "محتواة", "context": "السياق", @@ -683,6 +685,7 @@ "crop": "Crop", "curated_object_page_title": "أشياء", "current_device": "الجهاز الحالي", + "current_pin_code": "الرقم السري الحالي", "current_server_address": "Current server address", "custom_locale": "لغة مخصصة", "custom_locale_description": "تنسيق التواريخ والأرقام بناءً على اللغة والمنطقة", @@ -1221,6 +1224,7 @@ "new_api_key": "مفتاح API جديد", "new_password": "كلمة المرور الجديدة", "new_person": "شخص جديد", + "new_pin_code": "الرقم السري الجديد", "new_user_created": "تم إنشاء مستخدم جديد", "new_version_available": "إصدار جديد متاح", "newest_first": "الأحدث أولاً", @@ -1338,6 +1342,9 @@ "photos_count": "{count, plural, one {{count, number} صورة} other {{count, number} صور}}", "photos_from_previous_years": "صور من السنوات السابقة", "pick_a_location": "اختر موقعًا", + "pin_code_changed_successfully": "تم تغير الرقم السري", + "pin_code_reset_successfully": "تم اعادة تعيين الرقم السري", + "pin_code_setup_successfully": "تم انشاء رقم سري", "place": "مكان", "places": "الأماكن", "places_count": "{count, plural, one {{count, number} مكان} other {{count, number} أماكن}}", @@ -1368,7 +1375,7 @@ "public_share": "مشاركة عامة", "purchase_account_info": "داعم", "purchase_activated_subtitle": "شكرًا لك على دعمك لـ Immich والبرمجيات مفتوحة المصدر", - "purchase_activated_time": "تم التفعيل في {date, date}", + "purchase_activated_time": "تم التفعيل في {date}", "purchase_activated_title": "لقد تم تفعيل مفتاحك بنجاح", "purchase_button_activate": "تنشيط", "purchase_button_buy": "شراء", @@ -1594,6 +1601,7 @@ "settings": "الإعدادات", "settings_require_restart": "يرجى إعادة تشغيل لتطبيق هذا الإعداد", "settings_saved": "تم حفظ الإعدادات", + "setup_pin_code": "تحديد رقم سري", "share": "مشاركة", "share_add_photos": "إضافة الصور", "share_assets_selected": "{} selected", @@ -1778,6 +1786,8 @@ "trash_page_title": "Trash ({})", "trashed_items_will_be_permanently_deleted_after": "سيتم حذفُ العناصر المحذوفة نِهائيًا بعد {days, plural, one {# يوم} other {# أيام }}.", "type": "النوع", + "unable_to_change_pin_code": "تفيير الرقم السري غير ممكن", + "unable_to_setup_pin_code": "انشاء الرقم السري غير ممكن", "unarchive": "أخرج من الأرشيف", "unarchived_count": "{count, plural, other {غير مؤرشفة #}}", "unfavorite": "أزل التفضيل", @@ -1822,6 +1832,8 @@ "user": "مستخدم", "user_id": "معرف المستخدم", "user_liked": "قام {user} بالإعجاب {type, select, photo {بهذه الصورة} video {بهذا الفيديو} asset {بهذا المحتوى} other {بها}}", + "user_pin_code_settings": "الرقم السري", + "user_pin_code_settings_description": "تغير الرقم السري", "user_purchase_settings": "الشراء", "user_purchase_settings_description": "إدارة عملية الشراء الخاصة بك", "user_role_set": "قم بتعيين {user} كـ {role}", diff --git a/i18n/bg.json b/i18n/bg.json index 468b1637b0..f76ff9539f 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -1006,7 +1006,7 @@ "public_share": "Публично споделяне", "purchase_account_info": "Поддръжник", "purchase_activated_subtitle": "Благодарим ви, че подкрепяте Immich и софтуера с отворен код", - "purchase_activated_time": "Активиран на {date, date}", + "purchase_activated_time": "Активиран на {date}", "purchase_activated_title": "Вашият ключ беше успешно активиран", "purchase_button_activate": "Активирай", "purchase_button_buy": "Купи", diff --git a/i18n/bn.json b/i18n/bn.json index 0967ef424b..966d474111 100644 --- a/i18n/bn.json +++ b/i18n/bn.json @@ -1 +1,17 @@ -{} +{ + "about": "সম্পর্কে", + "account": "অ্যাকাউন্ট", + "account_settings": "অ্যাকাউন্ট সেটিংস", + "acknowledge": "স্বীকৃতি", + "action": "কার্য", + "action_common_update": "আপডেট", + "actions": "কর্ম", + "active": "সচল", + "activity": "কার্যকলাপ", + "add": "যোগ করুন", + "add_a_description": "একটি বিবরণ যোগ করুন", + "add_a_location": "একটি অবস্থান যোগ করুন", + "add_a_name": "একটি নাম যোগ করুন", + "add_a_title": "একটি শিরোনাম যোগ করুন", + "add_endpoint": "এন্ডপয়েন্ট যোগ করুন" +} diff --git a/i18n/ca.json b/i18n/ca.json index 38210ee5a9..bcd1225a84 100644 --- a/i18n/ca.json +++ b/i18n/ca.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "Estàs segur que vols desactivar tots els mètodes d'inici de sessió? L'inici de sessió quedarà completament desactivat.", "authentication_settings_reenable": "Per a tornar a habilitar, empra una Comanda de Servidor.", "background_task_job": "Tasques en segon pla", - "backup_database": "Còpia de la base de dades", - "backup_database_enable_description": "Habilitar còpies de la base de dades", - "backup_keep_last_amount": "Quantitat de còpies de seguretat anteriors per conservar", - "backup_settings": "Ajustes de les còpies de seguretat", - "backup_settings_description": "Gestionar la configuració de la còpia de seguretat de la base de dades", + "backup_database": "Fer un bolcat de la base de dades", + "backup_database_enable_description": "Habilitar bolcat de la base de dades", + "backup_keep_last_amount": "Quantitat de bolcats anteriors per conservar", + "backup_settings": "Configuració dels bolcats", + "backup_settings_description": "Gestionar la configuració bolcats de la base de dades. Nota: els treballs no es monitoritzen ni es notifiquen les fallades.", "check_all": "Marca-ho tot", "cleanup": "Neteja", "cleared_jobs": "Tasques esborrades per a: {job}", @@ -53,6 +53,7 @@ "confirm_email_below": "Per a confirmar, escriviu \"{email}\" a sota", "confirm_reprocess_all_faces": "Esteu segur que voleu reprocessar totes les cares? Això també esborrarà la gent que heu anomenat.", "confirm_user_password_reset": "Esteu segur que voleu reinicialitzar la contrasenya de l'usuari {user}?", + "confirm_user_pin_code_reset": "Esteu segur que voleu restablir el codi PIN de {user}?", "create_job": "Crear tasca", "cron_expression": "Expressió Cron", "cron_expression_description": "Estableix l'interval d'escaneig amb el format cron. Per obtenir més informació, consulteu, p.e Crontab Guru", @@ -192,6 +193,7 @@ "oauth_auto_register": "Registre automàtic", "oauth_auto_register_description": "Registra nous usuaris automàticament després d'iniciar sessió amb OAuth", "oauth_button_text": "Text del botó", + "oauth_client_secret_description": "Requerit si PKCE (Proof Key for Code Exchange) no està suportat pel proveïdor OAuth", "oauth_enable_description": "Iniciar sessió amb OAuth", "oauth_mobile_redirect_uri": "URI de redirecció mòbil", "oauth_mobile_redirect_uri_override": "Sobreescriu l'URI de redirecció mòbil", @@ -205,6 +207,8 @@ "oauth_storage_quota_claim_description": "Estableix automàticament la quota d'emmagatzematge de l'usuari al valor d'aquest paràmetre.", "oauth_storage_quota_default": "Quota d'emmagatzematge predeterminada (GiB)", "oauth_storage_quota_default_description": "Quota disponible en GB quan no s'estableixi cap valor (Entreu 0 per a quota il·limitada).", + "oauth_timeout": "Solicitud caducada", + "oauth_timeout_description": "Timeout per a sol·licituds en mil·lisegons", "offline_paths": "Rutes sense connexió", "offline_paths_description": "Aquests resultats poden ser deguts a l'eliminació manual de fitxers que no formen part d'una llibreria externa.", "password_enable_description": "Inicia sessió amb correu electrònic i contrasenya", @@ -345,6 +349,7 @@ "user_delete_delay_settings_description": "Nombre de dies després de la supressió per eliminar permanentment el compte i els elements d'un usuari. El treball de supressió d'usuaris s'executa a mitjanit per comprovar si hi ha usuaris preparats per eliminar. Els canvis en aquesta configuració s'avaluaran en la propera execució.", "user_delete_immediately": "El compte i els recursos de {user} es posaran a la cua per suprimir-los permanentment immediatament.", "user_delete_immediately_checkbox": "Posa en cua l'usuari i els recursos per suprimir-los immediatament", + "user_details": "Detalls d'usuari", "user_management": "Gestió d'usuaris", "user_password_has_been_reset": "La contrasenya de l'usuari ha estat restablida:", "user_password_reset_description": "Si us plau, proporcioneu la contrasenya temporal a l'usuari i informeu-los que haurà de canviar la contrasenya en el proper inici de sessió.", @@ -364,13 +369,17 @@ "admin_password": "Contrasenya de l'administrador", "administration": "Administrador", "advanced": "Avançat", - "advanced_settings_log_level_title": "Nivell de registre: {}", + "advanced_settings_enable_alternate_media_filter_subtitle": "Feu servir aquesta opció per filtrar els continguts multimèdia durant la sincronització segons criteris alternatius. Només proveu-ho si teniu problemes amb l'aplicació per detectar tots els àlbums.", + "advanced_settings_enable_alternate_media_filter_title": "Utilitza el filtre de sincronització d'àlbums de dispositius alternatius", + "advanced_settings_log_level_title": "Nivell de registre: {level}", "advanced_settings_prefer_remote_subtitle": "Alguns dispositius són molt lents en carregar miniatures dels elements del dispositiu. Activeu aquest paràmetre per carregar imatges remotes en el seu lloc.", "advanced_settings_prefer_remote_title": "Prefereix imatges remotes", "advanced_settings_proxy_headers_subtitle": "Definiu les capçaleres de proxy que Immich per enviar amb cada sol·licitud de xarxa", "advanced_settings_proxy_headers_title": "Capçaleres de proxy", "advanced_settings_self_signed_ssl_subtitle": "Omet la verificació del certificat SSL del servidor. Requerit per a certificats autosignats.", "advanced_settings_self_signed_ssl_title": "Permet certificats SSL autosignats", + "advanced_settings_sync_remote_deletions_subtitle": "Suprimeix o restaura automàticament un actiu en aquest dispositiu quan es realitzi aquesta acció al web", + "advanced_settings_sync_remote_deletions_title": "Sincronitza les eliminacions remotes", "advanced_settings_tile_subtitle": "Configuració avançada de l'usuari", "advanced_settings_troubleshooting_subtitle": "Habilita funcions addicionals per a la resolució de problemes", "advanced_settings_troubleshooting_title": "Resolució de problemes", @@ -393,9 +402,9 @@ "album_remove_user_confirmation": "Esteu segurs que voleu eliminar {user}?", "album_share_no_users": "Sembla que has compartit aquest àlbum amb tots els usuaris o no tens cap usuari amb qui compartir-ho.", "album_thumbnail_card_item": "1 element", - "album_thumbnail_card_items": "{} elements", + "album_thumbnail_card_items": "{count} elements", "album_thumbnail_card_shared": " · Compartit", - "album_thumbnail_shared_by": "Compartit per {}", + "album_thumbnail_shared_by": "Compartit per {user}", "album_updated": "Àlbum actualitzat", "album_updated_setting_description": "Rep una notificació per correu electrònic quan un àlbum compartit tingui recursos nous", "album_user_left": "Surt de {album}", @@ -433,7 +442,7 @@ "archive": "Arxiu", "archive_or_unarchive_photo": "Arxivar o desarxivar fotografia", "archive_page_no_archived_assets": "No s'ha trobat res arxivat", - "archive_page_title": "Arxiu({})", + "archive_page_title": "Arxiu({count})", "archive_size": "Mida de l'arxiu", "archive_size_description": "Configureu la mida de l'arxiu de les descàrregues (en GiB)", "archived": "Arxivat", @@ -470,18 +479,18 @@ "assets_added_to_album_count": "{count, plural, one {Afegit un element} other {Afegits # elements}} a l'àlbum", "assets_added_to_name_count": "{count, plural, one {S'ha afegit # recurs} other {S'han afegit # recursos}} a {hasName, select, true {{name}} other {new album}}", "assets_count": "{count, plural, one {# recurs} other {# recursos}}", - "assets_deleted_permanently": "{} element(s) esborrats permanentment", - "assets_deleted_permanently_from_server": "{} element(s) esborrats permanentment del servidor d'Immich", + "assets_deleted_permanently": "{count} element(s) esborrats permanentment", + "assets_deleted_permanently_from_server": "{count} element(s) esborrats permanentment del servidor d'Immich", "assets_moved_to_trash_count": "{count, plural, one {# recurs mogut} other {# recursos moguts}} a la paperera", "assets_permanently_deleted_count": "{count, plural, one {# recurs esborrat} other {# recursos esborrats}} permanentment", "assets_removed_count": "{count, plural, one {# element eliminat} other {# elements eliminats}}", - "assets_removed_permanently_from_device": "{} element(s) esborrat permanentment del dispositiu", + "assets_removed_permanently_from_device": "{count} element(s) esborrat permanentment del dispositiu", "assets_restore_confirmation": "Esteu segurs que voleu restaurar tots els teus actius? Aquesta acció no es pot desfer! Tingueu en compte que els recursos fora de línia no es poden restaurar d'aquesta manera.", "assets_restored_count": "{count, plural, one {# element restaurat} other {# elements restaurats}}", - "assets_restored_successfully": "{} element(s) recuperats correctament", - "assets_trashed": "{} element(s) enviat a la paperera", + "assets_restored_successfully": "{count} element(s) recuperats correctament", + "assets_trashed": "{count} element(s) enviat a la paperera", "assets_trashed_count": "{count, plural, one {# element enviat} other {# elements enviats}} a la paperera", - "assets_trashed_from_server": "{} element(s) enviat a la paperera del servidor d'Immich", + "assets_trashed_from_server": "{count} element(s) enviat a la paperera del servidor d'Immich", "assets_were_part_of_album_count": "{count, plural, one {L'element ja és} other {Els elements ja són}} part de l'àlbum", "authorized_devices": "Dispositius autoritzats", "automatic_endpoint_switching_subtitle": "Connecteu-vos localment a través de la Wi-Fi designada quan estigui disponible i utilitzeu connexions alternatives en altres llocs", @@ -490,7 +499,7 @@ "back_close_deselect": "Tornar, tancar o anul·lar la selecció", "background_location_permission": "Permís d'ubicació en segon pla", "background_location_permission_content": "Per canviar de xarxa quan s'executa en segon pla, Immich ha de *sempre* tenir accés a la ubicació precisa perquè l'aplicació pugui llegir el nom de la xarxa Wi-Fi", - "backup_album_selection_page_albums_device": "Àlbums al dispositiu ({})", + "backup_album_selection_page_albums_device": "Àlbums al dispositiu ({count})", "backup_album_selection_page_albums_tap": "Un toc per incloure, doble toc per excloure", "backup_album_selection_page_assets_scatter": "Els elements poden dispersar-se en diversos àlbums. Per tant, els àlbums es poden incloure o excloure durant el procés de còpia de seguretat.", "backup_album_selection_page_select_albums": "Selecciona àlbums", @@ -499,37 +508,37 @@ "backup_all": "Tots", "backup_background_service_backup_failed_message": "No s'ha pogut copiar els elements. Tornant a intentar…", "backup_background_service_connection_failed_message": "No s'ha pogut connectar al servidor. Tornant a intentar…", - "backup_background_service_current_upload_notification": "Pujant {}", - "backup_background_service_default_notification": "Cercant nous elements...", + "backup_background_service_current_upload_notification": "Pujant {filename}", + "backup_background_service_default_notification": "Cercant nous elements…", "backup_background_service_error_title": "Error copiant", - "backup_background_service_in_progress_notification": "Copiant els teus elements", - "backup_background_service_upload_failure_notification": "Error al pujar {}", + "backup_background_service_in_progress_notification": "Copiant els teus elements…", + "backup_background_service_upload_failure_notification": "Error en pujar {filename}", "backup_controller_page_albums": "Copia els àlbums", "backup_controller_page_background_app_refresh_disabled_content": "Activa l'actualització en segon pla de l'aplicació a Configuració > General > Actualització en segon pla per utilitzar la copia de seguretat en segon pla.", "backup_controller_page_background_app_refresh_disabled_title": "Actualització en segon pla desactivada", "backup_controller_page_background_app_refresh_enable_button_text": "Vés a configuració", "backup_controller_page_background_battery_info_link": "Mostra'm com", - "backup_controller_page_background_battery_info_message": "Per obtenir la millor experiència de copia de seguretat en segon pla, desactiveu qualsevol optimització de bateria que restringeixi l'activitat en segon pla per a Immich.\n\nAtès que això és específic del dispositiu, busqueu la informació necessària per al fabricant del vostre dispositiu", + "backup_controller_page_background_battery_info_message": "Per obtenir la millor experiència de còpia de seguretat en segon pla, desactiveu qualsevol optimització de bateria que restringeixi l'activitat en segon pla per a Immich.\n\nAtès que això és específic del dispositiu, busqueu la informació necessària per al fabricant del vostre dispositiu.", "backup_controller_page_background_battery_info_ok": "D'acord", "backup_controller_page_background_battery_info_title": "Optimitzacions de bateria", "backup_controller_page_background_charging": "Només mentre es carrega", "backup_controller_page_background_configure_error": "No s'ha pogut configurar el servei en segon pla", - "backup_controller_page_background_delay": "Retard en la copia de seguretat de nous elements: {}", - "backup_controller_page_background_description": "Activeu el servei en segon pla per copiar automàticament tots els nous elements sense haver d'obrir l'aplicació.", + "backup_controller_page_background_delay": "Retard en la còpia de seguretat de nous elements: {duration}", + "backup_controller_page_background_description": "Activeu el servei en segon pla per copiar automàticament tots els nous elements sense haver d'obrir l'aplicació", "backup_controller_page_background_is_off": "La còpia automàtica en segon pla està desactivada", "backup_controller_page_background_is_on": "La còpia automàtica en segon pla està activada", "backup_controller_page_background_turn_off": "Desactiva el servei en segon pla", "backup_controller_page_background_turn_on": "Activa el servei en segon pla", - "backup_controller_page_background_wifi": "Només amb WiFi", + "backup_controller_page_background_wifi": "Només amb Wi-Fi", "backup_controller_page_backup": "Còpia", "backup_controller_page_backup_selected": "Seleccionat: ", "backup_controller_page_backup_sub": "Fotografies i vídeos copiats", - "backup_controller_page_created": "Creat el: {}", + "backup_controller_page_created": "Creat el: {date}", "backup_controller_page_desc_backup": "Activeu la còpia de seguretat per pujar automàticament els nous elements al servidor en obrir l'aplicació.", "backup_controller_page_excluded": "Exclosos: ", - "backup_controller_page_failed": "Fallats ({})", - "backup_controller_page_filename": "Nom de l'arxiu: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Fallats ({count})", + "backup_controller_page_filename": "Nom de l'arxiu: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informació de la còpia", "backup_controller_page_none_selected": "Cap seleccionat", "backup_controller_page_remainder": "Restant", @@ -538,7 +547,7 @@ "backup_controller_page_start_backup": "Inicia la còpia", "backup_controller_page_status_off": "La copia de seguretat està desactivada", "backup_controller_page_status_on": "La copia de seguretat està activada", - "backup_controller_page_storage_format": "{} de {} utilitzats", + "backup_controller_page_storage_format": "{used} de {total} utilitzats", "backup_controller_page_to_backup": "Àlbums a copiar", "backup_controller_page_total_sub": "Totes les fotografies i vídeos dels àlbums seleccionats", "backup_controller_page_turn_off": "Desactiva la còpia de seguretat", @@ -563,21 +572,21 @@ "bulk_keep_duplicates_confirmation": "Esteu segur que voleu mantenir {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això resoldrà tots els grups duplicats sense eliminar res.", "bulk_trash_duplicates_confirmation": "Esteu segur que voleu enviar a les escombraries {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això mantindrà el recurs més gran de cada grup i eliminarà la resta de duplicats.", "buy": "Comprar Immich", - "cache_settings_album_thumbnails": "Miniatures de la pàgina de la biblioteca ({} elements)", + "cache_settings_album_thumbnails": "Miniatures de la pàgina de la biblioteca ({count} elements)", "cache_settings_clear_cache_button": "Neteja la memòria cau", "cache_settings_clear_cache_button_title": "Neteja la memòria cau de l'aplicació. Això impactarà significativament el rendiment fins que la memòria cau es torni a reconstruir.", "cache_settings_duplicated_assets_clear_button": "NETEJA", - "cache_settings_duplicated_assets_subtitle": "Fotos i vídeos que estan a la llista negra de l'aplicació.", - "cache_settings_duplicated_assets_title": "Elements duplicats ({})", - "cache_settings_image_cache_size": "Mida de la memòria cau de imatges ({} elements)", + "cache_settings_duplicated_assets_subtitle": "Fotos i vídeos que estan a la llista negra de l'aplicació", + "cache_settings_duplicated_assets_title": "Elements duplicats ({count})", + "cache_settings_image_cache_size": "Mida de la memòria cau d'imatges ({count} elements)", "cache_settings_statistics_album": "Miniatures de la biblioteca", - "cache_settings_statistics_assets": "{} elements ({})", + "cache_settings_statistics_assets": "{count} elements ({size})", "cache_settings_statistics_full": "Imatges completes", "cache_settings_statistics_shared": "Miniatures d'àlbums compartits", "cache_settings_statistics_thumbnail": "Miniatures", "cache_settings_statistics_title": "Ús de memòria cau", "cache_settings_subtitle": "Controla el comportament de la memòria cau de l'aplicació mòbil Immich", - "cache_settings_thumbnail_size": "Mida de la memòria cau de les miniatures ({} elements)", + "cache_settings_thumbnail_size": "Mida de la memòria cau de les miniatures ({count} elements)", "cache_settings_tile_subtitle": "Controla el comportament de l'emmagatzematge local", "cache_settings_tile_title": "Emmagatzematge local", "cache_settings_title": "Configuració de la memòria cau", @@ -603,6 +612,7 @@ "change_password_form_new_password": "Nova contrasenya", "change_password_form_password_mismatch": "Les contrasenyes no coincideixen", "change_password_form_reenter_new_password": "Torna a introduir la nova contrasenya", + "change_pin_code": "Canviar el codi PIN", "change_your_password": "Canvia la teva contrasenya", "changed_visibility_successfully": "Visibilitat canviada amb èxit", "check_all": "Marqueu-ho tot", @@ -643,11 +653,12 @@ "confirm_delete_face": "Estàs segur que vols eliminar la cara de {name} de les cares reconegudes?", "confirm_delete_shared_link": "Esteu segurs que voleu eliminar aquest enllaç compartit?", "confirm_keep_this_delete_others": "Excepte aquest element, tots els altres de la pila se suprimiran. Esteu segur que voleu continuar?", + "confirm_new_pin_code": "Confirma el nou codi PIN", "confirm_password": "Confirmació de contrasenya", "contain": "Contingut", "context": "Context", "continue": "Continuar", - "control_bottom_app_bar_album_info_shared": "{} elements - Compartits", + "control_bottom_app_bar_album_info_shared": "{count} elements - Compartits", "control_bottom_app_bar_create_new_album": "Crea un àlbum nou", "control_bottom_app_bar_delete_from_immich": "Suprimeix del Immich", "control_bottom_app_bar_delete_from_local": "Suprimeix del dispositiu", @@ -685,9 +696,11 @@ "create_tag_description": "Crear una nova etiqueta. Per les etiquetes aniuades, escriu la ruta comperta de l'etiqueta, incloses les barres diagonals.", "create_user": "Crea un usuari", "created": "Creat", + "created_at": "Creat", "crop": "Retalla", "curated_object_page_title": "Coses", "current_device": "Dispositiu actual", + "current_pin_code": "Codi PIN actual", "current_server_address": "Adreça actual del servidor", "custom_locale": "Localització personalitzada", "custom_locale_description": "Format de dates i números segons la llengua i regió", @@ -711,7 +724,7 @@ "delete": "Esborra", "delete_album": "Esborra l'àlbum", "delete_api_key_prompt": "Esteu segurs que voleu eliminar aquesta clau API?", - "delete_dialog_alert": "Aquests elements seran eliminats de manera permanent d'Immich i del vostre dispositiu.", + "delete_dialog_alert": "Aquests elements seran eliminats de manera permanent d'Immich i del vostre dispositiu", "delete_dialog_alert_local": "Aquests elements s'eliminaran permanentment del vostre dispositiu, però encara estaran disponibles al servidor Immich", "delete_dialog_alert_local_non_backed_up": "Alguns dels elements no tenen còpia de seguretat a Immich i s'eliminaran permanentment del dispositiu", "delete_dialog_alert_remote": "Aquests elements s'eliminaran permanentment del servidor Immich", @@ -756,7 +769,7 @@ "download_enqueue": "Descàrrega en cua", "download_error": "Error de descàrrega", "download_failed": "Descàrrega ha fallat", - "download_filename": "arxiu: {}", + "download_filename": "arxiu: {filename}", "download_finished": "Descàrrega acabada", "download_include_embedded_motion_videos": "Vídeos incrustats", "download_include_embedded_motion_videos_description": "Incloure vídeos incrustats en fotografies en moviment com un arxiu separat", @@ -800,6 +813,7 @@ "editor_crop_tool_h2_aspect_ratios": "Relació d'aspecte", "editor_crop_tool_h2_rotation": "Rotació", "email": "Correu electrònic", + "email_notifications": "Correu electrònic de notificacions", "empty_folder": "Aquesta carpeta és buida", "empty_trash": "Buidar la paperera", "empty_trash_confirmation": "Esteu segur que voleu buidar la paperera? Això eliminarà tots els recursos a la paperera permanentment d'Immich.\nNo podeu desfer aquesta acció!", @@ -807,12 +821,12 @@ "enabled": "Activat", "end_date": "Data final", "enqueued": "En cua", - "enter_wifi_name": "Introdueix el nom de WiFi", + "enter_wifi_name": "Introdueix el nom de Wi-Fi", "error": "Error", "error_change_sort_album": "No s'ha pogut canviar l'ordre d'ordenació dels àlbums", "error_delete_face": "Error esborrant cara de les cares reconegudes", "error_loading_image": "Error carregant la imatge", - "error_saving_image": "Error: {}", + "error_saving_image": "Error: {error}", "error_title": "Error - Quelcom ha anat malament", "errors": { "cannot_navigate_next_asset": "No es pot navegar a l'element següent", @@ -842,10 +856,12 @@ "failed_to_keep_this_delete_others": "No s'ha pogut conservar aquest element i suprimir els altres", "failed_to_load_asset": "No s'ha pogut carregar l'element", "failed_to_load_assets": "No s'han pogut carregar els elements", + "failed_to_load_notifications": "Error en carregar les notificacions", "failed_to_load_people": "No s'han pogut carregar les persones", "failed_to_remove_product_key": "No s'ha pogut eliminar la clau del producte", "failed_to_stack_assets": "No s'han pogut apilar els elements", "failed_to_unstack_assets": "No s'han pogut desapilar els elements", + "failed_to_update_notification_status": "Error en actualitzar l'estat de les notificacions", "import_path_already_exists": "Aquesta ruta d'importació ja existeix.", "incorrect_email_or_password": "Correu electrònic o contrasenya incorrectes", "paths_validation_failed": "{paths, plural, one {# ruta} other {# rutes}} no ha pogut validar", @@ -913,6 +929,7 @@ "unable_to_remove_reaction": "No es pot eliminar la reacció", "unable_to_repair_items": "No es poden reparar els elements", "unable_to_reset_password": "No es pot restablir la contrasenya", + "unable_to_reset_pin_code": "No es pot restablir el codi PIN", "unable_to_resolve_duplicate": "No es pot resoldre el duplicat", "unable_to_restore_assets": "No es poden restaurar els recursos", "unable_to_restore_trash": "No es pot restaurar la paperera", @@ -941,15 +958,15 @@ "unable_to_upload_file": "No es pot carregar el fitxer" }, "exif": "Exif", - "exif_bottom_sheet_description": "Afegeix descripció", + "exif_bottom_sheet_description": "Afegeix descripció...", "exif_bottom_sheet_details": "DETALLS", "exif_bottom_sheet_location": "UBICACIÓ", "exif_bottom_sheet_people": "PERSONES", "exif_bottom_sheet_person_add_person": "Afegir nom", - "exif_bottom_sheet_person_age": "Edat {}", - "exif_bottom_sheet_person_age_months": "Edat {} mesos", - "exif_bottom_sheet_person_age_year_months": "Edat 1 any, {} mesos", - "exif_bottom_sheet_person_age_years": "Edat {}", + "exif_bottom_sheet_person_age": "Edat {age}", + "exif_bottom_sheet_person_age_months": "Edat {months} mesos", + "exif_bottom_sheet_person_age_year_months": "Edat 1 any, {months} mesos", + "exif_bottom_sheet_person_age_years": "Edat {years}", "exit_slideshow": "Surt de la presentació de diapositives", "expand_all": "Ampliar-ho tot", "experimental_settings_new_asset_list_subtitle": "Treball en curs", @@ -967,7 +984,7 @@ "external": "Extern", "external_libraries": "Llibreries externes", "external_network": "Xarxa externa", - "external_network_sheet_info": "Quan no estigui a la xarxa WiFi preferida, l'aplicació es connectarà al servidor mitjançant el primer dels URL següents a què pot arribar, començant de dalt a baix.", + "external_network_sheet_info": "Quan no estigui a la xarxa Wi-Fi preferida, l'aplicació es connectarà al servidor mitjançant el primer dels URL següents a què pot arribar, començant de dalt a baix", "face_unassigned": "Sense assignar", "failed": "Fallat", "failed_to_load_assets": "Error carregant recursos", @@ -985,6 +1002,7 @@ "filetype": "Tipus d'arxiu", "filter": "Filtrar", "filter_people": "Filtra persones", + "filter_places": "Filtrar per llocs", "find_them_fast": "Trobeu-los ràpidament pel nom amb la cerca", "fix_incorrect_match": "Corregiu la coincidència incorrecta", "folder": "Carpeta", @@ -1033,11 +1051,12 @@ "home_page_delete_remote_err_local": "Elements locals a la selecció d'eliminació remota, ometent", "home_page_favorite_err_local": "Encara no es pot afegir a preferits elements locals, ometent", "home_page_favorite_err_partner": "Encara no es pot afegir a preferits elements de companys, ometent", - "home_page_first_time_notice": "Si és la primera vegada que utilitzes l'app, si us plau, assegura't d'escollir un àlbum de còpia de seguretat perquè la línia de temps pugui carregar fotos i vídeos als àlbums.", + "home_page_first_time_notice": "Si és la primera vegada que utilitzes l'app, si us plau, assegura't d'escollir un àlbum de còpia de seguretat perquè la línia de temps pugui carregar fotos i vídeos als àlbums", "home_page_share_err_local": "No es poden compartir els elements locals a través d'un enllaç, ometent", "home_page_upload_err_limit": "Només es poden pujar un màxim de 30 elements alhora, ometent", "host": "Amfitrió", "hour": "Hora", + "id": "ID", "ignore_icloud_photos": "Ignora fotos d'iCloud", "ignore_icloud_photos_description": "Les fotos emmagatzemades a iCloud no es penjaran al servidor Immich", "image": "Imatge", @@ -1113,7 +1132,7 @@ "local_network": "Xarxa local", "local_network_sheet_info": "L'aplicació es connectarà al servidor mitjançant aquest URL quan utilitzeu la xarxa Wi-Fi especificada", "location_permission": "Permís d'ubicació", - "location_permission_content": "Per utilitzar la funció de canvi automàtic, Immich necessita un permís de ubicació precisa perquè pugui llegir el nom de la xarxa WiFi actual", + "location_permission_content": "Per utilitzar la funció de canvi automàtic, Immich necessita un permís d'ubicació precisa perquè pugui llegir el nom de la xarxa Wi-Fi actual", "location_picker_choose_on_map": "Escollir en el mapa", "location_picker_latitude_error": "Introdueix una latitud vàlida", "location_picker_latitude_hint": "Introdueix aquí la latitud", @@ -1137,7 +1156,7 @@ "login_form_err_trailing_whitespace": "Espai en blanc al final", "login_form_failed_get_oauth_server_config": "Error en iniciar sessió amb OAuth, comprova l'URL del servidor", "login_form_failed_get_oauth_server_disable": "La funcionalitat OAuth no està disponible en aquest servidor", - "login_form_failed_login": "Error en iniciar sessió, comprova l'URL del servidor, el correu electrònic i la contrasenya.", + "login_form_failed_login": "Error en iniciar sessió, comprova l'URL del servidor, el correu electrònic i la contrasenya", "login_form_handshake_exception": "S'ha produït una excepció de handshake amb el servidor. Activa el suport per certificats autofirmats a la configuració si estàs fent servir un certificat autofirmat.", "login_form_password_hint": "contrasenya", "login_form_save_login": "Mantingues identificat", @@ -1163,8 +1182,8 @@ "manage_your_devices": "Gestioneu els vostres dispositius connectats", "manage_your_oauth_connection": "Gestioneu la vostra connexió OAuth", "map": "Mapa", - "map_assets_in_bound": "{} foto", - "map_assets_in_bounds": "{} fotos", + "map_assets_in_bound": "{count} foto", + "map_assets_in_bounds": "{count} fotos", "map_cannot_get_user_location": "No es pot obtenir la ubicació de l'usuari", "map_location_dialog_yes": "Sí", "map_location_picker_page_use_location": "Utilitzar aquesta ubicació", @@ -1178,15 +1197,18 @@ "map_settings": "Paràmetres de mapa", "map_settings_dark_mode": "Mode fosc", "map_settings_date_range_option_day": "Últimes 24 hores", - "map_settings_date_range_option_days": "Darrers {} dies", + "map_settings_date_range_option_days": "Darrers {days} dies", "map_settings_date_range_option_year": "Any passat", - "map_settings_date_range_option_years": "Darrers {} anys", + "map_settings_date_range_option_years": "Darrers {years} anys", "map_settings_dialog_title": "Configuració del mapa", "map_settings_include_show_archived": "Incloure arxivats", "map_settings_include_show_partners": "Incloure companys", "map_settings_only_show_favorites": "Mostra només preferits", "map_settings_theme_settings": "Tema del Mapa", "map_zoom_to_see_photos": "Allunya per veure fotos", + "mark_all_as_read": "Marcar-ho tot com a llegit", + "mark_as_read": "Marcar com ha llegit", + "marked_all_as_read": "Marcat tot com a llegit", "matches": "Coincidències", "media_type": "Tipus de mitjà", "memories": "Records", @@ -1196,7 +1218,7 @@ "memories_start_over": "Torna a començar", "memories_swipe_to_close": "Llisca per tancar", "memories_year_ago": "Fa un any", - "memories_years_ago": "Fa {} anys", + "memories_years_ago": "Fa {years, plural, other {# years}} anys", "memory": "Record", "memory_lane_title": "Línia de records {title}", "menu": "Menú", @@ -1213,9 +1235,11 @@ "month": "Mes", "monthly_title_text_date_format": "MMMM y", "more": "Més", + "moved_to_archive": "S'han mogut {count, plural, one {# asset} other {# assets}} a l'arxiu", + "moved_to_library": "S'ha mogut {count, plural, one {# asset} other {# assets}} a la llibreria", "moved_to_trash": "S'ha mogut a la paperera", "multiselect_grid_edit_date_time_err_read_only": "No es pot canviar la data del fitxer(s) de només lectura, ometent", - "multiselect_grid_edit_gps_err_read_only": "No es pot canviar la localització de fitxers de només lectura. Saltant.", + "multiselect_grid_edit_gps_err_read_only": "No es pot canviar la localització de fitxers de només lectura, saltant", "mute_memories": "Silenciar records", "my_albums": "Els meus àlbums", "name": "Nom", @@ -1227,6 +1251,7 @@ "new_api_key": "Nova clau de l'API", "new_password": "Nova contrasenya", "new_person": "Persona nova", + "new_pin_code": "Nou codi PIN", "new_user_created": "Nou usuari creat", "new_version_available": "NOVA VERSIÓ DISPONIBLE", "newest_first": "El més nou primer", @@ -1245,6 +1270,8 @@ "no_favorites_message": "Afegiu preferits per trobar les millors fotos i vídeos a l'instant", "no_libraries_message": "Creeu una llibreria externa per veure les vostres fotos i vídeos", "no_name": "Sense nom", + "no_notifications": "No hi ha notificacions", + "no_people_found": "No s'han trobat coincidències de persones", "no_places": "No hi ha llocs", "no_results": "Sense resultats", "no_results_description": "Proveu un sinònim o una paraula clau més general", @@ -1275,6 +1302,7 @@ "onboarding_welcome_user": "Benvingut, {user}", "online": "En línia", "only_favorites": "Només preferits", + "open": "Obrir", "open_in_map_view": "Obrir a la vista del mapa", "open_in_openstreetmap": "Obre a OpenStreetMap", "open_the_search_filters": "Obriu els filtres de cerca", @@ -1298,7 +1326,7 @@ "partner_page_partner_add_failed": "No s'ha pogut afegir el company", "partner_page_select_partner": "Escull company", "partner_page_shared_to_title": "Compartit amb", - "partner_page_stop_sharing_content": "{} ja no podrà accedir a les teves fotos.", + "partner_page_stop_sharing_content": "{partner} ja no podrà accedir a les teves fotos.", "partner_sharing": "Compartició amb companys", "partners": "Companys", "password": "Contrasenya", @@ -1344,6 +1372,9 @@ "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", "photos_from_previous_years": "Fotos d'anys anteriors", "pick_a_location": "Triar una ubicació", + "pin_code_changed_successfully": "Codi PIN canviat correctament", + "pin_code_reset_successfully": "S'ha restablert correctament el codi PIN", + "pin_code_setup_successfully": "S'ha configurat correctament un codi PIN", "place": "Lloc", "places": "Llocs", "places_count": "{count, plural, one {{count, number} Lloc} other {{count, number} Llocs}}", @@ -1361,6 +1392,7 @@ "previous_or_next_photo": "Foto anterior o següent", "primary": "Primària", "privacy": "Privacitat", + "profile": "Perfil", "profile_drawer_app_logs": "Registres", "profile_drawer_client_out_of_date_major": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió major.", "profile_drawer_client_out_of_date_minor": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió menor.", @@ -1374,7 +1406,7 @@ "public_share": "Compartit públicament", "purchase_account_info": "Contribuent", "purchase_activated_subtitle": "Gràcies per donar suport a Immich i al programari de codi obert", - "purchase_activated_time": "Activat el {date, date}", + "purchase_activated_time": "Activat el {date}", "purchase_activated_title": "La teva clau s'ha activat correctament", "purchase_button_activate": "Activar", "purchase_button_buy": "Comprar", @@ -1419,6 +1451,8 @@ "recent_searches": "Cerques recents", "recently_added": "Afegit recentment", "recently_added_page_title": "Afegit recentment", + "recently_taken": "Fet recentment", + "recently_taken_page_title": "Fet recentment", "refresh": "Actualitzar", "refresh_encoded_videos": "Actualitza vídeos codificats", "refresh_faces": "Actualitzar cares", @@ -1461,6 +1495,7 @@ "reset": "Restablir", "reset_password": "Restablir contrasenya", "reset_people_visibility": "Restablir la visibilitat de les persones", + "reset_pin_code": "Restablir el codi PIN", "reset_to_default": "Restableix els valors predeterminats", "resolve_duplicates": "Resoldre duplicats", "resolved_all_duplicates": "Tots els duplicats resolts", @@ -1553,6 +1588,7 @@ "select_keep_all": "Mantén tota la selecció", "select_library_owner": "Selecciona el propietari de la bilbioteca", "select_new_face": "Selecciona nova cara", + "select_person_to_tag": "Selecciona una persona per etiquetar", "select_photos": "Tria fotografies", "select_trash_all": "Envia la selecció a la paperera", "select_user_for_sharing_page_err_album": "Error al crear l'àlbum", @@ -1583,12 +1619,12 @@ "setting_languages_apply": "Aplicar", "setting_languages_subtitle": "Canvia el llenguatge de l'aplicació", "setting_languages_title": "Idiomes", - "setting_notifications_notify_failures_grace_period": "Notifica les fallades de la còpia de seguretat en segon pla: {}", - "setting_notifications_notify_hours": "{} hores", + "setting_notifications_notify_failures_grace_period": "Notifica les fallades de la còpia de seguretat en segon pla: {duration}", + "setting_notifications_notify_hours": "{count} hores", "setting_notifications_notify_immediately": "immediatament", - "setting_notifications_notify_minutes": "{} minuts", + "setting_notifications_notify_minutes": "{count} minuts", "setting_notifications_notify_never": "mai", - "setting_notifications_notify_seconds": "{} segons", + "setting_notifications_notify_seconds": "{count} segons", "setting_notifications_single_progress_subtitle": "Informació detallada del progrés de la pujada de cada fitxer", "setting_notifications_single_progress_title": "Mostra el progrés detallat de la còpia de seguretat en segon pla", "setting_notifications_subtitle": "Ajusta les preferències de notificació", @@ -1600,9 +1636,10 @@ "settings": "Configuració", "settings_require_restart": "Si us plau, reinicieu Immich per a aplicar aquest canvi", "settings_saved": "Configuració desada", + "setup_pin_code": "Configurar un codi PIN", "share": "Comparteix", "share_add_photos": "Afegeix fotografies", - "share_assets_selected": "{} seleccionats", + "share_assets_selected": "{count} seleccionats", "share_dialog_preparing": "S'està preparant...", "shared": "Compartit", "shared_album_activities_input_disable": "Els comentaris estan desactivats", @@ -1616,32 +1653,32 @@ "shared_by_user": "Compartit per {user}", "shared_by_you": "Compartit per tu", "shared_from_partner": "Fotos de {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Pujat", + "shared_intent_upload_button_progress_text": "{current} / {total} Pujat", "shared_link_app_bar_title": "Enllaços compartits", "shared_link_clipboard_copied_massage": "S'ha copiat al porta-retalls", - "shared_link_clipboard_text": "Enllaç: {}\nContrasenya: {}", + "shared_link_clipboard_text": "Enllaç: {link}\nContrasenya: {password}", "shared_link_create_error": "S'ha produït un error en crear l'enllaç compartit", "shared_link_edit_description_hint": "Introduïu la descripció de compartició", "shared_link_edit_expire_after_option_day": "1 dia", - "shared_link_edit_expire_after_option_days": "{} dies", + "shared_link_edit_expire_after_option_days": "{count} dies", "shared_link_edit_expire_after_option_hour": "1 hora", - "shared_link_edit_expire_after_option_hours": "{} hores", + "shared_link_edit_expire_after_option_hours": "{count} hores", "shared_link_edit_expire_after_option_minute": "1 minut", - "shared_link_edit_expire_after_option_minutes": "{} minuts", - "shared_link_edit_expire_after_option_months": "{} mesos", - "shared_link_edit_expire_after_option_year": "any {}", + "shared_link_edit_expire_after_option_minutes": "{count} minuts", + "shared_link_edit_expire_after_option_months": "{count} mesos", + "shared_link_edit_expire_after_option_year": "any {count}", "shared_link_edit_password_hint": "Introduïu la contrasenya de compartició", "shared_link_edit_submit_button": "Actualitza l'enllaç", "shared_link_error_server_url_fetch": "No s'ha pogut obtenir l'URL del servidor", - "shared_link_expires_day": "Caduca d'aquí a {} dia", - "shared_link_expires_days": "Caduca d'aquí a {} dies", - "shared_link_expires_hour": "Caduca d'aquí a {} hora", - "shared_link_expires_hours": "Caduca d'aquí a {} hores", - "shared_link_expires_minute": "Caduca d'aquí a {} minut", - "shared_link_expires_minutes": "Caduca d'aquí a {} minuts", + "shared_link_expires_day": "Caduca d'aquí a {count} dia", + "shared_link_expires_days": "Caduca d'aquí a {count} dies", + "shared_link_expires_hour": "Caduca d'aquí a {count} hora", + "shared_link_expires_hours": "Caduca d'aquí a {count} hores", + "shared_link_expires_minute": "Caduca d'aquí a {count} minut", + "shared_link_expires_minutes": "Caduca d'aquí a {count} minuts", "shared_link_expires_never": "Caduca ∞", - "shared_link_expires_second": "Caduca d'aquí a {} segon", - "shared_link_expires_seconds": "Caduca d'aquí a {} segons", + "shared_link_expires_second": "Caduca d'aquí a {count} segon", + "shared_link_expires_seconds": "Caduca d'aquí a {count} segons", "shared_link_individual_shared": "Individual compartit", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Gestiona els enllaços compartits", @@ -1716,6 +1753,7 @@ "stop_sharing_photos_with_user": "Deixa de compartir les fotos amb aquest usuari", "storage": "Emmagatzematge", "storage_label": "Etiquetatge d'emmagatzematge", + "storage_quota": "Quota d'emmagatzematge", "storage_usage": "{used} de {available} en ús", "submit": "Envia", "suggestions": "Suggeriments", @@ -1742,7 +1780,7 @@ "theme_selection": "Selecció de tema", "theme_selection_description": "Activa automàticament el tema fosc o clar en funció de les preferències del sistema del navegador", "theme_setting_asset_list_storage_indicator_title": "Mostra l'indicador d'emmagatzematge als títols dels elements", - "theme_setting_asset_list_tiles_per_row_title": "Nombre d'elements per fila ({})", + "theme_setting_asset_list_tiles_per_row_title": "Nombre d'elements per fila ({count})", "theme_setting_colorful_interface_subtitle": "Apliqueu color primari a les superfícies de fons.", "theme_setting_colorful_interface_title": "Interfície colorida", "theme_setting_image_viewer_quality_subtitle": "Ajusta la qualitat del visor de detalls d'imatges", @@ -1777,13 +1815,15 @@ "trash_no_results_message": "Les imatges i vídeos que s'enviïn a la paperera es mostraran aquí.", "trash_page_delete_all": "Eliminar-ho tot", "trash_page_empty_trash_dialog_content": "Segur que voleu eliminar els elements? Aquests elements seran eliminats permanentment de Immich", - "trash_page_info": "Els elements que s'enviïn a la paperera s'eliminaran permanentment després de {} dies", + "trash_page_info": "Els elements que s'enviïn a la paperera s'eliminaran permanentment després de {days} dies", "trash_page_no_assets": "No hi ha elements a la paperera", "trash_page_restore_all": "Restaura-ho tot", "trash_page_select_assets_btn": "Selecciona elements", - "trash_page_title": "Paperera ({})", + "trash_page_title": "Paperera ({count})", "trashed_items_will_be_permanently_deleted_after": "Els elements que s'enviïn a la paperera s'eliminaran permanentment després de {days, plural, one {# dia} other {# dies}}.", "type": "Tipus", + "unable_to_change_pin_code": "No es pot canviar el codi PIN", + "unable_to_setup_pin_code": "No s'ha pogut configurar el codi PIN", "unarchive": "Desarxivar", "unarchived_count": "{count, plural, other {# elements desarxivats}}", "unfavorite": "Reverteix preferit", @@ -1807,6 +1847,7 @@ "untracked_files": "Fitxers no monitoritzats", "untracked_files_decription": "Aquests fitxers no estan monitoritzats per l'aplicació. Poden ser el resultat de moviments errats, descàrregues interrompudes o deixats enrere per error", "up_next": "Pròxim", + "updated_at": "Actualitzat", "updated_password": "Contrasenya actualitzada", "upload": "Pujar", "upload_concurrency": "Concurrència de pujades", @@ -1819,15 +1860,18 @@ "upload_status_errors": "Errors", "upload_status_uploaded": "Carregat", "upload_success": "Pujada correcta, actualitza la pàgina per veure nous recursos de pujada.", - "upload_to_immich": "Puja a Immich ({})", + "upload_to_immich": "Puja a Immich ({count})", "uploading": "Pujant", "url": "URL", "usage": "Ús", "use_current_connection": "utilitzar la connexió actual", "use_custom_date_range": "Fes servir un rang de dates personalitzat", "user": "Usuari", + "user_has_been_deleted": "Aquest usuari ha sigut eliminat.", "user_id": "ID d'usuari", "user_liked": "A {user} li ha agradat {type, select, photo {aquesta foto} video {aquest vídeo} asset {aquest recurs} other {}}", + "user_pin_code_settings": "Codi PIN", + "user_pin_code_settings_description": "Gestiona el teu codi PIN", "user_purchase_settings": "Compra", "user_purchase_settings_description": "Gestiona la teva compra", "user_role_set": "Establir {user} com a {role}", @@ -1876,11 +1920,11 @@ "week": "Setmana", "welcome": "Benvingut", "welcome_to_immich": "Benvingut a immich", - "wifi_name": "Nom WiFi", + "wifi_name": "Nom Wi-Fi", "year": "Any", "years_ago": "Fa {years, plural, one {# any} other {# anys}}", "yes": "Sí", "you_dont_have_any_shared_links": "No tens cap enllaç compartit", - "your_wifi_name": "El teu nom WiFi", + "your_wifi_name": "Nom del teu Wi-Fi", "zoom_image": "Ampliar Imatge" } diff --git a/i18n/cs.json b/i18n/cs.json index 039df198d9..c425bc6c2b 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -53,6 +53,7 @@ "confirm_email_below": "Pro potvrzení zadejte níže \"{email}\"", "confirm_reprocess_all_faces": "Opravdu chcete znovu zpracovat všechny obličeje? Tím se vymažou i pojmenované osoby.", "confirm_user_password_reset": "Opravdu chcete obnovit heslo uživatele {user}?", + "confirm_user_pin_code_reset": "Opravdu chcete resetovat PIN kód uživatele {user}?", "create_job": "Vytvořit úlohu", "cron_expression": "Výraz cron", "cron_expression_description": "Nastavte interval prohledávání pomocí cron formátu. Další informace naleznete např. v Crontab Guru", @@ -348,6 +349,7 @@ "user_delete_delay_settings_description": "Počet dní po odstranění, po kterých bude odstraněn účet a položky uživatele. Úloha odstraňování uživatelů se spouští o půlnoci a kontroluje uživatele, kteří jsou připraveni k odstranění. Změny tohoto nastavení se vyhodnotí při dalším spuštění.", "user_delete_immediately": "Účet a položky uživatele {user} budou zařazeny do fronty k trvalému smazání okamžitě.", "user_delete_immediately_checkbox": "Uživatele a položky zařadit do fronty k okamžitému smazání", + "user_details": "Podrobnosti o uživateli", "user_management": "Správa uživatelů", "user_password_has_been_reset": "Heslo uživatele bylo obnoveno:", "user_password_reset_description": "Poskytněte uživateli dočasné heslo a informujte ho, že si ho bude muset při příštím přihlášení změnit.", @@ -369,7 +371,7 @@ "advanced": "Pokročilé", "advanced_settings_enable_alternate_media_filter_subtitle": "Tuto možnost použijte k filtrování médií během synchronizace na základě alternativních kritérií. Tuto možnost vyzkoušejte pouze v případě, že máte problémy s detekcí všech alb v aplikaci.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTÁLNÍ] Použít alternativní filtr pro synchronizaci alb zařízení", - "advanced_settings_log_level_title": "Úroveň protokolování: {}", + "advanced_settings_log_level_title": "Úroveň protokolování: {level}", "advanced_settings_prefer_remote_subtitle": "U některých zařízení je načítání miniatur z prostředků v zařízení velmi pomalé. Aktivujte toto nastavení, aby se místo toho načítaly vzdálené obrázky.", "advanced_settings_prefer_remote_title": "Preferovat vzdálené obrázky", "advanced_settings_proxy_headers_subtitle": "Definice hlaviček proxy serveru, které by měl Immich odesílat s každým síťovým požadavkem", @@ -381,8 +383,8 @@ "advanced_settings_tile_subtitle": "Pokročilé uživatelské nastavení", "advanced_settings_troubleshooting_subtitle": "Zobrazit dodatečné vlastnosti pro řešení problémů", "advanced_settings_troubleshooting_title": "Řešení problémů", - "age_months": "{months, plural, one {# měsíc} few {# měsíce} other {# měsíců}}", - "age_year_months": "1 rok a {months, plural, one {# měsíc} few {# měsíce} other {# měsíců}}", + "age_months": "Věk {months, plural, one {# měsíc} few {# měsíce} other {# měsíců}}", + "age_year_months": "Věk 1 rok, {months, plural, one {# měsíc} other {# měsíce}}", "age_years": "{years, plural, one {# rok} few {# roky} other {# let}}", "album_added": "Přidáno album", "album_added_notification_setting_description": "Dostávat e-mailové oznámení, když jste přidáni do sdíleného alba", @@ -400,9 +402,9 @@ "album_remove_user_confirmation": "Opravdu chcete odebrat uživatele {user}?", "album_share_no_users": "Zřejmě jste toto album sdíleli se všemi uživateli, nebo nemáte žádného uživatele, se kterým byste ho mohli sdílet.", "album_thumbnail_card_item": "1 položka", - "album_thumbnail_card_items": "{} položek", + "album_thumbnail_card_items": "{count} položek", "album_thumbnail_card_shared": " · Sdíleno", - "album_thumbnail_shared_by": "Sdílel(a) {}", + "album_thumbnail_shared_by": "Sdílel(a) {user}", "album_updated": "Album aktualizováno", "album_updated_setting_description": "Dostávat e-mailová oznámení o nových položkách sdíleného alba", "album_user_left": "Opustil {album}", @@ -440,7 +442,7 @@ "archive": "Archiv", "archive_or_unarchive_photo": "Archivovat nebo odarchivovat fotku", "archive_page_no_archived_assets": "Nebyla nalezena žádná archivovaná média", - "archive_page_title": "Archiv ({})", + "archive_page_title": "Archiv ({count})", "archive_size": "Velikost archivu", "archive_size_description": "Nastavte velikost archivu pro stahování (v GiB)", "archived": "Archiv", @@ -477,18 +479,18 @@ "assets_added_to_album_count": "Do alba {count, plural, one {byla přidána # položka} few {byly přidány # položky} other {bylo přidáno # položek}}", "assets_added_to_name_count": "{count, plural, one {Přidána # položka} few {Přidány # položky} other {Přidáno # položek}} do {hasName, select, true {alba {name}} other {nového alba}}", "assets_count": "{count, plural, one {# položka} few {# položky} other {# položek}}", - "assets_deleted_permanently": "{} položek trvale odstraněno", - "assets_deleted_permanently_from_server": "{} položek trvale odstraněno z Immich serveru", + "assets_deleted_permanently": "{count} položek trvale odstraněno", + "assets_deleted_permanently_from_server": "{count} položek trvale odstraněno z Immich serveru", "assets_moved_to_trash_count": "Do koše {count, plural, one {přesunuta # položka} few {přesunuty # položky} other {přesunuto # položek}}", "assets_permanently_deleted_count": "Trvale {count, plural, one {smazána # položka} few {smazány # položky} other {smazáno # položek}}", "assets_removed_count": "{count, plural, one {Odstraněna # položka} few {Odstraněny # položky} other {Odstraněno # položek}}", - "assets_removed_permanently_from_device": "{} položek trvale odstraněno z vašeho zařízení", + "assets_removed_permanently_from_device": "{count} položek trvale odstraněno z vašeho zařízení", "assets_restore_confirmation": "Opravdu chcete obnovit všechny vyhozené položky? Tuto akci nelze vrátit zpět! Upozorňujeme, že tímto způsobem nelze obnovit žádné offline položky.", "assets_restored_count": "{count, plural, one {Obnovena # položka} few {Obnoveny # položky} other {Obnoveno # položek}}", - "assets_restored_successfully": "{} položek úspěšně obnoveno", - "assets_trashed": "{} položek vyhozeno do koše", + "assets_restored_successfully": "{count} položek úspěšně obnoveno", + "assets_trashed": "{count} položek vyhozeno do koše", "assets_trashed_count": "{count, plural, one {Vyhozena # položka} few {Vyhozeny # položky} other {Vyhozeno # položek}}", - "assets_trashed_from_server": "{} položek vyhozeno do koše na Immich serveru", + "assets_trashed_from_server": "{count} položek vyhozeno do koše na Immich serveru", "assets_were_part_of_album_count": "{count, plural, one {Položka byla} other {Položky byly}} součástí alba", "authorized_devices": "Autorizovaná zařízení", "automatic_endpoint_switching_subtitle": "Připojit se místně přes určenou Wi-Fi, pokud je k dispozici, a používat alternativní připojení jinde", @@ -497,7 +499,7 @@ "back_close_deselect": "Zpět, zavřít nebo zrušit výběr", "background_location_permission": "Povolení polohy na pozadí", "background_location_permission_content": "Aby bylo možné přepínat sítě při běhu na pozadí, musí mít Immich *vždy* přístup k přesné poloze, aby mohl zjistit název Wi-Fi sítě", - "backup_album_selection_page_albums_device": "Alba v zařízení ({})", + "backup_album_selection_page_albums_device": "Alba v zařízení ({count})", "backup_album_selection_page_albums_tap": "Klepnutím na položku ji zahrnete, opětovným klepnutím ji vyloučíte", "backup_album_selection_page_assets_scatter": "Položky mohou být roztroušeny ve více albech. To umožňuje zahrnout nebo vyloučit alba během procesu zálohování.", "backup_album_selection_page_select_albums": "Vybraná alba", @@ -506,11 +508,11 @@ "backup_all": "Vše", "backup_background_service_backup_failed_message": "Zálohování médií selhalo. Zkouším to znovu…", "backup_background_service_connection_failed_message": "Nepodařilo se připojit k serveru. Zkouším to znovu…", - "backup_background_service_current_upload_notification": "Nahrávání {}", + "backup_background_service_current_upload_notification": "Nahrávání {filename}", "backup_background_service_default_notification": "Kontrola nových médií…", "backup_background_service_error_title": "Chyba zálohování", "backup_background_service_in_progress_notification": "Zálohování vašich médií…", - "backup_background_service_upload_failure_notification": "Nepodařilo se nahrát {}", + "backup_background_service_upload_failure_notification": "Nepodařilo se nahrát {filename}", "backup_controller_page_albums": "Zálohovaná alba", "backup_controller_page_background_app_refresh_disabled_content": "Povolte obnovení aplikace na pozadí v Nastavení > Obecné > Obnovení aplikace na pozadí, abyste mohli používat zálohování na pozadí.", "backup_controller_page_background_app_refresh_disabled_title": "Obnovování aplikací na pozadí je vypnuté", @@ -521,7 +523,7 @@ "backup_controller_page_background_battery_info_title": "Optimalizace baterie", "backup_controller_page_background_charging": "Pouze během nabíjení", "backup_controller_page_background_configure_error": "Nepodařilo se nakonfigurovat službu na pozadí", - "backup_controller_page_background_delay": "Zpoždění zálohování nových médií: {}", + "backup_controller_page_background_delay": "Zpoždění zálohování nových médií: {duration}", "backup_controller_page_background_description": "Povolte službu na pozadí pro automatické zálohování všech nových položek bez nutnosti otevření aplikace", "backup_controller_page_background_is_off": "Automatické zálohování na pozadí je vypnuto", "backup_controller_page_background_is_on": "Automatické zálohování na pozadí je zapnuto", @@ -531,12 +533,12 @@ "backup_controller_page_backup": "Zálohování", "backup_controller_page_backup_selected": "Vybrané: ", "backup_controller_page_backup_sub": "Zálohované fotografie a videa", - "backup_controller_page_created": "Vytvořeno: {}", + "backup_controller_page_created": "Vytvořeno: {date}", "backup_controller_page_desc_backup": "Zapněte zálohování na popředí, aby se nové položky automaticky nahrávaly na server při otevření aplikace.", "backup_controller_page_excluded": "Vyloučeno: ", - "backup_controller_page_failed": "Nepodařilo se ({})", - "backup_controller_page_filename": "Název souboru: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Nepodařilo se ({count})", + "backup_controller_page_filename": "Název souboru: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informace o zálohování", "backup_controller_page_none_selected": "Žádné vybrané", "backup_controller_page_remainder": "Zbývá", @@ -545,7 +547,7 @@ "backup_controller_page_start_backup": "Spustit zálohování", "backup_controller_page_status_off": "Automatické zálohování na popředí je vypnuto", "backup_controller_page_status_on": "Automatické zálohování na popředí je zapnuto", - "backup_controller_page_storage_format": "{} z {} použitých", + "backup_controller_page_storage_format": "{used} z {total} použitých", "backup_controller_page_to_backup": "Alba, která mají být zálohována", "backup_controller_page_total_sub": "Všechny jedinečné fotografie a videa z vybraných alb", "backup_controller_page_turn_off": "Vypnout zálohování na popředí", @@ -570,21 +572,21 @@ "bulk_keep_duplicates_confirmation": "Opravdu si chcete ponechat {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se vyřeší všechny duplicitní skupiny, aniž by se cokoli odstranilo.", "bulk_trash_duplicates_confirmation": "Opravdu chcete hromadně vyhodit {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se zachová největší položka z každé skupiny a všechny ostatní duplikáty se vyhodí.", "buy": "Zakoupit Immich", - "cache_settings_album_thumbnails": "Náhledy stránek knihovny (položek {})", + "cache_settings_album_thumbnails": "Náhledy stránek knihovny ({count} položek)", "cache_settings_clear_cache_button": "Vymazat vyrovnávací paměť", "cache_settings_clear_cache_button_title": "Vymaže vyrovnávací paměť aplikace. To výrazně ovlivní výkon aplikace, dokud se vyrovnávací paměť neobnoví.", "cache_settings_duplicated_assets_clear_button": "VYMAZAT", "cache_settings_duplicated_assets_subtitle": "Fotografie a videa, které aplikace zařadila na černou listinu", - "cache_settings_duplicated_assets_title": "Duplicitní položky ({})", - "cache_settings_image_cache_size": "Velikost vyrovnávací paměti (položek {})", + "cache_settings_duplicated_assets_title": "Duplicitní položky ({count})", + "cache_settings_image_cache_size": "Velikost vyrovnávací paměti ({count} položek)", "cache_settings_statistics_album": "Knihovna náhledů", - "cache_settings_statistics_assets": "{} položky ({})", + "cache_settings_statistics_assets": "{count, plural, one {# položka} few {# položky} other {# položek}} ({size})", "cache_settings_statistics_full": "Kompletní fotografie", "cache_settings_statistics_shared": "Sdílené náhledy alb", "cache_settings_statistics_thumbnail": "Náhledy", "cache_settings_statistics_title": "Použití vyrovnávací paměti", "cache_settings_subtitle": "Ovládání chování mobilní aplikace Immich v mezipaměti", - "cache_settings_thumbnail_size": "Velikost vyrovnávací paměti náhledů (položek {})", + "cache_settings_thumbnail_size": "Velikost vyrovnávací paměti náhledů ({count, plural, one {# položka} few {# položky} other {# položek}})", "cache_settings_tile_subtitle": "Ovládání chování místního úložiště", "cache_settings_tile_title": "Místní úložiště", "cache_settings_title": "Nastavení vyrovnávací paměti", @@ -610,6 +612,7 @@ "change_password_form_new_password": "Nové heslo", "change_password_form_password_mismatch": "Hesla se neshodují", "change_password_form_reenter_new_password": "Znovu zadejte nové heslo", + "change_pin_code": "Změnit PIN kód", "change_your_password": "Změna vašeho hesla", "changed_visibility_successfully": "Změna viditelnosti proběhla úspěšně", "check_all": "Zkontrolovat vše", @@ -650,11 +653,12 @@ "confirm_delete_face": "Opravdu chcete z položky odstranit obličej osoby {name}?", "confirm_delete_shared_link": "Opravdu chcete odstranit tento sdílený odkaz?", "confirm_keep_this_delete_others": "Všechny ostatní položky v tomto uskupení mimo této budou odstraněny. Opravdu chcete pokračovat?", + "confirm_new_pin_code": "Potvrzení nového PIN kódu", "confirm_password": "Potvrzení hesla", "contain": "Obsah", "context": "Kontext", "continue": "Pokračovat", - "control_bottom_app_bar_album_info_shared": "{} položky – sdílené", + "control_bottom_app_bar_album_info_shared": "{count, plural, one {# položka – sdílená} few {# položky – sdílené} other {# položek – sdílených}}", "control_bottom_app_bar_create_new_album": "Vytvořit nové album", "control_bottom_app_bar_delete_from_immich": "Smazat ze serveru Immich", "control_bottom_app_bar_delete_from_local": "Smazat ze zařízení", @@ -692,9 +696,11 @@ "create_tag_description": "Vytvoření nové značky. U vnořených značek zadejte celou cestu ke značce včetně dopředných lomítek.", "create_user": "Vytvořit uživatele", "created": "Vytvořeno", + "created_at": "Vytvořeno", "crop": "Oříznout", "curated_object_page_title": "Věci", "current_device": "Současné zařízení", + "current_pin_code": "Aktuální PIN kód", "current_server_address": "Aktuální adresa serveru", "custom_locale": "Vlastní lokalizace", "custom_locale_description": "Formátovat datumy a čísla podle jazyka a oblasti", @@ -763,7 +769,7 @@ "download_enqueue": "Stahování ve frontě", "download_error": "Chyba při stahování", "download_failed": "Stahování selhalo", - "download_filename": "soubor: {}", + "download_filename": "soubor: {filename}", "download_finished": "Stahování dokončeno", "download_include_embedded_motion_videos": "Vložená videa", "download_include_embedded_motion_videos_description": "Zahrnout videa vložená do pohyblivých fotografií jako samostatný soubor", @@ -807,6 +813,7 @@ "editor_crop_tool_h2_aspect_ratios": "Poměr stran", "editor_crop_tool_h2_rotation": "Otočení", "email": "E-mail", + "email_notifications": "E-mailová oznámení", "empty_folder": "Tato složka je prázdná", "empty_trash": "Vyprázdnit koš", "empty_trash_confirmation": "Opravdu chcete vysypat koš? Tím se z Immiche trvale odstraní všechny položky v koši.\nTuto akci nelze vrátit zpět!", @@ -819,7 +826,7 @@ "error_change_sort_album": "Nepodařilo se změnit pořadí alba", "error_delete_face": "Chyba při odstraňování obličeje z položky", "error_loading_image": "Chyba při načítání obrázku", - "error_saving_image": "Chyba: {}", + "error_saving_image": "Chyba: {error}", "error_title": "Chyba - Něco se pokazilo", "errors": { "cannot_navigate_next_asset": "Nelze přejít na další položku", @@ -922,6 +929,7 @@ "unable_to_remove_reaction": "Nelze odstranit reakci", "unable_to_repair_items": "Nelze opravit položky", "unable_to_reset_password": "Nelze obnovit heslo", + "unable_to_reset_pin_code": "Nelze resetovat PIN kód", "unable_to_resolve_duplicate": "Nelze vyřešit duplicitu", "unable_to_restore_assets": "Nelze obnovit položky", "unable_to_restore_trash": "Nelze obnovit koš", @@ -955,10 +963,10 @@ "exif_bottom_sheet_location": "POLOHA", "exif_bottom_sheet_people": "LIDÉ", "exif_bottom_sheet_person_add_person": "Přidat jméno", - "exif_bottom_sheet_person_age": "{} let", - "exif_bottom_sheet_person_age_months": "{} měsíců", - "exif_bottom_sheet_person_age_year_months": "1 rok a {} měsíců", - "exif_bottom_sheet_person_age_years": "{} let", + "exif_bottom_sheet_person_age": "Věk {age, plural, one {# rok} few {# roky} other {# let}}", + "exif_bottom_sheet_person_age_months": "Věk {months, plural, one {# měsíc} few {# měsíce} other {# měsíců}}", + "exif_bottom_sheet_person_age_year_months": "Věk 1 rok, {months, plural, one {# měsíc} other {# měsíce}}", + "exif_bottom_sheet_person_age_years": "Věk {years, plural, one {# rok} few {# roky} other {# let}}", "exit_slideshow": "Ukončit prezentaci", "expand_all": "Rozbalit vše", "experimental_settings_new_asset_list_subtitle": "Zpracovávám", @@ -1048,6 +1056,7 @@ "home_page_upload_err_limit": "Lze nahrát nejvýše 30 položek najednou, přeskakuji", "host": "Hostitel", "hour": "Hodina", + "id": "ID", "ignore_icloud_photos": "Ignorovat fotografie na iCloudu", "ignore_icloud_photos_description": "Fotografie uložené na iCloudu se nebudou nahrávat na Immich server", "image": "Obrázek", @@ -1173,8 +1182,8 @@ "manage_your_devices": "Správa přihlášených zařízení", "manage_your_oauth_connection": "Správa OAuth propojení", "map": "Mapa", - "map_assets_in_bound": "{} fotka", - "map_assets_in_bounds": "{} fotek", + "map_assets_in_bound": "{count, plural, one {# fotka} few {# fotky} other {# fotek}}", + "map_assets_in_bounds": "{count, plural, one {# fotka} few {# fotky} other {# fotek}}", "map_cannot_get_user_location": "Nelze zjistit polohu uživatele", "map_location_dialog_yes": "Ano", "map_location_picker_page_use_location": "Použít tuto polohu", @@ -1188,9 +1197,9 @@ "map_settings": "Nastavení mapy", "map_settings_dark_mode": "Tmavý režim", "map_settings_date_range_option_day": "Posledních 24 hodin", - "map_settings_date_range_option_days": "Posledních {} dní", + "map_settings_date_range_option_days": "Posledních {days, plural, one {# den} few {# dny} other {# dní}}", "map_settings_date_range_option_year": "Poslední rok", - "map_settings_date_range_option_years": "Poslední {} roky", + "map_settings_date_range_option_years": "Poslední {years, plural, one {# rok} few {# roky} other {# roky}}", "map_settings_dialog_title": "Nastavení map", "map_settings_include_show_archived": "Zahrnout archivované", "map_settings_include_show_partners": "Včetně partnerů", @@ -1209,7 +1218,7 @@ "memories_start_over": "Začít znovu", "memories_swipe_to_close": "Přejetím nahoru zavřete", "memories_year_ago": "Před rokem", - "memories_years_ago": "Před {} lety", + "memories_years_ago": "Před {years, plural, one {# rokem} few {# roky} other {# lety}}", "memory": "Vzpomínka", "memory_lane_title": "Řada vzpomínek {title}", "menu": "Nabídka", @@ -1242,6 +1251,7 @@ "new_api_key": "Nový API klíč", "new_password": "Nové heslo", "new_person": "Nová osoba", + "new_pin_code": "Nový PIN kód", "new_user_created": "Vytvořen nový uživatel", "new_version_available": "NOVÁ VERZE K DISPOZICI", "newest_first": "Nejnovější první", @@ -1316,7 +1326,7 @@ "partner_page_partner_add_failed": "Nepodařilo se přidat partnera", "partner_page_select_partner": "Vyberte partnera", "partner_page_shared_to_title": "Sdíleno", - "partner_page_stop_sharing_content": "{} již nebude mít přístup k vašim fotografiím.", + "partner_page_stop_sharing_content": "{partner} již nebude mít přístup k vašim fotografiím.", "partner_sharing": "Sdílení mezi partnery", "partners": "Partneři", "password": "Heslo", @@ -1362,6 +1372,9 @@ "photos_count": "{count, plural, one {{count, number} fotka} few {{count, number} fotky} other {{count, number} fotek}}", "photos_from_previous_years": "Fotky z předchozích let", "pick_a_location": "Vyberte polohu", + "pin_code_changed_successfully": "PIN kód byl úspěšně změněn", + "pin_code_reset_successfully": "PIN kód úspěšně resetován", + "pin_code_setup_successfully": "PIN kód úspěšně nastaven", "place": "Místo", "places": "Místa", "places_count": "{count, plural, one {{count, number} místo} few {{count, number} místa} other {{count, number} míst}}", @@ -1379,6 +1392,7 @@ "previous_or_next_photo": "Předchozí nebo další fotka", "primary": "Primární", "privacy": "Soukromí", + "profile": "Profil", "profile_drawer_app_logs": "Logy", "profile_drawer_client_out_of_date_major": "Mobilní aplikace je zastaralá. Aktualizujte ji na nejnovější hlavní verzi.", "profile_drawer_client_out_of_date_minor": "Mobilní aplikace je zastaralá. Aktualizujte ji na nejnovější verzi.", @@ -1392,7 +1406,7 @@ "public_share": "Veřejné sdílení", "purchase_account_info": "Podporovatel", "purchase_activated_subtitle": "Děkujeme vám za podporu aplikace Immich a softwaru s otevřeným zdrojovým kódem", - "purchase_activated_time": "Aktivováno dne {date, date}", + "purchase_activated_time": "Aktivováno dne {date}", "purchase_activated_title": "Váš klíč byl úspěšně aktivován", "purchase_button_activate": "Aktivovat", "purchase_button_buy": "Koupit", @@ -1481,6 +1495,7 @@ "reset": "Výchozí", "reset_password": "Obnovit heslo", "reset_people_visibility": "Obnovit viditelnost lidí", + "reset_pin_code": "Resetovat PIN kód", "reset_to_default": "Obnovit výchozí nastavení", "resolve_duplicates": "Vyřešit duplicity", "resolved_all_duplicates": "Vyřešeny všechny duplicity", @@ -1604,12 +1619,12 @@ "setting_languages_apply": "Použít", "setting_languages_subtitle": "Změna jazyka aplikace", "setting_languages_title": "Jazyk", - "setting_notifications_notify_failures_grace_period": "Oznámení o selhání zálohování na pozadí: {}", - "setting_notifications_notify_hours": "{} hodin", + "setting_notifications_notify_failures_grace_period": "Oznámení o selhání zálohování na pozadí: {duration}", + "setting_notifications_notify_hours": "{count, plural, one {# hodina} few {# hodiny} other {# hodin}}", "setting_notifications_notify_immediately": "okamžitě", - "setting_notifications_notify_minutes": "{} minut", + "setting_notifications_notify_minutes": "{count, plural, one {# minuta} few {# minuty} other {# minut}}", "setting_notifications_notify_never": "nikdy", - "setting_notifications_notify_seconds": "{} sekundy", + "setting_notifications_notify_seconds": "{count, plural, one {# sekunda} few {# sekundy} other {# sekund}}", "setting_notifications_single_progress_subtitle": "Podrobné informace o průběhu nahrávání položky", "setting_notifications_single_progress_title": "Zobrazit průběh detailů zálohování na pozadí", "setting_notifications_subtitle": "Přizpůsobení předvoleb oznámení", @@ -1621,9 +1636,10 @@ "settings": "Nastavení", "settings_require_restart": "Pro použití tohoto nastavení restartujte Immich", "settings_saved": "Nastavení uloženo", + "setup_pin_code": "Nastavení PIN kódu", "share": "Sdílet", "share_add_photos": "Přidat fotografie", - "share_assets_selected": "{} vybráno", + "share_assets_selected": "{count} vybráno", "share_dialog_preparing": "Připravuji...", "shared": "Sdílené", "shared_album_activities_input_disable": "Komentář je vypnutý", @@ -1637,32 +1653,32 @@ "shared_by_user": "Sdílel(a) {user}", "shared_by_you": "Sdíleli jste", "shared_from_partner": "Fotky od {partner}", - "shared_intent_upload_button_progress_text": "{} / {} nahráno", + "shared_intent_upload_button_progress_text": "{current} / {total} nahráno", "shared_link_app_bar_title": "Sdílené odkazy", "shared_link_clipboard_copied_massage": "Zkopírováno do schránky", - "shared_link_clipboard_text": "Odkaz: {}\nHeslo: {}", + "shared_link_clipboard_text": "Odkaz: {link}\nHeslo: {password}", "shared_link_create_error": "Chyba při vytváření sdíleného odkazu", "shared_link_edit_description_hint": "Zadejte popis sdílení", "shared_link_edit_expire_after_option_day": "1 den", - "shared_link_edit_expire_after_option_days": "{} dní", + "shared_link_edit_expire_after_option_days": "{count, plural, one {# den} few {# dny} other {# dní}}", "shared_link_edit_expire_after_option_hour": "1 hodina", - "shared_link_edit_expire_after_option_hours": "{} hodin", + "shared_link_edit_expire_after_option_hours": "{count, plural, one {# hodina} few {# hodiny} other {# hodin}}", "shared_link_edit_expire_after_option_minute": "1 minuta", - "shared_link_edit_expire_after_option_minutes": "{} minut", - "shared_link_edit_expire_after_option_months": "{} měsíce", - "shared_link_edit_expire_after_option_year": "{} rok", + "shared_link_edit_expire_after_option_minutes": "{count, plural, one {# minuta} few {# minuty} other {# minut}}", + "shared_link_edit_expire_after_option_months": "{count, plural, one {# měsíc} few {# měsíce} other {# měsíců}}", + "shared_link_edit_expire_after_option_year": "{count, plural, one {# rok} few {# roky} other {# let}}", "shared_link_edit_password_hint": "Zadejte heslo pro sdílení", "shared_link_edit_submit_button": "Aktualizovat odkaz", "shared_link_error_server_url_fetch": "Nelze načíst url serveru", - "shared_link_expires_day": "Vyprší za {} den", - "shared_link_expires_days": "Vyprší za {} dní", - "shared_link_expires_hour": "Vyprší za {} hodinu", - "shared_link_expires_hours": "Vyprší za {} hodin", - "shared_link_expires_minute": "Vyprší za {} minutu", - "shared_link_expires_minutes": "Vyprší za {} minut", + "shared_link_expires_day": "Vyprší za {count, plural, one {# den} few {# dny} other {# dní}}", + "shared_link_expires_days": "Vyprší za {count, plural, one {# den} few {# dny} other {# dní}}", + "shared_link_expires_hour": "Vyprší za {count, plural, one {# hodina} few {# hodiny} other {# hodin}}", + "shared_link_expires_hours": "Vyprší za {count, plural, one {# hodina} few {# hodiny} other {# hodin}}", + "shared_link_expires_minute": "Vyprší za {count, plural, one {# minuta} few {# minuty} other {# minut}}", + "shared_link_expires_minutes": "Vyprší za {count, plural, one {# minuta} few {# minuty} other {# minut}}", "shared_link_expires_never": "Platnost ∞", - "shared_link_expires_second": "Vyprší za {} sekundu", - "shared_link_expires_seconds": "Vyprší za {} sekund", + "shared_link_expires_second": "Vyprší za {count, plural, one {# sekunda} few {# sekundy} other {# sekund}}", + "shared_link_expires_seconds": "Vyprší za {count, plural, one {# sekunda} few {# sekundy} other {# sekund}}", "shared_link_individual_shared": "Individuální sdílení", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Spravovat sdílené odkazy", @@ -1737,6 +1753,7 @@ "stop_sharing_photos_with_user": "Přestat sdílet své fotky s tímto uživatelem", "storage": "Velikost úložiště", "storage_label": "Štítek úložiště", + "storage_quota": "Kvóta úložiště", "storage_usage": "Využito {used} z {available}", "submit": "Odeslat", "suggestions": "Návrhy", @@ -1763,7 +1780,7 @@ "theme_selection": "Výběr motivu", "theme_selection_description": "Automatické nastavení světlého nebo tmavého motivu podle systémových preferencí prohlížeče", "theme_setting_asset_list_storage_indicator_title": "Zobrazit indikátor úložiště na dlaždicích položek", - "theme_setting_asset_list_tiles_per_row_title": "Počet položek na řádek ({})", + "theme_setting_asset_list_tiles_per_row_title": "Počet položek na řádek ({count})", "theme_setting_colorful_interface_subtitle": "Použít hlavní barvu na povrchy pozadí.", "theme_setting_colorful_interface_title": "Barevné rozhraní", "theme_setting_image_viewer_quality_subtitle": "Přizpůsobení kvality detailů prohlížeče obrázků", @@ -1798,13 +1815,15 @@ "trash_no_results_message": "Zde se zobrazí odstraněné fotky a videa.", "trash_page_delete_all": "Smazat všechny", "trash_page_empty_trash_dialog_content": "Chcete vyprázdnit svoje vyhozené položky? Tyto položky budou trvale odstraněny z aplikace", - "trash_page_info": "Vyhozené položky budou trvale smazány po {} dnech", + "trash_page_info": "Vyhozené položky budou trvale smazány po {count, plural, one {# dni} other {# dnech}}", "trash_page_no_assets": "Žádné vyhozené položky", "trash_page_restore_all": "Obnovit všechny", "trash_page_select_assets_btn": "Vybrat položky", - "trash_page_title": "Koš ({})", + "trash_page_title": "Koš ({count})", "trashed_items_will_be_permanently_deleted_after": "Smazané položky budou trvale odstraněny po {days, plural, one {# dni} other {# dnech}}.", "type": "Typ", + "unable_to_change_pin_code": "Nelze změnit PIN kód", + "unable_to_setup_pin_code": "Nelze nastavit PIN kód", "unarchive": "Odarchivovat", "unarchived_count": "{count, plural, one {Odarchivována #} few {Odarchivovány #} other {Odarchivováno #}}", "unfavorite": "Zrušit oblíbení", @@ -1828,6 +1847,7 @@ "untracked_files": "Nesledované soubory", "untracked_files_decription": "Tyto soubory nejsou aplikaci známy. Mohou být výsledkem neúspěšných přesunů, přerušeného nahrávání nebo mohou zůstat pozadu kvůli chybě", "up_next": "To je prozatím vše", + "updated_at": "Aktualizováno", "updated_password": "Heslo aktualizováno", "upload": "Nahrát", "upload_concurrency": "Souběžnost nahrávání", @@ -1840,15 +1860,18 @@ "upload_status_errors": "Chyby", "upload_status_uploaded": "Nahráno", "upload_success": "Nahrání proběhlo úspěšně, obnovením stránky se zobrazí nově nahrané položky.", - "upload_to_immich": "Nahrát do Immiche ({})", + "upload_to_immich": "Nahrát do Immich ({count})", "uploading": "Nahrávání", "url": "URL", "usage": "Využití", "use_current_connection": "použít aktuální připojení", "use_custom_date_range": "Použít vlastní rozsah dat", "user": "Uživatel", + "user_has_been_deleted": "Tento uživatel byl smazán.", "user_id": "ID uživatele", "user_liked": "Uživateli {user} se {type, select, photo {líbila tato fotka} video {líbilo toto video} asset {líbila tato položka} other {to líbilo}}", + "user_pin_code_settings": "PIN kód", + "user_pin_code_settings_description": "Správa vašeho PIN kódu", "user_purchase_settings": "Nákup", "user_purchase_settings_description": "Správa vašeho nákupu", "user_role_set": "Uživatel {user} nastaven jako {role}", diff --git a/i18n/da.json b/i18n/da.json index e5e9e017aa..bf853a5fcb 100644 --- a/i18n/da.json +++ b/i18n/da.json @@ -1374,7 +1374,7 @@ "public_share": "Offentlig deling", "purchase_account_info": "Supporter", "purchase_activated_subtitle": "Tak fordi du støtter Immich og open source-software", - "purchase_activated_time": "Aktiveret den {date, date}", + "purchase_activated_time": "Aktiveret den {date}", "purchase_activated_title": "Din nøgle er blevet aktiveret", "purchase_button_activate": "Aktiver", "purchase_button_buy": "Køb", diff --git a/i18n/de.json b/i18n/de.json index f0b0763886..a33319ee02 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -53,6 +53,7 @@ "confirm_email_below": "Bestätige, indem du unten \"{email}\" eingibst", "confirm_reprocess_all_faces": "Bist du sicher, dass du alle Gesichter erneut verarbeiten möchtest? Dies löscht auch alle bereits benannten Personen.", "confirm_user_password_reset": "Bist du sicher, dass du das Passwort für {user} zurücksetzen möchtest?", + "confirm_user_pin_code_reset": "Bist du sicher, dass du den PIN Code von {user} zurücksetzen möchtest?", "create_job": "Aufgabe erstellen", "cron_expression": "Cron-Ausdruck", "cron_expression_description": "Stellen Sie das Scanintervall im Cron-Format ein. Weitere Informationen finden Sie beispielsweise unter Crontab Guru", @@ -348,6 +349,7 @@ "user_delete_delay_settings_description": "Gibt die Anzahl der Tage bis zur endgültigen Löschung eines Kontos und seiner Dateien an. Der Benutzerlöschauftrag wird täglich um Mitternacht ausgeführt, um zu überprüfen, ob Nutzer zur Löschung bereit sind. Änderungen an dieser Einstellung werden erst bei der nächsten Ausführung berücksichtigt.", "user_delete_immediately": "Das Konto und die Dateien von {user} werden sofort für eine permanente Löschung in die Warteschlange gestellt.", "user_delete_immediately_checkbox": "Benutzer und Dateien zur sofortigen Löschung in die Warteschlange stellen", + "user_details": "Benutzerdetails", "user_management": "Benutzerverwaltung", "user_password_has_been_reset": "Das Passwort des Benutzers wurde zurückgesetzt:", "user_password_reset_description": "Bitte gib dem Benutzer das temporäre Passwort und informiere ihn, dass das Passwort beim nächsten Login geändert werden muss.", @@ -369,7 +371,7 @@ "advanced": "Erweitert", "advanced_settings_enable_alternate_media_filter_subtitle": "Verwende diese Option, um Medien während der Synchronisierung nach anderen Kriterien zu filtern. Versuchen dies nur, wenn Probleme mit der Erkennung aller Alben durch die App auftreten.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTELL] Benutze alternativen Filter für Synchronisierung der Gerätealben", - "advanced_settings_log_level_title": "Log-Level: {name}", + "advanced_settings_log_level_title": "Log-Level: {level}", "advanced_settings_prefer_remote_subtitle": "Einige Geräte sind sehr langsam beim Laden von Miniaturbildern direkt aus dem Gerät. Aktivieren Sie diese Einstellung, um stattdessen die Server-Bilder zu laden.", "advanced_settings_prefer_remote_title": "Server-Bilder bevorzugen", "advanced_settings_proxy_headers_subtitle": "Definiere einen Proxy-Header, den Immich bei jeder Netzwerkanfrage mitschicken soll", @@ -400,9 +402,9 @@ "album_remove_user_confirmation": "Bist du sicher, dass du {user} entfernen willst?", "album_share_no_users": "Es sieht so aus, als hättest du dieses Album mit allen Benutzern geteilt oder du hast keine Benutzer, mit denen du teilen kannst.", "album_thumbnail_card_item": "1 Element", - "album_thumbnail_card_items": "{} Elemente", + "album_thumbnail_card_items": "{count} Elemente", "album_thumbnail_card_shared": " · Geteilt", - "album_thumbnail_shared_by": "Geteilt von {}", + "album_thumbnail_shared_by": "Geteilt von {user}", "album_updated": "Album aktualisiert", "album_updated_setting_description": "Erhalte eine E-Mail-Benachrichtigung, wenn ein freigegebenes Album neue Dateien enthält", "album_user_left": "{album} verlassen", @@ -440,7 +442,7 @@ "archive": "Archiv", "archive_or_unarchive_photo": "Foto archivieren bzw. Archivierung aufheben", "archive_page_no_archived_assets": "Keine archivierten Inhalte gefunden", - "archive_page_title": "Archiv ({})", + "archive_page_title": "Archiv ({count})", "archive_size": "Archivgröße", "archive_size_description": "Archivgröße für Downloads konfigurieren (in GiB)", "archived": "Archiviert", @@ -477,18 +479,18 @@ "assets_added_to_album_count": "{count, plural, one {# Datei} other {# Dateien}} zum Album hinzugefügt", "assets_added_to_name_count": "{count, plural, one {# Element} other {# Elemente}} zu {hasName, select, true {{name}} other {neuem Album}} hinzugefügt", "assets_count": "{count, plural, one {# Datei} other {# Dateien}}", - "assets_deleted_permanently": "{} Element(e) permanent gelöscht", - "assets_deleted_permanently_from_server": "{} Element(e) permanent vom Immich-Server gelöscht", + "assets_deleted_permanently": "{count} Element(e) permanent gelöscht", + "assets_deleted_permanently_from_server": "{count} Element(e) permanent vom Immich-Server gelöscht", "assets_moved_to_trash_count": "{count, plural, one {# Datei} other {# Dateien}} in den Papierkorb verschoben", "assets_permanently_deleted_count": "{count, plural, one {# Datei} other {# Dateien}} endgültig gelöscht", "assets_removed_count": "{count, plural, one {# Datei} other {# Dateien}} entfernt", - "assets_removed_permanently_from_device": "{} Element(e) permanent von Ihrem Gerät gelöscht", + "assets_removed_permanently_from_device": "{count} Element(e) permanent von Ihrem Gerät gelöscht", "assets_restore_confirmation": "Bist du sicher, dass du alle Dateien aus dem Papierkorb wiederherstellen willst? Diese Aktion kann nicht rückgängig gemacht werden! Beachte, dass Offline-Dateien auf diese Weise nicht wiederhergestellt werden können.", "assets_restored_count": "{count, plural, one {# Datei} other {# Dateien}} wiederhergestellt", - "assets_restored_successfully": "{} Element(e) erfolgreich wiederhergestellt", - "assets_trashed": "{} Element(e) gelöscht", + "assets_restored_successfully": "{count} Element(e) erfolgreich wiederhergestellt", + "assets_trashed": "{count} Element(e) gelöscht", "assets_trashed_count": "{count, plural, one {# Datei} other {# Dateien}} in den Papierkorb verschoben", - "assets_trashed_from_server": "{} Element(e) vom Immich-Server gelöscht", + "assets_trashed_from_server": "{count} Element(e) vom Immich-Server gelöscht", "assets_were_part_of_album_count": "{count, plural, one {# Datei ist} other {# Dateien sind}} bereits im Album vorhanden", "authorized_devices": "Verwendete Geräte", "automatic_endpoint_switching_subtitle": "Verbinden Sie sich lokal über ein bestimmtes WLAN, wenn es verfügbar ist, und verwenden Sie andere Verbindungsmöglichkeiten anderswo", @@ -497,7 +499,7 @@ "back_close_deselect": "Zurück, Schließen oder Abwählen", "background_location_permission": "Hintergrund Standortfreigabe", "background_location_permission_content": "Um im Hintergrund zwischen den Netzwerken wechseln zu können, muss Immich *immer* Zugriff auf den genauen Standort haben, damit die App den Namen des WLAN-Netzwerks ermitteln kann", - "backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})", + "backup_album_selection_page_albums_device": "Alben auf dem Gerät ({count})", "backup_album_selection_page_albums_tap": "Einmalig das Album antippen um es zu sichern, doppelt antippen um es nicht mehr zu sichern", "backup_album_selection_page_assets_scatter": "Elemente (Fotos / Videos) können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden.", "backup_album_selection_page_select_albums": "Alben auswählen", @@ -506,11 +508,11 @@ "backup_all": "Alle", "backup_background_service_backup_failed_message": "Es trat ein Fehler bei der Sicherung auf. Erneuter Versuch…", "backup_background_service_connection_failed_message": "Es konnte keine Verbindung zum Server hergestellt werden. Erneuter Versuch…", - "backup_background_service_current_upload_notification": "Lädt {} hoch", + "backup_background_service_current_upload_notification": "Lädt {filename} hoch", "backup_background_service_default_notification": "Suche nach neuen Elementen…", "backup_background_service_error_title": "Fehler bei der Sicherung", "backup_background_service_in_progress_notification": "Elemente werden gesichert…", - "backup_background_service_upload_failure_notification": "Konnte {} nicht hochladen", + "backup_background_service_upload_failure_notification": "Konnte {filename} nicht hochladen", "backup_controller_page_albums": "Gesicherte Alben", "backup_controller_page_background_app_refresh_disabled_content": "Aktiviere Hintergrundaktualisierungen in Einstellungen -> Allgemein -> Hintergrundaktualisierungen um Sicherungen im Hintergrund zu ermöglichen.", "backup_controller_page_background_app_refresh_disabled_title": "Hintergrundaktualisierungen sind deaktiviert", @@ -521,7 +523,7 @@ "backup_controller_page_background_battery_info_title": "Batterieoptimierungen", "backup_controller_page_background_charging": "Nur während des Ladens", "backup_controller_page_background_configure_error": "Konnte Hintergrundservice nicht konfigurieren", - "backup_controller_page_background_delay": "Sicherung neuer Elemente verzögern um: {}", + "backup_controller_page_background_delay": "Sicherung neuer Elemente verzögern um: {duration}", "backup_controller_page_background_description": "Schalte den Hintergrundservice ein, um neue Elemente automatisch im Hintergrund zu sichern ohne die App zu öffnen", "backup_controller_page_background_is_off": "Automatische Sicherung im Hintergrund ist deaktiviert", "backup_controller_page_background_is_on": "Automatische Sicherung im Hintergrund ist aktiviert", @@ -531,12 +533,12 @@ "backup_controller_page_backup": "Sicherung", "backup_controller_page_backup_selected": "Ausgewählt: ", "backup_controller_page_backup_sub": "Gesicherte Fotos und Videos", - "backup_controller_page_created": "Erstellt am: {}", + "backup_controller_page_created": "Erstellt am: {date}", "backup_controller_page_desc_backup": "Aktiviere die Sicherung, um Elemente immer automatisch auf den Server zu laden, während du die App benutzt.", "backup_controller_page_excluded": "Ausgeschlossen: ", - "backup_controller_page_failed": "Fehlgeschlagen ({})", - "backup_controller_page_filename": "Dateiname: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Fehlgeschlagen ({count})", + "backup_controller_page_filename": "Dateiname: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informationen zur Sicherung", "backup_controller_page_none_selected": "Keine ausgewählt", "backup_controller_page_remainder": "Verbleibend", @@ -545,7 +547,7 @@ "backup_controller_page_start_backup": "Sicherung starten", "backup_controller_page_status_off": "Sicherung im Vordergrund ist inaktiv", "backup_controller_page_status_on": "Sicherung im Vordergrund ist aktiv", - "backup_controller_page_storage_format": "{} von {} genutzt", + "backup_controller_page_storage_format": "{used} von {total} genutzt", "backup_controller_page_to_backup": "Zu sichernde Alben", "backup_controller_page_total_sub": "Alle Fotos und Videos", "backup_controller_page_turn_off": "Sicherung im Vordergrund ausschalten", @@ -570,21 +572,21 @@ "bulk_keep_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} behalten möchtest? Dies wird alle Duplikat-Gruppen auflösen ohne etwas zu löschen.", "bulk_trash_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien gemeinsam}} in den Papierkorb verschieben möchtest? Dies wird die größte Datei jeder Gruppe behalten und alle anderen Duplikate in den Papierkorb verschieben.", "buy": "Immich erwerben", - "cache_settings_album_thumbnails": "Vorschaubilder der Bibliothek ({} Elemente)", + "cache_settings_album_thumbnails": "Vorschaubilder der Bibliothek ({count} Elemente)", "cache_settings_clear_cache_button": "Zwischenspeicher löschen", "cache_settings_clear_cache_button_title": "Löscht den Zwischenspeicher der App. Dies wird die Leistungsfähigkeit der App deutlich einschränken, bis der Zwischenspeicher wieder aufgebaut wurde.", "cache_settings_duplicated_assets_clear_button": "LEEREN", "cache_settings_duplicated_assets_subtitle": "Fotos und Videos, die von der App blockiert werden", - "cache_settings_duplicated_assets_title": "Duplikate ({})", - "cache_settings_image_cache_size": "Bilder im Zwischenspeicher ({} Bilder)", + "cache_settings_duplicated_assets_title": "Duplikate ({count})", + "cache_settings_image_cache_size": "Bilder im Zwischenspeicher ({count} Bilder)", "cache_settings_statistics_album": "Vorschaubilder der Bibliothek", - "cache_settings_statistics_assets": "{} Elemente ({})", + "cache_settings_statistics_assets": "{count} Elemente ({size})", "cache_settings_statistics_full": "Originalbilder", "cache_settings_statistics_shared": "Vorschaubilder geteilter Alben", "cache_settings_statistics_thumbnail": "Vorschaubilder", "cache_settings_statistics_title": "Zwischenspeicher-Nutzung", "cache_settings_subtitle": "Kontrollieren, wie Immich den Zwischenspeicher nutzt", - "cache_settings_thumbnail_size": "Vorschaubilder im Zwischenspeicher ({} Bilder)", + "cache_settings_thumbnail_size": "Vorschaubilder im Zwischenspeicher ({count} Bilder)", "cache_settings_tile_subtitle": "Lokalen Speicher verwalten", "cache_settings_tile_title": "Lokaler Speicher", "cache_settings_title": "Zwischenspeicher Einstellungen", @@ -610,6 +612,7 @@ "change_password_form_new_password": "Neues Passwort", "change_password_form_password_mismatch": "Passwörter stimmen nicht überein", "change_password_form_reenter_new_password": "Passwort erneut eingeben", + "change_pin_code": "PIN Code ändern", "change_your_password": "Ändere dein Passwort", "changed_visibility_successfully": "Die Sichtbarkeit wurde erfolgreich geändert", "check_all": "Alle prüfen", @@ -650,11 +653,12 @@ "confirm_delete_face": "Bist du sicher dass du das Gesicht von {name} aus der Datei entfernen willst?", "confirm_delete_shared_link": "Bist du sicher, dass du diesen geteilten Link löschen willst?", "confirm_keep_this_delete_others": "Alle anderen Dateien im Stapel bis auf diese werden gelöscht. Bist du sicher, dass du fortfahren möchten?", + "confirm_new_pin_code": "Neuen PIN Code bestätigen", "confirm_password": "Passwort bestätigen", "contain": "Vollständig", "context": "Kontext", "continue": "Fortsetzen", - "control_bottom_app_bar_album_info_shared": "{} Elemente · Geteilt", + "control_bottom_app_bar_album_info_shared": "{count} Elemente · Geteilt", "control_bottom_app_bar_create_new_album": "Neues Album erstellen", "control_bottom_app_bar_delete_from_immich": "Aus Immich löschen", "control_bottom_app_bar_delete_from_local": "Vom Gerät löschen", @@ -692,9 +696,11 @@ "create_tag_description": "Erstelle einen neuen Tag. Für verschachtelte Tags, gib den gesamten Pfad inklusive Schrägstrich an.", "create_user": "Nutzer erstellen", "created": "Erstellt", + "created_at": "Erstellt", "crop": "Zuschneiden", "curated_object_page_title": "Dinge", "current_device": "Aktuelles Gerät", + "current_pin_code": "Aktueller PIN Code", "current_server_address": "Aktuelle Serveradresse", "custom_locale": "Benutzerdefinierte Sprache", "custom_locale_description": "Datumsangaben und Zahlen je nach Sprache und Land formatieren", @@ -763,7 +769,7 @@ "download_enqueue": "Download in die Warteschlange gesetzt", "download_error": "Download fehlerhaft", "download_failed": "Download fehlerhaft", - "download_filename": "Datei: {}", + "download_filename": "Datei: {filename}", "download_finished": "Download abgeschlossen", "download_include_embedded_motion_videos": "Eingebettete Videos", "download_include_embedded_motion_videos_description": "Videos, die in Bewegungsfotos eingebettet sind, als separate Datei einfügen", @@ -807,6 +813,7 @@ "editor_crop_tool_h2_aspect_ratios": "Seitenverhältnisse", "editor_crop_tool_h2_rotation": "Drehung", "email": "E-Mail", + "email_notifications": "E-Mail Benachrichtigungen", "empty_folder": "Dieser Ordner ist leer", "empty_trash": "Papierkorb leeren", "empty_trash_confirmation": "Bist du sicher, dass du den Papierkorb leeren willst?\nDies entfernt alle Dateien im Papierkorb endgültig aus Immich und kann nicht rückgängig gemacht werden!", @@ -819,7 +826,7 @@ "error_change_sort_album": "Ändern der Anzeigereihenfolge fehlgeschlagen", "error_delete_face": "Fehler beim Löschen des Gesichts", "error_loading_image": "Fehler beim Laden des Bildes", - "error_saving_image": "Fehler: {}", + "error_saving_image": "Fehler: {error}", "error_title": "Fehler - Etwas ist schief gelaufen", "errors": { "cannot_navigate_next_asset": "Kann nicht zur nächsten Datei navigieren", @@ -922,6 +929,7 @@ "unable_to_remove_reaction": "Reaktion kann nicht entfernt werden", "unable_to_repair_items": "Objekte können nicht repariert werden", "unable_to_reset_password": "Passwort kann nicht zurückgesetzt werden", + "unable_to_reset_pin_code": "Zurücksetzen des PIN Code nicht möglich", "unable_to_resolve_duplicate": "Duplikate können nicht aufgelöst werden", "unable_to_restore_assets": "Dateien konnten nicht wiederhergestellt werden", "unable_to_restore_trash": "Papierkorb kann nicht wiederhergestellt werden", @@ -955,10 +963,10 @@ "exif_bottom_sheet_location": "STANDORT", "exif_bottom_sheet_people": "PERSONEN", "exif_bottom_sheet_person_add_person": "Namen hinzufügen", - "exif_bottom_sheet_person_age": "Alter {}", - "exif_bottom_sheet_person_age_months": "{} Monate alt", - "exif_bottom_sheet_person_age_year_months": "1 Jahr, {} Monate alt", - "exif_bottom_sheet_person_age_years": "Alter {}", + "exif_bottom_sheet_person_age": "Alter {age}", + "exif_bottom_sheet_person_age_months": "{months} Monate alt", + "exif_bottom_sheet_person_age_year_months": "1 Jahr, {months} Monate alt", + "exif_bottom_sheet_person_age_years": "Alter {years}", "exit_slideshow": "Diashow beenden", "expand_all": "Alle aufklappen", "experimental_settings_new_asset_list_subtitle": "In Arbeit", @@ -1048,6 +1056,7 @@ "home_page_upload_err_limit": "Es können max. 30 Elemente gleichzeitig hochgeladen werden, überspringen", "host": "Host", "hour": "Stunde", + "id": "ID", "ignore_icloud_photos": "iCloud Fotos ignorieren", "ignore_icloud_photos_description": "Fotos, die in der iCloud gespeichert sind, werden nicht auf den immich Server hochgeladen", "image": "Bild", @@ -1173,8 +1182,8 @@ "manage_your_devices": "Deine eingeloggten Geräte verwalten", "manage_your_oauth_connection": "Deine OAuth-Verknüpfung verwalten", "map": "Karte", - "map_assets_in_bound": "{} Foto", - "map_assets_in_bounds": "{} Fotos", + "map_assets_in_bound": "{count} Foto", + "map_assets_in_bounds": "{count} Fotos", "map_cannot_get_user_location": "Standort konnte nicht ermittelt werden", "map_location_dialog_yes": "Ja", "map_location_picker_page_use_location": "Aufnahmeort verwenden", @@ -1188,9 +1197,9 @@ "map_settings": "Karteneinstellungen", "map_settings_dark_mode": "Dunkler Modus", "map_settings_date_range_option_day": "Letzte 24 Stunden", - "map_settings_date_range_option_days": "Letzten {} Tage", + "map_settings_date_range_option_days": "Letzten {days} Tage", "map_settings_date_range_option_year": "Letztes Jahr", - "map_settings_date_range_option_years": "Letzten {} Jahre", + "map_settings_date_range_option_years": "Letzten {years} Jahre", "map_settings_dialog_title": "Karteneinstellungen", "map_settings_include_show_archived": "Archivierte anzeigen", "map_settings_include_show_partners": "Partner einbeziehen", @@ -1209,7 +1218,7 @@ "memories_start_over": "Erneut beginnen", "memories_swipe_to_close": "Nach oben Wischen zum schließen", "memories_year_ago": "ein Jahr her", - "memories_years_ago": "Vor {} Jahren", + "memories_years_ago": "Vor {years} Jahren", "memory": "Erinnerung", "memory_lane_title": "Foto-Erinnerungen {title}", "menu": "Menü", @@ -1242,6 +1251,7 @@ "new_api_key": "Neuer API-Schlüssel", "new_password": "Neues Passwort", "new_person": "Neue Person", + "new_pin_code": "Neuer PIN Code", "new_user_created": "Neuer Benutzer wurde erstellt", "new_version_available": "NEUE VERSION VERFÜGBAR", "newest_first": "Neueste zuerst", @@ -1316,7 +1326,7 @@ "partner_page_partner_add_failed": "Fehler beim Partner hinzufügen", "partner_page_select_partner": "Partner auswählen", "partner_page_shared_to_title": "Geteilt mit", - "partner_page_stop_sharing_content": "{} wird nicht mehr auf deine Fotos zugreifen können.", + "partner_page_stop_sharing_content": "{partner} wird nicht mehr auf deine Fotos zugreifen können.", "partner_sharing": "Partner-Sharing", "partners": "Partner", "password": "Passwort", @@ -1362,6 +1372,9 @@ "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", "photos_from_previous_years": "Fotos von vorherigen Jahren", "pick_a_location": "Wähle einen Ort", + "pin_code_changed_successfully": "PIN Code erfolgreich geändert", + "pin_code_reset_successfully": "PIN Code erfolgreich zurückgesetzt", + "pin_code_setup_successfully": "PIN Code erfolgreich festgelegt", "place": "Ort", "places": "Orte", "places_count": "{count, plural, one {{count, number} Ort} other {{count, number} Orte}}", @@ -1379,6 +1392,7 @@ "previous_or_next_photo": "Vorheriges oder nächstes Foto", "primary": "Primär", "privacy": "Privatsphäre", + "profile": "Profil", "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Major-Version.", "profile_drawer_client_out_of_date_minor": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Minor-Version.", @@ -1392,7 +1406,7 @@ "public_share": "Öffentliche Freigabe", "purchase_account_info": "Unterstützer", "purchase_activated_subtitle": "Danke für die Unterstützung von Immich und Open-Source Software", - "purchase_activated_time": "Aktiviert am {date, date}", + "purchase_activated_time": "Aktiviert am {date}", "purchase_activated_title": "Dein Schlüssel wurde erfolgreich aktiviert", "purchase_button_activate": "Aktivieren", "purchase_button_buy": "Kaufen", @@ -1481,6 +1495,7 @@ "reset": "Zurücksetzen", "reset_password": "Passwort zurücksetzen", "reset_people_visibility": "Sichtbarkeit von Personen zurücksetzen", + "reset_pin_code": "PIN Code zurücksetzen", "reset_to_default": "Auf Standard zurücksetzen", "resolve_duplicates": "Duplikate entfernen", "resolved_all_duplicates": "Alle Duplikate aufgelöst", @@ -1604,12 +1619,12 @@ "setting_languages_apply": "Anwenden", "setting_languages_subtitle": "App-Sprache ändern", "setting_languages_title": "Sprachen", - "setting_notifications_notify_failures_grace_period": "Benachrichtigung bei Fehler(n) in der Hintergrundsicherung: {}", - "setting_notifications_notify_hours": "{} Stunden", + "setting_notifications_notify_failures_grace_period": "Benachrichtigung bei Fehler(n) in der Hintergrundsicherung: {duration}", + "setting_notifications_notify_hours": "{count} Stunden", "setting_notifications_notify_immediately": "sofort", - "setting_notifications_notify_minutes": "{} Minuten", + "setting_notifications_notify_minutes": "{count} Minuten", "setting_notifications_notify_never": "niemals", - "setting_notifications_notify_seconds": "{} Sekunden", + "setting_notifications_notify_seconds": "{count} Sekunden", "setting_notifications_single_progress_subtitle": "Detaillierter Upload-Fortschritt für jedes Element", "setting_notifications_single_progress_title": "Zeige den detaillierten Fortschritt der Hintergrundsicherung", "setting_notifications_subtitle": "Benachrichtigungen anpassen", @@ -1621,9 +1636,10 @@ "settings": "Einstellungen", "settings_require_restart": "Bitte starte Immich neu, um diese Einstellung anzuwenden", "settings_saved": "Einstellungen gespeichert", + "setup_pin_code": "Einen PIN Code festlegen", "share": "Teilen", "share_add_photos": "Fotos hinzufügen", - "share_assets_selected": "{} ausgewählt", + "share_assets_selected": "{count} ausgewählt", "share_dialog_preparing": "Vorbereiten...", "shared": "Geteilt", "shared_album_activities_input_disable": "Kommentare sind deaktiviert", @@ -1637,32 +1653,32 @@ "shared_by_user": "Von {user} geteilt", "shared_by_you": "Von dir geteilt", "shared_from_partner": "Fotos von {partner}", - "shared_intent_upload_button_progress_text": "{} / {} hochgeladen", + "shared_intent_upload_button_progress_text": "{current} / {total} hochgeladen", "shared_link_app_bar_title": "Geteilte Links", "shared_link_clipboard_copied_massage": "Link kopiert", - "shared_link_clipboard_text": "Link: {}\nPasswort: {}", + "shared_link_clipboard_text": "Link: {link}\nPasswort: {password}", "shared_link_create_error": "Fehler beim Erstellen der Linkfreigabe", "shared_link_edit_description_hint": "Beschreibung eingeben", "shared_link_edit_expire_after_option_day": "1 Tag", - "shared_link_edit_expire_after_option_days": "{} Tagen", + "shared_link_edit_expire_after_option_days": "{count} Tagen", "shared_link_edit_expire_after_option_hour": "1 Stunde", - "shared_link_edit_expire_after_option_hours": "{} Stunden", + "shared_link_edit_expire_after_option_hours": "{count} Stunden", "shared_link_edit_expire_after_option_minute": "1 Minute", - "shared_link_edit_expire_after_option_minutes": "{} Minuten", - "shared_link_edit_expire_after_option_months": "{} Monaten", - "shared_link_edit_expire_after_option_year": "{} Jahr", + "shared_link_edit_expire_after_option_minutes": "{count} Minuten", + "shared_link_edit_expire_after_option_months": "{count} Monaten", + "shared_link_edit_expire_after_option_year": "{count} Jahr", "shared_link_edit_password_hint": "Passwort eingeben", "shared_link_edit_submit_button": "Link aktualisieren", "shared_link_error_server_url_fetch": "Fehler beim Ermitteln der Server-URL", - "shared_link_expires_day": "Läuft ab in {} Tag", - "shared_link_expires_days": "Läuft ab in {} Tagen", - "shared_link_expires_hour": "Läuft ab in {} Stunde", - "shared_link_expires_hours": "Läuft ab in {} Stunden", - "shared_link_expires_minute": "Läuft ab in {} Minute", - "shared_link_expires_minutes": "Läuft ab in {} Minuten", + "shared_link_expires_day": "Läuft ab in {count} Tag", + "shared_link_expires_days": "Läuft ab in {count} Tagen", + "shared_link_expires_hour": "Läuft ab in {count} Stunde", + "shared_link_expires_hours": "Läuft ab in {count} Stunden", + "shared_link_expires_minute": "Läuft ab in {count} Minute", + "shared_link_expires_minutes": "Läuft ab in {count} Minuten", "shared_link_expires_never": "Läuft nie ab", - "shared_link_expires_second": "Läuft ab in {} Sekunde", - "shared_link_expires_seconds": "Läuft ab in {} Sekunden", + "shared_link_expires_second": "Läuft ab in {count} Sekunde", + "shared_link_expires_seconds": "Läuft ab in {count} Sekunden", "shared_link_individual_shared": "Individuell geteilt", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Geteilte Links verwalten", @@ -1737,6 +1753,7 @@ "stop_sharing_photos_with_user": "Aufhören Fotos mit diesem Benutzer zu teilen", "storage": "Speicherplatz", "storage_label": "Speicherpfad", + "storage_quota": "Speicherplatz-Kontingent", "storage_usage": "{used} von {available} verwendet", "submit": "Bestätigen", "suggestions": "Vorschläge", @@ -1763,7 +1780,7 @@ "theme_selection": "Themenauswahl", "theme_selection_description": "Automatische Einstellung des Themes auf Hell oder Dunkel, je nach Systemeinstellung des Browsers", "theme_setting_asset_list_storage_indicator_title": "Forschrittsbalken der Sicherung auf dem Vorschaubild", - "theme_setting_asset_list_tiles_per_row_title": "Anzahl der Elemente pro Reihe ({})", + "theme_setting_asset_list_tiles_per_row_title": "Anzahl der Elemente pro Reihe ({count})", "theme_setting_colorful_interface_subtitle": "Primärfarbe auf App-Hintergrund anwenden.", "theme_setting_colorful_interface_title": "Farbige UI-Oberfläche", "theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters", @@ -1798,13 +1815,15 @@ "trash_no_results_message": "Gelöschte Fotos und Videos werden hier angezeigt.", "trash_page_delete_all": "Alle löschen", "trash_page_empty_trash_dialog_content": "Elemente im Papierkorb löschen? Diese Elemente werden dauerhaft aus Immich entfernt", - "trash_page_info": "Elemente im Papierkorb werden nach {} Tagen endgültig gelöscht", + "trash_page_info": "Elemente im Papierkorb werden nach {days} Tagen endgültig gelöscht", "trash_page_no_assets": "Es gibt keine Daten im Papierkorb", "trash_page_restore_all": "Alle wiederherstellen", "trash_page_select_assets_btn": "Elemente auswählen", - "trash_page_title": "Papierkorb ({})", + "trash_page_title": "Papierkorb ({count})", "trashed_items_will_be_permanently_deleted_after": "Gelöschte Objekte werden nach {days, plural, one {# Tag} other {# Tagen}} endgültig gelöscht.", "type": "Typ", + "unable_to_change_pin_code": "PIN Code konnte nicht geändert werden", + "unable_to_setup_pin_code": "PIN Code konnte nicht festgelegt werden", "unarchive": "Entarchivieren", "unarchived_count": "{count, plural, other {# entarchiviert}}", "unfavorite": "Entfavorisieren", @@ -1828,6 +1847,7 @@ "untracked_files": "Unverfolgte Dateien", "untracked_files_decription": "Diese Dateien werden nicht von der Application getrackt. Sie können das Ergebnis fehlgeschlagener Verschiebungen, unterbrochener Uploads oder aufgrund eines Fehlers sein", "up_next": "Weiter", + "updated_at": "Aktualisiert", "updated_password": "Passwort aktualisiert", "upload": "Hochladen", "upload_concurrency": "Parallelität beim Hochladen", @@ -1840,15 +1860,18 @@ "upload_status_errors": "Fehler", "upload_status_uploaded": "Hochgeladen", "upload_success": "Hochladen erfolgreich. Aktualisiere die Seite, um neue hochgeladene Dateien zu sehen.", - "upload_to_immich": "Auf Immich hochladen ({})", + "upload_to_immich": "Auf Immich hochladen ({count})", "uploading": "Wird hochgeladen", "url": "URL", "usage": "Verwendung", "use_current_connection": "aktuelle Verbindung verwenden", "use_custom_date_range": "Stattdessen einen benutzerdefinierten Datumsbereich verwenden", "user": "Nutzer", + "user_has_been_deleted": "Dieser Benutzer wurde gelöscht.", "user_id": "Nutzer-ID", "user_liked": "{type, select, photo {Dieses Foto} video {Dieses Video} asset {Diese Datei} other {Dies}} gefällt {user}", + "user_pin_code_settings": "PIN Code", + "user_pin_code_settings_description": "Verwalte deinen PIN Code", "user_purchase_settings": "Kauf", "user_purchase_settings_description": "Kauf verwalten", "user_role_set": "{user} als {role} festlegen", diff --git a/i18n/el.json b/i18n/el.json index 305f34e7d0..7db47eac46 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -53,6 +53,7 @@ "confirm_email_below": "Για επιβεβαίωση, πληκτρολογήστε \"{email}\" παρακάτω", "confirm_reprocess_all_faces": "Είστε βέβαιοι ότι θέλετε να επεξεργαστείτε ξανά όλα τα πρόσωπα; Αυτό θα εκκαθαρίσει ακόμα και τα άτομα στα οποία έχετε ήδη ορίσει το όνομα.", "confirm_user_password_reset": "Είστε βέβαιοι ότι θέλετε να επαναφέρετε τον κωδικό πρόσβασης του χρήστη {user};", + "confirm_user_pin_code_reset": "Είστε βέβαιοι ότι θέλετε να επαναφέρετε τον κωδικό PIN του χρήστη {user};", "create_job": "Δημιουργία εργασίας", "cron_expression": "Σύνταξη Cron", "cron_expression_description": "Ορίστε το διάστημα σάρωσης χρησιμοποιώντας τη μορφή cron. Για περισσότερες πληροφορίες, ανατρέξτε π.χ. στο Crontab Guru", @@ -345,6 +346,7 @@ "user_delete_delay_settings_description": "Αριθμός ημερών μετά την αφαίρεση, για την οριστική διαγραφή του λογαριασμού και των αρχείων ενός χρήστη. Η εργασία διαγραφής χρηστών εκτελείται τα μεσάνυχτα, για να ελέγξει ποιοι χρήστες είναι έτοιμοι για διαγραφή. Οι αλλαγές σε αυτή τη ρύθμιση θα αξιολογηθούν κατά την επόμενη εκτέλεση.", "user_delete_immediately": "Ο λογαριασμός και τα αρχεία του/της {user} θα μπουν στην ουρά για οριστική διαγραφή, άμεσα.", "user_delete_immediately_checkbox": "Βάλε τον χρήστη και τα αρχεία του στην ουρά για άμεση διαγραφή", + "user_details": "Λεπτομέρειες χρήστη", "user_management": "Διαχείριση χρηστών", "user_password_has_been_reset": "Ο κωδικός πρόσβασης του χρήστη έχει επαναρυθμιστεί:", "user_password_reset_description": "Παρακαλώ παρέχετε τον προσωρινό κωδικό πρόσβασης στον χρήστη και ενημερώστε τον ότι θα πρέπει να τον αλλάξει, κατά την επόμενη σύνδεσή του.", @@ -366,7 +368,7 @@ "advanced": "Για προχωρημένους", "advanced_settings_enable_alternate_media_filter_subtitle": "Χρησιμοποιήστε αυτήν την επιλογή για να φιλτράρετε τα μέσα ενημέρωσης κατά τον συγχρονισμό με βάση εναλλακτικά κριτήρια. Δοκιμάστε αυτή τη δυνατότητα μόνο αν έχετε προβλήματα με την εφαρμογή που εντοπίζει όλα τα άλμπουμ.", "advanced_settings_enable_alternate_media_filter_title": "[ΠΕΙΡΑΜΑΤΙΚΟ] Χρήση εναλλακτικού φίλτρου συγχρονισμού άλμπουμ συσκευής", - "advanced_settings_log_level_title": "Επίπεδο σύνδεσης: {}", + "advanced_settings_log_level_title": "Επίπεδο σύνδεσης: {level}", "advanced_settings_prefer_remote_subtitle": "Μερικές συσκευές αργούν πολύ να φορτώσουν μικρογραφίες από αρχεία στη συσκευή. Ενεργοποιήστε αυτήν τη ρύθμιση για να φορτώνονται αντί αυτού απομακρυσμένες εικόνες.", "advanced_settings_prefer_remote_title": "Προτίμηση απομακρυσμένων εικόνων", "advanced_settings_proxy_headers_subtitle": "Καθορισμός κεφαλίδων διακομιστή μεσολάβησης που το Immich πρέπει να στέλνει με κάθε αίτημα δικτύου", @@ -397,9 +399,9 @@ "album_remove_user_confirmation": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε τον/την {user};", "album_share_no_users": "Φαίνεται ότι έχετε κοινοποιήσει αυτό το άλμπουμ σε όλους τους χρήστες ή δεν έχετε χρήστες για να το κοινοποιήσετε.", "album_thumbnail_card_item": "1 αντικείμενο", - "album_thumbnail_card_items": "{} αντικείμενα", + "album_thumbnail_card_items": "{count} αντικείμενα", "album_thumbnail_card_shared": " Κοινόχρηστο", - "album_thumbnail_shared_by": "Κοινοποιημένο από {}", + "album_thumbnail_shared_by": "Κοινοποιημένο από {user}", "album_updated": "Το άλμπουμ, ενημερώθηκε", "album_updated_setting_description": "Λάβετε ειδοποίηση μέσω email όταν ένα κοινόχρηστο άλμπουμ έχει νέα αρχεία", "album_user_left": "Αποχωρήσατε από το {album}", @@ -437,7 +439,7 @@ "archive": "Αρχείο", "archive_or_unarchive_photo": "Αρχειοθέτηση ή αποαρχειοθέτηση φωτογραφίας", "archive_page_no_archived_assets": "Δε βρέθηκαν αρχειοθετημένα στοιχεία", - "archive_page_title": "Αρχείο ({})", + "archive_page_title": "Αρχείο ({count})", "archive_size": "Μέγεθος Αρχείου", "archive_size_description": "Ρυθμίστε το μέγεθος του αρχείου για λήψεις (σε GiB)", "archived": "Αρχείο", @@ -474,15 +476,15 @@ "assets_added_to_album_count": "Προστέθηκε {count, plural, one {# αρχείο} other {# αρχεία}} στο άλμπουμ", "assets_added_to_name_count": "Προστέθηκε {count, plural, one {# αρχείο} other {# αρχεία}} στο {hasName, select, true {{name}} other {νέο άλμπουμ}}", "assets_count": "{count, plural, one {# αρχείο} other {# αρχεία}}", - "assets_deleted_permanently": "{} τα στοιχεία διαγράφηκαν οριστικά", - "assets_deleted_permanently_from_server": "{} τα στοιχεία διαγράφηκαν οριστικά από το διακομιστή Immich", + "assets_deleted_permanently": "{count} τα στοιχεία διαγράφηκαν οριστικά", + "assets_deleted_permanently_from_server": "{count} στοιχεία διαγράφηκαν οριστικά από το διακομιστή Immich", "assets_moved_to_trash_count": "Μετακινήθηκαν {count, plural, one {# αρχείο} other {# αρχεία}} στον κάδο απορριμμάτων", "assets_permanently_deleted_count": "Διαγράφηκαν μόνιμα {count, plural, one {# αρχείο} other {# αρχεία}}", "assets_removed_count": "Αφαιρέθηκαν {count, plural, one {# αρχείο} other {# αρχεία}}", - "assets_removed_permanently_from_device": "{} τα στοιχεία καταργήθηκαν οριστικά από τη συσκευή σας", + "assets_removed_permanently_from_device": "{count} στοιχεία καταργήθηκαν οριστικά από τη συσκευή σας", "assets_restore_confirmation": "Είστε βέβαιοι ότι θέλετε να επαναφέρετε όλα τα στοιχεία που βρίσκονται στον κάδο απορριμμάτων; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί! Λάβετε υπόψη ότι δεν θα είναι δυνατή η επαναφορά στοιχείων εκτός σύνδεσης.", "assets_restored_count": "Έγινε επαναφορά {count, plural, one {# στοιχείου} other {# στοιχείων}}", - "assets_restored_successfully": "{} τα στοιχεία αποκαταστάθηκαν με επιτυχία", + "assets_restored_successfully": "{count} στοιχεία αποκαταστάθηκαν με επιτυχία", "assets_trashed": "{} στοιχεία μεταφέρθηκαν στον κάδο απορριμμάτων", "assets_trashed_count": "Μετακιν. στον κάδο απορριμάτων {count, plural, one {# στοιχείο} other {# στοιχεία}}", "assets_trashed_from_server": "{} στοιχεία μεταφέρθηκαν στον κάδο απορριμμάτων από το διακομιστή Immich", @@ -1385,7 +1387,7 @@ "public_share": "Δημόσια Κοινή Χρήση", "purchase_account_info": "Υποστηρικτής", "purchase_activated_subtitle": "Σας ευχαριστούμε για την υποστήριξη του Immich και λογισμικών ανοιχτού κώδικα", - "purchase_activated_time": "Ενεργοποιήθηκε στις {date, date}", + "purchase_activated_time": "Ενεργοποιήθηκε στις {date}", "purchase_activated_title": "Το κλειδί σας ενεργοποιήθηκε με επιτυχία", "purchase_button_activate": "Ενεργοποίηση", "purchase_button_buy": "Αγορά", diff --git a/i18n/en.json b/i18n/en.json index b9331df5db..66b6e3afe0 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1228,7 +1228,7 @@ "memories_start_over": "Start Over", "memories_swipe_to_close": "Swipe up to close", "memories_year_ago": "A year ago", - "memories_years_ago": "{years} years ago", + "memories_years_ago": "{years, plural, other {# years}} ago", "memory": "Memory", "memory_lane_title": "Memory Lane {title}", "menu": "Menu", @@ -1424,7 +1424,7 @@ "public_share": "Public Share", "purchase_account_info": "Supporter", "purchase_activated_subtitle": "Thank you for supporting Immich and open-source software", - "purchase_activated_time": "Activated on {date, date}", + "purchase_activated_time": "Activated on {date}", "purchase_activated_title": "Your key has been successfully activated", "purchase_button_activate": "Activate", "purchase_button_buy": "Buy", diff --git a/i18n/es.json b/i18n/es.json index 1c46646c0d..7cebb2a4f0 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -53,6 +53,7 @@ "confirm_email_below": "Para confirmar, escribe \"{email}\" a continuación", "confirm_reprocess_all_faces": "¿Estás seguro de que deseas reprocesar todas las caras? Esto borrará a todas las personas que nombraste.", "confirm_user_password_reset": "¿Estás seguro de que quieres restablecer la contraseña de {user}?", + "confirm_user_pin_code_reset": "Está seguro de que quiere restablecer el PIN de {user}?", "create_job": "Crear trabajo", "cron_expression": "Expresión CRON", "cron_expression_description": "Establece el intervalo de escaneo utilizando el formato CRON. Para más información puedes consultar, por ejemplo, Crontab Guru", @@ -192,6 +193,7 @@ "oauth_auto_register": "Registro automático", "oauth_auto_register_description": "Registre automáticamente nuevos usuarios después de iniciar sesión con OAuth", "oauth_button_text": "Texto del botón", + "oauth_client_secret_description": "Requerido si PKCE (Prueba de clave para el intercambio de códigos) no es compatible con el proveedor OAuth", "oauth_enable_description": "Iniciar sesión con OAuth", "oauth_mobile_redirect_uri": "URI de redireccionamiento móvil", "oauth_mobile_redirect_uri_override": "Sobreescribir URI de redirección móvil", @@ -205,6 +207,8 @@ "oauth_storage_quota_claim_description": "Establezca automáticamente la cuota de almacenamiento del usuario al valor de esta solicitud.", "oauth_storage_quota_default": "Cuota de almacenamiento predeterminada (GiB)", "oauth_storage_quota_default_description": "Cuota en GiB que se utilizará cuando no se proporcione ninguna por defecto (ingrese 0 para una cuota ilimitada).", + "oauth_timeout": "Expiración de solicitud", + "oauth_timeout_description": "Tiempo de espera de solicitudes en milisegundos", "offline_paths": "Rutas sin conexión", "offline_paths_description": "Estos resultados pueden deberse al eliminar manualmente archivos que no son parte de una biblioteca externa.", "password_enable_description": "Iniciar sesión con correo electrónico y contraseña", @@ -345,6 +349,7 @@ "user_delete_delay_settings_description": "Número de días después de la eliminación para eliminar permanentemente la cuenta y los activos de un usuario. El trabajo de eliminación de usuarios se ejecuta a medianoche para comprobar si hay usuarios que estén listos para su eliminación. Los cambios a esta configuración se evaluarán en la próxima ejecución.", "user_delete_immediately": "La cuenta {user} y los archivos se pondrán en cola para su eliminación permanente inmediatamente.", "user_delete_immediately_checkbox": "Poner en cola la eliminación inmediata de usuarios y elementos", + "user_details": "Detalles de Usuario", "user_management": "Gestión de usuarios", "user_password_has_been_reset": "La contraseña del usuario ha sido restablecida:", "user_password_reset_description": "Proporcione una contraseña temporal al usuario e infórmele que deberá cambiar la contraseña en su próximo inicio de sesión.", @@ -366,7 +371,7 @@ "advanced": "Avanzada", "advanced_settings_enable_alternate_media_filter_subtitle": "Usa esta opción para filtrar medios durante la sincronización según criterios alternativos. Intenta esto solo si tienes problemas con que la aplicación detecte todos los álbumes.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Usar filtro alternativo de sincronización de álbumes del dispositivo", - "advanced_settings_log_level_title": "Nivel de registro: {}", + "advanced_settings_log_level_title": "Nivel de registro: {level}", "advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de los elementos encontrados en el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.", "advanced_settings_prefer_remote_title": "Preferir imágenes remotas", "advanced_settings_proxy_headers_subtitle": "Configura headers HTTP que Immich incluirá en cada petición de red", @@ -397,9 +402,9 @@ "album_remove_user_confirmation": "¿Estás seguro de que quieres eliminar a {user}?", "album_share_no_users": "Parece que has compartido este álbum con todos los usuarios o no tienes ningún usuario con quien compartirlo.", "album_thumbnail_card_item": "1 elemento", - "album_thumbnail_card_items": "{} elementos", + "album_thumbnail_card_items": "{count} elementos", "album_thumbnail_card_shared": " · Compartido", - "album_thumbnail_shared_by": "Compartido por {}", + "album_thumbnail_shared_by": "Compartido por {user}", "album_updated": "Album actualizado", "album_updated_setting_description": "Reciba una notificación por correo electrónico cuando un álbum compartido tenga nuevos archivos", "album_user_left": "Salida {album}", @@ -437,7 +442,7 @@ "archive": "Archivo", "archive_or_unarchive_photo": "Archivar o restaurar foto", "archive_page_no_archived_assets": "No se encontraron elementos archivados", - "archive_page_title": "Archivo ({})", + "archive_page_title": "Archivo ({count})", "archive_size": "Tamaño del archivo", "archive_size_description": "Configure el tamaño del archivo para descargas (en GB)", "archived": "Archivado", @@ -474,18 +479,18 @@ "assets_added_to_album_count": "Añadido {count, plural, one {# asset} other {# assets}} al álbum", "assets_added_to_name_count": "Añadido {count, plural, one {# asset} other {# assets}} a {hasName, select, true {{name}} other {new album}}", "assets_count": "{count, plural, one {# activo} other {# activos}}", - "assets_deleted_permanently": "{} elemento(s) eliminado(s) permanentemente", - "assets_deleted_permanently_from_server": "{} recurso(s) eliminado(s) de forma permanente del servidor de Immich", + "assets_deleted_permanently": "{count} elemento(s) eliminado(s) permanentemente", + "assets_deleted_permanently_from_server": "{count} recurso(s) eliminado(s) de forma permanente del servidor de Immich", "assets_moved_to_trash_count": "{count, plural, one {# elemento movido} other {# elementos movidos}} a la papelera", "assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# elemento} other {# elementos}}", "assets_removed_count": "Eliminado {count, plural, one {# elemento} other {# elementos}}", - "assets_removed_permanently_from_device": "{} elemento(s) eliminado(s) permanentemente de su dispositivo", + "assets_removed_permanently_from_device": "{count} elemento(s) eliminado(s) permanentemente de su dispositivo", "assets_restore_confirmation": "¿Estás seguro de que quieres restaurar todos tus activos eliminados? ¡No puede deshacer esta acción! Tenga en cuenta que los archivos sin conexión no se pueden restaurar de esta manera.", "assets_restored_count": "Restaurado {count, plural, one {# elemento} other {# elementos}}", - "assets_restored_successfully": "{} elemento(s) restaurado(s) exitosamente", - "assets_trashed": "{} elemento(s) eliminado(s)", + "assets_restored_successfully": "{count} elemento(s) restaurado(s) exitosamente", + "assets_trashed": "{count} elemento(s) eliminado(s)", "assets_trashed_count": "Borrado {count, plural, one {# elemento} other {# elementos}}", - "assets_trashed_from_server": "{} recurso(s) enviado(s) a la papelera desde el servidor de Immich", + "assets_trashed_from_server": "{count} recurso(s) enviado(s) a la papelera desde el servidor de Immich", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} ya forma parte del álbum", "authorized_devices": "Dispositivos Autorizados", "automatic_endpoint_switching_subtitle": "Conectarse localmente a través de la Wi-Fi designada cuando esté disponible y usar conexiones alternativas en otros lugares", @@ -494,7 +499,7 @@ "back_close_deselect": "Atrás, cerrar o anular la selección", "background_location_permission": "Permiso de ubicación en segundo plano", "background_location_permission_content": "Para poder cambiar de red mientras se ejecuta en segundo plano, Immich debe tener *siempre* acceso a la ubicación precisa para que la aplicación pueda leer el nombre de la red Wi-Fi", - "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})", + "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({count})", "backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir", "backup_album_selection_page_assets_scatter": "Los elementos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.", "backup_album_selection_page_select_albums": "Seleccionar Álbumes", @@ -503,11 +508,11 @@ "backup_all": "Todos", "backup_background_service_backup_failed_message": "Error al copiar elementos. Reintentando…", "backup_background_service_connection_failed_message": "Error al conectar con el servidor. Reintentando…", - "backup_background_service_current_upload_notification": "Subiendo {}", + "backup_background_service_current_upload_notification": "Subiendo {filename}", "backup_background_service_default_notification": "Comprobando nuevos elementos…", "backup_background_service_error_title": "Error de copia de seguridad", "backup_background_service_in_progress_notification": "Creando copia de seguridad de tus elementos…", - "backup_background_service_upload_failure_notification": "Error al subir {}", + "backup_background_service_upload_failure_notification": "Error al subir {filename}", "backup_controller_page_albums": "Álbumes de copia de seguridad", "backup_controller_page_background_app_refresh_disabled_content": "Activa la actualización en segundo plano de la aplicación en Configuración > General > Actualización en segundo plano para usar la copia de seguridad en segundo plano.", "backup_controller_page_background_app_refresh_disabled_title": "Actualización en segundo plano desactivada", @@ -518,7 +523,7 @@ "backup_controller_page_background_battery_info_title": "Optimizaciones de batería", "backup_controller_page_background_charging": "Solo mientras se carga", "backup_controller_page_background_configure_error": "Error al configurar el servicio en segundo plano", - "backup_controller_page_background_delay": "Retrasar la copia de seguridad de los nuevos elementos: {}", + "backup_controller_page_background_delay": "Retrasar la copia de seguridad de los nuevos elementos: {duration}", "backup_controller_page_background_description": "Activa el servicio en segundo plano para copiar automáticamente cualquier nuevos elementos sin necesidad de abrir la aplicación", "backup_controller_page_background_is_off": "La copia de seguridad en segundo plano automática está desactivada", "backup_controller_page_background_is_on": "La copia de seguridad en segundo plano automática está activada", @@ -528,12 +533,12 @@ "backup_controller_page_backup": "Copia de Seguridad", "backup_controller_page_backup_selected": "Seleccionado: ", "backup_controller_page_backup_sub": "Fotos y videos respaldados", - "backup_controller_page_created": "Creado el: {}", + "backup_controller_page_created": "Creado el: {date}", "backup_controller_page_desc_backup": "Active la copia de seguridad para subir automáticamente los nuevos elementos al servidor cuando se abre la aplicación.", "backup_controller_page_excluded": "Excluido: ", - "backup_controller_page_failed": "Fallidos ({})", - "backup_controller_page_filename": "Nombre del archivo: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Fallidos ({count})", + "backup_controller_page_filename": "Nombre del archivo: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Información de la Copia de Seguridad", "backup_controller_page_none_selected": "Ninguno seleccionado", "backup_controller_page_remainder": "Restante", @@ -542,7 +547,7 @@ "backup_controller_page_start_backup": "Iniciar copia de seguridad", "backup_controller_page_status_off": "La copia de seguridad está desactivada", "backup_controller_page_status_on": "La copia de seguridad está activada", - "backup_controller_page_storage_format": "{} de {} usadas", + "backup_controller_page_storage_format": "{used} de {total} usadas", "backup_controller_page_to_backup": "Álbumes a respaldar", "backup_controller_page_total_sub": "Todas las fotos y vídeos únicos de los álbumes seleccionados", "backup_controller_page_turn_off": "Apagar la copia de seguridad", @@ -567,21 +572,21 @@ "bulk_keep_duplicates_confirmation": "¿Estas seguro de que desea mantener {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto resolverá todos los grupos duplicados sin borrar nada.", "bulk_trash_duplicates_confirmation": "¿Estas seguro de que desea eliminar masivamente {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto mantendrá el archivo más grande de cada grupo y eliminará todos los demás duplicados.", "buy": "Comprar Immich", - "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} elementos)", + "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({count} elementos)", "cache_settings_clear_cache_button": "Borrar caché", "cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.", "cache_settings_duplicated_assets_clear_button": "LIMPIAR", "cache_settings_duplicated_assets_subtitle": "Fotos y vídeos en la lista negra de la app", - "cache_settings_duplicated_assets_title": "Elementos duplicados ({})", - "cache_settings_image_cache_size": "Tamaño de la caché de imágenes ({} elementos)", + "cache_settings_duplicated_assets_title": "Elementos duplicados ({count})", + "cache_settings_image_cache_size": "Tamaño de la caché de imágenes ({count} elementos)", "cache_settings_statistics_album": "Miniaturas de la biblioteca", - "cache_settings_statistics_assets": "{} elementos ({})", + "cache_settings_statistics_assets": "{count} elementos ({size})", "cache_settings_statistics_full": "Imágenes completas", "cache_settings_statistics_shared": "Miniaturas de álbumes compartidos", "cache_settings_statistics_thumbnail": "Miniaturas", "cache_settings_statistics_title": "Uso de caché", "cache_settings_subtitle": "Controla el comportamiento del almacenamiento en caché de la aplicación móvil Immich", - "cache_settings_thumbnail_size": "Tamaño de la caché de miniaturas ({} elementos)", + "cache_settings_thumbnail_size": "Tamaño de la caché de miniaturas ({count} elementos)", "cache_settings_tile_subtitle": "Controla el comportamiento del almacenamiento local", "cache_settings_tile_title": "Almacenamiento local", "cache_settings_title": "Configuración de la caché", @@ -607,6 +612,7 @@ "change_password_form_new_password": "Nueva Contraseña", "change_password_form_password_mismatch": "Las contraseñas no coinciden", "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña", + "change_pin_code": "Cambiar PIN", "change_your_password": "Cambia tu contraseña", "changed_visibility_successfully": "Visibilidad cambiada correctamente", "check_all": "Comprobar todo", @@ -647,11 +653,12 @@ "confirm_delete_face": "¿Estás seguro que deseas eliminar la cara de {name} del archivo?", "confirm_delete_shared_link": "¿Estás seguro de que deseas eliminar este enlace compartido?", "confirm_keep_this_delete_others": "Todos los demás activos de la pila se eliminarán excepto este activo. ¿Está seguro de que quiere continuar?", + "confirm_new_pin_code": "Confirmar nuevo pin", "confirm_password": "Confirmar contraseña", "contain": "Incluido", "context": "Contexto", "continue": "Continuar", - "control_bottom_app_bar_album_info_shared": "{} elementos · Compartidos", + "control_bottom_app_bar_album_info_shared": "{count} elementos · Compartidos", "control_bottom_app_bar_create_new_album": "Crear nuevo álbum", "control_bottom_app_bar_delete_from_immich": "Borrar de Immich", "control_bottom_app_bar_delete_from_local": "Borrar del dispositivo", @@ -689,9 +696,11 @@ "create_tag_description": "Crear una nueva etiqueta. Para las etiquetas anidadas, ingresa la ruta completa de la etiqueta, incluidas las barras diagonales.", "create_user": "Crear usuario", "created": "Creado", + "created_at": "Creado", "crop": "Recortar", "curated_object_page_title": "Objetos", "current_device": "Dispositivo actual", + "current_pin_code": "PIN actual", "current_server_address": "Dirección actual del servidor", "custom_locale": "Configuración regional personalizada", "custom_locale_description": "Formatear fechas y números según el idioma y la región", @@ -739,7 +748,7 @@ "description": "Descripción", "description_input_hint_text": "Agregar descripción...", "description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles", - "details": "DETALLES", + "details": "Detalles", "direction": "Dirección", "disabled": "Deshabilitado", "disallow_edits": "Bloquear edición", @@ -760,7 +769,7 @@ "download_enqueue": "Descarga en cola", "download_error": "Error al descargar", "download_failed": "Descarga fallida", - "download_filename": "archivo: {}", + "download_filename": "archivo: {filename}", "download_finished": "Descarga completada", "download_include_embedded_motion_videos": "Vídeos incrustados", "download_include_embedded_motion_videos_description": "Incluir vídeos incrustados en fotografías en movimiento como un archivo separado", @@ -804,6 +813,7 @@ "editor_crop_tool_h2_aspect_ratios": "Proporciones del aspecto", "editor_crop_tool_h2_rotation": "Rotación", "email": "Correo", + "email_notifications": "Notificaciones por correo electrónico", "empty_folder": "Esta carpeta está vacía", "empty_trash": "Vaciar papelera", "empty_trash_confirmation": "¿Estás seguro de que quieres vaciar la papelera? Esto eliminará permanentemente todos los archivos de la basura de Immich.\n¡No puedes deshacer esta acción!", @@ -816,7 +826,7 @@ "error_change_sort_album": "No se pudo cambiar el orden de visualización del álbum", "error_delete_face": "Error al eliminar la cara del archivo", "error_loading_image": "Error al cargar la imagen", - "error_saving_image": "Error: {}", + "error_saving_image": "Error: {error}", "error_title": "Error: algo salió mal", "errors": { "cannot_navigate_next_asset": "No puedes navegar al siguiente archivo", @@ -919,6 +929,7 @@ "unable_to_remove_reaction": "No se puede eliminar la reacción", "unable_to_repair_items": "No se pueden reparar los items", "unable_to_reset_password": "No se puede restablecer la contraseña", + "unable_to_reset_pin_code": "No se ha podido restablecer el PIN", "unable_to_resolve_duplicate": "No se resolver duplicado", "unable_to_restore_assets": "No se pueden restaurar los archivos", "unable_to_restore_trash": "No se puede restaurar la papelera", @@ -952,10 +963,10 @@ "exif_bottom_sheet_location": "UBICACIÓN", "exif_bottom_sheet_people": "PERSONAS", "exif_bottom_sheet_person_add_person": "Añadir nombre", - "exif_bottom_sheet_person_age": "Antigüedad {}", - "exif_bottom_sheet_person_age_months": "Antigüedad {} meses", - "exif_bottom_sheet_person_age_year_months": "Antigüedad 1 año, {} meses", - "exif_bottom_sheet_person_age_years": "Antigüedad {}", + "exif_bottom_sheet_person_age": "Edad {age}", + "exif_bottom_sheet_person_age_months": "Edad {months} meses", + "exif_bottom_sheet_person_age_year_months": "Edad 1 año, {months} meses", + "exif_bottom_sheet_person_age_years": "Edad {years}", "exit_slideshow": "Salir de la presentación", "expand_all": "Expandir todo", "experimental_settings_new_asset_list_subtitle": "Trabajo en progreso", @@ -1045,6 +1056,7 @@ "home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultáneamente, omitiendo", "host": "Host", "hour": "Hora", + "id": "ID", "ignore_icloud_photos": "Ignorar fotos de iCloud", "ignore_icloud_photos_description": "Las fotos almacenadas en iCloud no se subirán a Immich", "image": "Imagen", @@ -1170,8 +1182,8 @@ "manage_your_devices": "Administre sus dispositivos conectados", "manage_your_oauth_connection": "Administra tu conexión OAuth", "map": "Mapa", - "map_assets_in_bound": "{} foto", - "map_assets_in_bounds": "{} fotos", + "map_assets_in_bound": "{count} foto", + "map_assets_in_bounds": "{count} fotos", "map_cannot_get_user_location": "No se pudo obtener la posición del usuario", "map_location_dialog_yes": "Sí", "map_location_picker_page_use_location": "Usar esta ubicación", @@ -1185,9 +1197,9 @@ "map_settings": "Ajustes mapa", "map_settings_dark_mode": "Modo oscuro", "map_settings_date_range_option_day": "Últimas 24 horas", - "map_settings_date_range_option_days": "Últimos {} días", + "map_settings_date_range_option_days": "Últimos {days} días", "map_settings_date_range_option_year": "Último año", - "map_settings_date_range_option_years": "Últimos {} años", + "map_settings_date_range_option_years": "Últimos {years} años", "map_settings_dialog_title": "Ajustes mapa", "map_settings_include_show_archived": "Incluir archivados", "map_settings_include_show_partners": "Incluir Parejas", @@ -1206,7 +1218,7 @@ "memories_start_over": "Empezar de nuevo", "memories_swipe_to_close": "Desliza para cerrar", "memories_year_ago": "Hace un año", - "memories_years_ago": "Hace {} años", + "memories_years_ago": "Hace {years} años", "memory": "Recuerdo", "memory_lane_title": "Baúl de los recuerdos {title}", "menu": "Menú", @@ -1223,6 +1235,8 @@ "month": "Mes", "monthly_title_text_date_format": "MMMM y", "more": "Mas", + "moved_to_archive": "Movido(s) {count, plural, one {# recurso} other {# recursos}} a archivo", + "moved_to_library": "Movido(s) {count, plural, one {# recurso} other {# recursos}} a biblioteca", "moved_to_trash": "Movido a la papelera", "multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha del archivo(s) de solo lectura, omitiendo", "multiselect_grid_edit_gps_err_read_only": "No se puede editar la ubicación de activos de solo lectura, omitiendo", @@ -1232,11 +1246,12 @@ "name_or_nickname": "Nombre o apodo", "networking_settings": "Red", "networking_subtitle": "Configuraciones de acceso por URL al servidor", - "never": "nunca", + "never": "Nunca", "new_album": "Nuevo álbum", "new_api_key": "Nueva clave API", "new_password": "Nueva contraseña", "new_person": "Nueva persona", + "new_pin_code": "Nuevo PIN", "new_user_created": "Nuevo usuario creado", "new_version_available": "NUEVA VERSIÓN DISPONIBLE", "newest_first": "El más reciente primero", @@ -1256,6 +1271,7 @@ "no_libraries_message": "Crea una biblioteca externa para ver tus fotos y vídeos", "no_name": "Sin nombre", "no_notifications": "Ninguna notificación", + "no_people_found": "No se encontraron personas coincidentes", "no_places": "Sin lugares", "no_results": "Sin resultados", "no_results_description": "Pruebe con un sinónimo o una palabra clave más general", @@ -1310,7 +1326,7 @@ "partner_page_partner_add_failed": "No se pudo añadir el socio", "partner_page_select_partner": "Seleccionar compañero", "partner_page_shared_to_title": "Compartido con", - "partner_page_stop_sharing_content": "{} ya no podrá acceder a tus fotos.", + "partner_page_stop_sharing_content": "{partner} ya no podrá acceder a tus fotos.", "partner_sharing": "Compartir con invitados", "partners": "Invitados", "password": "Contraseña", @@ -1356,6 +1372,9 @@ "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", "photos_from_previous_years": "Fotos de años anteriores", "pick_a_location": "Elige una ubicación", + "pin_code_changed_successfully": "PIN cambiado exitosamente", + "pin_code_reset_successfully": "PIN restablecido exitosamente", + "pin_code_setup_successfully": "PIN establecido exitosamente", "place": "Lugar", "places": "Lugares", "places_count": "{count, plural, one {{count, number} Lugar} other {{count, number} Lugares}}", @@ -1373,6 +1392,7 @@ "previous_or_next_photo": "Foto anterior o siguiente", "primary": "Básico", "privacy": "Privacidad", + "profile": "Perfil", "profile_drawer_app_logs": "Registros", "profile_drawer_client_out_of_date_major": "La app está desactualizada. Por favor actualiza a la última versión principal.", "profile_drawer_client_out_of_date_minor": "La app está desactualizada. Por favor actualiza a la última versión menor.", @@ -1386,7 +1406,7 @@ "public_share": "Compartir públicamente", "purchase_account_info": "Seguidor", "purchase_activated_subtitle": "Gracias por apoyar a Immich y al software de código abierto", - "purchase_activated_time": "Activado el {date, date}", + "purchase_activated_time": "Activado el {date}", "purchase_activated_title": "Su clave ha sido activada correctamente", "purchase_button_activate": "Activar", "purchase_button_buy": "Comprar", @@ -1475,6 +1495,7 @@ "reset": "Reiniciar", "reset_password": "Restablecer la contraseña", "reset_people_visibility": "Restablecer la visibilidad de las personas", + "reset_pin_code": "Restablecer PIN", "reset_to_default": "Restablecer los valores predeterminados", "resolve_duplicates": "Resolver duplicados", "resolved_all_duplicates": "Todos los duplicados resueltos", @@ -1567,6 +1588,7 @@ "select_keep_all": "Conservar todo", "select_library_owner": "Seleccionar propietario de la biblioteca", "select_new_face": "Seleccionar nueva cara", + "select_person_to_tag": "Elija una persona a etiquetar", "select_photos": "Seleccionar Fotos", "select_trash_all": "Seleccionar eliminar todo", "select_user_for_sharing_page_err_album": "Fallo al crear el álbum", @@ -1597,12 +1619,12 @@ "setting_languages_apply": "Aplicar", "setting_languages_subtitle": "Cambia el idioma de la aplicación", "setting_languages_title": "Idiomas", - "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}", - "setting_notifications_notify_hours": "{} horas", + "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {duration}", + "setting_notifications_notify_hours": "{count} horas", "setting_notifications_notify_immediately": "inmediatamente", - "setting_notifications_notify_minutes": "{} minutos", + "setting_notifications_notify_minutes": "{count} minutos", "setting_notifications_notify_never": "nunca", - "setting_notifications_notify_seconds": "{} segundos", + "setting_notifications_notify_seconds": "{count} segundos", "setting_notifications_single_progress_subtitle": "Información detallada del progreso de subida de cada archivo", "setting_notifications_single_progress_title": "Mostrar progreso detallado de copia de seguridad en segundo plano", "setting_notifications_subtitle": "Ajusta tus preferencias de notificación", @@ -1614,9 +1636,10 @@ "settings": "Ajustes", "settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste", "settings_saved": "Ajustes guardados", + "setup_pin_code": "Establecer un PIN", "share": "Compartir", "share_add_photos": "Agregar fotos", - "share_assets_selected": "{} seleccionado(s)", + "share_assets_selected": "{count} seleccionado(s)", "share_dialog_preparing": "Preparando...", "shared": "Compartido", "shared_album_activities_input_disable": "Los comentarios están deshabilitados", @@ -1630,32 +1653,32 @@ "shared_by_user": "Compartido por {user}", "shared_by_you": "Compartido por ti", "shared_from_partner": "Fotos de {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Cargado(s)", + "shared_intent_upload_button_progress_text": "{current} / {total} Cargado(s)", "shared_link_app_bar_title": "Enlaces compartidos", "shared_link_clipboard_copied_massage": "Copiado al portapapeles", - "shared_link_clipboard_text": "Enlace: {}\nContraseña: {}", + "shared_link_clipboard_text": "Enlace: {link}\nContraseña: {password}", "shared_link_create_error": "Error creando el enlace compartido", "shared_link_edit_description_hint": "Introduce la descripción del enlace", "shared_link_edit_expire_after_option_day": "1 día", - "shared_link_edit_expire_after_option_days": "{} días", + "shared_link_edit_expire_after_option_days": "{count} días", "shared_link_edit_expire_after_option_hour": "1 hora", - "shared_link_edit_expire_after_option_hours": "{} horas", + "shared_link_edit_expire_after_option_hours": "{count} horas", "shared_link_edit_expire_after_option_minute": "1 minuto", - "shared_link_edit_expire_after_option_minutes": "{} minutos", - "shared_link_edit_expire_after_option_months": "{} meses", - "shared_link_edit_expire_after_option_year": "{} año", + "shared_link_edit_expire_after_option_minutes": "{count} minutos", + "shared_link_edit_expire_after_option_months": "{count} meses", + "shared_link_edit_expire_after_option_year": "{count} año", "shared_link_edit_password_hint": "Introduce la contraseña del enlace", "shared_link_edit_submit_button": "Actualizar enlace", "shared_link_error_server_url_fetch": "No se puede adquirir la URL del servidor", - "shared_link_expires_day": "Caduca en {} día", - "shared_link_expires_days": "Caduca en {} días", - "shared_link_expires_hour": "Caduca en {} hora", - "shared_link_expires_hours": "Caduca en {} horas", - "shared_link_expires_minute": "Caduca en {} minuto", - "shared_link_expires_minutes": "Caduca en {} minutos", + "shared_link_expires_day": "Caduca en {count} día", + "shared_link_expires_days": "Caduca en {count} días", + "shared_link_expires_hour": "Caduca en {count} hora", + "shared_link_expires_hours": "Caduca en {count} horas", + "shared_link_expires_minute": "Caduca en {count} minuto", + "shared_link_expires_minutes": "Caduca en {count} minutos", "shared_link_expires_never": "Caduca ∞", - "shared_link_expires_second": "Caduca en {} segundo", - "shared_link_expires_seconds": "Caduca en {} segundos", + "shared_link_expires_second": "Caduca en {count} segundo", + "shared_link_expires_seconds": "Caduca en {count} segundos", "shared_link_individual_shared": "Compartido individualmente", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Administrar enlaces compartidos", @@ -1730,6 +1753,7 @@ "stop_sharing_photos_with_user": "Deja de compartir tus fotos con este usuario", "storage": "Espacio de almacenamiento", "storage_label": "Etiqueta de almacenamiento", + "storage_quota": "Cuota de Almacenamiento", "storage_usage": "{used} de {available} en uso", "submit": "Enviar", "suggestions": "Sugerencias", @@ -1756,7 +1780,7 @@ "theme_selection": "Selección de tema", "theme_selection_description": "Establece el tema automáticamente como \"claro\" u \"oscuro\" según las preferencias del sistema/navegador", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamiento en las miniaturas de los archivos", - "theme_setting_asset_list_tiles_per_row_title": "Número de elementos por fila ({})", + "theme_setting_asset_list_tiles_per_row_title": "Número de elementos por fila ({count})", "theme_setting_colorful_interface_subtitle": "Aplicar el color primario a las superficies de fondo.", "theme_setting_colorful_interface_title": "Color de Interfaz", "theme_setting_image_viewer_quality_subtitle": "Ajustar la calidad del visor de detalles de imágenes", @@ -1791,13 +1815,15 @@ "trash_no_results_message": "Las fotos y videos que se envíen a la papelera aparecerán aquí.", "trash_page_delete_all": "Eliminar todos", "trash_page_empty_trash_dialog_content": "¿Está seguro que quiere eliminar los elementos? Estos elementos serán eliminados de Immich permanentemente", - "trash_page_info": "Los archivos en la papelera serán eliminados automáticamente de forma permanente después de {} días", + "trash_page_info": "Los archivos en la papelera serán eliminados automáticamente de forma permanente después de {days} días", "trash_page_no_assets": "No hay elementos en la papelera", "trash_page_restore_all": "Restaurar todos", "trash_page_select_assets_btn": "Seleccionar elementos", - "trash_page_title": "Papelera ({})", + "trash_page_title": "Papelera ({count})", "trashed_items_will_be_permanently_deleted_after": "Los elementos en la papelera serán eliminados permanentemente tras {days, plural, one {# día} other {# días}}.", "type": "Tipo", + "unable_to_change_pin_code": "No se ha podido cambiar el PIN", + "unable_to_setup_pin_code": "No se ha podido establecer el PIN", "unarchive": "Desarchivar", "unarchived_count": "{count, plural, one {# No archivado} other {# No archivados}}", "unfavorite": "Retirar favorito", @@ -1821,6 +1847,7 @@ "untracked_files": "Archivos no monitorizados", "untracked_files_decription": "Estos archivos no están siendo monitorizados por la aplicación. Es posible que sean resultado de errores al moverlos, subidas interrumpidas o por un fallo de la aplicación", "up_next": "A continuación", + "updated_at": "Actualizado", "updated_password": "Contraseña actualizada", "upload": "Subir", "upload_concurrency": "Subidas simultáneas", @@ -1833,7 +1860,7 @@ "upload_status_errors": "Errores", "upload_status_uploaded": "Subido", "upload_success": "Subida realizada correctamente, actualice la página para ver los nuevos recursos de subida.", - "upload_to_immich": "Subir a Immich ({})", + "upload_to_immich": "Subir a Immich ({count})", "uploading": "Subiendo", "url": "URL", "usage": "Uso", @@ -1842,6 +1869,8 @@ "user": "Usuario", "user_id": "ID de usuario", "user_liked": "{user} le gustó {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", + "user_pin_code_settings": "PIN", + "user_pin_code_settings_description": "Gestione su PIN", "user_purchase_settings": "Compra", "user_purchase_settings_description": "Gestiona tu compra", "user_role_set": "Establecer {user} como {role}", diff --git a/i18n/et.json b/i18n/et.json index fce760e3a3..49050c96fd 100644 --- a/i18n/et.json +++ b/i18n/et.json @@ -53,6 +53,7 @@ "confirm_email_below": "Kinnitamiseks sisesta allpool \"{email}\"", "confirm_reprocess_all_faces": "Kas oled kindel, et soovid kõik näod uuesti töödelda? See eemaldab kõik nimega isikud.", "confirm_user_password_reset": "Kas oled kindel, et soovid kasutaja {user} parooli lähtestada?", + "confirm_user_pin_code_reset": "Kas oled kindel, et soovid kasutaja {user} PIN-koodi lähtestada?", "create_job": "Lisa tööde", "cron_expression": "Cron avaldis", "cron_expression_description": "Sea skaneerimise intervall cron formaadis. Rohkema info jaoks vaata nt. Crontab Guru", @@ -348,6 +349,7 @@ "user_delete_delay_settings_description": "Päevade arv, pärast mida kustutatakse eemaldatud kasutaja konto ja üksused jäädavalt. Kasutajate kustutamise tööde käivitub keskööl, et otsida kustutamiseks valmis kasutajaid. Selle seadistuse muudatused rakenduvad järgmisel käivitumisel.", "user_delete_immediately": "Kasutaja {user} konto ja üksused suunatakse koheselt jäädavale kustutamisele.", "user_delete_immediately_checkbox": "Suuna kasutaja ja üksused jäädavale kustutamisele", + "user_details": "Kasutaja detailid", "user_management": "Kasutajate haldus", "user_password_has_been_reset": "Kasutaja parool on lähtestatud:", "user_password_reset_description": "Sisesta kasutajale ajutine parool ja teavita teda, et järgmisel sisselogimisel tuleb parool ära muuta.", @@ -369,7 +371,7 @@ "advanced": "Täpsemad valikud", "advanced_settings_enable_alternate_media_filter_subtitle": "Kasuta seda valikut, et filtreerida sünkroonimise ajal üksuseid alternatiivsete kriteeriumite alusel. Proovi seda ainult siis, kui rakendusel on probleeme kõigi albumite tuvastamisega.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTAALNE] Kasuta alternatiivset seadme albumi sünkroonimise filtrit", - "advanced_settings_log_level_title": "Logimistase: {}", + "advanced_settings_log_level_title": "Logimistase: {level}", "advanced_settings_prefer_remote_subtitle": "Mõned seadmed laadivad seadmes olevate üksuste pisipilte piinavalt aeglaselt. Aktiveeri see seadistus, et laadida selle asemel kaugpilte.", "advanced_settings_prefer_remote_title": "Eelista kaugpilte", "advanced_settings_proxy_headers_subtitle": "Määra vaheserveri päised, mida Immich peaks iga päringuga saatma", @@ -378,6 +380,7 @@ "advanced_settings_self_signed_ssl_title": "Luba endasigneeritud SSL-sertifikaadid", "advanced_settings_sync_remote_deletions_subtitle": "Kustuta või taasta üksus selles seadmes automaatself, kui sama tegevus toimub veebis", "advanced_settings_sync_remote_deletions_title": "Sünkrooni kaugkustutamised [EKSPERIMENTAALNE]", + "advanced_settings_tile_subtitle": "Edasijõudnud kasutajate seaded", "advanced_settings_troubleshooting_subtitle": "Luba lisafunktsioonid tõrkeotsinguks", "advanced_settings_troubleshooting_title": "Tõrkeotsing", "age_months": "Vanus {months, plural, one {# kuu} other {# kuud}}", @@ -399,9 +402,9 @@ "album_remove_user_confirmation": "Kas oled kindel, et soovid kasutaja {user} eemaldada?", "album_share_no_users": "Paistab, et oled seda albumit kõikide kasutajatega jaganud, või pole ühtegi kasutajat, kellega jagada.", "album_thumbnail_card_item": "1 üksus", - "album_thumbnail_card_items": "{} üksust", + "album_thumbnail_card_items": "{count} üksust", "album_thumbnail_card_shared": " · Jagatud", - "album_thumbnail_shared_by": "Jagas {}", + "album_thumbnail_shared_by": "Jagas {user}", "album_updated": "Album muudetud", "album_updated_setting_description": "Saa teavitus e-posti teel, kui jagatud albumis on uusi üksuseid", "album_user_left": "Lahkutud albumist {album}", @@ -439,14 +442,15 @@ "archive": "Arhiiv", "archive_or_unarchive_photo": "Arhiveeri või taasta foto", "archive_page_no_archived_assets": "Arhiveeritud üksuseid ei leitud", - "archive_page_title": "Arhiveeri ({})", + "archive_page_title": "Arhiveeri ({count})", "archive_size": "Arhiivi suurus", "archive_size_description": "Seadista arhiivi suurus allalaadimiseks (GiB)", "archived": "Arhiveeritud", "archived_count": "{count, plural, other {# arhiveeritud}}", "are_these_the_same_person": "Kas need on sama isik?", "are_you_sure_to_do_this": "Kas oled kindel, et soovid seda teha?", - "asset_action_delete_err_read_only": "Kirjutuskaitstud üksuseid ei saa kustutada, jätan vahele", + "asset_action_delete_err_read_only": "Kirjutuskaitstud üksuseid ei saa kustutada, jäetakse vahele", + "asset_action_share_err_offline": "Ühenduseta üksuseid ei saa pärida, jäetakse vahele", "asset_added_to_album": "Lisatud albumisse", "asset_adding_to_album": "Albumisse lisamine…", "asset_description_updated": "Üksuse kirjeldus on muudetud", @@ -459,7 +463,7 @@ "asset_list_layout_settings_group_by": "Grupeeri üksused", "asset_list_layout_settings_group_by_month_day": "Kuu + päev", "asset_list_layout_sub_title": "Asetus", - "asset_list_settings_subtitle": "Fotoruudustiku paigutuse sätted", + "asset_list_settings_subtitle": "Fotoruudustiku asetuse sätted", "asset_list_settings_title": "Fotoruudustik", "asset_offline": "Üksus pole kättesaadav", "asset_offline_description": "Seda välise kogu üksust ei leitud kettalt. Abi saamiseks palun võta ühendust oma Immich'i administraatoriga.", @@ -475,49 +479,87 @@ "assets_added_to_album_count": "{count, plural, one {# üksus} other {# üksust}} albumisse lisatud", "assets_added_to_name_count": "{count, plural, one {# üksus} other {# üksust}} lisatud {hasName, select, true {albumisse {name}} other {uude albumisse}}", "assets_count": "{count, plural, one {# üksus} other {# üksust}}", - "assets_deleted_permanently": "{} üksus(t) jäädavalt kustutatud", - "assets_deleted_permanently_from_server": "{} üksus(t) Immich'i serverist jäädavalt kustutatud", + "assets_deleted_permanently": "{count} üksus(t) jäädavalt kustutatud", + "assets_deleted_permanently_from_server": "{count} üksus(t) Immich'i serverist jäädavalt kustutatud", "assets_moved_to_trash_count": "{count, plural, one {# üksus} other {# üksust}} liigutatud prügikasti", "assets_permanently_deleted_count": "{count, plural, one {# üksus} other {# üksust}} jäädavalt kustutatud", "assets_removed_count": "{count, plural, one {# üksus} other {# üksust}} eemaldatud", - "assets_removed_permanently_from_device": "{} üksus(t) seadmest jäädavalt eemaldatud", + "assets_removed_permanently_from_device": "{count} üksus(t) seadmest jäädavalt eemaldatud", "assets_restore_confirmation": "Kas oled kindel, et soovid oma prügikasti liigutatud üksused taastada? Seda ei saa tagasi võtta! Pane tähele, et sel meetodil ei saa taastada ühenduseta üksuseid.", "assets_restored_count": "{count, plural, one {# üksus} other {# üksust}} taastatud", - "assets_restored_successfully": "{} üksus(t) edukalt taastatud", - "assets_trashed": "{} üksus(t) liigutatud prügikasti", + "assets_restored_successfully": "{count} üksus(t) edukalt taastatud", + "assets_trashed": "{count} üksus(t) liigutatud prügikasti", "assets_trashed_count": "{count, plural, one {# üksus} other {# üksust}} liigutatud prügikasti", - "assets_trashed_from_server": "{} üksus(t) liigutatud Immich'i serveris prügikasti", + "assets_trashed_from_server": "{count} üksus(t) liigutatud Immich'i serveris prügikasti", "assets_were_part_of_album_count": "{count, plural, one {Üksus oli} other {Üksused olid}} juba osa albumist", "authorized_devices": "Autoriseeritud seadmed", "automatic_endpoint_switching_subtitle": "Ühendu lokaalselt üle valitud WiFi-võrgu, kui see on saadaval, ja kasuta mujal alternatiivseid ühendusi", "automatic_endpoint_switching_title": "Automaatne URL-i ümberlülitamine", "back": "Tagasi", "back_close_deselect": "Tagasi, sulge või tühista valik", + "background_location_permission": "Taustal asukoha luba", + "background_location_permission_content": "Et taustal töötades võrguühendust vahetada, peab Immich'il *alati* olema täpse asukoha luba, et rakendus saaks WiFi-võrgu nime lugeda", + "backup_album_selection_page_albums_device": "Albumid seadmel ({count})", + "backup_album_selection_page_albums_tap": "Puuduta kaasamiseks, topeltpuuduta välistamiseks", + "backup_album_selection_page_assets_scatter": "Üksused võivad olla jaotatud mitme albumi vahel. Seega saab albumeid varundamise protsessi kaasata või välistada.", "backup_album_selection_page_select_albums": "Vali albumid", "backup_album_selection_page_selection_info": "Valiku info", "backup_album_selection_page_total_assets": "Unikaalseid üksuseid kokku", "backup_all": "Kõik", + "backup_background_service_backup_failed_message": "Üksuste varundamine ebaõnnestus. Uuesti proovimine…", + "backup_background_service_connection_failed_message": "Serveriga ühendumine ebaõnnestus. Uuesti proovimine…", + "backup_background_service_current_upload_notification": "{filename} üleslaadimine", "backup_background_service_default_notification": "Uute üksuste kontrollimine…", "backup_background_service_error_title": "Varundamise viga", + "backup_background_service_in_progress_notification": "Sinu üksuste varundamine…", + "backup_background_service_upload_failure_notification": "Faili {filename} üleslaadimine ebaõnnestus", + "backup_controller_page_albums": "Varunduse albumid", "backup_controller_page_background_app_refresh_disabled_content": "Taustal varundamise kasutamiseks luba rakenduse taustal värskendamine: Seaded > Üldine > Rakenduse taustal värskendamine.", "backup_controller_page_background_app_refresh_disabled_title": "Rakenduse taustal värskendamine keelatud", + "backup_controller_page_background_app_refresh_enable_button_text": "Mine seadetesse", "backup_controller_page_background_battery_info_link": "Näita mulle, kuidas", + "backup_controller_page_background_battery_info_message": "Parima taustal varundamise kogemuse jaoks palun keela Immich'i puhul kõik taustategevust piiravad aku optimeerimised.\n\nKuna see on seadmespetsiifiline, otsi vajalikku teavet oma seadme tootja kohta.", "backup_controller_page_background_battery_info_ok": "OK", + "backup_controller_page_background_battery_info_title": "Aku optimeerimised", + "backup_controller_page_background_charging": "Ainult laadimise ajal", "backup_controller_page_background_configure_error": "Taustateenuse seadistamine ebaõnnestus", + "backup_controller_page_background_delay": "Oota uute üksuste varundamisega: {duration}", "backup_controller_page_background_description": "Lülita taustateenus sisse, et uusi üksuseid automaatselt varundada, ilma et peaks rakendust avama", - "backup_controller_page_background_is_off": "Automaatne varundamine on välja lülitatud", - "backup_controller_page_background_is_on": "Automaatne varundamine on sisse lülitatud", + "backup_controller_page_background_is_off": "Automaatne taustal varundamine on välja lülitatud", + "backup_controller_page_background_is_on": "Automaatne taustal varundamine on sisse lülitatud", "backup_controller_page_background_turn_off": "Lülita taustateenus välja", "backup_controller_page_background_turn_on": "Lülita taustateenus sisse", "backup_controller_page_background_wifi": "Ainult WiFi-võrgus", + "backup_controller_page_backup": "Varundamine", + "backup_controller_page_backup_selected": "Valitud: ", "backup_controller_page_backup_sub": "Varundatud fotod ja videod", + "backup_controller_page_created": "Lisatud: {date}", "backup_controller_page_desc_backup": "Lülita sisse esiplaanil varundamine, et rakenduse avamisel uued üksused automaatselt serverisse üles laadida.", + "backup_controller_page_excluded": "Välistatud: ", + "backup_controller_page_failed": "Ebaõnnestunud ({count})", + "backup_controller_page_filename": "Failinimi: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", + "backup_controller_page_info": "Varunduse info", + "backup_controller_page_none_selected": "Ühtegi pole valitud", + "backup_controller_page_remainder": "Ootel", + "backup_controller_page_remainder_sub": "Valitud fotod ja videod, mis on veel varundamise ootel", + "backup_controller_page_server_storage": "Serveri talletusruum", + "backup_controller_page_start_backup": "Alusta varundamist", + "backup_controller_page_status_off": "Automaatne esiplaanil varundamine on välja lülitatud", + "backup_controller_page_status_on": "Automaatne esiplaanil varundamine on sisse lülitatud", + "backup_controller_page_storage_format": "{used}/{total} kasutusel", "backup_controller_page_to_backup": "Albumid, mida varundada", "backup_controller_page_total_sub": "Kõik unikaalsed fotod ja videod valitud albumitest", + "backup_controller_page_turn_off": "Lülita esiplaanil varundus välja", + "backup_controller_page_turn_on": "Lülita esiplaanil varundus sisse", + "backup_controller_page_uploading_file_info": "Faili info üleslaadimine", "backup_err_only_album": "Ei saa ainsat albumit eemaldada", "backup_info_card_assets": "üksused", "backup_manual_cancelled": "Tühistatud", + "backup_manual_in_progress": "Üleslaadimine juba käib. Proovi hiljem uuesti", + "backup_manual_success": "Õnnestus", "backup_manual_title": "Üleslaadimise staatus", + "backup_options_page_title": "Varundamise valikud", "backup_setting_subtitle": "Halda taustal ja esiplaanil üleslaadimise seadeid", "backward": "Tagasi", "birthdate_saved": "Sünnikuupäev salvestatud", @@ -530,13 +572,24 @@ "bulk_keep_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} alles jätta? Sellega märgitakse kõik duplikaadigrupid lahendatuks ilma midagi kustutamata.", "bulk_trash_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} masskustutada? Sellega jäetakse alles iga grupi suurim üksus ning duplikaadid liigutatakse prügikasti.", "buy": "Osta Immich", + "cache_settings_album_thumbnails": "Kogu lehtede pisipildid ({count} üksust)", "cache_settings_clear_cache_button": "Tühjenda puhver", + "cache_settings_clear_cache_button_title": "Tühjendab rakenduse puhvri. See mõjutab oluliselt rakenduse jõudlust, kuni puhver uuesti täidetakse.", + "cache_settings_duplicated_assets_clear_button": "TÜHJENDA", + "cache_settings_duplicated_assets_subtitle": "Fotod ja videod, mis on rakenduse poolt mustfiltreeritud", + "cache_settings_duplicated_assets_title": "Dubleeritud üksused ({count})", + "cache_settings_image_cache_size": "Piltide puhvri suurus ({count} üksust)", "cache_settings_statistics_album": "Kogu pisipildid", + "cache_settings_statistics_assets": "{count} üksust ({size})", "cache_settings_statistics_full": "Täismõõdus pildid", "cache_settings_statistics_shared": "Jagatud albumite pisipildid", "cache_settings_statistics_thumbnail": "Pisipildid", "cache_settings_statistics_title": "Puhvri kasutus", - "cache_settings_thumbnail_size": "Pisipiltide puhvri suurus ({} üksust)", + "cache_settings_subtitle": "Juhi Immich'i rakenduse puhverdamist", + "cache_settings_thumbnail_size": "Pisipiltide puhvri suurus ({count} üksust)", + "cache_settings_tile_subtitle": "Juhi lokaalse talletuse käitumist", + "cache_settings_tile_title": "Lokaalne talletus", + "cache_settings_title": "Puhverdamise seaded", "camera": "Kaamera", "camera_brand": "Kaamera mark", "camera_model": "Kaamera mudel", @@ -547,6 +600,7 @@ "cannot_undo_this_action": "Sa ei saa seda tagasi võtta!", "cannot_update_the_description": "Kirjelduse muutmine ebaõnnestus", "change_date": "Muuda kuupäeva", + "change_display_order": "Muuda kuva järjekorda", "change_expiration_time": "Muuda aegumisaega", "change_location": "Muuda asukohta", "change_name": "Muuda nime", @@ -554,12 +608,17 @@ "change_password": "Parooli muutmine", "change_password_description": "See on su esimene kord süsteemi siseneda, või on tehtud taotlus parooli muutmiseks. Palun sisesta allpool uus parool.", "change_password_form_confirm_password": "Kinnita parool", + "change_password_form_description": "Hei {name},\n\nSa kas logid süsteemi esimest korda sisse, või on esitatud taotlus sinu parooli muutmiseks. Palun sisesta allpool uus parool.", "change_password_form_new_password": "Uus parool", "change_password_form_password_mismatch": "Paroolid ei klapi", "change_password_form_reenter_new_password": "Korda uut parooli", + "change_pin_code": "Muuda PIN-koodi", "change_your_password": "Muuda oma parooli", "changed_visibility_successfully": "Nähtavus muudetud", "check_all": "Märgi kõik", + "check_corrupt_asset_backup": "Otsi riknenud üksuste varukoopiaid", + "check_corrupt_asset_backup_button": "Teosta kontroll", + "check_corrupt_asset_backup_description": "Käivita see kontroll ainult WiFi-võrgus ja siis, kui kõik üksused on varundatud. See protseduur võib kesta mõne minuti.", "check_logs": "Vaata logisid", "choose_matching_people_to_merge": "Vali kattuvad isikud, mida ühendada", "city": "Linn", @@ -587,20 +646,27 @@ "comments_and_likes": "Kommentaarid ja meeldimised", "comments_are_disabled": "Kommentaarid on keelatud", "common_create_new_album": "Lisa uus album", + "common_server_error": "Kontrolli oma võrguühendust ja veendu, et server on kättesaadav ning rakenduse ja serveri versioonid on ühilduvad.", "completed": "Lõpetatud", "confirm": "Kinnita", "confirm_admin_password": "Kinnita administraatori parool", "confirm_delete_face": "Kas oled kindel, et soovid isiku {name} näo üksuselt kustutada?", "confirm_delete_shared_link": "Kas oled kindel, et soovid selle jagatud lingi kustutada?", "confirm_keep_this_delete_others": "Kõik muud üksused selles virnas kustutatakse. Kas oled kindel, et soovid jätkata?", + "confirm_new_pin_code": "Kinnita uus PIN-kood", "confirm_password": "Kinnita parool", "contain": "Mahuta ära", "context": "Kontekst", "continue": "Jätka", + "control_bottom_app_bar_album_info_shared": "{count} üksust · Jagatud", "control_bottom_app_bar_create_new_album": "Lisa uus album", + "control_bottom_app_bar_delete_from_immich": "Kustuta Immich'ist", "control_bottom_app_bar_delete_from_local": "Kustuta seadmest", "control_bottom_app_bar_edit_location": "Muuda asukohta", "control_bottom_app_bar_edit_time": "Muuda kuupäeva ja aega", + "control_bottom_app_bar_share_link": "Jaga linki", + "control_bottom_app_bar_share_to": "Jaga", + "control_bottom_app_bar_trash_from_immich": "Liiguta prügikasti", "copied_image_to_clipboard": "Pilt kopeeritud lõikelauale.", "copied_to_clipboard": "Kopeeritud lõikelauale!", "copy_error": "Kopeeri viga", @@ -615,25 +681,36 @@ "covers": "Kaanepildid", "create": "Lisa", "create_album": "Lisa album", + "create_album_page_untitled": "Pealkirjata", "create_library": "Lisa kogu", "create_link": "Lisa link", "create_link_to_share": "Lisa jagamiseks link", "create_link_to_share_description": "Luba kõigil, kellel on link, valitud pilte näha", + "create_new": "LISA UUS", "create_new_person": "Lisa uus isik", "create_new_person_hint": "Seosta valitud üksused uue isikuga", "create_new_user": "Lisa uus kasutaja", + "create_shared_album_page_share_add_assets": "LISA ÜKSUSEID", "create_shared_album_page_share_select_photos": "Vali fotod", "create_tag": "Lisa silt", "create_tag_description": "Lisa uus silt. Pesastatud siltide jaoks sisesta täielik tee koos kaldkriipsudega.", "create_user": "Lisa kasutaja", "created": "Lisatud", + "created_at": "Lisatud", + "crop": "Kärpimine", + "curated_object_page_title": "Asjad", "current_device": "Praegune seade", + "current_pin_code": "Praegune PIN-kood", + "current_server_address": "Praegune serveri aadress", "custom_locale": "Kohandatud lokaat", "custom_locale_description": "Vorminda kuupäevad ja arvud vastavalt keelele ja regioonile", + "daily_title_text_date": "d. MMMM", + "daily_title_text_date_year": "d. MMMM yyyy", "dark": "Tume", "date_after": "Kuupäev pärast", "date_and_time": "Kuupäev ja kellaaeg", "date_before": "Kuupäev enne", + "date_format": "d. MMMM y • HH:mm", "date_of_birth_saved": "Sünnikuupäev salvestatud", "date_range": "Kuupäevavahemik", "day": "Päev", @@ -647,6 +724,11 @@ "delete": "Kustuta", "delete_album": "Kustuta album", "delete_api_key_prompt": "Kas oled kindel, et soovid selle API võtme kustutada?", + "delete_dialog_alert": "Need üksused kustutatakse jäädavalt Immich'ist ja sinu seadmest", + "delete_dialog_alert_local": "Need üksused kustutatakse jäädavalt sinu seadmest, aga jäävad Immich'i serverisse alles", + "delete_dialog_alert_local_non_backed_up": "Mõned üksustest ei ole Immich'isse varundatud ning kustutatakse su seadmest jäädavalt", + "delete_dialog_alert_remote": "Need üksused kustutatakse jäädavalt Immich'i serverist", + "delete_dialog_ok_force": "Kustuta sellegipoolest", "delete_dialog_title": "Kustuta jäädavalt", "delete_duplicates_confirmation": "Kas oled kindel, et soovid need duplikaadid jäädavalt kustutada?", "delete_face": "Kustuta nägu", @@ -654,6 +736,7 @@ "delete_library": "Kustuta kogu", "delete_link": "Kustuta link", "delete_local_dialog_ok_backed_up_only": "Kustuta ainult varundatud", + "delete_local_dialog_ok_force": "Kustuta sellegipoolest", "delete_others": "Kustuta teised", "delete_shared_link": "Kustuta jagatud link", "delete_shared_link_dialog_title": "Kustuta jagatud link", @@ -664,6 +747,7 @@ "deletes_missing_assets": "Kustutab üksused, mis on kettalt puudu", "description": "Kirjeldus", "description_input_hint_text": "Lisa kirjeldus...", + "description_input_submit_error": "Viga kirjelduse muutmisel, rohkem infot leiad logist", "details": "Üksikasjad", "direction": "Suund", "disabled": "Välja lülitatud", @@ -685,17 +769,21 @@ "download_enqueue": "Allalaadimine ootel", "download_error": "Allalaadimise viga", "download_failed": "Allalaadimine ebaõnnestus", + "download_filename": "fail: {filename}", "download_finished": "Allalaadimine lõpetatud", "download_include_embedded_motion_videos": "Manustatud videod", "download_include_embedded_motion_videos_description": "Lisa liikuvatesse fotodesse manustatud videod eraldi failidena", + "download_notfound": "Allalaadimist ei leitud", "download_paused": "Allalaadimine peatatud", "download_settings": "Allalaadimine", "download_settings_description": "Halda üksuste allalaadimise seadeid", "download_started": "Allalaadimine alustatud", "download_sucess": "Allalaadimine õnnestus", "download_sucess_android": "Meediumid laaditi alla kataloogi DCIM/Immich", + "download_waiting_to_retry": "Uuesti proovimise ootel", "downloading": "Allalaadimine", "downloading_asset_filename": "Üksuse {filename} allalaadimine", + "downloading_media": "Meediumi allalaadimine", "drop_files_to_upload": "Failide üleslaadimiseks sikuta need ükskõik kuhu", "duplicates": "Duplikaadid", "duplicates_description": "Lahenda iga grupp, valides duplikaadid, kui neid on", @@ -725,17 +813,20 @@ "editor_crop_tool_h2_aspect_ratios": "Kuvasuhted", "editor_crop_tool_h2_rotation": "Pööre", "email": "E-post", + "email_notifications": "E-posti teavitused", "empty_folder": "See kaust on tühi", "empty_trash": "Tühjenda prügikast", "empty_trash_confirmation": "Kas oled kindel, et soovid prügikasti tühjendada? See eemaldab kõik seal olevad üksused Immich'ist jäädavalt.\nSeda tegevust ei saa tagasi võtta!", "enable": "Luba", "enabled": "Lubatud", "end_date": "Lõppkuupäev", + "enqueued": "Järjekorras", "enter_wifi_name": "Sisesta WiFi-võrgu nimi", "error": "Viga", "error_change_sort_album": "Albumi sorteerimisjärjestuse muutmine ebaõnnestus", "error_delete_face": "Viga näo kustutamisel", "error_loading_image": "Viga pildi laadimisel", + "error_saving_image": "Viga: {error}", "error_title": "Viga - midagi läks valesti", "errors": { "cannot_navigate_next_asset": "Järgmise üksuse juurde liikumine ebaõnnestus", @@ -838,6 +929,7 @@ "unable_to_remove_reaction": "Reaktsiooni eemaldamine ebaõnnestus", "unable_to_repair_items": "Üksuste parandamine ebaõnnestus", "unable_to_reset_password": "Parooli lähtestamine ebaõnnestus", + "unable_to_reset_pin_code": "PIN-koodi lähtestamine ebaõnnestus", "unable_to_resolve_duplicate": "Duplikaadi lahendamine ebaõnnestus", "unable_to_restore_assets": "Üksuste taastamine ebaõnnestus", "unable_to_restore_trash": "Prügikastist taastamine ebaõnnestus", @@ -871,8 +963,15 @@ "exif_bottom_sheet_location": "ASUKOHT", "exif_bottom_sheet_people": "ISIKUD", "exif_bottom_sheet_person_add_person": "Lisa nimi", + "exif_bottom_sheet_person_age": "Vanus {age}", + "exif_bottom_sheet_person_age_months": "Vanus {months} kuud", + "exif_bottom_sheet_person_age_year_months": "Vanus 1 aasta, {months} kuud", + "exif_bottom_sheet_person_age_years": "Vanus {years}", "exit_slideshow": "Sulge slaidiesitlus", "expand_all": "Näita kõik", + "experimental_settings_new_asset_list_subtitle": "Töös", + "experimental_settings_new_asset_list_title": "Luba eksperimentaalne fotoruudistik", + "experimental_settings_subtitle": "Kasuta omal vastutusel!", "experimental_settings_title": "Eksperimentaalne", "expire_after": "Aegub", "expired": "Aegunud", @@ -884,12 +983,16 @@ "extension": "Laiend", "external": "Väline", "external_libraries": "Välised kogud", + "external_network": "Väline võrk", "external_network_sheet_info": "Kui seade ei ole eelistatud WiFi-võrgus, ühendub rakendus serveriga allolevatest URL-idest esimese kättesaadava kaudu, alustades ülevalt", "face_unassigned": "Seostamata", + "failed": "Ebaõnnestus", "failed_to_load_assets": "Üksuste laadimine ebaõnnestus", + "failed_to_load_folder": "Kausta laadimine ebaõnnestus", "favorite": "Lemmik", "favorite_or_unfavorite_photo": "Lisa foto lemmikutesse või eemalda lemmikutest", "favorites": "Lemmikud", + "favorites_page_no_favorites": "Lemmikuid üksuseid ei leitud", "feature_photo_updated": "Esiletõstetud foto muudetud", "features": "Funktsioonid", "features_setting_description": "Halda rakenduse funktsioone", @@ -897,7 +1000,9 @@ "file_name_or_extension": "Failinimi või -laiend", "filename": "Failinimi", "filetype": "Failitüüp", + "filter": "Filter", "filter_people": "Filtreeri isikuid", + "filter_places": "Filtreeri kohti", "find_them_fast": "Leia teda kiiresti nime järgi otsides", "fix_incorrect_match": "Paranda ebaõige vaste", "folder": "Kaust", @@ -907,10 +1012,12 @@ "forward": "Edasi", "general": "Üldine", "get_help": "Küsi abi", + "get_wifiname_error": "WiFi-võrgu nime ei õnnestunud lugeda. Veendu, et oled andnud vajalikud load ja oled WiFi-võrguga ühendatud", "getting_started": "Alustamine", "go_back": "Tagasi", "go_to_folder": "Mine kausta", "go_to_search": "Otsingusse", + "grant_permission": "Anna luba", "group_albums_by": "Grupeeri albumid...", "group_country": "Grupeeri riigi kaupa", "group_no": "Ära grupeeri", @@ -934,19 +1041,24 @@ "hide_person": "Peida isik", "hide_unnamed_people": "Peida nimetud isikud", "home_page_add_to_album_conflicts": "{added} üksust lisati albumisse {album}. {failed} üksust oli juba albumis.", - "home_page_add_to_album_err_local": "Lokaalseid üksuseid ei saa veel albumisse lisada, jätan vahele", + "home_page_add_to_album_err_local": "Lokaalseid üksuseid ei saa veel albumisse lisada, jäetakse vahele", "home_page_add_to_album_success": "{added} üksust lisati albumisse {album}.", - "home_page_album_err_partner": "Partneri üksuseid ei saa veel albumisse lisada, jätan vahele", - "home_page_archive_err_local": "Lokaalseid üksuseid ei saa veel arhiveerida, jätan vahele", - "home_page_archive_err_partner": "Partneri üksuseid ei saa arhiveerida, jätan vahele", + "home_page_album_err_partner": "Partneri üksuseid ei saa veel albumisse lisada, jäetakse vahele", + "home_page_archive_err_local": "Lokaalseid üksuseid ei saa veel arhiveerida, jäetakse vahele", + "home_page_archive_err_partner": "Partneri üksuseid ei saa arhiveerida, jäetakse vahele", "home_page_building_timeline": "Ajajoone koostamine", - "home_page_delete_err_partner": "Partneri üksuseid ei saa kustutada, jätan vahele", - "home_page_favorite_err_local": "Lokaalseid üksuseid ei saa lemmikuks märkida, jätan vahele", - "home_page_favorite_err_partner": "Partneri üksuseid ei saa lemmikuks märkida, jätan vahele", - "home_page_share_err_local": "Lokaalseid üksuseid ei saa lingiga jagada, jätan vahele", + "home_page_delete_err_partner": "Partneri üksuseid ei saa kustutada, jäetakse vahele", + "home_page_delete_remote_err_local": "Kaugkustutamise valikus on lokaalsed üksused, jäetakse vahele", + "home_page_favorite_err_local": "Lokaalseid üksuseid ei saa lemmikuks märkida, jäetakse vahele", + "home_page_favorite_err_partner": "Partneri üksuseid ei saa lemmikuks märkida, jäetakse vahele", + "home_page_first_time_notice": "Kui see on su esimene kord rakendust kasutada, vali varunduse album, et ajajoon saaks sellest fotosid ja videosid kuvada", + "home_page_share_err_local": "Lokaalseid üksuseid ei saa lingiga jagada, jäetakse vahele", + "home_page_upload_err_limit": "Korraga saab üles laadida ainult 30 üksust, jäetakse vahele", "host": "Host", "hour": "Tund", + "id": "ID", "ignore_icloud_photos": "Ignoreeri iCloud fotosid", + "ignore_icloud_photos_description": "Fotosid, mis on iCloud'is, ei laadita üles Immich'i serverisse", "image": "Pilt", "image_alt_text_date": "{isVideo, select, true {Video} other {Pilt}} tehtud {date}", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} koos isikuga {person1}", @@ -958,8 +1070,10 @@ "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos isikutega {person1} ja {person2}", "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos isikutega {person1}, {person2} ja {person3}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos {person1}, {person2} ja veel {additionalCount, number} isikuga", + "image_saved_successfully": "Pilt salvestatud", "image_viewer_page_state_provider_download_started": "Allalaadimine alustatud", "image_viewer_page_state_provider_download_success": "Allalaadimine õnnestus", + "image_viewer_page_state_provider_share_error": "Jagamise viga", "immich_logo": "Immich'i logo", "immich_web_interface": "Immich'i veebiliides", "import_from_json": "Impordi JSON-formaadist", @@ -1000,8 +1114,11 @@ "level": "Tase", "library": "Kogu", "library_options": "Kogu seaded", + "library_page_device_albums": "Albumid seadmes", "library_page_new_album": "Uus album", "library_page_sort_asset_count": "Üksuste arv", + "library_page_sort_created": "Loomise aeg", + "library_page_sort_last_modified": "Viimase muutmise aeg", "library_page_sort_title": "Albumi pealkiri", "light": "Hele", "like_deleted": "Meeldimine kustutatud", @@ -1012,14 +1129,22 @@ "list": "Loend", "loading": "Laadimine", "loading_search_results_failed": "Otsitulemuste laadimine ebaõnnestus", + "local_network": "Kohalik võrk", "local_network_sheet_info": "Rakendus ühendub valitud Wi-Fi võrgus olles serveriga selle URL-i kaudu", + "location_permission": "Asukoha luba", "location_permission_content": "Automaatseks ümberlülitumiseks vajab Immich täpse asukoha luba, et saaks lugeda aktiivse WiFi-võrgu nime", "location_picker_choose_on_map": "Vali kaardil", + "location_picker_latitude_error": "Sisesta korrektne laiuskraad", + "location_picker_latitude_hint": "Sisesta laiuskraad siia", + "location_picker_longitude_error": "Sisesta korrektne pikkuskraad", + "location_picker_longitude_hint": "Sisesta pikkuskraad siia", "log_out": "Logi välja", "log_out_all_devices": "Logi kõigist seadmetest välja", "logged_out_all_devices": "Kõigist seadmetest välja logitud", "logged_out_device": "Seadmest välja logitud", "login": "Logi sisse", + "login_disabled": "Sisselogimine on keelatud", + "login_form_api_exception": "API viga. Kontrolli serveri URL-i ja proovi uuesti.", "login_form_back_button_text": "Tagasi", "login_form_email_hint": "sinunimi@email.com", "login_form_endpoint_hint": "http://serveri-ip:port", @@ -1029,11 +1154,16 @@ "login_form_err_invalid_url": "Vigane URL", "login_form_err_leading_whitespace": "Eelnevad tühikud", "login_form_err_trailing_whitespace": "Järgnevad tühikud", + "login_form_failed_get_oauth_server_config": "Viga OAuth abil sisenemisel, kontrolli serveri URL-i", + "login_form_failed_get_oauth_server_disable": "OAuth funktsionaalsus ei ole selles serveris saadaval", + "login_form_failed_login": "Viga sisselogimisel, kontrolli serveri URL-i, e-posti aadressi ja parooli", + "login_form_handshake_exception": "Serveriga suhtlemisel tekkis kätlemise viga. Kui kasutad endasigneeritud sertifikaati, luba seadetes endasigneeritud sertifikaatide tugi.", "login_form_password_hint": "parool", "login_form_save_login": "Jää sisselogituks", "login_form_server_empty": "Sisesta serveri URL.", "login_form_server_error": "Serveriga ühendumine ebaõnnestus.", "login_has_been_disabled": "Sisselogimine on keelatud.", + "login_password_changed_error": "Parooli muutmisel tekkis viga", "login_password_changed_success": "Parool edukalt uuendatud", "logout_all_device_confirmation": "Kas oled kindel, et soovid kõigist seadmetest välja logida?", "logout_this_device_confirmation": "Kas oled kindel, et soovid sellest seadmest välja logida?", @@ -1052,27 +1182,43 @@ "manage_your_devices": "Halda oma autenditud seadmeid", "manage_your_oauth_connection": "Halda oma OAuth ühendust", "map": "Kaart", - "map_assets_in_bound": "{} foto", - "map_assets_in_bounds": "{} fotot", + "map_assets_in_bound": "{count} foto", + "map_assets_in_bounds": "{count} fotot", + "map_cannot_get_user_location": "Ei saa kasutaja asukohta tuvastada", "map_location_dialog_yes": "Jah", "map_location_picker_page_use_location": "Kasuta seda asukohta", + "map_location_service_disabled_content": "Praeguse asukoha üksuste kuvamiseks tuleb lubada asukoha teenus. Kas soovid seda praegu lubada?", + "map_location_service_disabled_title": "Asukoha teenus keelatud", "map_marker_for_images": "Kaardimarker kohas {city}, {country} tehtud piltide jaoks", "map_marker_with_image": "Kaardimarker pildiga", + "map_no_assets_in_bounds": "Selles piirkonnas ei ole fotosid", + "map_no_location_permission_content": "Praeguse asukoha üksuste kuvamiseks on vaja asukoha luba. Kas soovid seda praegu lubada?", + "map_no_location_permission_title": "Asukoha luba keelatud", "map_settings": "Kaardi seaded", + "map_settings_dark_mode": "Tume režiim", "map_settings_date_range_option_day": "Viimased 24 tundi", - "map_settings_date_range_option_days": "Viimased {} päeva", + "map_settings_date_range_option_days": "Viimased {days} päeva", "map_settings_date_range_option_year": "Viimane aasta", - "map_settings_date_range_option_years": "Viimased {} aastat", + "map_settings_date_range_option_years": "Viimased {years} aastat", "map_settings_dialog_title": "Kaardi seaded", + "map_settings_include_show_archived": "Kaasa arhiveeritud", + "map_settings_include_show_partners": "Kaasa partnerid", + "map_settings_only_show_favorites": "Kuva ainult lemmikud", + "map_settings_theme_settings": "Kaardi teema", + "map_zoom_to_see_photos": "Fotode nägemiseks suumi välja", "mark_all_as_read": "Märgi kõik loetuks", "mark_as_read": "Märgi loetuks", "marked_all_as_read": "Kõik märgiti loetuks", "matches": "Ühtivad failid", "media_type": "Meediumi tüüp", "memories": "Mälestused", + "memories_all_caught_up": "Ongi kõik", + "memories_check_back_tomorrow": "Vaata homme juba uusi mälestusi", "memories_setting_description": "Halda, mida sa oma mälestustes näed", + "memories_start_over": "Alusta uuesti", + "memories_swipe_to_close": "Sulgemiseks pühi üles", "memories_year_ago": "Aasta tagasi", - "memories_years_ago": "{} aastat tagasi", + "memories_years_ago": "{years, plural, other {# aastat}} tagasi", "memory": "Mälestus", "memory_lane_title": "Mälestus {title}", "menu": "Menüü", @@ -1092,8 +1238,8 @@ "moved_to_archive": "{count, plural, one {# üksus} other {# üksust}} liigutatud arhiivi", "moved_to_library": "{count, plural, one {# üksus} other {# üksust}} liigutatud kogusse", "moved_to_trash": "Liigutatud prügikasti", - "multiselect_grid_edit_date_time_err_read_only": "Kirjutuskaitsega üksus(t)e kuupäeva ei saa muuta, jätan vahele", - "multiselect_grid_edit_gps_err_read_only": "Kirjutuskaitsega üksus(t)e asukohta ei saa muuta, jätan vahele", + "multiselect_grid_edit_date_time_err_read_only": "Kirjutuskaitsega üksus(t)e kuupäeva ei saa muuta, jäetakse vahele", + "multiselect_grid_edit_gps_err_read_only": "Kirjutuskaitsega üksus(t)e asukohta ei saa muuta, jäetakse vahele", "mute_memories": "Vaigista mälestused", "my_albums": "Minu albumid", "name": "Nimi", @@ -1105,6 +1251,7 @@ "new_api_key": "Uus API võti", "new_password": "Uus parool", "new_person": "Uus isik", + "new_pin_code": "Uus PIN-kood", "new_user_created": "Uus kasutaja lisatud", "new_version_available": "UUS VERSIOON SAADAVAL", "newest_first": "Uuemad eespool", @@ -1116,6 +1263,7 @@ "no_albums_yet": "Paistab, et sul pole veel ühtegi albumit.", "no_archived_assets_message": "Arhiveeri fotod ja videod, et neid Fotod vaatest peita", "no_assets_message": "KLIKI ESIMESE FOTO ÜLESLAADIMISEKS", + "no_assets_to_show": "Pole üksuseid, mida kuvada", "no_duplicates_found": "Ühtegi duplikaati ei leitud.", "no_exif_info_available": "Exif info pole saadaval", "no_explore_results_message": "Oma kogu avastamiseks laadi üles rohkem fotosid.", @@ -1123,13 +1271,16 @@ "no_libraries_message": "Lisa väline kogu oma fotode ja videote vaatamiseks", "no_name": "Nimetu", "no_notifications": "Teavitusi pole", + "no_people_found": "Kattuvaid isikuid ei leitud", "no_places": "Kohti ei ole", "no_results": "Vasteid pole", "no_results_description": "Proovi sünonüümi või üldisemat märksõna", "no_shared_albums_message": "Lisa album, et fotosid ja videosid teistega jagada", "not_in_any_album": "Pole üheski albumis", + "not_selected": "Ei ole valitud", "note_apply_storage_label_to_previously_uploaded assets": "Märkus: Et rakendada talletussilt varem üleslaaditud üksustele, käivita", "notes": "Märkused", + "notification_permission_dialog_content": "Teavituste lubamiseks mine Seadetesse ja vali lubamine.", "notification_permission_list_tile_content": "Anna luba teavituste saatmiseks.", "notification_permission_list_tile_enable_button": "Luba teavitused", "notification_permission_list_tile_title": "Teavituste luba", @@ -1170,8 +1321,12 @@ "partner_can_access_location": "Asukohad, kus su fotod tehti", "partner_list_user_photos": "Kasutaja {user} fotod", "partner_list_view_all": "Vaata kõiki", + "partner_page_empty_message": "Su fotod pole veel ühegi partneriga jagatud.", + "partner_page_no_more_users": "Pole rohkem kasutajaid, keda lisada", "partner_page_partner_add_failed": "Partneri lisamine ebaõnnestus", "partner_page_select_partner": "Vali partner", + "partner_page_shared_to_title": "Jagatud", + "partner_page_stop_sharing_content": "{partner} ei pääse rohkem su fotodele ligi.", "partner_sharing": "Partneriga jagamine", "partners": "Partnerid", "password": "Parool", @@ -1202,6 +1357,12 @@ "permanently_deleted_assets_count": "{count, plural, one {# üksus} other {# üksust}} jäädavalt kustutatud", "permission_onboarding_back": "Tagasi", "permission_onboarding_continue_anyway": "Jätka sellegipoolest", + "permission_onboarding_get_started": "Alusta", + "permission_onboarding_go_to_settings": "Mine seadetesse", + "permission_onboarding_permission_denied": "Luba keelatud. Immich'i kasutamiseks anna Seadetes fotode ja videote load.", + "permission_onboarding_permission_granted": "Luba antud! Oled valmis.", + "permission_onboarding_permission_limited": "Piiratud luba. Et Immich saaks tervet su galeriid varundada ja hallata, anna Seadetes luba fotodele ja videotele.", + "permission_onboarding_request": "Immich'il on vaja luba su fotode ja videote vaatamiseks.", "person": "Isik", "person_birthdate": "Sündinud {date}", "person_hidden": "{name}{hidden, select, true { (peidetud)} other {}}", @@ -1211,6 +1372,9 @@ "photos_count": "{count, plural, one {{count, number} foto} other {{count, number} fotot}}", "photos_from_previous_years": "Fotod varasematest aastatest", "pick_a_location": "Vali asukoht", + "pin_code_changed_successfully": "PIN-kood edukalt muudetud", + "pin_code_reset_successfully": "PIN-kood edukalt lähtestatud", + "pin_code_setup_successfully": "PIN-kood edukalt seadistatud", "place": "Asukoht", "places": "Kohad", "places_count": "{count, plural, one {{count, number} koht} other {{count, number} kohta}}", @@ -1228,15 +1392,21 @@ "previous_or_next_photo": "Eelmine või järgmine foto", "primary": "Peamine", "privacy": "Privaatsus", + "profile": "Profiil", "profile_drawer_app_logs": "Logid", + "profile_drawer_client_out_of_date_major": "Mobiilirakendus on aegunud. Palun uuenda uusimale suurele versioonile.", + "profile_drawer_client_out_of_date_minor": "Mobiilirakendus on aegunud. Palun uuenda uusimale väikesele versioonile.", + "profile_drawer_client_server_up_to_date": "Klient ja server on uuendatud", "profile_drawer_github": "GitHub", + "profile_drawer_server_out_of_date_major": "Server on aegunud. Palun uuenda uusimale suurele versioonile.", + "profile_drawer_server_out_of_date_minor": "Server on aegunud. Palun uuenda uusimale väikesele versioonile.", "profile_image_of_user": "Kasutaja {user} profiilipilt", "profile_picture_set": "Profiilipilt määratud.", "public_album": "Avalik album", "public_share": "Avalik jagamine", "purchase_account_info": "Toetaja", "purchase_activated_subtitle": "Aitäh, et toetad Immich'it ja avatud lähtekoodiga tarkvara", - "purchase_activated_time": "Aktiveeritud {date, date}", + "purchase_activated_time": "Aktiveeritud {date}", "purchase_activated_title": "Sinu võtme aktiveerimine õnnestus", "purchase_button_activate": "Aktiveeri", "purchase_button_buy": "Osta", @@ -1281,6 +1451,8 @@ "recent_searches": "Hiljutised otsingud", "recently_added": "Hiljuti lisatud", "recently_added_page_title": "Hiljuti lisatud", + "recently_taken": "Hiljuti tehtud", + "recently_taken_page_title": "Hiljuti tehtud", "refresh": "Värskenda", "refresh_encoded_videos": "Värskenda kodeeritud videod", "refresh_faces": "Värskenda näod", @@ -1323,6 +1495,7 @@ "reset": "Lähtesta", "reset_password": "Lähtesta parool", "reset_people_visibility": "Lähtesta isikute nähtavus", + "reset_pin_code": "Lähtesta PIN-kood", "reset_to_default": "Lähtesta", "resolve_duplicates": "Lahenda duplikaadid", "resolved_all_duplicates": "Kõik duplikaadid lahendatud", @@ -1342,6 +1515,7 @@ "saved_profile": "Profiil salvestatud", "saved_settings": "Seaded salvestatud", "say_something": "Ütle midagi", + "scaffold_body_error_occurred": "Tekkis viga", "scan_all_libraries": "Skaneeri kõik kogud", "scan_library": "Skaneeri", "scan_settings": "Skaneerimise seaded", @@ -1362,6 +1536,7 @@ "search_filter_date": "Kuupäev", "search_filter_date_interval": "{start} kuni {end}", "search_filter_date_title": "Vali kuupäevavahemik", + "search_filter_display_option_not_in_album": "Pole albumis", "search_filter_display_options": "Kuva valikud", "search_filter_filename": "Otsi failinime alusel", "search_filter_location": "Asukoht", @@ -1371,21 +1546,30 @@ "search_filter_people_title": "Vali isikud", "search_for": "Otsi", "search_for_existing_person": "Otsi olemasolevat isikut", + "search_no_more_result": "Rohkem vasteid pole", "search_no_people": "Isikuid ei ole", "search_no_people_named": "Ei ole isikuid nimega \"{name}\"", + "search_no_result": "Vasteid ei leitud, proovi muud otsinguterminit või kombinatsiooni", "search_options": "Otsingu valikud", "search_page_categories": "Kategooriad", + "search_page_motion_photos": "Liikuvad fotod", + "search_page_no_objects": "Objektide info pole saadaval", + "search_page_no_places": "Kohtade info pole saadaval", "search_page_screenshots": "Ekraanipildid", "search_page_search_photos_videos": "Otsi oma fotosid ja videosid", "search_page_selfies": "Selfid", "search_page_things": "Asjad", "search_page_view_all_button": "Vaata kõiki", + "search_page_your_activity": "Sinu aktiivsus", + "search_page_your_map": "Sinu kaart", "search_people": "Otsi inimesi", "search_places": "Otsi kohti", "search_rating": "Otsi hinnangu järgi...", "search_result_page_new_search_hint": "Uus otsing", "search_settings": "Otsi seadeid", "search_state": "Otsi osariiki...", + "search_suggestion_list_smart_search_hint_1": "Nutiotsing on vaikimisi lubatud, metaandmete otsimiseks kasuta süntaksit ", + "search_suggestion_list_smart_search_hint_2": "m:sinu-otsingu-termin", "search_tags": "Otsi silte...", "search_timezone": "Otsi ajavööndit...", "search_type": "Otsingu tüüp", @@ -1427,26 +1611,40 @@ "set_profile_picture": "Sea profiilipilt", "set_slideshow_to_fullscreen": "Kuva slaidiesitlus täisekraanil", "setting_image_viewer_help": "Detailivaatur laadib kõigepealt väikese pisipildi, seejärel keskmises mõõdus eelvaate (kui lubatud) ja lõpuks originaalpildi (kui lubatud).", + "setting_image_viewer_original_subtitle": "Lülita sisse, et laadida algne täisresolutsiooniga pilt (suur!). Lülita välja, et vähendada andmekasutust (nii võrgu kui seadme puhvri).", + "setting_image_viewer_original_title": "Laadi algne pilt", "setting_image_viewer_preview_subtitle": "Luba keskmise resolutsiooniga pildi laadimine. Keela, et laadida kohe originaalpilt või kasutada ainult pisipilti.", "setting_image_viewer_preview_title": "Laadi pildi eelvaade", "setting_image_viewer_title": "Pildid", "setting_languages_apply": "Rakenda", "setting_languages_subtitle": "Muuda rakenduse keelt", "setting_languages_title": "Keeled", - "setting_notifications_notify_hours": "{} tundi", + "setting_notifications_notify_failures_grace_period": "Teavita taustal varundamise vigadest: {duration}", + "setting_notifications_notify_hours": "{count} tundi", "setting_notifications_notify_immediately": "kohe", - "setting_notifications_notify_minutes": "{} minutit", + "setting_notifications_notify_minutes": "{count} minutit", "setting_notifications_notify_never": "mitte kunagi", - "setting_notifications_notify_seconds": "{} sekundit", + "setting_notifications_notify_seconds": "{count} sekundit", + "setting_notifications_single_progress_subtitle": "Detailne üleslaadimise edenemise info üksuse kohta", "setting_notifications_single_progress_title": "Kuva taustal varundamise detailset edenemist", "setting_notifications_subtitle": "Halda oma teavituste eelistusi", + "setting_notifications_total_progress_subtitle": "Üldine üleslaadimise edenemine (üksuseid tehtud/kokku)", "setting_notifications_total_progress_title": "Kuva taustal varundamise üldist edenemist", + "setting_video_viewer_looping_title": "Taasesitus", + "setting_video_viewer_original_video_subtitle": "Esita serverist video voogedastamisel originaal, isegi kui transkodeeritud video on saadaval. Võib põhjustada puhverdamist. Lokaalselt saadaolevad videod mängitakse originaalkvaliteediga sõltumata sellest seadest.", + "setting_video_viewer_original_video_title": "Sunni originaalvideo", "settings": "Seaded", + "settings_require_restart": "Selle seade rakendamiseks palun taaskäivita Immich", "settings_saved": "Seaded salvestatud", + "setup_pin_code": "Seadista PIN-kood", "share": "Jaga", "share_add_photos": "Lisa fotosid", - "share_assets_selected": "{} valitud", + "share_assets_selected": "{count} valitud", + "share_dialog_preparing": "Ettevalmistamine...", "shared": "Jagatud", + "shared_album_activities_input_disable": "Kommentaarid on keelatud", + "shared_album_activity_remove_content": "Kas soovid selle tegevuse kustutada?", + "shared_album_activity_remove_title": "Kustuta tegevus", "shared_album_section_people_action_error": "Viga albumist eemaldamisel/lahkumisel", "shared_album_section_people_action_leave": "Eemalda kasutaja albumist", "shared_album_section_people_action_remove_user": "Eemalda kasutaja albumist", @@ -1455,27 +1653,33 @@ "shared_by_user": "Jagas {user}", "shared_by_you": "Jagasid sina", "shared_from_partner": "Fotod partnerilt {partner}", + "shared_intent_upload_button_progress_text": "{current} / {total} üles laaditud", "shared_link_app_bar_title": "Jagatud lingid", "shared_link_clipboard_copied_massage": "Kopeeritud lõikelauale", - "shared_link_clipboard_text": "Link: {}\nParool: {}", + "shared_link_clipboard_text": "Link: {link}\nParool: {password}", "shared_link_create_error": "Viga jagatud lingi loomisel", + "shared_link_edit_description_hint": "Sisesta jagatud lingi kirjeldus", "shared_link_edit_expire_after_option_day": "1 päev", - "shared_link_edit_expire_after_option_days": "{} päeva", + "shared_link_edit_expire_after_option_days": "{count} päeva", "shared_link_edit_expire_after_option_hour": "1 tund", - "shared_link_edit_expire_after_option_hours": "{} tundi", + "shared_link_edit_expire_after_option_hours": "{count} tundi", "shared_link_edit_expire_after_option_minute": "1 minut", - "shared_link_edit_expire_after_option_minutes": "{} minutit", - "shared_link_edit_expire_after_option_months": "{} kuud", - "shared_link_edit_expire_after_option_year": "{} aasta", - "shared_link_expires_day": "Aegub {} päeva pärast", - "shared_link_expires_days": "Aegub {} päeva pärast", - "shared_link_expires_hour": "Aegub {} tunni pärast", - "shared_link_expires_hours": "Aegub {} tunni pärast", - "shared_link_expires_minute": "Aegub {} minuti pärast", - "shared_link_expires_minutes": "Aegub {} minuti pärast", + "shared_link_edit_expire_after_option_minutes": "{count} minutit", + "shared_link_edit_expire_after_option_months": "{count} kuud", + "shared_link_edit_expire_after_option_year": "{count} aasta", + "shared_link_edit_password_hint": "Sisesta jagatud lingi parool", + "shared_link_edit_submit_button": "Muuda link", + "shared_link_error_server_url_fetch": "Serveri URL-i ei leitud", + "shared_link_expires_day": "Aegub {count} päeva pärast", + "shared_link_expires_days": "Aegub {count} päeva pärast", + "shared_link_expires_hour": "Aegub {count} tunni pärast", + "shared_link_expires_hours": "Aegub {count} tunni pärast", + "shared_link_expires_minute": "Aegub {count} minuti pärast", + "shared_link_expires_minutes": "Aegub {count} minuti pärast", "shared_link_expires_never": "Ei aegu", - "shared_link_expires_second": "Aegub {} sekundi pärast", - "shared_link_expires_seconds": "Aegub {} sekundi pärast", + "shared_link_expires_second": "Aegub {count} sekundi pärast", + "shared_link_expires_seconds": "Aegub {count} sekundi pärast", + "shared_link_individual_shared": "Individuaalselt jagatud", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Halda jagatud linke", "shared_link_options": "Jagatud lingi valikud", @@ -1487,6 +1691,8 @@ "sharing": "Jagamine", "sharing_enter_password": "Palun sisesta selle lehe vaatamiseks salasõna.", "sharing_page_album": "Jagatud albumid", + "sharing_page_description": "Loo jagatud albumeid, et jagada fotosid ja videosid inimestega oma võrgustikus.", + "sharing_page_empty_list": "TÜHI LOEND", "sharing_sidebar_description": "Kuva külgmenüüs Jagamise linki", "sharing_silver_appbar_create_shared_album": "Uus jagatud album", "sharing_silver_appbar_share_partner": "Jaga partneriga", @@ -1547,6 +1753,7 @@ "stop_sharing_photos_with_user": "Lõpeta oma fotode selle kasutajaga jagamine", "storage": "Talletusruum", "storage_label": "Talletussilt", + "storage_quota": "Talletuskvoot", "storage_usage": "{used}/{available} kasutatud", "submit": "Saada", "suggestions": "Soovitused", @@ -1557,6 +1764,8 @@ "swap_merge_direction": "Muuda ühendamise suunda", "sync": "Sünkrooni", "sync_albums": "Sünkrooni albumid", + "sync_albums_manual_subtitle": "Sünkrooni kõik üleslaaditud videod ja fotod valitud varundusalbumitesse", + "sync_upload_album_setting_subtitle": "Loo ja laadi oma pildid ja videod üles Immich'isse valitud albumitesse", "tag": "Silt", "tag_assets": "Sildista üksuseid", "tag_created": "Lisatud silt: {tag}", @@ -1570,13 +1779,19 @@ "theme": "Teema", "theme_selection": "Teema valik", "theme_selection_description": "Sea automaatselt hele või tume teema vastavalt veebilehitseja eelistustele", - "theme_setting_colorful_interface_subtitle": "Rakenda taustapindadele primaarne värv.", + "theme_setting_asset_list_storage_indicator_title": "Kuva üksuste ruutudel talletusindikaatorit", + "theme_setting_asset_list_tiles_per_row_title": "Üksuste arv reas ({count})", + "theme_setting_colorful_interface_subtitle": "Rakenda taustapindadele põhivärv.", "theme_setting_colorful_interface_title": "Värviline kasutajaliides", "theme_setting_image_viewer_quality_subtitle": "Kohanda detailvaaturi kvaliteeti", "theme_setting_image_viewer_quality_title": "Pildivaaturi kvaliteet", + "theme_setting_primary_color_subtitle": "Vali värv põhitegevuste ja aktsentide jaoks.", "theme_setting_primary_color_title": "Põhivärv", "theme_setting_system_primary_color_title": "Kasuta süsteemset värvi", "theme_setting_system_theme_switch": "Automaatne (järgi süsteemi seadet)", + "theme_setting_theme_subtitle": "Vali rakenduse teema seade", + "theme_setting_three_stage_loading_subtitle": "Kolmeastmeline laadimine võib parandada laadimise jõudlust, aga põhjustab oluliselt suuremat võrgukoormust", + "theme_setting_three_stage_loading_title": "Luba kolmeastmeline laadimine", "they_will_be_merged_together": "Nad ühendatakse kokku", "third_party_resources": "Kolmanda osapoole ressursid", "time_based_memories": "Ajapõhised mälestused", @@ -1599,10 +1814,16 @@ "trash_emptied": "Prügikast tühjendatud", "trash_no_results_message": "Siia ilmuvad prügikasti liigutatud fotod ja videod.", "trash_page_delete_all": "Kustuta kõik", + "trash_page_empty_trash_dialog_content": "Kas soovid prügikasti liigutatud üksused kustutada? Need eemaldatakse Immich'ist jäädavalt", + "trash_page_info": "Prügikasti liigutatud üksused kustutatakse jäädavalt {days} päeva pärast", + "trash_page_no_assets": "Prügikastis üksuseid pole", "trash_page_restore_all": "Taasta kõik", "trash_page_select_assets_btn": "Vali üksused", + "trash_page_title": "Prügikast ({count})", "trashed_items_will_be_permanently_deleted_after": "Prügikasti tõstetud üksused kustutatakse jäädavalt {days, plural, one {# päeva} other {# päeva}} pärast.", "type": "Tüüp", + "unable_to_change_pin_code": "PIN-koodi muutmine ebaõnnestus", + "unable_to_setup_pin_code": "PIN-koodi seadistamine ebaõnnestus", "unarchive": "Taasta arhiivist", "unarchived_count": "{count, plural, other {# arhiivist taastatud}}", "unfavorite": "Eemalda lemmikutest", @@ -1626,9 +1847,12 @@ "untracked_files": "Mittejälgitavad failid", "untracked_files_decription": "Rakendus ei jälgi neid faile. Need võivad olla põhjustatud ebaõnnestunud liigutamisest, katkestatud üleslaadimisest või rakenduse veast", "up_next": "Järgmine", + "updated_at": "Uuendatud", "updated_password": "Parool muudetud", "upload": "Laadi üles", "upload_concurrency": "Üleslaadimise samaaegsus", + "upload_dialog_info": "Kas soovid valitud üksuse(d) serverisse varundada?", + "upload_dialog_title": "Üksuse üleslaadimine", "upload_errors": "Üleslaadimine lõpetatud {count, plural, one {# veaga} other {# veaga}}, uute üksuste nägemiseks värskenda lehte.", "upload_progress": "Ootel {remaining, number} - Töödeldud {processed, number}/{total, number}", "upload_skipped_duplicates": "{count, plural, one {# dubleeritud üksus} other {# dubleeritud üksust}} vahele jäetud", @@ -1636,13 +1860,18 @@ "upload_status_errors": "Vead", "upload_status_uploaded": "Üleslaaditud", "upload_success": "Üleslaadimine õnnestus, uute üksuste nägemiseks värskenda lehte.", + "upload_to_immich": "Laadi Immich'isse ({count})", "uploading": "Üleslaadimine", "url": "URL", "usage": "Kasutus", + "use_current_connection": "kasuta praegust ühendust", "use_custom_date_range": "Kasuta kohandatud kuupäevavahemikku", "user": "Kasutaja", + "user_has_been_deleted": "See kasutaja on kustutatud.", "user_id": "Kasutaja ID", "user_liked": "Kasutajale {user} meeldis {type, select, photo {see foto} video {see video} asset {see üksus} other {see}}", + "user_pin_code_settings": "PIN-kood", + "user_pin_code_settings_description": "Halda oma PIN-koodi", "user_purchase_settings": "Ost", "user_purchase_settings_description": "Halda oma ostu", "user_role_set": "Määra kasutajale {user} roll {role}", @@ -1653,10 +1882,15 @@ "users": "Kasutajad", "utilities": "Tööriistad", "validate": "Valideeri", + "validate_endpoint_error": "Sisesta korrektne URL", "variables": "Muutujad", "version": "Versioon", "version_announcement_closing": "Sinu sõber, Alex", "version_announcement_message": "Hei! Saadaval on uus Immich'i versioon. Palun võta aega, et lugeda väljalasketeadet ning veendu, et su seadistus on ajakohane, et vältida konfiguratsiooniprobleeme, eriti kui kasutad WatchTower'it või muud mehhanismi, mis Immich'it automaatselt uuendab.", + "version_announcement_overlay_release_notes": "väljalasketeadet", + "version_announcement_overlay_text_1": "Hei sõber, on saadaval uus versioon rakendusest", + "version_announcement_overlay_text_2": "palun võta aega, et lugeda ", + "version_announcement_overlay_text_3": " ning veendu, et su docker-compose ja .env seadistus on ajakohane, et vältida konfiguratsiooniprobleeme, eriti kui kasutad WatchTower'it või muud mehhanismi, mis serveripoolset rakendust automaatselt uuendab.", "version_announcement_overlay_title": "Uus serveri versioon saadaval 🎉", "version_history": "Versiooniajalugu", "version_history_item": "Versioon {version} paigaldatud {date}", @@ -1678,6 +1912,7 @@ "view_qr_code": "Vaata QR-koodi", "view_stack": "Vaata virna", "viewer_remove_from_stack": "Eemalda virnast", + "viewer_stack_use_as_main_asset": "Kasuta peamise üksusena", "viewer_unstack": "Eralda", "visibility_changed": "{count, plural, one {# isiku} other {# isiku}} nähtavus muudetud", "waiting": "Ootel", diff --git a/i18n/eu.json b/i18n/eu.json index 0967ef424b..f2840bfebd 100644 --- a/i18n/eu.json +++ b/i18n/eu.json @@ -1 +1,18 @@ -{} +{ + "active": "Martxan", + "add": "Gehitu", + "add_a_description": "Azalpena gehitu", + "add_a_name": "Izena gehitu", + "add_a_title": "Izenburua gehitu", + "add_more_users": "Erabiltzaile gehiago gehitu", + "add_photos": "Argazkiak gehitu", + "add_to_album": "Albumera gehitu", + "add_to_album_bottom_sheet_already_exists": "Dagoeneko {album} albumenean", + "add_to_shared_album": "Gehitu partekatutako albumera", + "add_url": "URL-a gehitu", + "added_to_favorites": "Faboritoetara gehituta", + "admin": { + "cleanup": "Garbiketa", + "image_quality": "Kalitatea" + } +} diff --git a/i18n/fa.json b/i18n/fa.json index 0b50274f4d..ae9dc26b09 100644 --- a/i18n/fa.json +++ b/i18n/fa.json @@ -4,6 +4,7 @@ "account_settings": "تنظیمات حساب کاربری", "acknowledge": "متوجه شدم", "action": "عملکرد", + "action_common_update": "به‌ روز‌رسانی", "actions": "عملکرد", "active": "فعال", "activity": "فعالیت", diff --git a/i18n/fi.json b/i18n/fi.json index e57c758419..a5de8c81d9 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -2,13 +2,13 @@ "about": "Tietoja", "account": "Tili", "account_settings": "Tilin asetukset", - "acknowledge": "Tiedostan", + "acknowledge": "Hyväksy", "action": "Toiminta", "action_common_update": "Päivitä", "actions": "Toimintoja", "active": "Aktiivinen", - "activity": "Aktiviteetti", - "activity_changed": "Aktiviteetti {enabled, select, true {otettu käyttöön} other {poistettu käytöstä}}", + "activity": "Tapahtumat", + "activity_changed": "Toiminto {enabled, select, true {otettu käyttöön} other {poistettu käytöstä}}", "add": "Lisää", "add_a_description": "Lisää kuvaus", "add_a_location": "Lisää sijainti", @@ -53,6 +53,7 @@ "confirm_email_below": "Kirjota \"{email}\" vahvistaaksesi", "confirm_reprocess_all_faces": "Haluatko varmasti käsitellä uudelleen kaikki kasvot? Tämä poistaa myös nimetyt henkilöt.", "confirm_user_password_reset": "Haluatko varmasti nollata käyttäjän {user} salasanan?", + "confirm_user_pin_code_reset": "Haluatko varmasti nollata käyttäjän {user} PIN-koodin?", "create_job": "Luo tehtävä", "cron_expression": "Cron-lauseke", "cron_expression_description": "Aseta skannausväli käyttämällä cron-formaattia. Lisätietoja linkistä. Crontab Guru", @@ -348,6 +349,7 @@ "user_delete_delay_settings_description": "Kuinka monta päivää poistamisen jälkeen käyttäjä ja hänen aineistonsa poistetaan pysyvästi. Joka keskiyö käydään läpi poistettavaksi merkityt käyttäjät. Tämä muutos astuu voimaan seuraavalla ajokerralla.", "user_delete_immediately": "{user}:n tili ja sen kohteet on ajastettu poistettavaksi heti.", "user_delete_immediately_checkbox": "Aseta tili ja sen kohteet jonoon välitöntä poistoa varten", + "user_details": "Käyttäjätiedot", "user_management": "Käyttäjien hallinta", "user_password_has_been_reset": "Käyttäjän salasana on nollattu:", "user_password_reset_description": "Anna väliaikainen salasana ja ohjeista käyttäjää vaihtamaan se seuraavan kirjautumisen yhteydessä.", @@ -369,11 +371,11 @@ "advanced": "Edistyneet", "advanced_settings_enable_alternate_media_filter_subtitle": "Käytä tätä vaihtoehtoa suodattaaksesi mediaa synkronoinnin aikana vaihtoehtoisten kriteerien perusteella. Kokeile tätä vain, jos sovelluksessa on ongelmia kaikkien albumien tunnistamisessa.", "advanced_settings_enable_alternate_media_filter_title": "[KOKEELLINEN] Käytä vaihtoehtoisen laitteen albumin synkronointisuodatinta", - "advanced_settings_log_level_title": "Kirjaustaso: {}", + "advanced_settings_log_level_title": "Kirjaustaso: {level}", "advanced_settings_prefer_remote_subtitle": "Jotkut laitteet ovat erittäin hitaita lataamaan esikatselukuvia laitteen kohteista. Aktivoi tämä asetus käyttääksesi etäkuvia.", "advanced_settings_prefer_remote_title": "Suosi etäkuvia", "advanced_settings_proxy_headers_subtitle": "Määritä välityspalvelimen otsikot(proxy headers), jotka Immichin tulisi lähettää jokaisen verkkopyynnön mukana", - "advanced_settings_proxy_headers_title": "Proxy Headers", + "advanced_settings_proxy_headers_title": "Välityspalvelimen otsikot", "advanced_settings_self_signed_ssl_subtitle": "Ohita SSL sertifikaattivarmennus palvelimen päätepisteellä. Vaaditaan self-signed -sertifikaateissa.", "advanced_settings_self_signed_ssl_title": "Salli self-signed SSL -sertifikaatit", "advanced_settings_sync_remote_deletions_subtitle": "Poista tai palauta kohde automaattisesti tällä laitteella, kun kyseinen toiminto suoritetaan verkossa", @@ -400,9 +402,9 @@ "album_remove_user_confirmation": "Oletko varma että haluat poistaa {user}?", "album_share_no_users": "Näyttää että olet jakanut tämän albumin kaikkien kanssa, tai sinulla ei ole käyttäjiä joille jakaa.", "album_thumbnail_card_item": "1 kohde", - "album_thumbnail_card_items": "{} kohdetta", + "album_thumbnail_card_items": "{count} kohdetta", "album_thumbnail_card_shared": " · Jaettu", - "album_thumbnail_shared_by": "Jakanut {}", + "album_thumbnail_shared_by": "Jakanut {user}", "album_updated": "Albumi päivitetty", "album_updated_setting_description": "Saa sähköpostia kun jaetussa albumissa on uutta sisältöä", "album_user_left": "Poistuttiin albumista {album}", @@ -440,7 +442,7 @@ "archive": "Arkisto", "archive_or_unarchive_photo": "Arkistoi kuva tai palauta arkistosta", "archive_page_no_archived_assets": "Arkistoituja kohteita ei löytynyt", - "archive_page_title": "Arkisto ({})", + "archive_page_title": "Arkisto ({count})", "archive_size": "Arkiston koko", "archive_size_description": "Määritä arkiston koko latauksissa (Gt)", "archived": "Arkistoitu", @@ -472,23 +474,23 @@ "asset_uploading": "Ladataan…", "asset_viewer_settings_subtitle": "Galleriakatseluohjelman asetusten hallinta", "asset_viewer_settings_title": "Katselin", - "assets": "kohdetta", + "assets": "Kohteet", "assets_added_count": "Lisätty {count, plural, one {# kohde} other {# kohdetta}}", "assets_added_to_album_count": "Albumiin lisätty {count, plural, one {# kohde} other {# kohdetta}}", "assets_added_to_name_count": "Lisätty {count, plural, one {# kohde} other {# kohdetta}} {hasName, select, true {{name}} other {uuteen albumiin}}", "assets_count": "{count, plural, one {# media} other {# mediaa}}", - "assets_deleted_permanently": "{} kohdetta poistettu pysyvästi", - "assets_deleted_permanently_from_server": "{} objektia poistettu pysyvästi Immich-palvelimelta", + "assets_deleted_permanently": "{count} kohdetta poistettu pysyvästi", + "assets_deleted_permanently_from_server": "{count} objektia poistettu pysyvästi Immich-palvelimelta", "assets_moved_to_trash_count": "Siirretty {count, plural, one {# media} other {# mediaa}} roskakoriin", "assets_permanently_deleted_count": "{count, plural, one {# media} other {# mediaa}} poistettu pysyvästi", "assets_removed_count": "{count, plural, one {# media} other {# mediaa}} poistettu", - "assets_removed_permanently_from_device": "{} kohdetta on poistettu pysyvästi laitteeltasi", - "assets_restore_confirmation": "Haluatko varmasti palauttaa kaikki roskakoriisi siirretyt resurssit? Tätä toimintoa ei voi peruuttaa! Huomaa, että offline-resursseja ei voida palauttaa tällä tavalla.", + "assets_removed_permanently_from_device": "{count} kohdetta on poistettu pysyvästi laitteeltasi", + "assets_restore_confirmation": "Haluatko varmasti palauttaa kaikki roskakoriisi siirretyt kohteet? Tätä toimintoa ei voi peruuttaa! Huomaa, että offline-kohteita ei voida palauttaa tällä tavalla.", "assets_restored_count": "{count, plural, one {# media} other {# mediaa}} palautettu", - "assets_restored_successfully": "{} kohdetta palautettu onnistuneesti", - "assets_trashed": "{} kohdetta siirretty roskakoriin", + "assets_restored_successfully": "{count} kohdetta palautettu onnistuneesti", + "assets_trashed": "{count} kohdetta siirretty roskakoriin", "assets_trashed_count": "{count, plural, one {# media} other {# mediaa}} siirretty roskakoriin", - "assets_trashed_from_server": "{} kohdetta siirretty roskakoriin Immich-palvelimelta", + "assets_trashed_from_server": "{count} kohdetta siirretty roskakoriin Immich-palvelimelta", "assets_were_part_of_album_count": "{count, plural, one {Media oli} other {Mediat olivat}} jo albumissa", "authorized_devices": "Valtuutetut laitteet", "automatic_endpoint_switching_subtitle": "Yhdistä paikallisesti nimetyn Wi-Fi-yhteyden kautta, kun se on saatavilla, ja käytä vaihtoehtoisia yhteyksiä muualla", @@ -497,20 +499,20 @@ "back_close_deselect": "Palaa, sulje tai poista valinnat", "background_location_permission": "Taustasijainnin käyttöoikeus", "background_location_permission_content": "Jotta sovellus voi vaihtaa verkkoa taustalla toimiessaan, Immichillä on *aina* oltava pääsy tarkkaan sijaintiin, jotta se voi lukea Wi-Fi-verkon nimen", - "backup_album_selection_page_albums_device": "Laitteen albumit ({})", + "backup_album_selection_page_albums_device": "Laitteen albumit ({count})", "backup_album_selection_page_albums_tap": "Napauta sisällyttääksesi, kaksoisnapauta jättääksesi pois", "backup_album_selection_page_assets_scatter": "Kohteet voivat olla hajaantuneina useisiin albumeihin. Albumeita voidaan sisällyttää varmuuskopiointiin tai jättää siitä pois.", "backup_album_selection_page_select_albums": "Valitse albumit", "backup_album_selection_page_selection_info": "Valintatiedot", - "backup_album_selection_page_total_assets": "Uniikkeja kohteita yhteensä", + "backup_album_selection_page_total_assets": "Ainulaatuisia kohteita yhteensä", "backup_all": "Kaikki", "backup_background_service_backup_failed_message": "Kohteiden varmuuskopiointi epäonnistui. Yritetään uudelleen…", "backup_background_service_connection_failed_message": "Palvelimeen ei saatu yhteyttä. Yritetään uudelleen…", - "backup_background_service_current_upload_notification": "Lähetetään {}", + "backup_background_service_current_upload_notification": "Lähetetään {filename}", "backup_background_service_default_notification": "Tarkistetaan uusia kohteita…", "backup_background_service_error_title": "Virhe varmuuskopioinnissa", "backup_background_service_in_progress_notification": "Varmuuskopioidaan kohteita…", - "backup_background_service_upload_failure_notification": "Lähetys epäonnistui {}", + "backup_background_service_upload_failure_notification": "Lähetys epäonnistui {filename}", "backup_controller_page_albums": "Varmuuskopioi albumit", "backup_controller_page_background_app_refresh_disabled_content": "Salli sovelluksen päivittäminen taustalla suorittaaksesi varmuuskopiointia taustalla: Asetukset > Yleiset > Appien päivitys taustalla.", "backup_controller_page_background_app_refresh_disabled_title": "Sovelluksen päivittäminen taustalla on pois päältä", @@ -521,7 +523,7 @@ "backup_controller_page_background_battery_info_title": "Akun optimointi", "backup_controller_page_background_charging": "Vain laitteen ollessa kytkettynä laturiin", "backup_controller_page_background_configure_error": "Taustapalvelun asettaminen epäonnistui", - "backup_controller_page_background_delay": "Viivästytä uusien kohteiden varmuuskopiointia: {}", + "backup_controller_page_background_delay": "Viivästytä uusien kohteiden varmuuskopiointia: {duration}", "backup_controller_page_background_description": "Kytke taustapalvelu päälle varmuuskopioidaksesi uudet kohteet automaattisesti, ilman sovelluksen avaamista", "backup_controller_page_background_is_off": "Automaattinen varmuuskopiointi taustalla on pois päältä", "backup_controller_page_background_is_on": "Automaattinen varmuuskopiointi taustalla on päällä", @@ -531,12 +533,12 @@ "backup_controller_page_backup": "Varmuuskopiointi", "backup_controller_page_backup_selected": "Valittu: ", "backup_controller_page_backup_sub": "Varmuuskopioidut kuvat ja videot", - "backup_controller_page_created": "Luotu: {}", + "backup_controller_page_created": "Luotu: {date}", "backup_controller_page_desc_backup": "Kytke varmuuskopiointi päälle lähettääksesi uudet kohteet palvelimelle automaattisesti.", "backup_controller_page_excluded": "Poissuljettu: ", - "backup_controller_page_failed": "Epäonnistui ({})", - "backup_controller_page_filename": "Tiedostonimi: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Epäonnistui ({count})", + "backup_controller_page_filename": "Tiedostonimi: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Varmuuskopioinnin tiedot", "backup_controller_page_none_selected": "Ei mitään", "backup_controller_page_remainder": "Jäljellä", @@ -545,14 +547,14 @@ "backup_controller_page_start_backup": "Aloita varmuuskopiointi", "backup_controller_page_status_off": "Varmuuskopiointi on pois päältä", "backup_controller_page_status_on": "Varmuuskopiointi on päällä", - "backup_controller_page_storage_format": "{} / {} käytetty", + "backup_controller_page_storage_format": "{used} / {total} käytetty", "backup_controller_page_to_backup": "Varmuuskopioitavat albumit", "backup_controller_page_total_sub": "Kaikki uniikit kuvat ja videot valituista albumeista", "backup_controller_page_turn_off": "Varmuuskopiointi pois päältä", "backup_controller_page_turn_on": "Varmuuskopiointi päälle", "backup_controller_page_uploading_file_info": "Tiedostojen lähetystiedot", "backup_err_only_album": "Vähintään yhden albumin tulee olla valittuna", - "backup_info_card_assets": "kohdetta", + "backup_info_card_assets": "kohteet", "backup_manual_cancelled": "Peruutettu", "backup_manual_in_progress": "Lähetys palvelimelle on jo käynnissä. Kokeile myöhemmin uudelleen", "backup_manual_success": "Onnistui", @@ -570,21 +572,21 @@ "bulk_keep_duplicates_confirmation": "Haluatko varmasti säilyttää {count, plural, one {# kaksoiskappaleen} other {# kaksoiskappaleet}}? Tämä merkitsee kaikki kaksoiskappaleet ratkaistuiksi, eikä poista mitään.", "bulk_trash_duplicates_confirmation": "Haluatko varmasti siirtää {count, plural, one {# kaksoiskappaleen} other {# kaksoiskappaleet}} roskakoriin? Tämä säilyttää kustakin mediasta kookkaimman ja siirtää loput roskakoriin.", "buy": "Osta lisenssi Immich:iin", - "cache_settings_album_thumbnails": "Kirjastosivun esikatselukuvat ({} kohdetta)", + "cache_settings_album_thumbnails": "Kirjastosivun esikatselukuvat ({count} kohdetta)", "cache_settings_clear_cache_button": "Tyhjennä välimuisti", "cache_settings_clear_cache_button_title": "Tyhjennä sovelluksen välimuisti. Tämä vaikuttaa merkittävästi sovelluksen suorituskykyyn, kunnes välimuisti on rakennettu uudelleen.", "cache_settings_duplicated_assets_clear_button": "Tyhjennä", "cache_settings_duplicated_assets_subtitle": "Sovelluksen mustalle listalle merkitsemät valokuvat ja videot", - "cache_settings_duplicated_assets_title": "Kaksoiskappaleet ({})", - "cache_settings_image_cache_size": "Kuvavälimuistin koko ({} kohdetta)", + "cache_settings_duplicated_assets_title": "Kaksoiskappaleet ({count})", + "cache_settings_image_cache_size": "Kuvavälimuistin koko ({count} kohdetta)", "cache_settings_statistics_album": "Kirjaston esikatselukuvat", - "cache_settings_statistics_assets": "{} kohdetta ({})", + "cache_settings_statistics_assets": "{count} kohdetta ({size})", "cache_settings_statistics_full": "Täysikokoiset kuvat", "cache_settings_statistics_shared": "Jaettujen albumien esikatselukuvat", "cache_settings_statistics_thumbnail": "Esikatselukuvat", "cache_settings_statistics_title": "Välimuistin käyttö", "cache_settings_subtitle": "Hallitse Immich-mobiilisovelluksen välimuistin käyttöä", - "cache_settings_thumbnail_size": "Esikatselukuvien välimuistin koko ({} kohdetta)", + "cache_settings_thumbnail_size": "Esikatselukuvien välimuistin koko ({count} kohdetta)", "cache_settings_tile_subtitle": "Hallitse paikallista tallenustilaa", "cache_settings_tile_title": "Paikallinen tallennustila", "cache_settings_title": "Välimuistin asetukset", @@ -610,6 +612,7 @@ "change_password_form_new_password": "Uusi salasana", "change_password_form_password_mismatch": "Salasanat eivät täsmää", "change_password_form_reenter_new_password": "Uusi salasana uudelleen", + "change_pin_code": "Vaihda PIN-koodi", "change_your_password": "Vaihda salasanasi", "changed_visibility_successfully": "Näkyvyys vaihdettu", "check_all": "Valitse kaikki", @@ -650,11 +653,12 @@ "confirm_delete_face": "Haluatko poistaa {name} kasvot kohteesta?", "confirm_delete_shared_link": "Haluatko varmasti poistaa tämän jaetun linkin?", "confirm_keep_this_delete_others": "Kuvapinon muut kuvat tätä lukuunottamatta poistetaan. Oletko varma, että haluat jatkaa?", + "confirm_new_pin_code": "Vahvista uusi PIN-koodi", "confirm_password": "Vahvista salasana", "contain": "Mahduta", "context": "Konteksti", "continue": "Jatka", - "control_bottom_app_bar_album_info_shared": "{} kohdetta · Jaettu", + "control_bottom_app_bar_album_info_shared": "{count} kohdetta · Jaettu", "control_bottom_app_bar_create_new_album": "Luo uusi albumi", "control_bottom_app_bar_delete_from_immich": "Poista Immichistä", "control_bottom_app_bar_delete_from_local": "Poista laitteelta", @@ -692,19 +696,21 @@ "create_tag_description": "Luo uusi tunniste. Sisäkkäisiä tunnisteita varten syötä tunnisteen täydellinen polku kauttaviivat mukaan luettuna.", "create_user": "Luo käyttäjä", "created": "Luotu", + "created_at": "Luotu", "crop": "Rajaa", "curated_object_page_title": "Asiat", "current_device": "Nykyinen laite", + "current_pin_code": "Nykyinen PIN-koodi", "current_server_address": "Nykyinen palvelinosoite", "custom_locale": "Muokatut maa-asetukset", "custom_locale_description": "Muotoile päivämäärät ja numerot perustuen alueen kieleen", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", + "daily_title_text_date": "E, dd MMM", + "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Tumma", "date_after": "Päivämäärän jälkeen", "date_and_time": "Päivämäärä ja aika", "date_before": "Päivä ennen", - "date_format": "E, LLL d, y • h:mm a", + "date_format": "E d. LLL y • hh:mm", "date_of_birth_saved": "Syntymäaika tallennettu", "date_range": "Päivämäärän rajaus", "day": "Päivä", @@ -738,11 +744,11 @@ "delete_tag_confirmation_prompt": "Haluatko varmasti poistaa tunnisteen {tagName}?", "delete_user": "Poista käyttäjä", "deleted_shared_link": "Jaettu linkki poistettu", - "deletes_missing_assets": "Poistaa levyltä puuttuvat resurssit", + "deletes_missing_assets": "Poistaa levyltä puuttuvat kohteet", "description": "Kuvaus", "description_input_hint_text": "Lisää kuvaus...", "description_input_submit_error": "Virhe kuvauksen päivittämisessä, tarkista lisätiedot lokista", - "details": "TIEDOT", + "details": "Tiedot", "direction": "Suunta", "disabled": "Poistettu käytöstä", "disallow_edits": "Älä salli muokkauksia", @@ -763,7 +769,7 @@ "download_enqueue": "Latausjonossa", "download_error": "Download Error", "download_failed": "Lataus epäonnistui", - "download_filename": "tiedosto: {}", + "download_filename": "tiedosto: {filename}", "download_finished": "Lataus valmis", "download_include_embedded_motion_videos": "Upotetut videot", "download_include_embedded_motion_videos_description": "Sisällytä liikekuviin upotetut videot erillisinä tiedostoina", @@ -807,6 +813,7 @@ "editor_crop_tool_h2_aspect_ratios": "Kuvasuhteet", "editor_crop_tool_h2_rotation": "Rotaatio", "email": "Sähköposti", + "email_notifications": "Sähköposti-ilmoitukset", "empty_folder": "Kansio on tyhjä", "empty_trash": "Tyhjennä roskakori", "empty_trash_confirmation": "Haluatko varmasti tyhjentää roskakorin? Tämä poistaa pysyvästi kaikki tiedostot Immich:stä.\nToimintoa ei voi perua!", @@ -819,7 +826,7 @@ "error_change_sort_album": "Albumin lajittelujärjestyksen muuttaminen epäonnistui", "error_delete_face": "Virhe kasvojen poistamisessa kohteesta", "error_loading_image": "Kuvan lataus ei onnistunut", - "error_saving_image": "Virhe: {}", + "error_saving_image": "Virhe: {error}", "error_title": "Virhe - Jotain meni pieleen", "errors": { "cannot_navigate_next_asset": "Seuraavaan mediaan ei voi siirtyä", @@ -922,6 +929,7 @@ "unable_to_remove_reaction": "Reaktion poistaminen epäonnistui", "unable_to_repair_items": "Kohteiden korjaaminen epäonnistui", "unable_to_reset_password": "Salasanan nollaaminen epäonnistui", + "unable_to_reset_pin_code": "PIN-koodin nollaaminen epäonnistui", "unable_to_resolve_duplicate": "Kaksoiskappaleen ratkaiseminen epäonnistui", "unable_to_restore_assets": "Kohteen palauttaminen epäonnistui", "unable_to_restore_trash": "Kohteiden palauttaminen epäonnistui", @@ -955,10 +963,10 @@ "exif_bottom_sheet_location": "SIJAINTI", "exif_bottom_sheet_people": "IHMISET", "exif_bottom_sheet_person_add_person": "Lisää nimi", - "exif_bottom_sheet_person_age": "Ikä {}", - "exif_bottom_sheet_person_age_months": "Ikä {} kuukautta", - "exif_bottom_sheet_person_age_year_months": "Ikä 1 vuosi, {} kuukautta", - "exif_bottom_sheet_person_age_years": "Ikä {}", + "exif_bottom_sheet_person_age": "Ikä {age}", + "exif_bottom_sheet_person_age_months": "Ikä {months} kuukautta", + "exif_bottom_sheet_person_age_year_months": "Ikä 1 vuosi, {months} kuukautta", + "exif_bottom_sheet_person_age_years": "Ikä {years}", "exit_slideshow": "Poistu diaesityksestä", "expand_all": "Laajenna kaikki", "experimental_settings_new_asset_list_subtitle": "Työn alla", @@ -1021,8 +1029,8 @@ "has_quota": "On kiintiö", "header_settings_add_header_tip": "Lisää otsikko", "header_settings_field_validator_msg": "Arvo ei voi olla tyhjä", - "header_settings_header_name_input": "Header name", - "header_settings_header_value_input": "Header value", + "header_settings_header_name_input": "Otsikon nimi", + "header_settings_header_value_input": "Otsikon arvo", "headers_settings_tile_subtitle": "Määritä välityspalvelimen otsikot, jotka sovelluksen tulisi lähettää jokaisen verkkopyynnön mukana", "headers_settings_tile_title": "Mukautettu proxy headers", "hi_user": "Hei {name} ({email})", @@ -1048,6 +1056,7 @@ "home_page_upload_err_limit": "Voit lähettää palvelimelle enintään 30 kohdetta kerrallaan, ohitetaan", "host": "Isäntä", "hour": "Tunti", + "id": "ID", "ignore_icloud_photos": "Ohita iCloud-kuvat", "ignore_icloud_photos_description": "iCloudiin tallennettuja kuvia ei ladata Immich-palvelimelle", "image": "Kuva", @@ -1173,8 +1182,8 @@ "manage_your_devices": "Hallitse sisäänkirjautuneita laitteitasi", "manage_your_oauth_connection": "Hallitse OAuth-yhteyttäsi", "map": "Kartta", - "map_assets_in_bound": "{} kuva", - "map_assets_in_bounds": "{} kuvaa", + "map_assets_in_bound": "{count} kuva", + "map_assets_in_bounds": "{count} kuvaa", "map_cannot_get_user_location": "Käyttäjän sijaintia ei voitu määrittää", "map_location_dialog_yes": "Kyllä", "map_location_picker_page_use_location": "Käytä tätä sijaintia", @@ -1188,9 +1197,9 @@ "map_settings": "Kartta-asetukset", "map_settings_dark_mode": "Tumma tila", "map_settings_date_range_option_day": "Viimeiset 24 tuntia", - "map_settings_date_range_option_days": "Viimeiset {} päivää", + "map_settings_date_range_option_days": "Viimeiset {days} päivää", "map_settings_date_range_option_year": "Viimeisin vuosi", - "map_settings_date_range_option_years": "Viimeiset {} vuotta", + "map_settings_date_range_option_years": "Viimeiset {years} vuotta", "map_settings_dialog_title": "Kartta-asetukset", "map_settings_include_show_archived": "Sisällytä arkistoidut", "map_settings_include_show_partners": "Sisällytä kumppanit", @@ -1209,7 +1218,7 @@ "memories_start_over": "Aloita alusta", "memories_swipe_to_close": "Pyyhkäise ylös sulkeaksesi", "memories_year_ago": "Vuosi sitten", - "memories_years_ago": "{} vuotta sitten", + "memories_years_ago": "{years, plural, other {# vuotta}} sitten", "memory": "Muisto", "memory_lane_title": "Muistojen polku {title}", "menu": "Valikko", @@ -1242,6 +1251,7 @@ "new_api_key": "Uusi API-avain", "new_password": "Uusi salasana", "new_person": "Uusi henkilö", + "new_pin_code": "Uusi PIN-koodi", "new_user_created": "Uusi käyttäjä lisätty", "new_version_available": "UUSI VERSIO SAATAVILLA", "newest_first": "Uusin ensin", @@ -1316,7 +1326,7 @@ "partner_page_partner_add_failed": "Kumppanin lisääminen epäonnistui", "partner_page_select_partner": "Valitse kumppani", "partner_page_shared_to_title": "Jaettu henkilöille", - "partner_page_stop_sharing_content": "{} ei voi enää käyttää kuviasi.", + "partner_page_stop_sharing_content": "{partner} ei voi enää käyttää kuviasi.", "partner_sharing": "Kumppanijako", "partners": "Kumppanit", "password": "Salasana", @@ -1342,7 +1352,7 @@ "permanent_deletion_warning_setting_description": "Näytä varoitus, kun poistat kohteita pysyvästi", "permanently_delete": "Poista pysyvästi", "permanently_delete_assets_count": "Poista pysyvästi {count, plural, one {kohde} other {kohteita}}", - "permanently_delete_assets_prompt": "Oletko varma, että haluat poistaa pysyvästi {count, plural, one {tämän kohteen?} other {nämä # kohteet?}} Tämä poistaa myös {count, plural, one {sen sen} other {ne niiden}} albumista.", + "permanently_delete_assets_prompt": "Haluatko varmasti poistaa pysyvästi {count, plural, one {tämän kohteen?} other {nämä # kohteet?}} Tämä poistaa myös {count, plural, one {sen} other {ne}} kaikista albumeista.", "permanently_deleted_asset": "Media poistettu pysyvästi", "permanently_deleted_assets_count": "{count, plural, one {# media} other {# mediaa}} poistettu pysyvästi", "permission_onboarding_back": "Takaisin", @@ -1362,6 +1372,9 @@ "photos_count": "{count, plural, one {{count, number} Kuva} other {{count, number} kuvaa}}", "photos_from_previous_years": "Kuvia edellisiltä vuosilta", "pick_a_location": "Valitse sijainti", + "pin_code_changed_successfully": "PIN-koodin vaihto onnistui", + "pin_code_reset_successfully": "PIN-koodin nollaus onnistui", + "pin_code_setup_successfully": "PIN-koodin asettaminen onnistui", "place": "Sijainti", "places": "Paikat", "places_count": "{count, plural, one {{count, number} Paikka} other {{count, number} Paikkaa}}", @@ -1379,6 +1392,7 @@ "previous_or_next_photo": "Edellinen tai seuraava kuva", "primary": "Ensisijainen", "privacy": "Yksityisyys", + "profile": "Profiili", "profile_drawer_app_logs": "Lokit", "profile_drawer_client_out_of_date_major": "Sovelluksen mobiiliversio on vanhentunut. Päivitä viimeisimpään merkittävään versioon.", "profile_drawer_client_out_of_date_minor": "Sovelluksen mobiiliversio on vanhentunut. Päivitä viimeisimpään versioon.", @@ -1392,7 +1406,7 @@ "public_share": "Julkinen jako", "purchase_account_info": "Tukija", "purchase_activated_subtitle": "Kiitos Immichin ja avoimen lähdekoodin ohjelmiston tukemisesta", - "purchase_activated_time": "Aktivoitu {date, date}", + "purchase_activated_time": "Aktivoitu {date}", "purchase_activated_title": "Avaimesi on aktivoitu onnistuneesti", "purchase_button_activate": "Aktivoi", "purchase_button_buy": "Osta", @@ -1481,6 +1495,7 @@ "reset": "Nollaa", "reset_password": "Nollaa salasana", "reset_people_visibility": "Nollaa henkilöiden näkyvyysasetukset", + "reset_pin_code": "Nollaa PIN-koodi", "reset_to_default": "Palauta oletusasetukset", "resolve_duplicates": "Ratkaise kaksoiskappaleet", "resolved_all_duplicates": "Kaikki kaksoiskappaleet selvitetty", @@ -1604,14 +1619,14 @@ "setting_languages_apply": "Käytä", "setting_languages_subtitle": "Vaihda sovelluksen kieli", "setting_languages_title": "Kieli", - "setting_notifications_notify_failures_grace_period": "Ilmoita taustavarmuuskopioinnin epäonnistumisista: {}", - "setting_notifications_notify_hours": "{} tuntia", + "setting_notifications_notify_failures_grace_period": "Ilmoita taustalla tapahtuvista varmuuskopiointivirheistä: {duration}", + "setting_notifications_notify_hours": "{count} tuntia", "setting_notifications_notify_immediately": "heti", - "setting_notifications_notify_minutes": "{} minuuttia", + "setting_notifications_notify_minutes": "{count} minuuttia", "setting_notifications_notify_never": "ei koskaan", - "setting_notifications_notify_seconds": "{} sekuntia", + "setting_notifications_notify_seconds": "{count} sekuntia", "setting_notifications_single_progress_subtitle": "Yksityiskohtainen tieto palvelimelle lähettämisen edistymisestä kohteittain", - "setting_notifications_single_progress_title": "Näytä taustavarmuuskopioinnin eidstminen", + "setting_notifications_single_progress_title": "Näytä taustavarmuuskopioinnin edistyminen", "setting_notifications_subtitle": "Ilmoitusasetusten määrittely", "setting_notifications_total_progress_subtitle": "Lähetyksen yleinen edistyminen (kohteita lähetetty/yhteensä)", "setting_notifications_total_progress_title": "Näytä taustavarmuuskopioinnin kokonaisedistyminen", @@ -1621,9 +1636,10 @@ "settings": "Asetukset", "settings_require_restart": "Käynnistä Immich uudelleen ottaaksesti tämän asetuksen käyttöön", "settings_saved": "Asetukset tallennettu", + "setup_pin_code": "Määritä PIN-koodi", "share": "Jaa", "share_add_photos": "Lisää kuvia", - "share_assets_selected": "{} valittu", + "share_assets_selected": "{count} valittu", "share_dialog_preparing": "Valmistellaan...", "shared": "Jaettu", "shared_album_activities_input_disable": "Kommentointi on kytketty pois päältä", @@ -1637,32 +1653,32 @@ "shared_by_user": "Käyttäjän {user} jakama", "shared_by_you": "Sinun jakamasi", "shared_from_partner": "Kumppanin {partner} kuvia", - "shared_intent_upload_button_progress_text": "{} / {} Lähetetty", + "shared_intent_upload_button_progress_text": "{current} / {total} Lähetetty", "shared_link_app_bar_title": "Jaetut linkit", "shared_link_clipboard_copied_massage": "Kopioitu leikepöydältä", - "shared_link_clipboard_text": "Linkki: {}\nSalasana: {}", + "shared_link_clipboard_text": "Linkki: {link}\nSalasana: {password}", "shared_link_create_error": "Jaetun linkin luomisessa tapahtui virhe", "shared_link_edit_description_hint": "Lisää jaon kuvaus", "shared_link_edit_expire_after_option_day": "1 päivä", - "shared_link_edit_expire_after_option_days": "{} päivää", + "shared_link_edit_expire_after_option_days": "{count} päivää", "shared_link_edit_expire_after_option_hour": "1 tunti", - "shared_link_edit_expire_after_option_hours": "{} tuntia", + "shared_link_edit_expire_after_option_hours": "{count} tuntia", "shared_link_edit_expire_after_option_minute": "1 minuutti", - "shared_link_edit_expire_after_option_minutes": "{} minuuttia", - "shared_link_edit_expire_after_option_months": "{} kuukautta", - "shared_link_edit_expire_after_option_year": "{} vuosi", + "shared_link_edit_expire_after_option_minutes": "{count} minuuttia", + "shared_link_edit_expire_after_option_months": "{count} kuukautta", + "shared_link_edit_expire_after_option_year": "{count} vuosi", "shared_link_edit_password_hint": "Syötä jaon salasana", "shared_link_edit_submit_button": "Päivitä linkki", "shared_link_error_server_url_fetch": "Palvelimen URL-osoitetta ei voitu hakea", - "shared_link_expires_day": "Vanhenee {} päivässä", - "shared_link_expires_days": "Vanhenee {} päivässä", - "shared_link_expires_hour": "Vanhenee {} tunnissa", - "shared_link_expires_hours": "Vanhenee {} tunnissa", - "shared_link_expires_minute": "Vanhenee {} minuutissa", - "shared_link_expires_minutes": "Vanhenee {} minuutissa", + "shared_link_expires_day": "Vanhenee {count} päivässä", + "shared_link_expires_days": "Vanhenee {count} päivässä", + "shared_link_expires_hour": "Vanhenee {count} tunnissa", + "shared_link_expires_hours": "Vanhenee {count} tunnissa", + "shared_link_expires_minute": "Vanhenee {count} minuutissa", + "shared_link_expires_minutes": "Vanhenee {count} minuutissa", "shared_link_expires_never": "Voimassaolo päättyy ∞", - "shared_link_expires_second": "Vanhenee {} sekunnissa", - "shared_link_expires_seconds": "Vanhenee {} sekunnissa", + "shared_link_expires_second": "Vanhenee {count} sekunnissa", + "shared_link_expires_seconds": "Vanhenee {count} sekunnissa", "shared_link_individual_shared": "Yksilöllisesti jaettu", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Hallitse jaettuja linkkejä", @@ -1737,6 +1753,7 @@ "stop_sharing_photos_with_user": "Päätä kuviesi jakaminen tämän käyttäjän kanssa", "storage": "Tallennustila", "storage_label": "Tallennustilan nimike", + "storage_quota": "Tallennuskiintiö", "storage_usage": "{used} / {available} käytetty", "submit": "Lähetä", "suggestions": "Ehdotukset", @@ -1763,7 +1780,7 @@ "theme_selection": "Teeman valinta", "theme_selection_description": "Aseta vaalea tai tumma tila automaattisesti perustuen selaimesi asetuksiin", "theme_setting_asset_list_storage_indicator_title": "Näytä tallennustilan ilmaisin kohteiden kuvakkeissa", - "theme_setting_asset_list_tiles_per_row_title": "Kohteiden määrä rivillä ({})", + "theme_setting_asset_list_tiles_per_row_title": "Kohteiden määrä rivillä ({count})", "theme_setting_colorful_interface_subtitle": "Levitä pääväri taustalle.", "theme_setting_colorful_interface_title": "Värikäs käyttöliittymä", "theme_setting_image_viewer_quality_subtitle": "Säädä kuvien katselun laatua", @@ -1798,13 +1815,15 @@ "trash_no_results_message": "Roskakorissa olevat kuvat ja videot näytetään täällä.", "trash_page_delete_all": "Poista kaikki", "trash_page_empty_trash_dialog_content": "Haluatko tyhjentää roskakorin? Kohteet poistetaan lopullisesti Immich:sta", - "trash_page_info": "Roskakorissa olevat kohteet poistetaan pysyvästi {} päivän kuluttua", + "trash_page_info": "Roskakorissa olevat kohteet poistetaan pysyvästi {days} päivän kuluttua", "trash_page_no_assets": "Ei poistettuja kohteita", "trash_page_restore_all": "Palauta kaikki", "trash_page_select_assets_btn": "Valitse kohteet", - "trash_page_title": "Roskakori", + "trash_page_title": "Roskakori ({count})", "trashed_items_will_be_permanently_deleted_after": "Roskakorin kohteet poistetaan pysyvästi {days, plural, one {# päivän} other {# päivän}} päästä.", "type": "Tyyppi", + "unable_to_change_pin_code": "PIN-koodin vaihtaminen epäonnistui", + "unable_to_setup_pin_code": "PIN-koodin määrittäminen epäonnistui", "unarchive": "Palauta arkistosta", "unarchived_count": "{count, plural, other {# poistettu arkistosta}}", "unfavorite": "Poista suosikeista", @@ -1828,6 +1847,7 @@ "untracked_files": "Tiedostot joita ei seurata", "untracked_files_decription": "Järjestelmä ei seuraa näitä tiedostoja. Ne voivat johtua epäonnistuneista siirroista, keskeytyneistä latauksista, tai ovat jääneet ohjelmavian seurauksena", "up_next": "Seuraavaksi", + "updated_at": "Päivitetty", "updated_password": "Salasana päivitetty", "upload": "Siirrä palvelimelle", "upload_concurrency": "Latausten samanaikaisuus", @@ -1840,15 +1860,18 @@ "upload_status_errors": "Virheet", "upload_status_uploaded": "Ladattu", "upload_success": "Lataus onnistui. Päivitä sivu jotta näet latauksesi.", - "upload_to_immich": "Lähetä Immichiin ({})", + "upload_to_immich": "Lähetä Immichiin ({count})", "uploading": "Lähettään", "url": "URL", "usage": "Käyttö", "use_current_connection": "käytä nykyistä yhteyttä", "use_custom_date_range": "Käytä omaa aikaväliä", "user": "Käyttäjä", + "user_has_been_deleted": "Käyttäjä on poistettu.", "user_id": "Käyttäjän ID", "user_liked": "{user} tykkäsi {type, select, photo {kuvasta} video {videosta} asset {mediasta} other {tästä}}", + "user_pin_code_settings": "PIN-koodi", + "user_pin_code_settings_description": "Hallinnoi PIN-koodiasi", "user_purchase_settings": "Osta", "user_purchase_settings_description": "Hallitse ostostasi", "user_role_set": "Tee käyttäjästä {user} {role}", @@ -1897,11 +1920,11 @@ "week": "Viikko", "welcome": "Tervetuloa", "welcome_to_immich": "Tervetuloa Immichiin", - "wifi_name": "WiFi Name", + "wifi_name": "Wi-Fi-verkon nimi", "year": "Vuosi", "years_ago": "{years, plural, one {# vuosi} other {# vuotta}} sitten", "yes": "Kyllä", "you_dont_have_any_shared_links": "Sinulla ei ole jaettuja linkkejä", - "your_wifi_name": "Your WiFi name", + "your_wifi_name": "Wi-Fi-verkkosi nimi", "zoom_image": "Zoomaa kuvaa" } diff --git a/i18n/fr.json b/i18n/fr.json index 2576eb954f..acb819da66 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -53,6 +53,7 @@ "confirm_email_below": "Pour confirmer, tapez « {email} » ci-dessous", "confirm_reprocess_all_faces": "Êtes-vous sûr de vouloir retraiter tous les visages ? Cela effacera également les personnes déjà identifiées.", "confirm_user_password_reset": "Êtes-vous sûr de vouloir réinitialiser le mot de passe de {user} ?", + "confirm_user_pin_code_reset": "Êtes-vous sûr de vouloir réinitialiser le code PIN de l'utilisateur {user} ?", "create_job": "Créer une tâche", "cron_expression": "Expression cron", "cron_expression_description": "Définir l'intervalle d'analyse à l'aide d'une expression cron. Pour plus d'informations, voir Crontab Guru", @@ -63,7 +64,7 @@ "external_library_created_at": "Bibliothèque externe (créée le {date})", "external_library_management": "Gestion de la bibliothèque externe", "face_detection": "Détection des visages", - "face_detection_description": "Détection des visages dans les médias à l'aide de l'apprentissage automatique. Pour les vidéos, seule la miniature est prise en compte. « Actualiser » (re)traite tous les médias. « Réinitialiser » retraite tous les visages en repartant de zéro. « Manquant » met en file d'attente les médias qui n'ont pas encore été pris en compte. Lorsque la détection est terminée, tous les visages détectés sont ensuite mis en file d'attente pour la reconnaissance faciale.", + "face_detection_description": "Détection des visages dans les médias à l'aide de l'apprentissage automatique. Pour les vidéos, seule la miniature est prise en compte. « Actualiser » (re)traite tous les médias. « Réinitialiser » retraite tous les visages en repartant de zéro. « Manquant » met en file d'attente les médias qui n'ont pas encore été traités. Lorsque la détection est terminée, les visages détectés seront mis en file d'attente pour la reconnaissance faciale.", "facial_recognition_job_description": "Regrouper les visages détectés en personnes. Cette étape est exécutée une fois la détection des visages terminée. « Réinitialiser » (re)regroupe tous les visages. « Manquant » met en file d'attente les visages auxquels aucune personne n'a été attribuée.", "failed_job_command": "La commande {command} a échoué pour la tâche : {job}", "force_delete_user_warning": "ATTENTION : Cette opération entraîne la suppression immédiate de l'utilisateur et de tous ses médias. Cette opération ne peut être annulée et les fichiers ne peuvent être récupérés.", @@ -348,6 +349,7 @@ "user_delete_delay_settings_description": "Nombre de jours après la validation pour supprimer définitivement le compte et les médias d'un utilisateur. La suppression des utilisateurs se lance à minuit. Les modifications apportées à ce paramètre seront pris en compte lors de la prochaine exécution.", "user_delete_immediately": "Le compte et les médias de {user} seront mis en file d'attente en vue d'une suppression permanente immédiatement.", "user_delete_immediately_checkbox": "Mise en file d'attente d'un utilisateur et de médias en vue d'une suppression immédiate", + "user_details": "Détails utilisateur", "user_management": "Gestion des utilisateurs", "user_password_has_been_reset": "Le mot de passe de l'utilisateur a été réinitialisé :", "user_password_reset_description": "Veuillez fournir le mot de passe temporaire à l'utilisateur et informez-le qu'il devra le changer à sa première connexion.", @@ -369,7 +371,7 @@ "advanced": "Avancé", "advanced_settings_enable_alternate_media_filter_subtitle": "Utilisez cette option pour filtrer les média durant la synchronisation avec des critères alternatifs. N'utilisez cela que lorsque l'application n'arrive pas à détecter tout les albums.", "advanced_settings_enable_alternate_media_filter_title": "[EXPÉRIMENTAL] Utiliser le filtre de synchronisation d'album alternatif", - "advanced_settings_log_level_title": "Niveau de journalisation : {}", + "advanced_settings_log_level_title": "Niveau de journalisation : {level}", "advanced_settings_prefer_remote_subtitle": "Certains appareils sont très lents à charger des miniatures à partir de ressources présentes sur l'appareil. Activez ce paramètre pour charger des images externes à la place.", "advanced_settings_prefer_remote_title": "Préférer les images externes", "advanced_settings_proxy_headers_subtitle": "Ajoutez des en-têtes personnalisés à chaque requête réseau", @@ -400,9 +402,9 @@ "album_remove_user_confirmation": "Êtes-vous sûr de vouloir supprimer {user} ?", "album_share_no_users": "Il semble que vous ayez partagé cet album avec tous les utilisateurs ou que vous n'ayez aucun utilisateur avec lequel le partager.", "album_thumbnail_card_item": "1 élément", - "album_thumbnail_card_items": "{} éléments", + "album_thumbnail_card_items": "{count} éléments", "album_thumbnail_card_shared": " · Partagé", - "album_thumbnail_shared_by": "Partagé par {}", + "album_thumbnail_shared_by": "Partagé par {user}", "album_updated": "Album mis à jour", "album_updated_setting_description": "Recevoir une notification par courriel lorsqu'un album partagé a de nouveaux médias", "album_user_left": "{album} quitté", @@ -440,7 +442,7 @@ "archive": "Archive", "archive_or_unarchive_photo": "Archiver ou désarchiver une photo", "archive_page_no_archived_assets": "Aucun élément archivé n'a été trouvé", - "archive_page_title": "Archive ({})", + "archive_page_title": "Archive ({count})", "archive_size": "Taille de l'archive", "archive_size_description": "Configurer la taille de l'archive maximale pour les téléchargements (en Go)", "archived": "Archives", @@ -477,18 +479,18 @@ "assets_added_to_album_count": "{count, plural, one {# média ajouté} other {# médias ajoutés}} à l'album", "assets_added_to_name_count": "{count, plural, one {# média ajouté} other {# médias ajoutés}} à {hasName, select, true {{name}} other {new album}}", "assets_count": "{count, plural, one {# média} other {# médias}}", - "assets_deleted_permanently": "{} média(s) supprimé(s) définitivement", - "assets_deleted_permanently_from_server": "{} média(s) supprimé(s) définitivement du serveur Immich", + "assets_deleted_permanently": "{count} média(s) supprimé(s) définitivement", + "assets_deleted_permanently_from_server": "{count} média(s) supprimé(s) définitivement du serveur Immich", "assets_moved_to_trash_count": "{count, plural, one {# média déplacé} other {# médias déplacés}} dans la corbeille", "assets_permanently_deleted_count": "{count, plural, one {# média supprimé} other {# médias supprimés}} définitivement", "assets_removed_count": "{count, plural, one {# média supprimé} other {# médias supprimés}}", - "assets_removed_permanently_from_device": "{} média(s) supprimé(s) définitivement de votre appareil", + "assets_removed_permanently_from_device": "{count} média(s) supprimé(s) définitivement de votre appareil", "assets_restore_confirmation": "Êtes-vous sûr de vouloir restaurer tous vos médias de la corbeille ? Vous ne pouvez pas annuler cette action ! Notez que les médias hors ligne ne peuvent être restaurés de cette façon.", "assets_restored_count": "{count, plural, one {# média restauré} other {# médias restaurés}}", - "assets_restored_successfully": "Élément restauré avec succès", - "assets_trashed": "{} média(s) déplacé(s) vers la corbeille", + "assets_restored_successfully": "{count} élément(s) restauré(s) avec succès", + "assets_trashed": "{count} média(s) déplacé(s) vers la corbeille", "assets_trashed_count": "{count, plural, one {# média} other {# médias}} mis à la corbeille", - "assets_trashed_from_server": "{} média(s) déplacé(s) vers la corbeille du serveur Immich", + "assets_trashed_from_server": "{count} média(s) déplacé(s) vers la corbeille du serveur Immich", "assets_were_part_of_album_count": "{count, plural, one {Un média est} other {Des médias sont}} déjà dans l'album", "authorized_devices": "Appareils autorisés", "automatic_endpoint_switching_subtitle": "Se connecter localement lorsque connecté au WI-FI spécifié mais utiliser une adresse alternative lorsque connecté à un autre réseau", @@ -497,7 +499,7 @@ "back_close_deselect": "Retournez en arrière, fermez ou désélectionnez", "background_location_permission": "Permission de localisation en arrière plan", "background_location_permission_content": "Afin de pouvoir changer d'adresse en arrière plan, Immich doit avoir *en permanence* accès à la localisation précise, afin d'accéder au le nom du réseau Wi-Fi utilisé", - "backup_album_selection_page_albums_device": "Albums sur l'appareil ({})", + "backup_album_selection_page_albums_device": "Albums sur l'appareil ({count})", "backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure", "backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.", "backup_album_selection_page_select_albums": "Sélectionner les albums", @@ -506,11 +508,11 @@ "backup_all": "Tout", "backup_background_service_backup_failed_message": "Échec de la sauvegarde des médias. Nouvelle tentative…", "backup_background_service_connection_failed_message": "Impossible de se connecter au serveur. Nouvelle tentative…", - "backup_background_service_current_upload_notification": "Téléversement {}", + "backup_background_service_current_upload_notification": "Téléversement de {filename}", "backup_background_service_default_notification": "Recherche de nouveaux médias…", "backup_background_service_error_title": "Erreur de sauvegarde", "backup_background_service_in_progress_notification": "Sauvegarde de vos médias…", - "backup_background_service_upload_failure_notification": "Échec lors du téléversement {}", + "backup_background_service_upload_failure_notification": "Échec lors du téléversement de {filename}", "backup_controller_page_albums": "Sauvegarder les albums", "backup_controller_page_background_app_refresh_disabled_content": "Activez le rafraîchissement de l'application en arrière-plan dans Paramètres > Général > Rafraîchissement de l'application en arrière-plan afin d'utiliser la sauvegarde en arrière-plan.", "backup_controller_page_background_app_refresh_disabled_title": "Rafraîchissement de l'application en arrière-plan désactivé", @@ -521,7 +523,7 @@ "backup_controller_page_background_battery_info_title": "Optimisation de la batterie", "backup_controller_page_background_charging": "Seulement pendant la charge", "backup_controller_page_background_configure_error": "Échec de la configuration du service d'arrière-plan", - "backup_controller_page_background_delay": "Retarder la sauvegarde des nouveaux médias : {}", + "backup_controller_page_background_delay": "Retarder la sauvegarde des nouveaux médias : {duration}", "backup_controller_page_background_description": "Activez le service d'arrière-plan pour sauvegarder automatiquement tous les nouveaux médias sans avoir à ouvrir l'application", "backup_controller_page_background_is_off": "La sauvegarde automatique en arrière-plan est désactivée", "backup_controller_page_background_is_on": "La sauvegarde automatique en arrière-plan est activée", @@ -531,12 +533,12 @@ "backup_controller_page_backup": "Sauvegardé", "backup_controller_page_backup_selected": "Sélectionné : ", "backup_controller_page_backup_sub": "Photos et vidéos sauvegardées", - "backup_controller_page_created": "Créé le : {}", + "backup_controller_page_created": "Créé le : {date}", "backup_controller_page_desc_backup": "Activez la sauvegarde au premier plan pour téléverser automatiquement les nouveaux médias sur le serveur lors de l'ouverture de l'application.", "backup_controller_page_excluded": "Exclus : ", - "backup_controller_page_failed": "Échec de l'opération ({})", - "backup_controller_page_filename": "Nom du fichier : {} [{}]", - "backup_controller_page_id": "ID : {}", + "backup_controller_page_failed": "Échec de l'opération ({count})", + "backup_controller_page_filename": "Nom du fichier : {filename} [{size}]", + "backup_controller_page_id": "ID : {id}", "backup_controller_page_info": "Informations de sauvegarde", "backup_controller_page_none_selected": "Aucune sélection", "backup_controller_page_remainder": "Restant", @@ -545,7 +547,7 @@ "backup_controller_page_start_backup": "Démarrer la sauvegarde", "backup_controller_page_status_off": "La sauvegarde est désactivée", "backup_controller_page_status_on": "La sauvegarde est activée", - "backup_controller_page_storage_format": "{} sur {} utilisés", + "backup_controller_page_storage_format": "{used} sur {total} utilisés", "backup_controller_page_to_backup": "Albums à sauvegarder", "backup_controller_page_total_sub": "Toutes les photos et vidéos uniques des albums sélectionnés", "backup_controller_page_turn_off": "Désactiver la sauvegarde", @@ -570,21 +572,21 @@ "bulk_keep_duplicates_confirmation": "Êtes-vous sûr de vouloir conserver {count, plural, one {# doublon} other {# doublons}} ? Cela résoudra tous les groupes de doublons sans rien supprimer.", "bulk_trash_duplicates_confirmation": "Êtes-vous sûr de vouloir mettre à la corbeille {count, plural, one {# doublon} other {# doublons}} ? Cette opération permet de conserver le plus grand média de chaque groupe et de mettre à la corbeille tous les autres doublons.", "buy": "Acheter Immich", - "cache_settings_album_thumbnails": "Page des miniatures de la bibliothèque ({} médias)", + "cache_settings_album_thumbnails": "Page des miniatures de la bibliothèque ({count} médias)", "cache_settings_clear_cache_button": "Effacer le cache", "cache_settings_clear_cache_button_title": "Efface le cache de l'application. Cela aura un impact significatif sur les performances de l'application jusqu'à ce que le cache soit reconstruit.", "cache_settings_duplicated_assets_clear_button": "EFFACER", "cache_settings_duplicated_assets_subtitle": "Photos et vidéos qui sont exclues par l'application", - "cache_settings_duplicated_assets_title": "Médias dupliqués ({})", - "cache_settings_image_cache_size": "Taille du cache des images ({} médias)", + "cache_settings_duplicated_assets_title": "Médias dupliqués ({count})", + "cache_settings_image_cache_size": "Taille du cache des images ({count} médias)", "cache_settings_statistics_album": "Miniatures de la bibliothèque", - "cache_settings_statistics_assets": "{} médias ({})", + "cache_settings_statistics_assets": "{count} médias ({size})", "cache_settings_statistics_full": "Images complètes", "cache_settings_statistics_shared": "Miniatures de l'album partagé", "cache_settings_statistics_thumbnail": "Miniatures", "cache_settings_statistics_title": "Utilisation du cache", "cache_settings_subtitle": "Contrôler le comportement de mise en cache de l'application mobile Immich", - "cache_settings_thumbnail_size": "Taille du cache des miniatures ({} médias)", + "cache_settings_thumbnail_size": "Taille du cache des miniatures ({count} médias)", "cache_settings_tile_subtitle": "Contrôler le comportement du stockage local", "cache_settings_tile_title": "Stockage local", "cache_settings_title": "Paramètres de mise en cache", @@ -610,13 +612,14 @@ "change_password_form_new_password": "Nouveau mot de passe", "change_password_form_password_mismatch": "Les mots de passe ne correspondent pas", "change_password_form_reenter_new_password": "Saisissez à nouveau le nouveau mot de passe", + "change_pin_code": "Changer le code PIN", "change_your_password": "Changer votre mot de passe", "changed_visibility_successfully": "Visibilité modifiée avec succès", "check_all": "Tout sélectionner", "check_corrupt_asset_backup": "Vérifier la corruption des éléments enregistrés", "check_corrupt_asset_backup_button": "Vérifier", "check_corrupt_asset_backup_description": "Lancer cette vérification uniquement lorsque connecté à un réseau Wi-Fi et que tout le contenu a été enregistré. Cette procédure peut durer plusieurs minutes.", - "check_logs": "Vérifier les logs", + "check_logs": "Vérifier les journaux", "choose_matching_people_to_merge": "Choisir les personnes à fusionner", "city": "Ville", "clear": "Effacer", @@ -650,11 +653,12 @@ "confirm_delete_face": "Êtes-vous sûr de vouloir supprimer le visage de {name} du média ?", "confirm_delete_shared_link": "Voulez-vous vraiment supprimer ce lien partagé ?", "confirm_keep_this_delete_others": "Tous les autres médias dans la pile seront supprimés sauf celui-ci. Êtes-vous sûr de vouloir continuer ?", + "confirm_new_pin_code": "Confirmer le nouveau code PIN", "confirm_password": "Confirmer le mot de passe", "contain": "Contenu", "context": "Contexte", "continue": "Continuer", - "control_bottom_app_bar_album_info_shared": "{} médias · Partagés", + "control_bottom_app_bar_album_info_shared": "{count} médias · Partagés", "control_bottom_app_bar_create_new_album": "Créer un nouvel album", "control_bottom_app_bar_delete_from_immich": "Supprimer de Immich", "control_bottom_app_bar_delete_from_local": "Supprimer de l'appareil", @@ -692,9 +696,11 @@ "create_tag_description": "Créer une nouvelle étiquette. Pour les étiquettes imbriquées, veuillez entrer le chemin complet de l'étiquette, y compris les caractères \"/\".", "create_user": "Créer un utilisateur", "created": "Créé", + "created_at": "Créé à", "crop": "Recadrer", "curated_object_page_title": "Objets", "current_device": "Appareil actuel", + "current_pin_code": "Code PIN actuel", "current_server_address": "Adresse actuelle du serveur", "custom_locale": "Paramètres régionaux personnalisés", "custom_locale_description": "Afficher les dates et nombres en fonction des paramètres régionaux", @@ -763,7 +769,7 @@ "download_enqueue": "Téléchargement en attente", "download_error": "Erreur de téléchargement", "download_failed": "Téléchargement échoué", - "download_filename": "fichier : {}", + "download_filename": "fichier : {filename}", "download_finished": "Téléchargement terminé", "download_include_embedded_motion_videos": "Vidéos intégrées", "download_include_embedded_motion_videos_description": "Inclure des vidéos intégrées dans les photos de mouvement comme un fichier séparé", @@ -807,6 +813,7 @@ "editor_crop_tool_h2_aspect_ratios": "Rapports hauteur/largeur", "editor_crop_tool_h2_rotation": "Rotation", "email": "Courriel", + "email_notifications": "Notifications email", "empty_folder": "Ce dossier est vide", "empty_trash": "Vider la corbeille", "empty_trash_confirmation": "Êtes-vous sûr de vouloir vider la corbeille ? Cela supprimera définitivement de Immich tous les médias qu'elle contient.\nVous ne pouvez pas annuler cette action !", @@ -819,7 +826,7 @@ "error_change_sort_album": "Impossible de modifier l'ordre de tri des albums", "error_delete_face": "Erreur lors de la suppression du visage pour le média", "error_loading_image": "Erreur de chargement de l'image", - "error_saving_image": "Erreur : {}", + "error_saving_image": "Erreur : {error}", "error_title": "Erreur - Quelque chose s'est mal passé", "errors": { "cannot_navigate_next_asset": "Impossible de naviguer jusqu'au prochain média", @@ -922,6 +929,7 @@ "unable_to_remove_reaction": "Impossible de supprimer la réaction", "unable_to_repair_items": "Impossible de réparer les éléments", "unable_to_reset_password": "Impossible de réinitialiser le mot de passe", + "unable_to_reset_pin_code": "Impossible de réinitialiser le code PIN", "unable_to_resolve_duplicate": "Impossible de résoudre le doublon", "unable_to_restore_assets": "Impossible de restaurer les médias", "unable_to_restore_trash": "Impossible de restaurer la corbeille", @@ -955,10 +963,10 @@ "exif_bottom_sheet_location": "LOCALISATION", "exif_bottom_sheet_people": "PERSONNES", "exif_bottom_sheet_person_add_person": "Ajouter un nom", - "exif_bottom_sheet_person_age": "Âge {}", - "exif_bottom_sheet_person_age_months": "Âge {} mois", - "exif_bottom_sheet_person_age_year_months": "Âge 1 an, {} mois", - "exif_bottom_sheet_person_age_years": "Âge {}", + "exif_bottom_sheet_person_age": "Âge {age}", + "exif_bottom_sheet_person_age_months": "Âge {months} mois", + "exif_bottom_sheet_person_age_year_months": "Âge 1 an, {months} mois", + "exif_bottom_sheet_person_age_years": "Âge {years}", "exit_slideshow": "Quitter le diaporama", "expand_all": "Tout développer", "experimental_settings_new_asset_list_subtitle": "En cours de développement", @@ -1048,6 +1056,7 @@ "home_page_upload_err_limit": "Impossible de téléverser plus de 30 médias en même temps, demande ignorée", "host": "Hôte", "hour": "Heure", + "id": "ID", "ignore_icloud_photos": "Ignorer les photos iCloud", "ignore_icloud_photos_description": "Les photos stockées sur iCloud ne sont pas téléversées sur le serveur Immich", "image": "Image", @@ -1173,8 +1182,8 @@ "manage_your_devices": "Gérer vos appareils", "manage_your_oauth_connection": "Gérer votre connexion OAuth", "map": "Carte", - "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", + "map_assets_in_bound": "{count} photo", + "map_assets_in_bounds": "{count} photos", "map_cannot_get_user_location": "Impossible d'obtenir la localisation de l'utilisateur", "map_location_dialog_yes": "Oui", "map_location_picker_page_use_location": "Utiliser ma position", @@ -1188,9 +1197,9 @@ "map_settings": "Paramètres de la carte", "map_settings_dark_mode": "Mode sombre", "map_settings_date_range_option_day": "Dernières 24 heures", - "map_settings_date_range_option_days": "{} derniers jours", + "map_settings_date_range_option_days": "{days} derniers jours", "map_settings_date_range_option_year": "Année passée", - "map_settings_date_range_option_years": "{} dernières années", + "map_settings_date_range_option_years": "{years} dernières années", "map_settings_dialog_title": "Paramètres de la carte", "map_settings_include_show_archived": "Inclure les archives", "map_settings_include_show_partners": "Inclure les partenaires", @@ -1209,7 +1218,7 @@ "memories_start_over": "Recommencer", "memories_swipe_to_close": "Balayez vers le haut pour fermer", "memories_year_ago": "Il y a un an", - "memories_years_ago": "Il y a {} ans", + "memories_years_ago": "Il y a {years, plural, other {# ans}}", "memory": "Souvenir", "memory_lane_title": "Fil de souvenirs {title}", "menu": "Menu", @@ -1242,6 +1251,7 @@ "new_api_key": "Nouvelle clé API", "new_password": "Nouveau mot de passe", "new_person": "Nouvelle personne", + "new_pin_code": "Nouveau code PIN", "new_user_created": "Nouvel utilisateur créé", "new_version_available": "NOUVELLE VERSION DISPONIBLE", "newest_first": "Récents en premier", @@ -1316,7 +1326,7 @@ "partner_page_partner_add_failed": "Échec de l'ajout d'un partenaire", "partner_page_select_partner": "Sélectionner un partenaire", "partner_page_shared_to_title": "Partagé avec", - "partner_page_stop_sharing_content": "{} ne pourra plus accéder à vos photos.", + "partner_page_stop_sharing_content": "{partner} ne pourra plus accéder à vos photos.", "partner_sharing": "Partage avec les partenaires", "partners": "Partenaires", "password": "Mot de passe", @@ -1362,6 +1372,9 @@ "photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} Photos}}", "photos_from_previous_years": "Photos des années précédentes", "pick_a_location": "Choisissez un lieu", + "pin_code_changed_successfully": "Code PIN changé avec succès", + "pin_code_reset_successfully": "Réinitialisation du code PIN réussie", + "pin_code_setup_successfully": "Définition du code PIN réussie", "place": "Lieu", "places": "Lieux", "places_count": "{count, plural, one {{count, number} Lieu} other {{count, number} Lieux}}", @@ -1379,6 +1392,7 @@ "previous_or_next_photo": "Photo précédente ou suivante", "primary": "Primaire", "privacy": "Vie privée", + "profile": "Profile", "profile_drawer_app_logs": "Journaux", "profile_drawer_client_out_of_date_major": "L'application mobile est obsolète. Veuillez effectuer la mise à jour vers la dernière version majeure.", "profile_drawer_client_out_of_date_minor": "L'application mobile est obsolète. Veuillez effectuer la mise à jour vers la dernière version mineure.", @@ -1392,7 +1406,7 @@ "public_share": "Partage public", "purchase_account_info": "Contributeur", "purchase_activated_subtitle": "Merci d'avoir apporté votre soutien à Immich et aux logiciels open source", - "purchase_activated_time": "Activé le {date, date}", + "purchase_activated_time": "Activé le {date}", "purchase_activated_title": "Votre clé a été activée avec succès", "purchase_button_activate": "Activer", "purchase_button_buy": "Acheter", @@ -1481,6 +1495,7 @@ "reset": "Réinitialiser", "reset_password": "Réinitialiser le mot de passe", "reset_people_visibility": "Réinitialiser la visibilité des personnes", + "reset_pin_code": "Réinitialiser le code PIN", "reset_to_default": "Rétablir les valeurs par défaut", "resolve_duplicates": "Résoudre les doublons", "resolved_all_duplicates": "Résolution de tous les doublons", @@ -1604,12 +1619,12 @@ "setting_languages_apply": "Appliquer", "setting_languages_subtitle": "Changer la langue de l'application", "setting_languages_title": "Langues", - "setting_notifications_notify_failures_grace_period": "Notifier les échecs de la sauvegarde en arrière-plan : {}", - "setting_notifications_notify_hours": "{} heures", + "setting_notifications_notify_failures_grace_period": "Notifier les échecs de la sauvegarde en arrière-plan : {duration}", + "setting_notifications_notify_hours": "{count} heures", "setting_notifications_notify_immediately": "immédiatement", - "setting_notifications_notify_minutes": "{} minutes", + "setting_notifications_notify_minutes": "{count} minutes", "setting_notifications_notify_never": "jamais", - "setting_notifications_notify_seconds": "{} secondes", + "setting_notifications_notify_seconds": "{count} secondes", "setting_notifications_single_progress_subtitle": "Informations détaillées sur la progression du téléversement par média", "setting_notifications_single_progress_title": "Afficher la progression du détail de la sauvegarde en arrière-plan", "setting_notifications_subtitle": "Ajustez vos préférences de notification", @@ -1621,9 +1636,10 @@ "settings": "Paramètres", "settings_require_restart": "Veuillez redémarrer Immich pour appliquer ce paramètre", "settings_saved": "Paramètres sauvegardés", + "setup_pin_code": "Définir un code PIN", "share": "Partager", "share_add_photos": "Ajouter des photos", - "share_assets_selected": "{} sélectionné(s)", + "share_assets_selected": "{count} sélectionné(s)", "share_dialog_preparing": "Préparation...", "shared": "Partagé", "shared_album_activities_input_disable": "Les commentaires sont désactivés", @@ -1637,32 +1653,32 @@ "shared_by_user": "Partagé par {user}", "shared_by_you": "Partagé par vous", "shared_from_partner": "Photos de {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Téléversé", + "shared_intent_upload_button_progress_text": "{current} / {total} Téléversé(s)", "shared_link_app_bar_title": "Liens partagés", "shared_link_clipboard_copied_massage": "Copié dans le presse-papier", - "shared_link_clipboard_text": "Lien : {}\nMot de passe : {}", + "shared_link_clipboard_text": "Lien : {link}\nMot de passe : {password}", "shared_link_create_error": "Erreur pendant la création du lien partagé", "shared_link_edit_description_hint": "Saisir la description du partage", "shared_link_edit_expire_after_option_day": "1 jour", - "shared_link_edit_expire_after_option_days": "{} jours", + "shared_link_edit_expire_after_option_days": "{count} jours", "shared_link_edit_expire_after_option_hour": "1 heure", - "shared_link_edit_expire_after_option_hours": "{} heures", + "shared_link_edit_expire_after_option_hours": "{count} heures", "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{} minutes", - "shared_link_edit_expire_after_option_months": "{} mois", - "shared_link_edit_expire_after_option_year": "{} an", + "shared_link_edit_expire_after_option_minutes": "{count} minutes", + "shared_link_edit_expire_after_option_months": "{count} mois", + "shared_link_edit_expire_after_option_year": "{count} an", "shared_link_edit_password_hint": "Saisir le mot de passe de partage", "shared_link_edit_submit_button": "Mettre à jour le lien", "shared_link_error_server_url_fetch": "Impossible de récupérer l'url du serveur", - "shared_link_expires_day": "Expire dans {} jour", - "shared_link_expires_days": "Expire dans {} jours", - "shared_link_expires_hour": "Expire dans {} heure", - "shared_link_expires_hours": "Expire dans {} heures", - "shared_link_expires_minute": "Expire dans {} minute", - "shared_link_expires_minutes": "Expire dans {} minutes", + "shared_link_expires_day": "Expire dans {count} jour", + "shared_link_expires_days": "Expire dans {count} jours", + "shared_link_expires_hour": "Expire dans {count} heure", + "shared_link_expires_hours": "Expire dans {count} heures", + "shared_link_expires_minute": "Expire dans {count} minute", + "shared_link_expires_minutes": "Expire dans {count} minutes", "shared_link_expires_never": "Expire ∞", - "shared_link_expires_second": "Expire dans {} seconde", - "shared_link_expires_seconds": "Expire dans {} secondes", + "shared_link_expires_second": "Expire dans {count} seconde", + "shared_link_expires_seconds": "Expire dans {count} secondes", "shared_link_individual_shared": "Partagé individuellement", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Gérer les liens partagés", @@ -1737,6 +1753,7 @@ "stop_sharing_photos_with_user": "Arrêter de partager vos photos avec cet utilisateur", "storage": "Stockage", "storage_label": "Étiquette de stockage", + "storage_quota": "Quota de stockage", "storage_usage": "{used} sur {available} utilisé", "submit": "Soumettre", "suggestions": "Suggestions", @@ -1763,7 +1780,7 @@ "theme_selection": "Sélection du thème", "theme_selection_description": "Ajuster automatiquement le thème clair ou sombre via les préférences système", "theme_setting_asset_list_storage_indicator_title": "Afficher l'indicateur de stockage sur les tuiles des éléments", - "theme_setting_asset_list_tiles_per_row_title": "Nombre de médias par ligne ({})", + "theme_setting_asset_list_tiles_per_row_title": "Nombre de médias par ligne ({count})", "theme_setting_colorful_interface_subtitle": "Appliquer la couleur principale sur les surfaces d'arrière-plan.", "theme_setting_colorful_interface_title": "Interface colorée", "theme_setting_image_viewer_quality_subtitle": "Ajustez la qualité de la visionneuse d'images détaillées", @@ -1798,13 +1815,15 @@ "trash_no_results_message": "Les photos et vidéos supprimées s'afficheront ici.", "trash_page_delete_all": "Tout supprimer", "trash_page_empty_trash_dialog_content": "Voulez-vous vider les médias de la corbeille ? Ces objets seront définitivement retirés d'Immich", - "trash_page_info": "Les médias mis à la corbeille seront définitivement supprimés au bout de {} jours", + "trash_page_info": "Les médias mis à la corbeille seront définitivement supprimés au bout de {days} jours", "trash_page_no_assets": "Aucun élément dans la corbeille", "trash_page_restore_all": "Tout restaurer", "trash_page_select_assets_btn": "Sélectionner les éléments", - "trash_page_title": "Corbeille ({})", + "trash_page_title": "Corbeille ({count})", "trashed_items_will_be_permanently_deleted_after": "Les éléments dans la corbeille seront supprimés définitivement après {days, plural, one {# jour} other {# jours}}.", "type": "Type", + "unable_to_change_pin_code": "Impossible de changer le code PIN", + "unable_to_setup_pin_code": "Impossible de définir le code PIN", "unarchive": "Désarchiver", "unarchived_count": "{count, plural, one {# supprimé} other {# supprimés}} de l'archive", "unfavorite": "Enlever des favoris", @@ -1828,6 +1847,7 @@ "untracked_files": "Fichiers non suivis", "untracked_files_decription": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat de déplacements échoués, de téléversements interrompus ou abandonnés pour cause de bug", "up_next": "Suite", + "updated_at": "Mis à jour à", "updated_password": "Mot de passe mis à jour", "upload": "Téléverser", "upload_concurrency": "Téléversements simultanés", @@ -1840,15 +1860,18 @@ "upload_status_errors": "Erreurs", "upload_status_uploaded": "Téléversé", "upload_success": "Téléversement réussi. Rafraîchir la page pour voir les nouveaux médias téléversés.", - "upload_to_immich": "Téléverser vers Immich ({})", + "upload_to_immich": "Téléverser vers Immich ({count})", "uploading": "Téléversement en cours", "url": "URL", "usage": "Utilisation", "use_current_connection": "Utiliser le réseau actuel", "use_custom_date_range": "Utilisez une plage de date personnalisée à la place", "user": "Utilisateur", + "user_has_been_deleted": "Cet utilisateur à été supprimé.", "user_id": "ID Utilisateur", "user_liked": "{user} a aimé {type, select, photo {cette photo} video {cette vidéo} asset {ce média} other {ceci}}", + "user_pin_code_settings": "Code PIN", + "user_pin_code_settings_description": "Gérer votre code PIN", "user_purchase_settings": "Achat", "user_purchase_settings_description": "Gérer votre achat", "user_role_set": "Définir {user} comme {role}", diff --git a/i18n/gl.json b/i18n/gl.json index 498a986768..8f630303d3 100644 --- a/i18n/gl.json +++ b/i18n/gl.json @@ -366,7 +366,7 @@ "advanced": "Avanzado", "advanced_settings_enable_alternate_media_filter_subtitle": "Usa esta opción para filtrar medios durante a sincronización baseándose en criterios alternativos. Só proba isto se tes problemas coa aplicación detectando todos os álbums.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Usar filtro alternativo de sincronización de álbums do dispositivo", - "advanced_settings_log_level_title": "Nivel de rexistro: {}", + "advanced_settings_log_level_title": "Nivel de rexistro: {level}", "advanced_settings_prefer_remote_subtitle": "Algúns dispositivos son extremadamente lentos para cargar miniaturas de activos no dispositivo. Active esta configuración para cargar imaxes remotas no seu lugar.", "advanced_settings_prefer_remote_title": "Preferir imaxes remotas", "advanced_settings_proxy_headers_subtitle": "Definir cabeceiras de proxy que Immich debería enviar con cada solicitude de rede", @@ -397,9 +397,9 @@ "album_remove_user_confirmation": "Estás seguro de que queres eliminar a {user}?", "album_share_no_users": "Parece que compartiches este álbum con todos os usuarios ou non tes ningún usuario co que compartir.", "album_thumbnail_card_item": "1 elemento", - "album_thumbnail_card_items": "{} elementos", + "album_thumbnail_card_items": "{count} elementos", "album_thumbnail_card_shared": " · Compartido", - "album_thumbnail_shared_by": "Compartido por {}", + "album_thumbnail_shared_by": "Compartido por {user}", "album_updated": "Álbum actualizado", "album_updated_setting_description": "Recibir unha notificación por correo electrónico cando un álbum compartido teña novos activos", "album_user_left": "Saíu de {album}", @@ -437,7 +437,7 @@ "archive": "Arquivo", "archive_or_unarchive_photo": "Arquivar ou desarquivar foto", "archive_page_no_archived_assets": "Non se atoparon activos arquivados", - "archive_page_title": "Arquivo ({})", + "archive_page_title": "Arquivo ({count})", "archive_size": "Tamaño do arquivo", "archive_size_description": "Configurar o tamaño do arquivo para descargas (en GiB)", "archived": "Arquivado", @@ -474,27 +474,27 @@ "assets_added_to_album_count": "Engadido {count, plural, one {# activo} other {# activos}} ao álbum", "assets_added_to_name_count": "Engadido {count, plural, one {# activo} other {# activos}} a {hasName, select, true {{name}} other {novo álbum}}", "assets_count": "{count, plural, one {# activo} other {# activos}}", - "assets_deleted_permanently": "{} activo(s) eliminado(s) permanentemente", - "assets_deleted_permanently_from_server": "{} activo(s) eliminado(s) permanentemente do servidor Immich", + "assets_deleted_permanently": "{count} activo(s) eliminado(s) permanentemente", + "assets_deleted_permanently_from_server": "{count} activo(s) eliminado(s) permanentemente do servidor Immich", "assets_moved_to_trash_count": "Movido {count, plural, one {# activo} other {# activos}} ao lixo", "assets_permanently_deleted_count": "Eliminados permanentemente {count, plural, one {# activo} other {# activos}}", "assets_removed_count": "Eliminados {count, plural, one {# activo} other {# activos}}", - "assets_removed_permanently_from_device": "{} activo(s) eliminado(s) permanentemente do teu dispositivo", + "assets_removed_permanently_from_device": "{count} activo(s) eliminado(s) permanentemente do teu dispositivo", "assets_restore_confirmation": "Estás seguro de que queres restaurar todos os seus activos no lixo? Non podes desfacer esta acción! Ten en conta que calquera activo fóra de liña non pode ser restaurado desta maneira.", "assets_restored_count": "Restaurados {count, plural, one {# activo} other {# activos}}", - "assets_restored_successfully": "{} activo(s) restaurado(s) correctamente", - "assets_trashed": "{} activo(s) movido(s) ao lixo", + "assets_restored_successfully": "{count} activo(s) restaurado(s) correctamente", + "assets_trashed": "{count} activo(s) movido(s) ao lixo", "assets_trashed_count": "Movido {count, plural, one {# activo} other {# activos}} ao lixo", - "assets_trashed_from_server": "{} activo(s) movido(s) ao lixo desde o servidor Immich", + "assets_trashed_from_server": "{count} activo(s) movido(s) ao lixo desde o servidor Immich", "assets_were_part_of_album_count": "{count, plural, one {O activo xa era} other {Os activos xa eran}} parte do álbum", "authorized_devices": "Dispositivos Autorizados", - "automatic_endpoint_switching_subtitle": "Conectar localmente a través de Wi-Fi designada cando estea dispoñible e usar conexións alternativas noutros lugares", + "automatic_endpoint_switching_subtitle": "Conectar localmente a través da wifi designada cando estea dispoñible e usar conexións alternativas noutros lugares", "automatic_endpoint_switching_title": "Cambio automático de URL", "back": "Atrás", "back_close_deselect": "Atrás, pechar ou deseleccionar", "background_location_permission": "Permiso de ubicación en segundo plano", - "background_location_permission_content": "Para cambiar de rede cando se executa en segundo plano, Immich debe ter *sempre* acceso á ubicación precisa para que a aplicación poida ler o nome da rede Wi-Fi", - "backup_album_selection_page_albums_device": "Álbums no dispositivo ({})", + "background_location_permission_content": "Para cambiar de rede cando se executa en segundo plano, Immich debe ter *sempre* acceso á ubicación precisa para que a aplicación poida ler o nome da rede wifi", + "backup_album_selection_page_albums_device": "Álbums no dispositivo ({count})", "backup_album_selection_page_albums_tap": "Tocar para incluír, dobre toque para excluír", "backup_album_selection_page_assets_scatter": "Os activos poden dispersarse por varios álbums. Polo tanto, os álbums poden incluírse ou excluírse durante o proceso de copia de seguridade.", "backup_album_selection_page_select_albums": "Seleccionar álbums", @@ -503,11 +503,11 @@ "backup_all": "Todo", "backup_background_service_backup_failed_message": "Erro ao facer copia de seguridade dos activos. Reintentando…", "backup_background_service_connection_failed_message": "Erro ao conectar co servidor. Reintentando…", - "backup_background_service_current_upload_notification": "Subindo {}", + "backup_background_service_current_upload_notification": "Subindo {filename}", "backup_background_service_default_notification": "Comprobando novos activos…", "backup_background_service_error_title": "Erro na copia de seguridade", "backup_background_service_in_progress_notification": "Facendo copia de seguridade dos teus activos…", - "backup_background_service_upload_failure_notification": "Erro ao subir {}", + "backup_background_service_upload_failure_notification": "Erro ao subir {filename}", "backup_controller_page_albums": "Álbums da Copia de Seguridade", "backup_controller_page_background_app_refresh_disabled_content": "Active a actualización de aplicacións en segundo plano en Axustes > Xeral > Actualización en segundo plano para usar a copia de seguridade en segundo plano.", "backup_controller_page_background_app_refresh_disabled_title": "Actualización de aplicacións en segundo plano desactivada", @@ -518,22 +518,22 @@ "backup_controller_page_background_battery_info_title": "Optimizacións da batería", "backup_controller_page_background_charging": "Só mentres se carga", "backup_controller_page_background_configure_error": "Erro ao configurar o servizo en segundo plano", - "backup_controller_page_background_delay": "Atrasar copia de seguridade de novos activos: {}", + "backup_controller_page_background_delay": "Atrasar copia de seguridade de novos activos: {duration}", "backup_controller_page_background_description": "Active o servizo en segundo plano para facer copia de seguridade automaticamente de calquera activo novo sen necesidade de abrir a aplicación", "backup_controller_page_background_is_off": "A copia de seguridade automática en segundo plano está desactivada", "backup_controller_page_background_is_on": "A copia de seguridade automática en segundo plano está activada", "backup_controller_page_background_turn_off": "Desactivar servizo en segundo plano", "backup_controller_page_background_turn_on": "Activar servizo en segundo plano", - "backup_controller_page_background_wifi": "Só con WiFi", + "backup_controller_page_background_wifi": "Só con wifi", "backup_controller_page_backup": "Copia de Seguridade", "backup_controller_page_backup_selected": "Seleccionado: ", "backup_controller_page_backup_sub": "Fotos e vídeos con copia de seguridade", - "backup_controller_page_created": "Creado o: {}", + "backup_controller_page_created": "Creado o: {date}", "backup_controller_page_desc_backup": "Active a copia de seguridade en primeiro plano para cargar automaticamente novos activos ao servidor ao abrir a aplicación.", "backup_controller_page_excluded": "Excluído: ", - "backup_controller_page_failed": "Fallado ({})", - "backup_controller_page_filename": "Nome do ficheiro: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Fallado ({count})", + "backup_controller_page_filename": "Nome do ficheiro: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Información da Copia de Seguridade", "backup_controller_page_none_selected": "Ningún seleccionado", "backup_controller_page_remainder": "Restante", @@ -542,7 +542,7 @@ "backup_controller_page_start_backup": "Iniciar Copia de Seguridade", "backup_controller_page_status_off": "A copia de seguridade automática en primeiro plano está desactivada", "backup_controller_page_status_on": "A copia de seguridade automática en primeiro plano está activada", - "backup_controller_page_storage_format": "{} de {} usado", + "backup_controller_page_storage_format": "{used} de {total} usado", "backup_controller_page_to_backup": "Álbums para facer copia de seguridade", "backup_controller_page_total_sub": "Todas as fotos e vídeos únicos dos álbums seleccionados", "backup_controller_page_turn_off": "Desactivar copia de seguridade en primeiro plano", @@ -567,21 +567,21 @@ "bulk_keep_duplicates_confirmation": "Estás seguro de que queres conservar {count, plural, one {# activo duplicado} other {# activos duplicados}}? Isto resolverá todos os grupos duplicados sen eliminar nada.", "bulk_trash_duplicates_confirmation": "Estás seguro de que queres mover masivamente ao lixo {count, plural, one {# activo duplicado} other {# activos duplicados}}? Isto conservará o activo máis grande de cada grupo e moverá ao lixo todos os demais duplicados.", "buy": "Comprar Immich", - "cache_settings_album_thumbnails": "Miniaturas da páxina da biblioteca ({} activos)", + "cache_settings_album_thumbnails": "Miniaturas da páxina da biblioteca ({count} activos)", "cache_settings_clear_cache_button": "Borrar caché", "cache_settings_clear_cache_button_title": "Borra a caché da aplicación. Isto afectará significativamente o rendemento da aplicación ata que a caché se reconstruíu.", "cache_settings_duplicated_assets_clear_button": "BORRAR", "cache_settings_duplicated_assets_subtitle": "Fotos e vídeos que están na lista negra da aplicación", - "cache_settings_duplicated_assets_title": "Activos Duplicados ({})", - "cache_settings_image_cache_size": "Tamaño da caché de imaxes ({} activos)", + "cache_settings_duplicated_assets_title": "Activos Duplicados ({count})", + "cache_settings_image_cache_size": "Tamaño da caché de imaxes ({count} activos)", "cache_settings_statistics_album": "Miniaturas da biblioteca", - "cache_settings_statistics_assets": "{} activos ({})", + "cache_settings_statistics_assets": "{count} activos ({size})", "cache_settings_statistics_full": "Imaxes completas", "cache_settings_statistics_shared": "Miniaturas de álbums compartidos", "cache_settings_statistics_thumbnail": "Miniaturas", "cache_settings_statistics_title": "Uso da caché", "cache_settings_subtitle": "Controlar o comportamento da caché da aplicación móbil Immich", - "cache_settings_thumbnail_size": "Tamaño da caché de miniaturas ({} activos)", + "cache_settings_thumbnail_size": "Tamaño da caché de miniaturas ({count} activos)", "cache_settings_tile_subtitle": "Controlar o comportamento do almacenamento local", "cache_settings_tile_title": "Almacenamento Local", "cache_settings_title": "Configuración da Caché", @@ -612,7 +612,7 @@ "check_all": "Marcar todo", "check_corrupt_asset_backup": "Comprobar copias de seguridade de activos corruptos", "check_corrupt_asset_backup_button": "Realizar comprobación", - "check_corrupt_asset_backup_description": "Execute esta comprobación só a través de Wi-Fi e unha vez que todos os activos teñan copia de seguridade. O procedemento pode tardar uns minutos.", + "check_corrupt_asset_backup_description": "Execute esta comprobación só a través da wifi e unha vez que todos os activos teñan copia de seguridade. O procedemento pode tardar uns minutos.", "check_logs": "Comprobar Rexistros", "choose_matching_people_to_merge": "Elixir persoas coincidentes para fusionar", "city": "Cidade", @@ -651,7 +651,7 @@ "contain": "Conter", "context": "Contexto", "continue": "Continuar", - "control_bottom_app_bar_album_info_shared": "{} elementos · Compartidos", + "control_bottom_app_bar_album_info_shared": "{count} elementos · Compartidos", "control_bottom_app_bar_create_new_album": "Crear novo álbum", "control_bottom_app_bar_delete_from_immich": "Eliminar de Immich", "control_bottom_app_bar_delete_from_local": "Eliminar do dispositivo", @@ -760,7 +760,7 @@ "download_enqueue": "Descarga en cola", "download_error": "Erro na Descarga", "download_failed": "Descarga fallada", - "download_filename": "ficheiro: {}", + "download_filename": "ficheiro: {filename}", "download_finished": "Descarga finalizada", "download_include_embedded_motion_videos": "Vídeos incrustados", "download_include_embedded_motion_videos_description": "Incluír vídeos incrustados en fotos en movemento como un ficheiro separado", @@ -811,12 +811,12 @@ "enabled": "Activado", "end_date": "Data de fin", "enqueued": "En cola", - "enter_wifi_name": "Introducir nome da WiFi", + "enter_wifi_name": "Introducir nome da wifi", "error": "Erro", "error_change_sort_album": "Erro ao cambiar a orde de clasificación do álbum", "error_delete_face": "Erro ao eliminar a cara do activo", "error_loading_image": "Erro ao cargar a imaxe", - "error_saving_image": "Erro: {}", + "error_saving_image": "Erro: {error}", "error_title": "Erro - Algo saíu mal", "errors": { "cannot_navigate_next_asset": "Non se pode navegar ao seguinte activo", @@ -846,10 +846,12 @@ "failed_to_keep_this_delete_others": "Erro ao conservar este activo e eliminar os outros activos", "failed_to_load_asset": "Erro ao cargar o activo", "failed_to_load_assets": "Erro ao cargar activos", + "failed_to_load_notifications": "Erro ao cargar as notificacións", "failed_to_load_people": "Erro ao cargar persoas", "failed_to_remove_product_key": "Erro ao eliminar a chave do produto", "failed_to_stack_assets": "Erro ao apilar activos", "failed_to_unstack_assets": "Erro ao desapilar activos", + "failed_to_update_notification_status": "Erro ao actualizar o estado das notificacións", "import_path_already_exists": "Esta ruta de importación xa existe.", "incorrect_email_or_password": "Correo electrónico ou contrasinal incorrectos", "paths_validation_failed": "{paths, plural, one {# ruta fallou} other {# rutas fallaron}} na validación", @@ -950,10 +952,10 @@ "exif_bottom_sheet_location": "UBICACIÓN", "exif_bottom_sheet_people": "PERSOAS", "exif_bottom_sheet_person_add_person": "Engadir nome", - "exif_bottom_sheet_person_age": "Idade {}", - "exif_bottom_sheet_person_age_months": "Idade {} meses", - "exif_bottom_sheet_person_age_year_months": "Idade 1 ano, {} meses", - "exif_bottom_sheet_person_age_years": "Idade {}", + "exif_bottom_sheet_person_age": "Idade {age}", + "exif_bottom_sheet_person_age_months": "Idade {months} meses", + "exif_bottom_sheet_person_age_year_months": "Idade 1 ano, {months} meses", + "exif_bottom_sheet_person_age_years": "Idade {years}", "exit_slideshow": "Saír da Presentación", "expand_all": "Expandir todo", "experimental_settings_new_asset_list_subtitle": "Traballo en progreso", @@ -971,7 +973,7 @@ "external": "Externo", "external_libraries": "Bibliotecas Externas", "external_network": "Rede externa", - "external_network_sheet_info": "Cando non estea na rede WiFi preferida, a aplicación conectarase ao servidor a través da primeira das seguintes URLs que poida alcanzar, comezando de arriba a abaixo", + "external_network_sheet_info": "Cando non estea na rede wifi preferida, a aplicación conectarase ao servidor a través da primeira das seguintes URLs que poida alcanzar, comezando de arriba a abaixo", "face_unassigned": "Sen asignar", "failed": "Fallado", "failed_to_load_assets": "Erro ao cargar activos", @@ -999,7 +1001,7 @@ "forward": "Adiante", "general": "Xeral", "get_help": "Obter Axuda", - "get_wifiname_error": "Non se puido obter o nome da Wi-Fi. Asegúrate de que concedeu os permisos necesarios e está conectado a unha rede Wi-Fi", + "get_wifiname_error": "Non se puido obter o nome da wifi. Asegúrate de que concedeu os permisos necesarios e está conectado a unha rede wifi", "getting_started": "Primeiros Pasos", "go_back": "Volver", "go_to_folder": "Ir ao cartafol", @@ -1116,9 +1118,9 @@ "loading": "Cargando", "loading_search_results_failed": "Erro ao cargar os resultados da busca", "local_network": "Rede local", - "local_network_sheet_info": "A aplicación conectarase ao servidor a través desta URL cando use a rede Wi-Fi especificada", + "local_network_sheet_info": "A aplicación conectarase ao servidor a través desta URL cando use a rede wifi especificada", "location_permission": "Permiso de ubicación", - "location_permission_content": "Para usar a función de cambio automático, Immich necesita permiso de ubicación precisa para poder ler o nome da rede WiFi actual", + "location_permission_content": "Para usar a función de cambio automático, Immich necesita permiso de ubicación precisa para poder ler o nome da rede wifi actual", "location_picker_choose_on_map": "Elixir no mapa", "location_picker_latitude_error": "Introducir unha latitude válida", "location_picker_latitude_hint": "Introduza a túa latitude aquí", @@ -1168,8 +1170,8 @@ "manage_your_devices": "Xestionar os teus dispositivos con sesión iniciada", "manage_your_oauth_connection": "Xestionar a túa conexión OAuth", "map": "Mapa", - "map_assets_in_bound": "{} foto", - "map_assets_in_bounds": "{} fotos", + "map_assets_in_bound": "{count} foto", + "map_assets_in_bounds": "{count} fotos", "map_cannot_get_user_location": "Non se pode obter a ubicación do usuario", "map_location_dialog_yes": "Si", "map_location_picker_page_use_location": "Usar esta ubicación", @@ -1183,15 +1185,18 @@ "map_settings": "Configuración do mapa", "map_settings_dark_mode": "Modo escuro", "map_settings_date_range_option_day": "Últimas 24 horas", - "map_settings_date_range_option_days": "Últimos {} días", + "map_settings_date_range_option_days": "Últimos {days} días", "map_settings_date_range_option_year": "Último ano", - "map_settings_date_range_option_years": "Últimos {} anos", + "map_settings_date_range_option_years": "Últimos {years} anos", "map_settings_dialog_title": "Configuración do Mapa", "map_settings_include_show_archived": "Incluír Arquivados", "map_settings_include_show_partners": "Incluír Compañeiros/as", "map_settings_only_show_favorites": "Mostrar Só Favoritos", "map_settings_theme_settings": "Tema do Mapa", "map_zoom_to_see_photos": "Alonxe o zoom para ver fotos", + "mark_all_as_read": "Marcar todo como lido", + "mark_as_read": "Marcar como lido", + "marked_all_as_read": "Marcado todo como lido", "matches": "Coincidencias", "media_type": "Tipo de medio", "memories": "Recordos", @@ -1201,7 +1206,7 @@ "memories_start_over": "Comezar de novo", "memories_swipe_to_close": "Deslizar cara arriba para pechar", "memories_year_ago": "Hai un ano", - "memories_years_ago": "Hai {} anos", + "memories_years_ago": "Hai {years} anos", "memory": "Recordo", "memory_lane_title": "Camiño dos Recordos {title}", "menu": "Menú", @@ -1250,6 +1255,7 @@ "no_favorites_message": "Engade favoritos para atopar rapidamente as túas mellores fotos e vídeos", "no_libraries_message": "Crea unha biblioteca externa para ver as túas fotos e vídeos", "no_name": "Sen Nome", + "no_notifications": "Sen notificacións", "no_places": "Sen lugares", "no_results": "Sen resultados", "no_results_description": "Proba cun sinónimo ou palabra chave máis xeral", @@ -1304,7 +1310,7 @@ "partner_page_partner_add_failed": "Erro ao engadir compañeiro/a", "partner_page_select_partner": "Seleccionar compañeiro/a", "partner_page_shared_to_title": "Compartido con", - "partner_page_stop_sharing_content": "{} xa non poderás acceder ás túas fotos.", + "partner_page_stop_sharing_content": "{partner} xa non poderá acceder ás túas fotos.", "partner_sharing": "Compartición con Compañeiro/a", "partners": "Compañeiros/as", "password": "Contrasinal", @@ -1380,7 +1386,7 @@ "public_share": "Compartir Público", "purchase_account_info": "Seguidor/a", "purchase_activated_subtitle": "Grazas por apoiar Immich e o software de código aberto", - "purchase_activated_time": "Activado o {date, date}", + "purchase_activated_time": "Activado o {date}", "purchase_activated_title": "A súa chave activouse correctamente", "purchase_button_activate": "Activar", "purchase_button_buy": "Comprar", @@ -1425,6 +1431,8 @@ "recent_searches": "Buscas recentes", "recently_added": "Engadido recentemente", "recently_added_page_title": "Engadido Recentemente", + "recently_taken": "Recentemente tomado", + "recently_taken_page_title": "Recentemente Tomado", "refresh": "Actualizar", "refresh_encoded_videos": "Actualizar vídeos codificados", "refresh_faces": "Actualizar caras", @@ -1589,12 +1597,12 @@ "setting_languages_apply": "Aplicar", "setting_languages_subtitle": "Cambiar a lingua da aplicación", "setting_languages_title": "Linguas", - "setting_notifications_notify_failures_grace_period": "Notificar fallos da copia de seguridade en segundo plano: {}", - "setting_notifications_notify_hours": "{} horas", + "setting_notifications_notify_failures_grace_period": "Notificar fallos da copia de seguridade en segundo plano: {duration}", + "setting_notifications_notify_hours": "{count} horas", "setting_notifications_notify_immediately": "inmediatamente", - "setting_notifications_notify_minutes": "{} minutos", + "setting_notifications_notify_minutes": "{count} minutos", "setting_notifications_notify_never": "nunca", - "setting_notifications_notify_seconds": "{} segundos", + "setting_notifications_notify_seconds": "{count} segundos", "setting_notifications_single_progress_subtitle": "Información detallada do progreso da carga por activo", "setting_notifications_single_progress_title": "Mostrar progreso detallado da copia de seguridade en segundo plano", "setting_notifications_subtitle": "Axustar as túas preferencias de notificación", @@ -1608,7 +1616,7 @@ "settings_saved": "Configuración gardada", "share": "Compartir", "share_add_photos": "Engadir fotos", - "share_assets_selected": "{} seleccionados", + "share_assets_selected": "{count} seleccionados", "share_dialog_preparing": "Preparando...", "shared": "Compartido", "shared_album_activities_input_disable": "O comentario está desactivado", @@ -1622,32 +1630,32 @@ "shared_by_user": "Compartido por {user}", "shared_by_you": "Compartido por ti", "shared_from_partner": "Fotos de {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Subidos", + "shared_intent_upload_button_progress_text": "{current} / {total} Subidos", "shared_link_app_bar_title": "Ligazóns Compartidas", "shared_link_clipboard_copied_massage": "Copiado ao portapapeis", - "shared_link_clipboard_text": "Ligazón: {}\nContrasinal: {}", + "shared_link_clipboard_text": "Ligazón: {link}\nContrasinal: {password}", "shared_link_create_error": "Erro ao crear ligazón compartida", "shared_link_edit_description_hint": "Introduza a descrición da compartición", "shared_link_edit_expire_after_option_day": "1 día", - "shared_link_edit_expire_after_option_days": "{} días", + "shared_link_edit_expire_after_option_days": "{count} días", "shared_link_edit_expire_after_option_hour": "1 hora", - "shared_link_edit_expire_after_option_hours": "{} horas", + "shared_link_edit_expire_after_option_hours": "{count} horas", "shared_link_edit_expire_after_option_minute": "1 minuto", - "shared_link_edit_expire_after_option_minutes": "{} minutos", - "shared_link_edit_expire_after_option_months": "{} meses", - "shared_link_edit_expire_after_option_year": "{} ano", + "shared_link_edit_expire_after_option_minutes": "{count} minutos", + "shared_link_edit_expire_after_option_months": "{count} meses", + "shared_link_edit_expire_after_option_year": "{count} ano", "shared_link_edit_password_hint": "Introduza o contrasinal da compartición", "shared_link_edit_submit_button": "Actualizar ligazón", "shared_link_error_server_url_fetch": "Non se pode obter a url do servidor", - "shared_link_expires_day": "Caduca en {} día", - "shared_link_expires_days": "Caduca en {} días", - "shared_link_expires_hour": "Caduca en {} hora", - "shared_link_expires_hours": "Caduca en {} horas", - "shared_link_expires_minute": "Caduca en {} minuto", - "shared_link_expires_minutes": "Caduca en {} minutos", + "shared_link_expires_day": "Caduca en {count} día", + "shared_link_expires_days": "Caduca en {count} días", + "shared_link_expires_hour": "Caduca en {count} hora", + "shared_link_expires_hours": "Caduca en {count} horas", + "shared_link_expires_minute": "Caduca en {count} minuto", + "shared_link_expires_minutes": "Caduca en {count} minutos", "shared_link_expires_never": "Caduca ∞", - "shared_link_expires_second": "Caduca en {} segundo", - "shared_link_expires_seconds": "Caduca en {} segundos", + "shared_link_expires_second": "Caduca en {count} segundo", + "shared_link_expires_seconds": "Caduca en {count} segundos", "shared_link_individual_shared": "Compartido individualmente", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Xestionar ligazóns Compartidas", @@ -1748,7 +1756,7 @@ "theme_selection": "Selección de tema", "theme_selection_description": "Establecer automaticamente o tema a claro ou escuro baseándose na preferencia do sistema do teu navegador", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamento nas tellas de activos", - "theme_setting_asset_list_tiles_per_row_title": "Número de activos por fila ({})", + "theme_setting_asset_list_tiles_per_row_title": "Número de activos por fila ({count})", "theme_setting_colorful_interface_subtitle": "Aplicar cor primaria ás superficies de fondo.", "theme_setting_colorful_interface_title": "Interface colorida", "theme_setting_image_viewer_quality_subtitle": "Axustar a calidade do visor de imaxes de detalle", @@ -1783,11 +1791,11 @@ "trash_no_results_message": "As fotos e vídeos movidos ao lixo aparecerán aquí.", "trash_page_delete_all": "Eliminar Todo", "trash_page_empty_trash_dialog_content": "Queres baleirar os teus activos no lixo? Estes elementos eliminaranse permanentemente de Immich", - "trash_page_info": "Os elementos no lixo eliminaranse permanentemente despois de {} días", + "trash_page_info": "Os elementos no lixo eliminaranse permanentemente despois de {days} días", "trash_page_no_assets": "Non hai activos no lixo", "trash_page_restore_all": "Restaurar Todo", "trash_page_select_assets_btn": "Seleccionar activos", - "trash_page_title": "Lixo ({})", + "trash_page_title": "Lixo ({count})", "trashed_items_will_be_permanently_deleted_after": "Os elementos no lixo eliminaranse permanentemente despois de {days, plural, one {# día} other {# días}}.", "type": "Tipo", "unarchive": "Desarquivar", @@ -1825,7 +1833,7 @@ "upload_status_errors": "Erros", "upload_status_uploaded": "Subido", "upload_success": "Subida exitosa, actualice a páxina para ver os novos activos subidos.", - "upload_to_immich": "Subir a Immich ({})", + "upload_to_immich": "Subir a Immich ({count})", "uploading": "Subindo", "url": "URL", "usage": "Uso", @@ -1882,11 +1890,11 @@ "week": "Semana", "welcome": "Benvido/a", "welcome_to_immich": "Benvido/a a Immich", - "wifi_name": "Nome da Wi-Fi", + "wifi_name": "Nome da wifi", "year": "Ano", "years_ago": "Hai {years, plural, one {# ano} other {# anos}}", "yes": "Si", "you_dont_have_any_shared_links": "Non tes ningunha ligazón compartida", - "your_wifi_name": "O nome da túa Wi-Fi", + "your_wifi_name": "O nome da túa wifi", "zoom_image": "Ampliar Imaxe" } diff --git a/i18n/he.json b/i18n/he.json index 1efe67b428..f2849e8e05 100644 --- a/i18n/he.json +++ b/i18n/he.json @@ -53,6 +53,7 @@ "confirm_email_below": "כדי לאשר, יש להקליד \"{email}\" למטה", "confirm_reprocess_all_faces": "האם באמת ברצונך לעבד מחדש את כל הפנים? זה גם ינקה אנשים בעלי שם.", "confirm_user_password_reset": "האם באמת ברצונך לאפס את הסיסמה של המשתמש {user}?", + "confirm_user_pin_code_reset": "האם אתה בטוח שברצונך לאפס את קוד ה PIN של {user}?", "create_job": "צור עבודה", "cron_expression": "ביטוי cron", "cron_expression_description": "הגדר את מרווח הסריקה באמצעות תבנית ה- cron. למידע נוסף נא לפנות למשל אל Crontab Guru", @@ -610,6 +611,7 @@ "change_password_form_new_password": "סיסמה חדשה", "change_password_form_password_mismatch": "סיסמאות לא תואמות", "change_password_form_reenter_new_password": "הכנס שוב סיסמה חדשה", + "change_pin_code": "שנה קוד PIN", "change_your_password": "החלף את הסיסמה שלך", "changed_visibility_successfully": "הנראות שונתה בהצלחה", "check_all": "לסמן הכל", @@ -650,6 +652,7 @@ "confirm_delete_face": "האם באמת ברצונך למחוק את הפנים של {name} מהתמונה?", "confirm_delete_shared_link": "האם באמת ברצונך למחוק את הקישור המשותף הזה?", "confirm_keep_this_delete_others": "כל שאר תמונות שבערימה יימחקו למעט תמונה זאת. האם באמת ברצונך להמשיך?", + "confirm_new_pin_code": "אשר קוד PIN חדש", "confirm_password": "אשר סיסמה", "contain": "מכיל", "context": "הקשר", @@ -695,6 +698,7 @@ "crop": "חתוך", "curated_object_page_title": "דברים", "current_device": "מכשיר נוכחי", + "current_pin_code": "קוד PIN הנוכחי", "current_server_address": "כתובת שרת נוכחית", "custom_locale": "אזור שפה מותאם אישית", "custom_locale_description": "עצב תאריכים ומספרים על סמך השפה והאזור", @@ -922,6 +926,7 @@ "unable_to_remove_reaction": "לא ניתן להסיר תגובה", "unable_to_repair_items": "לא ניתן לתקן פריטים", "unable_to_reset_password": "לא ניתן לאפס סיסמה", + "unable_to_reset_pin_code": "לא ניתן לאפס קוד PIN", "unable_to_resolve_duplicate": "לא ניתן לפתור כפילות", "unable_to_restore_assets": "לא ניתן לשחזר תמונות", "unable_to_restore_trash": "לא ניתן לשחזר אשפה", @@ -1240,6 +1245,7 @@ "new_api_key": "מפתח API חדש", "new_password": "סיסמה חדשה", "new_person": "אדם חדש", + "new_pin_code": "קוד PIN חדש", "new_user_created": "משתמש חדש נוצר", "new_version_available": "גרסה חדשה זמינה", "newest_first": "החדש ביותר ראשון", @@ -1360,6 +1366,9 @@ "photos_count": "{count, plural, one {תמונה {count, number}} other {{count, number} תמונות}}", "photos_from_previous_years": "תמונות משנים קודמות", "pick_a_location": "בחר מיקום", + "pin_code_changed_successfully": "קוד ה PIN שונה בהצלחה", + "pin_code_reset_successfully": "קוד PIN אופס בהצלחה", + "pin_code_setup_successfully": "קוד PIN הוגדר בהצלחה", "place": "מקום", "places": "מקומות", "places_count": "{count, plural, one {מקום {count, number}} other {{count, number} מקומות}}", @@ -1479,6 +1488,7 @@ "reset": "איפוס", "reset_password": "איפוס סיסמה", "reset_people_visibility": "אפס את נראות האנשים", + "reset_pin_code": "אפס קוד PIN", "reset_to_default": "אפס לברירת מחדל", "resolve_duplicates": "פתור כפילויות", "resolved_all_duplicates": "כל הכפילויות נפתרו", @@ -1619,6 +1629,7 @@ "settings": "הגדרות", "settings_require_restart": "אנא הפעל מחדש את היישום כדי להחיל הגדרה זו", "settings_saved": "ההגדרות נשמרו", + "setup_pin_code": "הגדר קוד PIN", "share": "שתף", "share_add_photos": "הוסף תמונות", "share_assets_selected": "{} נבחרו", @@ -1803,6 +1814,8 @@ "trash_page_title": "אשפה ({})", "trashed_items_will_be_permanently_deleted_after": "פריטים באשפה ימחקו לצמיתות לאחר {days, plural, one {יום #} other {# ימים}}.", "type": "סוג", + "unable_to_change_pin_code": "לא ניתן לשנות את קוד ה PIN", + "unable_to_setup_pin_code": "לא ניתן להגדיר קוד PIN", "unarchive": "הוצא מארכיון", "unarchived_count": "{count, plural, other {# הוצאו מהארכיון}}", "unfavorite": "לא מועדף", @@ -1847,6 +1860,8 @@ "user": "משתמש", "user_id": "מזהה משתמש", "user_liked": "{user} אהב את {type, select, photo {התמונה הזאת} video {הסרטון הזה} asset {התמונה הזאת} other {זה}}", + "user_pin_code_settings": "קוד PIN", + "user_pin_code_settings_description": "נהל את קוד ה PIN שלך", "user_purchase_settings": "רכישה", "user_purchase_settings_description": "ניהול הרכישה שלך", "user_role_set": "הגדר את {user} בתור {role}", diff --git a/i18n/hr.json b/i18n/hr.json index 4566544b50..2172777fa5 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -1380,7 +1380,7 @@ "public_share": "Javno dijeljenje", "purchase_account_info": "Podržava softver", "purchase_activated_subtitle": "Hvala što podržavate Immich i softver otvorenog koda", - "purchase_activated_time": "Aktivirano {date, date}", + "purchase_activated_time": "Aktivirano {date}", "purchase_activated_title": "Vaš ključ je uspješno aktiviran", "purchase_button_activate": "Aktiviraj", "purchase_button_buy": "Kupi", diff --git a/i18n/hu.json b/i18n/hu.json index b3de1ac19d..153473e0eb 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "Biztosan letiltod az összes bejelentkezési módot? A bejelentkezés teljesen le lesz tiltva.", "authentication_settings_reenable": "Az újbóli engedélyezéshez használj egySzerver Parancsot.", "background_task_job": "Háttérfeladatok", - "backup_database": "Adatbázis Biztonsági Mentése", - "backup_database_enable_description": "Adatbázis biztonsági mentések engedélyezése", - "backup_keep_last_amount": "Megőrizendő korábbi biztonsági mentések száma", - "backup_settings": "Biztonsági mentés beállításai", - "backup_settings_description": "Adatbázis mentési beállításainak kezelése", + "backup_database": "Adatbázis lementése", + "backup_database_enable_description": "Adatbázis mentések engedélyezése", + "backup_keep_last_amount": "Megőrizendő korábbi mentések száma", + "backup_settings": "Adatbázis mentés beállításai", + "backup_settings_description": "Adatbázis mentés beállításainak kezelése. Megjegyzés: Ezek a feladatok nincsenek felügyelve, így nem kapsz értesítés meghiúsulás esetén.", "check_all": "Összes Kipiálása", "cleanup": "Takarítás", "cleared_jobs": "{job}: feladatai törölve", @@ -53,6 +53,7 @@ "confirm_email_below": "A megerősítéshez írd be, hogy \"{email}\"", "confirm_reprocess_all_faces": "Biztos vagy benne, hogy újra fel szeretnéd dolgozni az összes arcot? Ez a már elnevezett személyeket is törli.", "confirm_user_password_reset": "Biztosan vissza szeretnéd állítani {user} jelszavát?", + "confirm_user_pin_code_reset": "Biztos, hogy vissza akarod állítani {user} PIN-kódját?", "create_job": "Feladat létrehozása", "cron_expression": "Cron kifejezés", "cron_expression_description": "A beolvasási időköz beállítása a cron formátummal. További információért lásd pl. Crontab Guru", @@ -192,6 +193,7 @@ "oauth_auto_register": "Automatikus regisztráció", "oauth_auto_register_description": "Új felhasználók automatikus regisztrálása az OAuth használatával történő bejelentkezés után", "oauth_button_text": "Gomb szövege", + "oauth_client_secret_description": "Kötelező, ha az OAuth szolgáltató nem támogatja a PKCE-t (Proof Key for Code Exchange)", "oauth_enable_description": "Bejelentkezés OAuth használatával", "oauth_mobile_redirect_uri": "Mobil átirányítási URI", "oauth_mobile_redirect_uri_override": "Mobil átirányítási URI felülírás", @@ -205,6 +207,8 @@ "oauth_storage_quota_claim_description": "A felhasználó tárhelykvótájának automatikus beállítása ennek az igényeltre.", "oauth_storage_quota_default": "Alapértelmezett tárhelykvóta (GiB)", "oauth_storage_quota_default_description": "Alapértelmezett tárhely kvóta GiB-ban, amennyiben a felhasználó nem jelezte az igényét (A korlátlan tárhelyhez 0-t adj meg).", + "oauth_timeout": "Kérés időkorlátja", + "oauth_timeout_description": "Kérések időkorlátja milliszekundumban", "offline_paths": "Offline Útvonalak", "offline_paths_description": "Ezek az eredmények olyan fájlok kézi törlésének tudhatók be, amelyek nem részei külső képtárnak.", "password_enable_description": "Bejelentkezés emaillel és jelszóval", @@ -364,13 +368,16 @@ "admin_password": "Admin Jelszó", "administration": "Adminisztráció", "advanced": "Haladó", - "advanced_settings_log_level_title": "Naplózás szintje: {}", + "advanced_settings_enable_alternate_media_filter_subtitle": "Ezzel a beállítással a szinkronizálás során alternatív kritériumok alapján szűrheted a fájlokat. Csak akkor próbáld ki, ha problémáid vannak azzal, hogy az alkalmazás nem ismeri fel az összes albumot.", + "advanced_settings_enable_alternate_media_filter_title": "[KÍSÉRLETI] Alternatív eszköz album szinkronizálási szűrő használata", + "advanced_settings_log_level_title": "Naplózás szintje: {level}", "advanced_settings_prefer_remote_subtitle": "Néhány eszköz fájdalmasan lassan tölti be az eszközön lévő bélyegképeket. Ez a beállítás inkább a távoli képeket tölti be helyettük.", "advanced_settings_prefer_remote_title": "Távoli képek előnyben részesítése", "advanced_settings_proxy_headers_subtitle": "Add meg azokat a proxy fejléceket, amiket az app elküldjön minden hálózati kérésnél", "advanced_settings_proxy_headers_title": "Proxy Fejlécek", "advanced_settings_self_signed_ssl_subtitle": "Nem ellenőrzi a szerver SSL tanúsítványát. Önaláírt tanúsítvány esetén szükséges beállítás.", "advanced_settings_self_signed_ssl_title": "Önaláírt SSL tanúsítványok engedélyezése", + "advanced_settings_sync_remote_deletions_subtitle": "Automatikusan törölni vagy visszaállítani egy elemet ezen az eszközön, ha az adott műveletet a weben hajtották végre", "advanced_settings_tile_subtitle": "Haladó felhasználói beállítások", "advanced_settings_troubleshooting_subtitle": "További funkciók engedélyezése hibaelhárítás céljából", "advanced_settings_troubleshooting_title": "Hibaelhárítás", @@ -603,6 +610,7 @@ "change_password_form_new_password": "Új Jelszó", "change_password_form_password_mismatch": "A beírt jelszavak nem egyeznek", "change_password_form_reenter_new_password": "Jelszó (Még Egyszer)", + "change_pin_code": "PIN kód megváltoztatása", "change_your_password": "Jelszavad megváltoztatása", "changed_visibility_successfully": "Láthatóság sikeresen megváltoztatva", "check_all": "Mind Kijelöl", @@ -643,6 +651,7 @@ "confirm_delete_face": "Biztos, hogy törölni szeretnéd a(z) {name} arcát az elemről?", "confirm_delete_shared_link": "Biztosan törölni szeretnéd ezt a megosztott linket?", "confirm_keep_this_delete_others": "Minden más elem a készletben törlésre kerül, kivéve ezt az elemet. Biztosan folytatni szeretnéd?", + "confirm_new_pin_code": "Új PIN kód megerősítése", "confirm_password": "Jelszó megerősítése", "contain": "Belül", "context": "Kontextus", @@ -688,6 +697,7 @@ "crop": "Kivágás", "curated_object_page_title": "Dolgok", "current_device": "Ez az eszköz", + "current_pin_code": "Aktuális PIN kód", "current_server_address": "Jelenlegi szerver cím", "custom_locale": "Egyéni Területi Beállítás", "custom_locale_description": "Dátumok és számok formázása a nyelv és terület szerint", @@ -1227,6 +1237,7 @@ "new_api_key": "Új API Kulcs", "new_password": "Új jelszó", "new_person": "Új személy", + "new_pin_code": "Új PIN kód", "new_user_created": "Új felhasználó létrehozva", "new_version_available": "ÚJ VERZIÓ ÉRHETŐ EL", "newest_first": "Legújabb először", @@ -1344,6 +1355,9 @@ "photos_count": "{count, plural, one {{count, number} Fotó} other {{count, number} Fotó}}", "photos_from_previous_years": "Fényképek az előző évekből", "pick_a_location": "Hely választása", + "pin_code_changed_successfully": "Sikeres PIN kód változtatás", + "pin_code_reset_successfully": "Sikeres PIN kód visszaállítás", + "pin_code_setup_successfully": "Sikeres PIN kód beállítás", "place": "Hely", "places": "Helyek", "places_count": "{count, plural, one {{count, number} Helyszín} other {{count, number} Helyszín}}", @@ -1374,7 +1388,7 @@ "public_share": "Nyilvános Megosztás", "purchase_account_info": "Támogató", "purchase_activated_subtitle": "Köszönjük, hogy támogattad az Immich-et és a nyílt forráskódú szoftvereket", - "purchase_activated_time": "Aktiválva ekkor: {date, date}", + "purchase_activated_time": "Aktiválva ekkor: {date}", "purchase_activated_title": "Kulcs sikeresen aktiválva", "purchase_button_activate": "Aktiválás", "purchase_button_buy": "Vásárlás", @@ -1461,6 +1475,7 @@ "reset": "Visszaállítás", "reset_password": "Jelszó visszaállítása", "reset_people_visibility": "Személyek láthatóságának visszaállítása", + "reset_pin_code": "PIN kód visszaállítása", "reset_to_default": "Visszaállítás alapállapotba", "resolve_duplicates": "Duplikátumok feloldása", "resolved_all_duplicates": "Minden duplikátum feloldása", @@ -1600,6 +1615,7 @@ "settings": "Beállítások", "settings_require_restart": "Ennek a beállításnak az érvénybe lépéséhez indítsd újra az Immich-et", "settings_saved": "Beállítások elmentve", + "setup_pin_code": "PIN kód beállítása", "share": "Megosztás", "share_add_photos": "Fotók hozzáadása", "share_assets_selected": "{} kiválasztva", @@ -1784,6 +1800,8 @@ "trash_page_title": "Lomtár ({})", "trashed_items_will_be_permanently_deleted_after": "A lomtárban lévő elemek véglegesen törlésre kerülnek {days, plural, other {# nap}} múlva.", "type": "Típus", + "unable_to_change_pin_code": "Sikertelen PIN kód változtatás", + "unable_to_setup_pin_code": "Sikertelen PIN kód beállítás", "unarchive": "Archívumból kivesz", "unarchived_count": "{count, plural, other {# elem kivéve az archívumból}}", "unfavorite": "Kedvenc közül kivesz", @@ -1828,6 +1846,8 @@ "user": "Felhasználó", "user_id": "Felhasználó azonosítója", "user_liked": "{user} felhasználónak {type, select, photo {ez a fénykép} video {ez a videó} asset {ez az elem} other {ez}} tetszik", + "user_pin_code_settings": "PIN kód", + "user_pin_code_settings_description": "PIN kód kezelése", "user_purchase_settings": "Megvásárlás", "user_purchase_settings_description": "Vásárlás kezelése", "user_role_set": "{user} felhasználónak {role} jogkör biztosítása", diff --git a/i18n/hy.json b/i18n/hy.json index 34f0f05119..6d6600439a 100644 --- a/i18n/hy.json +++ b/i18n/hy.json @@ -370,7 +370,7 @@ "day": "Օր", "default_locale": "", "default_locale_description": "", - "delete": "", + "delete": "Ջնջել", "delete_album": "", "delete_api_key_prompt": "", "delete_key": "", diff --git a/i18n/id.json b/i18n/id.json index a75ef00991..0814941256 100644 --- a/i18n/id.json +++ b/i18n/id.json @@ -1383,7 +1383,7 @@ "public_share": "Pembagian Publik", "purchase_account_info": "Pendukung", "purchase_activated_subtitle": "Terima kasih telah mendukung Immich dan perangkat lunak sumber terbuka", - "purchase_activated_time": "Di aktivasi pada {date, date}", + "purchase_activated_time": "Di aktivasi pada {date}", "purchase_activated_title": "Kunci kamu telah sukses di aktivasi", "purchase_button_activate": "Aktifkan", "purchase_button_buy": "Beli", diff --git a/i18n/it.json b/i18n/it.json index 550f14beb9..304cbfe880 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -53,6 +53,7 @@ "confirm_email_below": "Per confermare, scrivi \"{email}\" qui sotto", "confirm_reprocess_all_faces": "Sei sicuro di voler riprocessare tutti i volti? Questo cancellerà tutte le persone nominate.", "confirm_user_password_reset": "Sei sicuro di voler resettare la password di {user}?", + "confirm_user_pin_code_reset": "Sicuro di voler resettare il codice PIN di {user}?", "create_job": "Crea Processo", "cron_expression": "Espressione Cron", "cron_expression_description": "Imposta il tempo di scansione utilizzando il formato Cron. Per ulteriori informazioni fare riferimento a Crontab Guru", @@ -206,7 +207,8 @@ "oauth_storage_quota_claim_description": "Imposta automaticamente il limite di archiviazione dell'utente in base al valore di questa dichiarazione di ambito(claim).", "oauth_storage_quota_default": "Limite predefinito di archiviazione (GiB)", "oauth_storage_quota_default_description": "Limite in GiB da usare quanto nessuna dichiarazione di ambito(claim) è stata fornita (Inserisci 0 per archiviazione illimitata).", - "oauth_timeout": "", + "oauth_timeout": "Timeout Richiesta", + "oauth_timeout_description": "Timeout per le richieste, espresso in millisecondi", "offline_paths": "Percorsi offline", "offline_paths_description": "Questi risultati potrebbero essere dovuti all'eliminazione manuale di file che non fanno parte di una libreria esterna.", "password_enable_description": "Login con email e password", @@ -347,6 +349,7 @@ "user_delete_delay_settings_description": "Numero di giorni dopo l'eliminazione per cancellare in modo definitivo l'account e gli asset di un utente. Il processo di cancellazione dell'utente viene eseguito a mezzanotte per verificare se esistono utenti pronti a essere eliminati. Le modifiche a questa impostazioni saranno prese in considerazione dalla prossima esecuzione.", "user_delete_immediately": "L'account e tutti gli asset dell'utente {user} verranno messi in coda per la cancellazione permanente immediata.", "user_delete_immediately_checkbox": "utente", + "user_details": "Dettagli Utente", "user_management": "Gestione Utenti", "user_password_has_been_reset": "La password dell'utente è stata reimpostata:", "user_password_reset_description": "Per favore inserisci una password temporanea per l'utente e informalo che dovrà cambiare la password al prossimo login.", @@ -368,7 +371,7 @@ "advanced": "Avanzate", "advanced_settings_enable_alternate_media_filter_subtitle": "Usa questa opzione per filtrare i contenuti multimediali durante la sincronizzazione in base a criteri alternativi. Prova questa opzione solo se riscontri problemi con il rilevamento di tutti gli album da parte dell'app.", "advanced_settings_enable_alternate_media_filter_title": "[SPERIMENTALE] Usa un filtro alternativo per la sincronizzazione degli album del dispositivo", - "advanced_settings_log_level_title": "Livello log: {}", + "advanced_settings_log_level_title": "Livello log: {level}", "advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono molto lenti a caricare le anteprime delle immagini dal dispositivo. Attivare questa impostazione per caricare invece le immagini remote.", "advanced_settings_prefer_remote_title": "Preferisci immagini remote", "advanced_settings_proxy_headers_subtitle": "Definisci gli header per i proxy che Immich dovrebbe inviare con ogni richiesta di rete", @@ -399,9 +402,9 @@ "album_remove_user_confirmation": "Sicuro di voler rimuovere l'utente {user}?", "album_share_no_users": "Sembra che tu abbia condiviso questo album con tutti gli utenti oppure non hai nessun utente con cui condividere.", "album_thumbnail_card_item": "1 elemento", - "album_thumbnail_card_items": "{} elementi", + "album_thumbnail_card_items": "{count} elementi", "album_thumbnail_card_shared": " · Condiviso", - "album_thumbnail_shared_by": "Condiviso da {}", + "album_thumbnail_shared_by": "Condiviso da {user}", "album_updated": "Album aggiornato", "album_updated_setting_description": "Ricevi una notifica email quando un album condiviso ha nuovi media", "album_user_left": "{album} abbandonato", @@ -439,7 +442,7 @@ "archive": "Archivio", "archive_or_unarchive_photo": "Archivia o ripristina foto", "archive_page_no_archived_assets": "Nessuna oggetto archiviato", - "archive_page_title": "Archivio ({})", + "archive_page_title": "Archivio ({count})", "archive_size": "Dimensioni Archivio", "archive_size_description": "Imposta le dimensioni dell'archivio per i download (in GiB)", "archived": "Archiviati", @@ -476,18 +479,18 @@ "assets_added_to_album_count": "{count, plural, one {# asset aggiunto} other {# asset aggiunti}} all'album", "assets_added_to_name_count": "Aggiunti {count, plural, one {# asset} other {# assets}} a {hasName, select, true {{name}} other {new album}}", "assets_count": "{count, plural, other {# asset}}", - "assets_deleted_permanently": "{} elementi cancellati definitivamente", - "assets_deleted_permanently_from_server": "{} elementi cancellati definitivamente dal server Immich", + "assets_deleted_permanently": "{count} elementi cancellati definitivamente", + "assets_deleted_permanently_from_server": "{count} elementi cancellati definitivamente dal server Immich", "assets_moved_to_trash_count": "{count, plural, one {# asset spostato} other {# asset spostati}} nel cestino", "assets_permanently_deleted_count": "{count, plural, one {# asset cancellato} other {# asset cancellati}} definitivamente", "assets_removed_count": "{count, plural, one {# asset rimosso} other {# asset rimossi}}", - "assets_removed_permanently_from_device": "{} elementi cancellati definitivamente dal tuo dispositivo", + "assets_removed_permanently_from_device": "{count} elementi cancellati definitivamente dal tuo dispositivo", "assets_restore_confirmation": "Sei sicuro di voler ripristinare tutti gli asset cancellati? Non puoi annullare questa azione! Tieni presente che eventuali risorse offline NON possono essere ripristinate in questo modo.", "assets_restored_count": "{count, plural, one {# asset ripristinato} other {# asset ripristinati}}", - "assets_restored_successfully": "{} elementi ripristinati", - "assets_trashed": "{} elementi cestinati", + "assets_restored_successfully": "{count} elementi ripristinati", + "assets_trashed": "{count} elementi cestinati", "assets_trashed_count": "{count, plural, one {Spostato # asset} other {Spostati # assets}} nel cestino", - "assets_trashed_from_server": "{} elementi cestinati dal server Immich", + "assets_trashed_from_server": "{count} elementi cestinati dal server Immich", "assets_were_part_of_album_count": "{count, plural, one {L'asset era} other {Gli asset erano}} già parte dell'album", "authorized_devices": "Dispositivi autorizzati", "automatic_endpoint_switching_subtitle": "Connetti localmente quando la rete Wi-Fi specificata è disponibile e usa le connessioni alternative negli altri casi", @@ -496,7 +499,7 @@ "back_close_deselect": "Indietro, chiudi o deseleziona", "background_location_permission": "Permesso di localizzazione in background", "background_location_permission_content": "Per fare in modo che sia possibile cambiare rete quando è in esecuzione in background, Immich deve *sempre* avere accesso alla tua posizione precisa in modo da poter leggere il nome della rete Wi-Fi", - "backup_album_selection_page_albums_device": "Album sul dispositivo ({})", + "backup_album_selection_page_albums_device": "Album sul dispositivo ({count})", "backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere", "backup_album_selection_page_assets_scatter": "Visto che le risorse possono trovarsi in più album, questi possono essere inclusi o esclusi dal backup.", "backup_album_selection_page_select_albums": "Seleziona gli album", @@ -505,11 +508,11 @@ "backup_all": "Tutti", "backup_background_service_backup_failed_message": "Impossibile caricare i contenuti. Riprovo…", "backup_background_service_connection_failed_message": "Impossibile connettersi al server. Riprovo…", - "backup_background_service_current_upload_notification": "Caricamento {}", + "backup_background_service_current_upload_notification": "Caricamento di {filename} in corso", "backup_background_service_default_notification": "Ricerca di nuovi contenuti…", "backup_background_service_error_title": "Errore di backup", "backup_background_service_in_progress_notification": "Backup dei tuoi contenuti…", - "backup_background_service_upload_failure_notification": "Impossibile caricare {}", + "backup_background_service_upload_failure_notification": "Impossibile caricare {filename}", "backup_controller_page_albums": "Backup Album", "backup_controller_page_background_app_refresh_disabled_content": "Attiva l'aggiornamento dell'app in background in Impostazioni > Generale > Aggiorna app in background per utilizzare backup in background.", "backup_controller_page_background_app_refresh_disabled_title": "Backup in background è disattivo", @@ -520,7 +523,7 @@ "backup_controller_page_background_battery_info_title": "Ottimizzazioni batteria", "backup_controller_page_background_charging": "Solo durante la ricarica", "backup_controller_page_background_configure_error": "Impossibile configurare i servizi in background", - "backup_controller_page_background_delay": "Ritarda il backup di nuovi elementi: {}", + "backup_controller_page_background_delay": "Ritarda il backup di nuovi elementi: {duration}", "backup_controller_page_background_description": "Abilita i servizi in background per fare il backup di tutti i nuovi contenuti senza la necessità di aprire l'app", "backup_controller_page_background_is_off": "Backup automatico disattivato", "backup_controller_page_background_is_on": "Backup automatico attivo", @@ -530,12 +533,12 @@ "backup_controller_page_backup": "Backup", "backup_controller_page_backup_selected": "Selezionati: ", "backup_controller_page_backup_sub": "Foto e video caricati", - "backup_controller_page_created": "Creato il: {}", + "backup_controller_page_created": "Creato il: {date}", "backup_controller_page_desc_backup": "Attiva il backup per eseguire il caricamento automatico sul server all'apertura dell'applicazione.", "backup_controller_page_excluded": "Esclusi: ", - "backup_controller_page_failed": "Falliti: ({})", - "backup_controller_page_filename": "Nome file: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Falliti: ({count})", + "backup_controller_page_filename": "Nome file: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informazioni sul backup", "backup_controller_page_none_selected": "Nessuna selezione", "backup_controller_page_remainder": "Rimanenti", @@ -544,7 +547,7 @@ "backup_controller_page_start_backup": "Avvia backup", "backup_controller_page_status_off": "Backup è disattivato", "backup_controller_page_status_on": "Backup è attivato", - "backup_controller_page_storage_format": "{} di {} usati", + "backup_controller_page_storage_format": "{used} di {total} usati", "backup_controller_page_to_backup": "Album da caricare", "backup_controller_page_total_sub": "Tutte le foto e i video unici caricati dagli album selezionati", "backup_controller_page_turn_off": "Disattiva backup", @@ -569,21 +572,21 @@ "bulk_keep_duplicates_confirmation": "Sei sicuro di voler tenere {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione risolverà tutti i gruppi duplicati senza cancellare nulla.", "bulk_trash_duplicates_confirmation": "Sei davvero sicuro di voler cancellare {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione manterrà l'asset più pesante di ogni gruppo e cancellerà permanentemente tutti gli altri duplicati.", "buy": "Acquista Immich", - "cache_settings_album_thumbnails": "Anteprime pagine librerie ({} elementi)", + "cache_settings_album_thumbnails": "Anteprime pagine librerie ({count} elementi)", "cache_settings_clear_cache_button": "Pulisci cache", "cache_settings_clear_cache_button_title": "Pulisce la cache dell'app. Questo impatterà significativamente le prestazioni dell''app fino a quando la cache non sarà rigenerata.", "cache_settings_duplicated_assets_clear_button": "PULISCI", "cache_settings_duplicated_assets_subtitle": "Foto e video che sono nella black list dell'applicazione", - "cache_settings_duplicated_assets_title": "Elementi duplicati ({})", - "cache_settings_image_cache_size": "Dimensione cache delle immagini ({} elementi)", + "cache_settings_duplicated_assets_title": "Elementi duplicati ({count})", + "cache_settings_image_cache_size": "Dimensione cache delle immagini ({count} elementi)", "cache_settings_statistics_album": "Anteprime librerie", - "cache_settings_statistics_assets": "{} elementi ({})", + "cache_settings_statistics_assets": "{count} elementi ({size})", "cache_settings_statistics_full": "Immagini complete", "cache_settings_statistics_shared": "Anteprime album condivisi", "cache_settings_statistics_thumbnail": "Anteprime", "cache_settings_statistics_title": "Uso della cache", "cache_settings_subtitle": "Controlla il comportamento della cache dell'applicazione mobile immich", - "cache_settings_thumbnail_size": "Dimensione cache anteprime ({} elementi)", + "cache_settings_thumbnail_size": "Dimensione cache anteprime ({count} elementi)", "cache_settings_tile_subtitle": "Controlla il comportamento dello storage locale", "cache_settings_tile_title": "Archiviazione locale", "cache_settings_title": "Impostazioni della Cache", @@ -609,6 +612,7 @@ "change_password_form_new_password": "Nuova Password", "change_password_form_password_mismatch": "Le password non coincidono", "change_password_form_reenter_new_password": "Inserisci ancora la nuova password", + "change_pin_code": "Cambia il codice PIN", "change_your_password": "Modifica la tua password", "changed_visibility_successfully": "Visibilità modificata con successo", "check_all": "Controlla Tutti", @@ -649,11 +653,12 @@ "confirm_delete_face": "Sei sicuro di voler cancellare il volto di {name} dall'asset?", "confirm_delete_shared_link": "Sei sicuro di voler eliminare questo link condiviso?", "confirm_keep_this_delete_others": "Tutti gli altri asset nello stack saranno eliminati, eccetto questo asset. Sei sicuro di voler continuare?", + "confirm_new_pin_code": "Conferma il nuovo codice PIN", "confirm_password": "Conferma password", "contain": "Adatta alla finestra", "context": "Contesto", "continue": "Continua", - "control_bottom_app_bar_album_info_shared": "{} elementi · Condivisi", + "control_bottom_app_bar_album_info_shared": "{count} elementi · Condivisi", "control_bottom_app_bar_create_new_album": "Crea nuovo album", "control_bottom_app_bar_delete_from_immich": "Elimina da Immich", "control_bottom_app_bar_delete_from_local": "Elimina dal dispositivo", @@ -691,9 +696,11 @@ "create_tag_description": "Crea un nuovo tag. Per i tag annidati, si prega di inserire il percorso completo del tag tra cui barre oblique.", "create_user": "Crea utente", "created": "Creato", + "created_at": "Creato il", "crop": "Ritaglia", "curated_object_page_title": "Oggetti", "current_device": "Dispositivo attuale", + "current_pin_code": "Attuale codice PIN", "current_server_address": "Indirizzo del server in uso", "custom_locale": "Localizzazione personalizzata", "custom_locale_description": "Formatta data e numeri in base alla lingua e al paese", @@ -762,7 +769,7 @@ "download_enqueue": "Download in coda", "download_error": "Errore durante il download", "download_failed": "Download fallito", - "download_filename": "file: {}", + "download_filename": "file: {filename}", "download_finished": "Download terminato", "download_include_embedded_motion_videos": "Video incorporati", "download_include_embedded_motion_videos_description": "Includere i video incorporati nelle foto in movimento come file separato", @@ -806,6 +813,7 @@ "editor_crop_tool_h2_aspect_ratios": "Proporzioni", "editor_crop_tool_h2_rotation": "Rotazione", "email": "Email", + "email_notifications": "Notifiche email", "empty_folder": "La cartella è vuota", "empty_trash": "Svuota cestino", "empty_trash_confirmation": "Sei sicuro di volere svuotare il cestino? Questo rimuoverà tutte le risorse nel cestino in modo permanente da Immich.\nNon puoi annullare questa azione!", @@ -818,7 +826,7 @@ "error_change_sort_album": "Errore nel cambiare l'ordine di degli album", "error_delete_face": "Errore nel cancellare la faccia dalla foto", "error_loading_image": "Errore nel caricamento dell'immagine", - "error_saving_image": "Errore: {}", + "error_saving_image": "Errore: {error}", "error_title": "Errore - Qualcosa è andato storto", "errors": { "cannot_navigate_next_asset": "Impossibile passare alla risorsa successiva", @@ -921,6 +929,7 @@ "unable_to_remove_reaction": "Impossibile rimuovere reazione", "unable_to_repair_items": "Impossibile riparare elementi", "unable_to_reset_password": "Impossibile reimpostare la password", + "unable_to_reset_pin_code": "Impossibile resettare il codice PIN", "unable_to_resolve_duplicate": "Impossibile risolvere duplicato", "unable_to_restore_assets": "Impossibile ripristinare gli asset", "unable_to_restore_trash": "Impossibile ripristinare cestino", @@ -954,10 +963,10 @@ "exif_bottom_sheet_location": "POSIZIONE", "exif_bottom_sheet_people": "PERSONE", "exif_bottom_sheet_person_add_person": "Aggiungi nome", - "exif_bottom_sheet_person_age": "Età {}", - "exif_bottom_sheet_person_age_months": "Età {} mesi", - "exif_bottom_sheet_person_age_year_months": "Età 1 anno e {} mesi", - "exif_bottom_sheet_person_age_years": "Età {}", + "exif_bottom_sheet_person_age": "Età {age}", + "exif_bottom_sheet_person_age_months": "Età {months} mesi", + "exif_bottom_sheet_person_age_year_months": "Età 1 anno e {months} mesi", + "exif_bottom_sheet_person_age_years": "Età {years}", "exit_slideshow": "Esci dalla presentazione", "expand_all": "Espandi tutto", "experimental_settings_new_asset_list_subtitle": "Lavori in corso", @@ -1047,6 +1056,7 @@ "home_page_upload_err_limit": "Puoi caricare al massimo 30 file per volta, ignora quelli in eccesso", "host": "Host", "hour": "Ora", + "id": "ID", "ignore_icloud_photos": "Ignora foto iCloud", "ignore_icloud_photos_description": "Le foto che sono memorizzate su iCloud non verranno caricate sul server Immich", "image": "Immagine", @@ -1172,8 +1182,8 @@ "manage_your_devices": "Gestisci i tuoi dispositivi collegati", "manage_your_oauth_connection": "Gestisci la tua connessione OAuth", "map": "Mappa", - "map_assets_in_bound": "{} foto", - "map_assets_in_bounds": "{} foto", + "map_assets_in_bound": "{count} foto", + "map_assets_in_bounds": "{count} foto", "map_cannot_get_user_location": "Non è possibile ottenere la posizione dell'utente", "map_location_dialog_yes": "Si", "map_location_picker_page_use_location": "Usa questa posizione", @@ -1187,9 +1197,9 @@ "map_settings": "Impostazioni Mappa", "map_settings_dark_mode": "Modalità scura", "map_settings_date_range_option_day": "Ultime 24 ore", - "map_settings_date_range_option_days": "Ultimi {} giorni", + "map_settings_date_range_option_days": "Ultimi {days} giorni", "map_settings_date_range_option_year": "Ultimo anno", - "map_settings_date_range_option_years": "Ultimi {} anni", + "map_settings_date_range_option_years": "Ultimi {years} anni", "map_settings_dialog_title": "Impostazioni Mappa", "map_settings_include_show_archived": "Includi Archiviati", "map_settings_include_show_partners": "Includi Partner", @@ -1208,7 +1218,7 @@ "memories_start_over": "Ricomincia", "memories_swipe_to_close": "Scorri sopra per chiudere", "memories_year_ago": "Una anno fa", - "memories_years_ago": "{} anni fa", + "memories_years_ago": "{years, plural, other {# years}} anni fa", "memory": "Memoria", "memory_lane_title": "Sentiero dei Ricordi {title}", "menu": "Menu", @@ -1225,7 +1235,8 @@ "month": "Mese", "monthly_title_text_date_format": "MMMM y", "more": "Di più", - "moved_to_archive": "", + "moved_to_archive": "Spostati {count, plural, one {# asset} other {# assets}} nell'archivio", + "moved_to_library": "Spostati {count, plural, one {# asset} other {# assets}} nella libreria", "moved_to_trash": "Spostato nel cestino", "multiselect_grid_edit_date_time_err_read_only": "Non puoi modificare la data di risorse in sola lettura, azione ignorata", "multiselect_grid_edit_gps_err_read_only": "Non puoi modificare la posizione di risorse in sola lettura, azione ignorata", @@ -1240,6 +1251,7 @@ "new_api_key": "Nuova Chiave di API", "new_password": "Nuova password", "new_person": "Nuova persona", + "new_pin_code": "Nuovo codice PIN", "new_user_created": "Nuovo utente creato", "new_version_available": "NUOVA VERSIONE DISPONIBILE", "newest_first": "Prima recenti", @@ -1314,7 +1326,7 @@ "partner_page_partner_add_failed": "Aggiunta del partner non riuscita", "partner_page_select_partner": "Seleziona partner", "partner_page_shared_to_title": "Condividi con", - "partner_page_stop_sharing_content": "{} non sarà più in grado di accedere alle tue foto.", + "partner_page_stop_sharing_content": "{partner} non sarà più in grado di accedere alle tue foto.", "partner_sharing": "Condivisione Compagno", "partners": "Compagni", "password": "Password", @@ -1360,6 +1372,9 @@ "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Foto}}", "photos_from_previous_years": "Foto degli anni scorsi", "pick_a_location": "Scegli una posizione", + "pin_code_changed_successfully": "Codice PIN cambiato", + "pin_code_reset_successfully": "Codice PIN resettato con successo", + "pin_code_setup_successfully": "Codice PIN cambiato con successo", "place": "Posizione", "places": "Luoghi", "places_count": "{count, plural, one {{count, number} Luogo} other {{count, number} Places}}", @@ -1377,6 +1392,7 @@ "previous_or_next_photo": "Precedente o prossima foto", "primary": "Primario", "privacy": "Privacy", + "profile": "Profilo", "profile_drawer_app_logs": "Registri", "profile_drawer_client_out_of_date_major": "L'applicazione non è aggiornata. Per favore aggiorna all'ultima versione principale.", "profile_drawer_client_out_of_date_minor": "L'applicazione non è aggiornata. Per favore aggiorna all'ultima versione minore.", @@ -1390,7 +1406,7 @@ "public_share": "Condivisione Pubblica", "purchase_account_info": "Contributore", "purchase_activated_subtitle": "Grazie per supportare Immich e i software open source", - "purchase_activated_time": "Attivato il {date, date}", + "purchase_activated_time": "Attivato il {date}", "purchase_activated_title": "La tua chiave è stata attivata con successo", "purchase_button_activate": "Attiva", "purchase_button_buy": "Acquista", @@ -1479,6 +1495,7 @@ "reset": "Ripristina", "reset_password": "Ripristina password", "reset_people_visibility": "Ripristina visibilità persone", + "reset_pin_code": "Resetta il codice PIN", "reset_to_default": "Ripristina i valori predefiniti", "resolve_duplicates": "Risolvi duplicati", "resolved_all_duplicates": "Tutti i duplicati sono stati risolti", @@ -1602,12 +1619,12 @@ "setting_languages_apply": "Applica", "setting_languages_subtitle": "Cambia la lingua dell'app", "setting_languages_title": "Lingue", - "setting_notifications_notify_failures_grace_period": "Notifica caricamenti falliti in background: {}", - "setting_notifications_notify_hours": "{} ore", + "setting_notifications_notify_failures_grace_period": "Notifica caricamenti falliti in background: {duration}", + "setting_notifications_notify_hours": "{count} ore", "setting_notifications_notify_immediately": "immediatamente", - "setting_notifications_notify_minutes": "{} minuti", + "setting_notifications_notify_minutes": "{count} minuti", "setting_notifications_notify_never": "mai", - "setting_notifications_notify_seconds": "{} secondi", + "setting_notifications_notify_seconds": "{count} secondi", "setting_notifications_single_progress_subtitle": "Informazioni dettagliate sul caricamento della risorsa", "setting_notifications_single_progress_title": "Mostra avanzamento dettagliato del backup in background", "setting_notifications_subtitle": "Cambia le impostazioni di notifica", @@ -1619,9 +1636,10 @@ "settings": "Impostazioni", "settings_require_restart": "Si prega di riavviare Immich perché vengano applicate le impostazioni", "settings_saved": "Impostazioni salvate", + "setup_pin_code": "Configura un codice PIN", "share": "Condivisione", "share_add_photos": "Aggiungi foto", - "share_assets_selected": "{} selezionati", + "share_assets_selected": "{count} selezionati", "share_dialog_preparing": "Preparo…", "shared": "Condivisi", "shared_album_activities_input_disable": "I commenti sono disabilitati", @@ -1635,32 +1653,32 @@ "shared_by_user": "Condiviso da {user}", "shared_by_you": "Condiviso da te", "shared_from_partner": "Foto da {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Caricati", + "shared_intent_upload_button_progress_text": "{current} / {total} Caricati", "shared_link_app_bar_title": "Link condivisi", "shared_link_clipboard_copied_massage": "Copiato negli appunti", - "shared_link_clipboard_text": "Link: {}\nPassword: {}", + "shared_link_clipboard_text": "Link: {link}\nPassword: {password}", "shared_link_create_error": "Si è verificato un errore durante la creazione del link condiviso", "shared_link_edit_description_hint": "Inserisci la descrizione della condivisione", "shared_link_edit_expire_after_option_day": "1 giorno", - "shared_link_edit_expire_after_option_days": "{} giorni", + "shared_link_edit_expire_after_option_days": "{count} giorni", "shared_link_edit_expire_after_option_hour": "1 ora", - "shared_link_edit_expire_after_option_hours": "{} ore", + "shared_link_edit_expire_after_option_hours": "{count} ore", "shared_link_edit_expire_after_option_minute": "1 minuto", - "shared_link_edit_expire_after_option_minutes": "{} minuti", - "shared_link_edit_expire_after_option_months": "{} mesi", - "shared_link_edit_expire_after_option_year": "{} anno", + "shared_link_edit_expire_after_option_minutes": "{count} minuti", + "shared_link_edit_expire_after_option_months": "{count} mesi", + "shared_link_edit_expire_after_option_year": "{count} anno", "shared_link_edit_password_hint": "Inserire la password di condivisione", "shared_link_edit_submit_button": "Aggiorna link", "shared_link_error_server_url_fetch": "Non è possibile trovare l'indirizzo del server", - "shared_link_expires_day": "Scade tra {} giorni", - "shared_link_expires_days": "Scade tra {} giorni", - "shared_link_expires_hour": "Scade tra {} ore", - "shared_link_expires_hours": "Scade tra {} ore", - "shared_link_expires_minute": "Scade tra {} minuto", - "shared_link_expires_minutes": "Scade tra {} minuti", + "shared_link_expires_day": "Scade tra {count} giorno", + "shared_link_expires_days": "Scade tra {count} giorni", + "shared_link_expires_hour": "Scade tra {count} ora", + "shared_link_expires_hours": "Scade tra {count} ore", + "shared_link_expires_minute": "Scade tra {count} minuto", + "shared_link_expires_minutes": "Scade tra {count} minuti", "shared_link_expires_never": "Scadenza ∞", - "shared_link_expires_second": "Scade tra {} secondo", - "shared_link_expires_seconds": "Scade tra {} secondi", + "shared_link_expires_second": "Scade tra {count} secondo", + "shared_link_expires_seconds": "Scade tra {count} secondi", "shared_link_individual_shared": "Condiviso individualmente", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Gestisci link condivisi", @@ -1735,6 +1753,7 @@ "stop_sharing_photos_with_user": "Interrompi la condivisione delle tue foto con questo utente", "storage": "Spazio di archiviazione", "storage_label": "Etichetta archiviazione", + "storage_quota": "Limite Archiviazione", "storage_usage": "{used} di {available} utilizzati", "submit": "Invia", "suggestions": "Suggerimenti", @@ -1761,7 +1780,7 @@ "theme_selection": "Selezione tema", "theme_selection_description": "Imposta automaticamente il tema chiaro o scuro in base all'impostazione del tuo browser", "theme_setting_asset_list_storage_indicator_title": "Mostra indicatore dello storage nei titoli dei contenuti", - "theme_setting_asset_list_tiles_per_row_title": "Numero di elementi per riga ({})", + "theme_setting_asset_list_tiles_per_row_title": "Numero di elementi per riga ({count})", "theme_setting_colorful_interface_subtitle": "Applica il colore primario alle superfici di sfondo.", "theme_setting_colorful_interface_title": "Interfaccia colorata", "theme_setting_image_viewer_quality_subtitle": "Cambia la qualità del dettaglio dell'immagine", @@ -1796,13 +1815,15 @@ "trash_no_results_message": "Le foto cestinate saranno mostrate qui.", "trash_page_delete_all": "Elimina tutti", "trash_page_empty_trash_dialog_content": "Vuoi eliminare gli elementi nel cestino? Questi elementi saranno eliminati definitivamente da Immich", - "trash_page_info": "Gli elementi cestinati saranno eliminati definitivamente dopo {} giorni", + "trash_page_info": "Gli elementi cestinati saranno eliminati definitivamente dopo {days} giorni", "trash_page_no_assets": "Nessun elemento cestinato", "trash_page_restore_all": "Ripristina tutto", "trash_page_select_assets_btn": "Seleziona elemento", - "trash_page_title": "Cestino ({})", + "trash_page_title": "Cestino ({count})", "trashed_items_will_be_permanently_deleted_after": "Gli elementi cestinati saranno eliminati definitivamente dopo {days, plural, one {# giorno} other {# giorni}}.", "type": "Tipo", + "unable_to_change_pin_code": "Impossibile cambiare il codice PIN", + "unable_to_setup_pin_code": "Impossibile configurare il codice PIN", "unarchive": "Annulla l'archiviazione", "unarchived_count": "{count, plural, other {Non archiviati #}}", "unfavorite": "Rimuovi preferito", @@ -1826,6 +1847,7 @@ "untracked_files": "File non tracciati", "untracked_files_decription": "Questi file non vengono tracciati dall'applicazione. Sono il risultato di spostamenti falliti, caricamenti interrotti, oppure sono stati abbandonati a causa di un bug", "up_next": "Prossimo", + "updated_at": "Aggiornato il", "updated_password": "Password aggiornata", "upload": "Carica", "upload_concurrency": "Caricamenti contemporanei", @@ -1838,15 +1860,18 @@ "upload_status_errors": "Errori", "upload_status_uploaded": "Caricato", "upload_success": "Caricamento completato con successo, aggiorna la pagina per vedere i nuovi asset caricati.", - "upload_to_immich": "Carica su Immich ({})", + "upload_to_immich": "Carica su Immich ({count})", "uploading": "Caricamento", "url": "URL", "usage": "Utilizzo", "use_current_connection": "usa la connessione attuale", "use_custom_date_range": "Altrimenti utilizza un intervallo date personalizzato", "user": "Utente", + "user_has_been_deleted": "L'utente è stato rimosso.", "user_id": "ID utente", "user_liked": "A {user} piace {type, select, photo {questa foto} video {questo video} asset {questo asset} other {questo elemento}}", + "user_pin_code_settings": "Codice PIN", + "user_pin_code_settings_description": "Gestisci il tuo codice PIN", "user_purchase_settings": "Acquisto", "user_purchase_settings_description": "Gestisci il tuo acquisto", "user_role_set": "Imposta {user} come {role}", diff --git a/i18n/ja.json b/i18n/ja.json index 2d0585f00c..8c91986730 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -53,6 +53,7 @@ "confirm_email_below": "確認のため、以下に \"{email}\" と入力してください", "confirm_reprocess_all_faces": "本当にすべての顔を再処理しますか? これにより名前が付けられた人物も消去されます。", "confirm_user_password_reset": "本当に {user} のパスワードをリセットしますか?", + "confirm_user_pin_code_reset": "{user}のPINコードをリセットしてよいですか?", "create_job": "ジョブの作成", "cron_expression": "Cron式", "cron_expression_description": "cronのフォーマットを使ってスキャン間隔を設定します。詳しくはCrontab Guruなどを参照してください", @@ -192,6 +193,7 @@ "oauth_auto_register": "自動登録", "oauth_auto_register_description": "OAuthでサインインしたあと、自動的に新規ユーザーを登録する", "oauth_button_text": "ボタンテキスト", + "oauth_client_secret_description": "OAuthプロバイダーがPKCEをサポートしていない場合は必要", "oauth_enable_description": "OAuthでログイン", "oauth_mobile_redirect_uri": "モバイル用リダイレクトURI", "oauth_mobile_redirect_uri_override": "モバイル用リダイレクトURI(上書き)", @@ -205,6 +207,8 @@ "oauth_storage_quota_claim_description": "ユーザーのストレージクォータをこのクレームの値に自動的に設定します。", "oauth_storage_quota_default": "デフォルトのストレージ割り当て(GiB)", "oauth_storage_quota_default_description": "クレームが提供されていない場合に使用されるクォータをGiB単位で設定します(無制限にする場合は0を入力してください)。", + "oauth_timeout": "リクエストタイムアウト", + "oauth_timeout_description": "リクエストのタイムアウトまでの時間(ms)", "offline_paths": "オフラインのパス", "offline_paths_description": "これらの結果は、外部ライブラリに属さないファイルを手動で削除したことによる可能性があります。", "password_enable_description": "メールアドレスとパスワードでログイン", @@ -406,7 +410,7 @@ "album_user_removed": "{user} を削除しました", "album_viewer_appbar_delete_confirm": "本当にこのアルバムを削除しますか?", "album_viewer_appbar_share_err_delete": "削除失敗", - "album_viewer_appbar_share_err_leave": "退出失敗", + "album_viewer_appbar_share_err_leave": "退出に失敗しました", "album_viewer_appbar_share_err_remove": "アルバムから写真を削除する際にエラー発生", "album_viewer_appbar_share_err_title": "タイトル変更の失敗", "album_viewer_appbar_share_leave": "アルバムから退出", @@ -462,7 +466,7 @@ "asset_list_settings_title": "グリッド", "asset_offline": "アセットはオフラインです", "asset_offline_description": "このアセットはオフラインです。 Immichはファイルの場所にアクセスできません。 アセットが利用可能であることを確認しライブラリを再スキャンしてください。", - "asset_restored_successfully": "{}項目を復元しました", + "asset_restored_successfully": "復元できました", "asset_skipped": "スキップ済", "asset_skipped_in_trash": "ゴミ箱の中", "asset_uploaded": "アップロード済", @@ -481,8 +485,8 @@ "assets_removed_count": "{count, plural, one {#項目} other {#項目}}を削除しました", "assets_removed_permanently_from_device": "デバイスから{}項目を完全に削除しました", "assets_restore_confirmation": "ごみ箱のアセットをすべて復元してもよろしいですか? この操作を元に戻すことはできません! オフラインのアセットはこの方法では復元できません。", - "assets_restored_count": "{count, plural, one {#個} other {#個}}のアセットを復元しました", - "assets_restored_successfully": "{}項目を復元しました", + "assets_restored_count": "{count, plural, one {#} other {#}}項目を復元しました", + "assets_restored_successfully": "{count}項目を復元しました", "assets_trashed": "{}項目をゴミ箱に移動しました", "assets_trashed_count": "{count, plural, one {#個} other {#個}}のアセットをごみ箱に移動しました", "assets_trashed_from_server": "サーバー上の{}項目をゴミ箱に移動しました", @@ -647,6 +651,7 @@ "confirm_delete_face": "本当に『{name}』の顔をアセットから削除しますか?", "confirm_delete_shared_link": "本当にこの共有リンクを削除しますか?", "confirm_keep_this_delete_others": "このアセット以外のアセットがスタックから削除されます。本当に削除しますか?", + "confirm_new_pin_code": "このPINコードを使う", "confirm_password": "確認", "contain": "収める", "context": "状況", @@ -674,7 +679,7 @@ "covers": "カバー", "create": "作成", "create_album": "アルバムを作成", - "create_album_page_untitled": "タイトルなし", + "create_album_page_untitled": "無題のタイトル", "create_library": "ライブラリを作成", "create_link": "リンクを作る", "create_link_to_share": "共有リンクを作る", @@ -692,6 +697,7 @@ "crop": "クロップ", "curated_object_page_title": "被写体", "current_device": "現在のデバイス", + "current_pin_code": "現在のPINコード", "current_server_address": "現在のサーバーURL", "custom_locale": "カスタムロケール", "custom_locale_description": "言語と地域に基づいて日付と数値をフォーマットします", @@ -919,6 +925,7 @@ "unable_to_remove_reaction": "リアクションを削除できません", "unable_to_repair_items": "アイテムを修復できません", "unable_to_reset_password": "パスワードをリセットできません", + "unable_to_reset_pin_code": "PINコードをリセットできませんでした", "unable_to_resolve_duplicate": "重複を解決できません", "unable_to_restore_assets": "アセットを復元できません", "unable_to_restore_trash": "ゴミ箱を復元できません", @@ -1200,10 +1207,10 @@ "matches": "マッチ", "media_type": "メディアタイプ", "memories": "メモリー", - "memories_all_caught_up": "すべて確認済み", - "memories_check_back_tomorrow": "明日もう一度確認してください", + "memories_all_caught_up": "これで全部です", + "memories_check_back_tomorrow": "また明日、見に来てくださいね", "memories_setting_description": "メモリーの内容を管理します", - "memories_start_over": "始める", + "memories_start_over": "もう一度見る", "memories_swipe_to_close": "上にスワイプして閉じる", "memories_year_ago": "一年前", "memories_years_ago": "{}年前", @@ -1223,6 +1230,8 @@ "month": "月", "monthly_title_text_date_format": "yyyy MM", "more": "もっと表示", + "moved_to_archive": "{count, plural, one {#} other {#}}項目をアーカイブしました", + "moved_to_library": "{count, plural, one {#} other {#}}項目をライブラリに移動しました", "moved_to_trash": "ゴミ箱に移動しました", "multiselect_grid_edit_date_time_err_read_only": "読み取り専用の項目の日付を変更できません", "multiselect_grid_edit_gps_err_read_only": "読み取り専用の項目の位置情報を変更できません", @@ -1237,6 +1246,7 @@ "new_api_key": "新しいAPI キー", "new_password": "新しいパスワード", "new_person": "新しい人物", + "new_pin_code": "新しいPINコード", "new_user_created": "新しいユーザーが作成されました", "new_version_available": "新しいバージョンが利用可能", "newest_first": "最新順", @@ -1256,6 +1266,7 @@ "no_libraries_message": "あなたの写真や動画を表示するための外部ライブラリを作成しましょう", "no_name": "名前なし", "no_notifications": "通知なし", + "no_people_found": "一致する人物が見つかりません", "no_places": "場所なし", "no_results": "結果がありません", "no_results_description": "同義語やより一般的なキーワードを試してください", @@ -1356,6 +1367,9 @@ "photos_count": "{count, plural, one {{count, number}枚の写真} other {{count, number}枚の写真}}", "photos_from_previous_years": "以前の年の写真", "pick_a_location": "場所を選択", + "pin_code_changed_successfully": "PINコードを変更しました", + "pin_code_reset_successfully": "PINコードをリセットしました", + "pin_code_setup_successfully": "PINコードをセットアップしました", "place": "場所", "places": "撮影場所", "places_count": "{count, plural, other {{count, number}箇所}}", @@ -1386,7 +1400,7 @@ "public_share": "公開共有", "purchase_account_info": "サポーター", "purchase_activated_subtitle": "Immich とオープンソース ソフトウェアを支援していただきありがとうございます", - "purchase_activated_time": "{date, date}にアクティベート", + "purchase_activated_time": "{date}にアクティベート", "purchase_activated_title": "キーは正常にアクティベートされました", "purchase_button_activate": "アクティベート", "purchase_button_buy": "購入", @@ -1475,13 +1489,14 @@ "reset": "リセット", "reset_password": "パスワードをリセット", "reset_people_visibility": "人物の非表示設定をリセット", + "reset_pin_code": "PINコードをリセット", "reset_to_default": "デフォルトにリセット", "resolve_duplicates": "重複を解決する", "resolved_all_duplicates": "全ての重複を解決しました", "restore": "復元", "restore_all": "全て復元", "restore_user": "ユーザーを復元", - "restored_asset": "アセットを復元しました", + "restored_asset": "項目を復元しました", "resume": "再開", "retry_upload": "アップロードを再試行", "review_duplicates": "重複を調査", @@ -1567,6 +1582,7 @@ "select_keep_all": "全て保持", "select_library_owner": "ライブラリ所有者を選択", "select_new_face": "新しい顔を選択", + "select_person_to_tag": "タグを付ける人物を選んでください", "select_photos": "写真を選択", "select_trash_all": "全て削除", "select_user_for_sharing_page_err_album": "アルバム作成に失敗", @@ -1614,6 +1630,7 @@ "settings": "設定", "settings_require_restart": "Immichを再起動して設定を適用してください", "settings_saved": "設定が保存されました", + "setup_pin_code": "PINコードをセットアップ", "share": "共有", "share_add_photos": "写真を追加", "share_assets_selected": "{}選択中", @@ -1623,8 +1640,8 @@ "shared_album_activity_remove_content": "このアクティビティを削除しますか", "shared_album_activity_remove_title": "アクティビティを削除します", "shared_album_section_people_action_error": "退出に失敗", - "shared_album_section_people_action_leave": "ユーザーをアルバムから退出", - "shared_album_section_people_action_remove_user": "ユーザーをアルバムから退出", + "shared_album_section_people_action_leave": "ユーザーをアルバムから退出させる", + "shared_album_section_people_action_remove_user": "ユーザーをアルバムから退出させる", "shared_album_section_people_title": "人物", "shared_by": "により共有", "shared_by_user": "{user} により共有", @@ -1798,6 +1815,8 @@ "trash_page_title": "ゴミ箱 ({})", "trashed_items_will_be_permanently_deleted_after": "ゴミ箱に入れられたアイテムは{days, plural, one {#日} other {#日}}後に完全に削除されます。", "type": "タイプ", + "unable_to_change_pin_code": "PINコードを変更できませんでした", + "unable_to_setup_pin_code": "PINコードをセットアップできませんでした", "unarchive": "アーカイブを解除", "unarchived_count": "{count, plural, other {#枚アーカイブを解除しました}}", "unfavorite": "お気に入りから外す", @@ -1842,6 +1861,8 @@ "user": "ユーザー", "user_id": "ユーザーID", "user_liked": "{user} が{type, select, photo {この写真を} video {この動画を} asset {このアセットを} other {}}いいねしました", + "user_pin_code_settings": "PINコード", + "user_pin_code_settings_description": "PINコードを管理", "user_purchase_settings": "購入", "user_purchase_settings_description": "購入を管理", "user_role_set": "{user} を{role}に設定しました", diff --git a/i18n/ka.json b/i18n/ka.json index 8a9aadb4c0..ddd55ece2c 100644 --- a/i18n/ka.json +++ b/i18n/ka.json @@ -4,27 +4,35 @@ "account_settings": "ანგარიშის პარამეტრები", "acknowledge": "მიღება", "action": "ქმედება", + "action_common_update": "განაახლე", "actions": "ქმედებები", "active": "აქტიური", "activity": "აქტივობა", - "add": "დამატება", + "activity_changed": "აქტივობა {enabled, select, true {ჩართული} other {გამორთული}}", + "add": "დაამატე", "add_a_description": "დაამატე აღწერა", "add_a_location": "დაამატე ადგილი", "add_a_name": "დაამატე სახელი", "add_a_title": "დაასათაურე", + "add_endpoint": "", + "add_exclusion_pattern": "დაამატე გამონაკლისი ნიმუში", "add_import_path": "დაამატე საიმპორტო მისამართი", "add_location": "დაამატე ადგილი", "add_more_users": "დაამატე მომხმარებლები", "add_partner": "დაამატე პარტნიორი", "add_path": "დაამატე მისამართი", "add_photos": "დაამატე ფოტოები", + "add_to": "დაამატე ...ში", "add_to_album": "დაამატე ალბომში", + "add_to_album_bottom_sheet_added": "დამატებულია {album}-ში", + "add_to_album_bottom_sheet_already_exists": "{album}-ში უკვე არსებობს", "add_to_shared_album": "დაამატე საზიარო ალბომში", "add_url": "დაამატე URL", "added_to_archive": "დაარქივდა", "added_to_favorites": "დაამატე რჩეულებში", "added_to_favorites_count": "{count, number} დაემატა რჩეულებში", "admin": { + "asset_offline_description": "ეს საგარეო ბიბლიოთეკის აქტივი დისკზე ვერ მოიძებნა და სანაგვეში იქნა მოთავსებული. თუ ფაილი ბიბლიოთეკის შიგნით მდებარეობს, შეამოწმეთ შესაბამისი აქტივი ტაიმლაინზე. ამ აქტივის აღსადგენად, დარწმუნდით რომ ქვემოთ მოცემული ფაილის მისამართი Immich-ის მიერ წვდომადია და დაასკანერეთ ბიბლიოთეკა.", "authentication_settings": "ავთენტიკაციის პარამეტრები", "authentication_settings_description": "პაროლის, OAuth-ის და სხვა ავტენთიფიკაციის პარამეტრების მართვა", "authentication_settings_disable_all": "ნამდვილად გინდა ავტორიზაციის ყველა მეთოდის გამორთვა? ავტორიზაციას ვეღარანაირად შეძლებ.", @@ -37,14 +45,22 @@ "backup_settings_description": "მონაცემთა ბაზის პარამეტრების ამრთვა. შენიშვნა: ამ დავალებების მონიტორინგი არ ხდება და თქვენ არ მოგივათ შეტყობინება, თუ ის ჩავარდება.", "check_all": "შეამოწმე ყველა", "cleanup": "გასუფთავება", + "cleared_jobs": "დავალებები {job}-ისათვის გაწმენდილია", + "config_set_by_file": "მიმდინარე კონფიგურაცია ფაილის მიერ არის დაყენებული", "confirm_delete_library": "ნამდვილად გინდა {library} ბიბლიოთეკის წაშლა?", + "confirm_delete_library_assets": "მართლა გსურთ ამ ბიბლიოთეკის წაშლა? ეს ქმედება Immich-იდან წაშლის ყველა მონიშნულ აქტივს და შეუქცევადია. ფაილები მყარ დისკზე ხელუხლებელი დარჩება.", "confirm_email_below": "დასადასტურებლად, ქვემოთ აკრიფე \"{email}\"", + "confirm_reprocess_all_faces": "მართლა გსურთ ყველა სახის თავიდან დამუშავება? ეს ქმედება ხალხისათვის მინიჭებულ სახელებს გაწმენდს.", "confirm_user_password_reset": "ნამდვილად გინდა {user}-(ი)ს პაროლის დარესეტება?", + "create_job": "შექმენი დავალება", + "cron_expression": "Cron გამოსახულება", "disable_login": "გამორთე ავტორიზაცია", "external_library_management": "გარე ბიბლიოთეკების მართვა", "face_detection": "სახის ამოცნობა", "image_format": "ფორმატი", + "image_format_description": "WebP ფორმატი JPEG-ზე პატარა ფაილებს აწარმოებს, მაგრამ მის დამზადებას უფრო მეტი დრო სჭირდება.", "image_fullsize_title": "სრული ზომის გამოსახულების პარამეტრები", + "image_prefer_wide_gamut": "უპირატესობა მიენიჭოს ფერის ფართე დიაპაზონს", "image_quality": "ხარისხი", "image_resolution": "გაფართოება", "image_settings": "გამოსახულების პარამეტრები", diff --git a/i18n/ko.json b/i18n/ko.json index 52a094c784..28665bbfcb 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -53,6 +53,7 @@ "confirm_email_below": "계속 진행하려면 아래에 \"{email}\" 입력", "confirm_reprocess_all_faces": "모든 얼굴을 다시 처리하시겠습니까? 이름이 지정된 인물을 포함한 모든 인물이 삭제됩니다.", "confirm_user_password_reset": "{user}님의 비밀번호를 재설정하시겠습니까?", + "confirm_user_pin_code_reset": "{user}님의 PIN 코드를 초기화하시겠습니까?", "create_job": "작업 생성", "cron_expression": "Cron 표현식", "cron_expression_description": "Cron 형식을 사용하여 스캔 주기를 설정합니다. 자세한 내용과 예시는 Crontab Guru를 참조하세요.", @@ -348,6 +349,7 @@ "user_delete_delay_settings_description": "사용자 계정과 항목이 완전히 삭제되기까지의 유예 기간(일)을 설정합니다. 사용자 삭제 작업은 매일 자정에 실행되어 삭제 대상 여부를 확인합니다. 이 설정의 변경 사항은 다음 작업 실행 시 반영됩니다.", "user_delete_immediately": "{user}님이 업로드한 항목이 영구적으로 삭제됩니다.", "user_delete_immediately_checkbox": "유예 기간 없이 즉시 삭제", + "user_details": "사용자 상세", "user_management": "사용자 관리", "user_password_has_been_reset": "사용자의 비밀번호가 초기화되었습니다:", "user_password_reset_description": "이 비밀번호를 해당 사용자에게 알려주세요. 임시 비밀번호로 로그인한 뒤 비밀번호를 반드시 변경해야 합니다.", @@ -369,7 +371,7 @@ "advanced": "고급", "advanced_settings_enable_alternate_media_filter_subtitle": "이 옵션을 사용하면 동기화 중 미디어를 대체 기준으로 필터링할 수 있습니다. 앱이 모든 앨범을 제대로 감지하지 못할 때만 사용하세요.", "advanced_settings_enable_alternate_media_filter_title": "대체 기기 앨범 동기화 필터 사용 (실험적)", - "advanced_settings_log_level_title": "로그 레벨: {}", + "advanced_settings_log_level_title": "로그 레벨: {level}", "advanced_settings_prefer_remote_subtitle": "일부 기기의 경우 기기 내의 섬네일을 로드하는 속도가 매우 느립니다. 서버 이미지를 대신 로드하려면 이 설정을 활성화하세요.", "advanced_settings_prefer_remote_title": "서버 이미지 선호", "advanced_settings_proxy_headers_subtitle": "네트워크 요청을 보낼 때 Immich가 사용할 프록시 헤더를 정의합니다.", @@ -400,9 +402,9 @@ "album_remove_user_confirmation": "{user}님을 앨범에서 제거하시겠습니까?", "album_share_no_users": "이미 모든 사용자와 앨범을 공유 중이거나 다른 사용자가 없는 것 같습니다.", "album_thumbnail_card_item": "항목 1개", - "album_thumbnail_card_items": "항목 {}개", + "album_thumbnail_card_items": "항목 {count}개", "album_thumbnail_card_shared": " · 공유됨", - "album_thumbnail_shared_by": "{}님이 공유함", + "album_thumbnail_shared_by": "{user}님이 공유함", "album_updated": "항목 추가 알림", "album_updated_setting_description": "공유 앨범에 항목이 추가된 경우 이메일 알림 받기", "album_user_left": "{album} 앨범에서 나옴", @@ -440,7 +442,7 @@ "archive": "보관함", "archive_or_unarchive_photo": "보관 처리 또는 해제", "archive_page_no_archived_assets": "보관된 항목 없음", - "archive_page_title": "보관함 ({})", + "archive_page_title": "보관함 ({count})", "archive_size": "압축 파일 크기", "archive_size_description": "다운로드할 압축 파일의 크기 구성 (GiB 단위)", "archived": "보관함", @@ -477,18 +479,18 @@ "assets_added_to_album_count": "앨범에 항목 {count, plural, one {#개} other {#개}} 추가됨", "assets_added_to_name_count": "{hasName, select, true {{name}} other {새 앨범}}에 항목 {count, plural, one {#개} other {#개}} 추가됨", "assets_count": "{count, plural, one {#개} other {#개}} 항목", - "assets_deleted_permanently": "{}개 항목이 영구적으로 삭제됨", - "assets_deleted_permanently_from_server": "서버에서 항목 {}개가 영구적으로 삭제됨", + "assets_deleted_permanently": "{count}개 항목이 영구적으로 삭제됨", + "assets_deleted_permanently_from_server": "서버에서 항목 {count}개가 영구적으로 삭제됨", "assets_moved_to_trash_count": "휴지통으로 항목 {count, plural, one {#개} other {#개}} 이동됨", "assets_permanently_deleted_count": "항목 {count, plural, one {#개} other {#개}}가 영구적으로 삭제됨", "assets_removed_count": "항목 {count, plural, one {#개} other {#개}}를 제거했습니다.", - "assets_removed_permanently_from_device": "기기에서 항목 {}개가 영구적으로 삭제됨", + "assets_removed_permanently_from_device": "기기에서 항목 {count}개가 영구적으로 삭제됨", "assets_restore_confirmation": "휴지통으로 이동된 항목을 모두 복원하시겠습니까? 이 작업은 되돌릴 수 없습니다! 누락된 항목의 경우 복원되지 않습니다.", "assets_restored_count": "항목 {count, plural, one {#개} other {#개}}를 복원했습니다.", - "assets_restored_successfully": "항목 {}개를 복원했습니다.", - "assets_trashed": "휴지통으로 항목 {}개 이동됨", + "assets_restored_successfully": "항목 {count}개를 복원했습니다.", + "assets_trashed": "휴지통으로 항목 {count}개 이동됨", "assets_trashed_count": "휴지통으로 항목 {count, plural, one {#개} other {#개}} 이동됨", - "assets_trashed_from_server": "서버에 있는 항목 {}개가 휴지통으로 이동되었습니다.", + "assets_trashed_from_server": "서버에서 항목 {count}개가 휴지통으로 이동됨", "assets_were_part_of_album_count": "앨범에 이미 존재하는 {count, plural, one {항목} other {항목}}입니다.", "authorized_devices": "인증된 기기", "automatic_endpoint_switching_subtitle": "지정된 Wi-Fi가 사용 가능한 경우 내부망을 통해 연결하고, 그렇지 않으면 다른 연결 방식을 사용합니다.", @@ -497,7 +499,7 @@ "back_close_deselect": "뒤로, 닫기, 선택 취소", "background_location_permission": "백그라운드 위치 권한", "background_location_permission_content": "백그라운드에서 네트워크를 전환하려면, Immich가 Wi-Fi 네트워크 이름을 확인할 수 있도록 '정확한 위치' 권한을 항상 허용해야 합니다.", - "backup_album_selection_page_albums_device": "기기의 앨범 ({})", + "backup_album_selection_page_albums_device": "기기의 앨범 ({count})", "backup_album_selection_page_albums_tap": "한 번 탭하면 포함되고, 두 번 탭하면 제외됩니다.", "backup_album_selection_page_assets_scatter": "각 항목은 여러 앨범에 포함될 수 있으며, 백업 진행 중에도 대상 앨범을 포함하거나 제외할 수 있습니다.", "backup_album_selection_page_select_albums": "앨범 선택", @@ -506,11 +508,11 @@ "backup_all": "모두", "backup_background_service_backup_failed_message": "항목을 백업하지 못했습니다. 다시 시도하는 중…", "backup_background_service_connection_failed_message": "서버에 연결하지 못했습니다. 다시 시도하는 중…", - "backup_background_service_current_upload_notification": "{} 업로드 중", + "backup_background_service_current_upload_notification": "{filename} 업로드 중", "backup_background_service_default_notification": "새로운 항목을 확인하는 중…", "backup_background_service_error_title": "백업 오류", "backup_background_service_in_progress_notification": "항목을 백업하는 중…", - "backup_background_service_upload_failure_notification": "{} 업로드 실패", + "backup_background_service_upload_failure_notification": "{filename} 업로드 실패", "backup_controller_page_albums": "백업할 앨범", "backup_controller_page_background_app_refresh_disabled_content": "백그라운드 백업을 사용하려면 설정 > 일반 > 백그라운드 앱 새로 고침에서 백그라운드 앱 새로 고침을 활성화하세요.", "backup_controller_page_background_app_refresh_disabled_title": "백그라운드 새로 고침 비활성화됨", @@ -521,7 +523,7 @@ "backup_controller_page_background_battery_info_title": "배터리 최적화", "backup_controller_page_background_charging": "충전 중에만", "backup_controller_page_background_configure_error": "백그라운드 서비스 구성 실패", - "backup_controller_page_background_delay": "새 미디어 백업 간격: {}", + "backup_controller_page_background_delay": "새 미디어 백업 딜레이: {duration}", "backup_controller_page_background_description": "앱을 열지 않아도 새로 추가된 항목이 자동으로 백업되도록 하려면 백그라운드 서비스를 활성화하세요.", "backup_controller_page_background_is_off": "백그라운드 자동 백업이 비활성화되었습니다.", "backup_controller_page_background_is_on": "백그라운드 자동 백업이 활성화되었습니다.", @@ -531,12 +533,12 @@ "backup_controller_page_backup": "백업", "backup_controller_page_backup_selected": "선택됨: ", "backup_controller_page_backup_sub": "백업된 사진 및 동영상", - "backup_controller_page_created": "생성일: {}", + "backup_controller_page_created": "생성일: {date}", "backup_controller_page_desc_backup": "포그라운드 백업을 활성화하여 앱을 시작할 때 새 항목을 서버에 자동으로 업로드하세요.", "backup_controller_page_excluded": "제외됨: ", - "backup_controller_page_failed": "실패 ({})", - "backup_controller_page_filename": "파일명: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "실패 ({count})", + "backup_controller_page_filename": "파일명: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "백업 정보", "backup_controller_page_none_selected": "선택된 항목 없음", "backup_controller_page_remainder": "남은 항목", @@ -545,7 +547,7 @@ "backup_controller_page_start_backup": "백업 시작", "backup_controller_page_status_off": "포그라운드 자동 백업이 비활성화되었습니다.", "backup_controller_page_status_on": "포그라운드 자동 백업이 활성화되었습니다.", - "backup_controller_page_storage_format": "{} 사용 중, 전체 {}", + "backup_controller_page_storage_format": "{total} 중 {used} 사용", "backup_controller_page_to_backup": "백업할 앨범 목록", "backup_controller_page_total_sub": "선택한 앨범의 고유한 사진 및 동영상", "backup_controller_page_turn_off": "비활성화", @@ -570,21 +572,21 @@ "bulk_keep_duplicates_confirmation": "중복된 항목 {count, plural, one {#개를} other {#개를}} 그대로 유지하시겠습니까? 이 작업은 어떤 항목도 삭제하지 않고, 모든 중복 그룹을 확인한 것으로 처리합니다.", "bulk_trash_duplicates_confirmation": "중복된 항목 {count, plural, one {#개를} other {#개를}} 일괄 휴지통으로 이동하시겠습니까? 이 작업은 각 그룹에서 가장 큰 항목만 남기고 나머지 중복 항목을 휴지통으로 이동합니다.", "buy": "Immich 구매", - "cache_settings_album_thumbnails": "라이브러리 페이지 섬네일 ({})", + "cache_settings_album_thumbnails": "라이브러리 페이지 섬네일 ({count} 항목)", "cache_settings_clear_cache_button": "캐시 지우기", "cache_settings_clear_cache_button_title": "앱 캐시를 지웁니다. 이 작업은 캐시가 다시 생성될 때까지 앱 성능에 상당한 영향을 미칠 수 있습니다.", "cache_settings_duplicated_assets_clear_button": "지우기", "cache_settings_duplicated_assets_subtitle": "업로드되지 않는 사진 및 동영상", - "cache_settings_duplicated_assets_title": "중복 항목 ({})", - "cache_settings_image_cache_size": "이미지 캐시 크기 ({})", + "cache_settings_duplicated_assets_title": "중복 항목 ({count})", + "cache_settings_image_cache_size": "이미지 캐시 크기 ({count} 항목)", "cache_settings_statistics_album": "라이브러리 섬네일", - "cache_settings_statistics_assets": "항목 {}개 ({})", + "cache_settings_statistics_assets": "항목 {count}개 ({size})", "cache_settings_statistics_full": "전체 이미지", "cache_settings_statistics_shared": "공유 앨범 섬네일", "cache_settings_statistics_thumbnail": "섬네일", "cache_settings_statistics_title": "캐시 사용률", "cache_settings_subtitle": "Immich 모바일 앱의 캐싱 동작 제어", - "cache_settings_thumbnail_size": "섬네일 캐시 크기 ({})", + "cache_settings_thumbnail_size": "섬네일 캐시 크기 ({count} 항목)", "cache_settings_tile_subtitle": "로컬 스토리지 동작 제어", "cache_settings_tile_title": "로컬 스토리지", "cache_settings_title": "캐시 설정", @@ -610,6 +612,7 @@ "change_password_form_new_password": "새 비밀번호 입력", "change_password_form_password_mismatch": "비밀번호가 일치하지 않습니다.", "change_password_form_reenter_new_password": "새 비밀번호 확인", + "change_pin_code": "PIN 코드 변경", "change_your_password": "비밀번호 변경", "changed_visibility_successfully": "표시 여부가 성공적으로 변경되었습니다.", "check_all": "모두 확인", @@ -650,11 +653,12 @@ "confirm_delete_face": "항목에서 {name}의 얼굴을 삭제하시겠습니까?", "confirm_delete_shared_link": "이 공유 링크를 삭제하시겠습니까?", "confirm_keep_this_delete_others": "이 항목을 제외한 스택의 모든 항목이 삭제됩니다. 계속하시겠습니까?", + "confirm_new_pin_code": "새 PIN 코드 확인", "confirm_password": "비밀번호 확인", "contain": "맞춤", "context": "내용", "continue": "계속", - "control_bottom_app_bar_album_info_shared": "항목 {}개 · 공유됨", + "control_bottom_app_bar_album_info_shared": "항목 {count}개 · 공유됨", "control_bottom_app_bar_create_new_album": "앨범 생성", "control_bottom_app_bar_delete_from_immich": "Immich에서 삭제", "control_bottom_app_bar_delete_from_local": "기기에서 삭제", @@ -692,9 +696,11 @@ "create_tag_description": "새 태그를 생성합니다. 하위 태그의 경우 /를 포함한 전체 태그명을 입력하세요.", "create_user": "사용자 생성", "created": "생성됨", + "created_at": "생성됨", "crop": "자르기", "curated_object_page_title": "사물", "current_device": "현재 기기", + "current_pin_code": "현재 PIN 코드", "current_server_address": "현재 서버 주소", "custom_locale": "사용자 지정 로케일", "custom_locale_description": "언어 및 지역에 따른 날짜 및 숫자 형식 지정", @@ -763,7 +769,7 @@ "download_enqueue": "대기열에 다운로드", "download_error": "다운로드 오류", "download_failed": "다운로드 실패", - "download_filename": "파일: {}", + "download_filename": "파일: {filename}", "download_finished": "다운로드가 완료되었습니다.", "download_include_embedded_motion_videos": "내장된 동영상", "download_include_embedded_motion_videos_description": "모션 포토에 내장된 동영상을 개별 파일로 포함", @@ -807,6 +813,7 @@ "editor_crop_tool_h2_aspect_ratios": "종횡비", "editor_crop_tool_h2_rotation": "회전", "email": "이메일", + "email_notifications": "이메일 알림", "empty_folder": "폴더가 비어 있음", "empty_trash": "휴지통 비우기", "empty_trash_confirmation": "휴지통을 비우시겠습니까? 휴지통에 있는 모든 항목이 Immich에서 영구적으로 삭제됩니다.\n이 작업은 되돌릴 수 없습니다!", @@ -819,7 +826,7 @@ "error_change_sort_album": "앨범 표시 순서 변경 실패", "error_delete_face": "얼굴 삭제 중 오류가 발생했습니다.", "error_loading_image": "이미지 로드 오류", - "error_saving_image": "오류: {}", + "error_saving_image": "오류: {error}", "error_title": "오류 - 문제가 발생했습니다", "errors": { "cannot_navigate_next_asset": "다음 항목으로 이동할 수 없습니다.", @@ -922,6 +929,7 @@ "unable_to_remove_reaction": "반응을 제거할 수 없습니다.", "unable_to_repair_items": "항목을 수리할 수 없습니다.", "unable_to_reset_password": "비밀번호를 초기화할 수 없습니다.", + "unable_to_reset_pin_code": "PIN 코드를 초기화할 수 없음", "unable_to_resolve_duplicate": "중복된 항목을 처리할 수 없습니다.", "unable_to_restore_assets": "항목을 복원할 수 없습니다.", "unable_to_restore_trash": "휴지통에서 항목을 복원할 수 없음", @@ -955,10 +963,10 @@ "exif_bottom_sheet_location": "위치", "exif_bottom_sheet_people": "인물", "exif_bottom_sheet_person_add_person": "이름 추가", - "exif_bottom_sheet_person_age": "{}세", - "exif_bottom_sheet_person_age_months": "생후 {}개월", - "exif_bottom_sheet_person_age_year_months": "생후 1년 {}개월", - "exif_bottom_sheet_person_age_years": "{}세", + "exif_bottom_sheet_person_age": "{age}세", + "exif_bottom_sheet_person_age_months": "생후 {months}개월", + "exif_bottom_sheet_person_age_year_months": "생후 1년 {months}개월", + "exif_bottom_sheet_person_age_years": "{years}세", "exit_slideshow": "슬라이드 쇼 종료", "expand_all": "모두 확장", "experimental_settings_new_asset_list_subtitle": "진행 중", @@ -1048,6 +1056,7 @@ "home_page_upload_err_limit": "한 번에 최대 30개의 항목만 업로드할 수 있습니다.", "host": "호스트", "hour": "시간", + "id": "ID", "ignore_icloud_photos": "iCloud 사진 제외", "ignore_icloud_photos_description": "iCloud에 저장된 사진이 Immich에 업로드되지 않습니다.", "image": "이미지", @@ -1173,8 +1182,8 @@ "manage_your_devices": "로그인된 기기 관리", "manage_your_oauth_connection": "OAuth 연결 관리", "map": "지도", - "map_assets_in_bound": "사진 {}개", - "map_assets_in_bounds": "사진 {}개", + "map_assets_in_bound": "사진 {count}개", + "map_assets_in_bounds": "사진 {count}개", "map_cannot_get_user_location": "사용자의 위치를 가져올 수 없습니다.", "map_location_dialog_yes": "예", "map_location_picker_page_use_location": "이 위치 사용", @@ -1188,9 +1197,9 @@ "map_settings": "지도 설정", "map_settings_dark_mode": "다크 모드", "map_settings_date_range_option_day": "지난 24시간", - "map_settings_date_range_option_days": "지난 {}일", + "map_settings_date_range_option_days": "지난 {days}일", "map_settings_date_range_option_year": "지난 1년", - "map_settings_date_range_option_years": "지난 {}년", + "map_settings_date_range_option_years": "지난 {years}년", "map_settings_dialog_title": "지도 설정", "map_settings_include_show_archived": "보관된 항목 포함", "map_settings_include_show_partners": "파트너가 공유한 항목 포함", @@ -1209,7 +1218,7 @@ "memories_start_over": "다시 보기", "memories_swipe_to_close": "위로 밀어서 닫기", "memories_year_ago": "1년 전", - "memories_years_ago": "{}년 전", + "memories_years_ago": "{years, plural, other {#년}} 전", "memory": "추억", "memory_lane_title": "{title} 추억", "menu": "메뉴", @@ -1242,6 +1251,7 @@ "new_api_key": "API 키 생성", "new_password": "새 비밀번호", "new_person": "새 인물 생성", + "new_pin_code": "새 PIN 코드", "new_user_created": "사용자가 생성되었습니다.", "new_version_available": "새 버전 사용 가능", "newest_first": "최신순", @@ -1316,7 +1326,7 @@ "partner_page_partner_add_failed": "파트너를 추가하지 못했습니다.", "partner_page_select_partner": "파트너 선택", "partner_page_shared_to_title": "공유 대상", - "partner_page_stop_sharing_content": "더 이상 {}님이 사진에 접근할 수 없습니다.", + "partner_page_stop_sharing_content": "더 이상 {partner}님이 사진에 접근할 수 없습니다.", "partner_sharing": "파트너와 공유", "partners": "파트너", "password": "비밀번호", @@ -1362,6 +1372,9 @@ "photos_count": "사진 {count, plural, one {{count, number}개} other {{count, number}개}}", "photos_from_previous_years": "지난 몇 년간의 사진", "pick_a_location": "위치 선택", + "pin_code_changed_successfully": "PIN 코드를 변경했습니다.", + "pin_code_reset_successfully": "PIN 코드를 초기화했습니다.", + "pin_code_setup_successfully": "PIN 코드를 설정했습니다.", "place": "장소", "places": "장소", "places_count": "{count, plural, one {{count, number} 장소} other {{count, number} 장소}}", @@ -1379,6 +1392,7 @@ "previous_or_next_photo": "이전/다음 사진으로", "primary": "주요", "privacy": "개인 정보", + "profile": "프로필", "profile_drawer_app_logs": "로그", "profile_drawer_client_out_of_date_major": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", "profile_drawer_client_out_of_date_minor": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", @@ -1392,7 +1406,7 @@ "public_share": "모든 사용자와 공유", "purchase_account_info": "서포터", "purchase_activated_subtitle": "Immich와 오픈 소스 소프트웨어를 지원해주셔서 감사합니다.", - "purchase_activated_time": "{date, date} 등록됨", + "purchase_activated_time": "{date} 등록됨", "purchase_activated_title": "제품 키가 성공적으로 등록되었습니다.", "purchase_button_activate": "등록", "purchase_button_buy": "구매", @@ -1481,6 +1495,7 @@ "reset": "초기화", "reset_password": "비밀번호 재설정", "reset_people_visibility": "인물 표시 여부 초기화", + "reset_pin_code": "PIN 코드 초기화", "reset_to_default": "기본값으로 복원", "resolve_duplicates": "중복된 항목 확인", "resolved_all_duplicates": "중복된 항목을 모두 처리했습니다.", @@ -1604,12 +1619,12 @@ "setting_languages_apply": "적용", "setting_languages_subtitle": "앱 언어 변경", "setting_languages_title": "언어", - "setting_notifications_notify_failures_grace_period": "백그라운드 백업 실패 알림: {}", - "setting_notifications_notify_hours": "{}시간", + "setting_notifications_notify_failures_grace_period": "백그라운드 백업 실패 알림: {duration}", + "setting_notifications_notify_hours": "{count}시간", "setting_notifications_notify_immediately": "즉시", - "setting_notifications_notify_minutes": "{}분", + "setting_notifications_notify_minutes": "{count}분", "setting_notifications_notify_never": "알리지 않음", - "setting_notifications_notify_seconds": "{}초", + "setting_notifications_notify_seconds": "{count}초", "setting_notifications_single_progress_subtitle": "개별 항목의 상세 업로드 정보 표시", "setting_notifications_single_progress_title": "백그라운드 백업 상세 진행률 표시", "setting_notifications_subtitle": "알림 기본 설정 조정", @@ -1621,9 +1636,10 @@ "settings": "설정", "settings_require_restart": "설정을 적용하려면 Immich를 다시 시작하세요.", "settings_saved": "설정이 저장되었습니다.", + "setup_pin_code": "PIN 코드 설정", "share": "공유", "share_add_photos": "사진 추가", - "share_assets_selected": "{}개 선택됨", + "share_assets_selected": "{count}개 선택됨", "share_dialog_preparing": "준비 중...", "shared": "공유됨", "shared_album_activities_input_disable": "댓글이 비활성화되었습니다", @@ -1637,32 +1653,32 @@ "shared_by_user": "{user}님이 공유함", "shared_by_you": "내가 공유함", "shared_from_partner": "{partner}님의 사진", - "shared_intent_upload_button_progress_text": "{} / {} 업로드됨", + "shared_intent_upload_button_progress_text": "전체 {total}개 중 {current}개 업로드됨", "shared_link_app_bar_title": "공유 링크", "shared_link_clipboard_copied_massage": "클립보드에 복사되었습니다.", - "shared_link_clipboard_text": "링크: {}\n비밀번호: {}", + "shared_link_clipboard_text": "링크: {link}\n비밀번호: {password}", "shared_link_create_error": "공유 링크 생성 중 문제가 발생했습니다.", "shared_link_edit_description_hint": "공유 링크 설명 입력", "shared_link_edit_expire_after_option_day": "1일", - "shared_link_edit_expire_after_option_days": "{}일", + "shared_link_edit_expire_after_option_days": "{count}일", "shared_link_edit_expire_after_option_hour": "1시간", - "shared_link_edit_expire_after_option_hours": "{}시간", + "shared_link_edit_expire_after_option_hours": "{count}시간", "shared_link_edit_expire_after_option_minute": "1분", - "shared_link_edit_expire_after_option_minutes": "{}분", - "shared_link_edit_expire_after_option_months": "{}개월", - "shared_link_edit_expire_after_option_year": "{}년", + "shared_link_edit_expire_after_option_minutes": "{count}분", + "shared_link_edit_expire_after_option_months": "{count}개월", + "shared_link_edit_expire_after_option_year": "{count}년", "shared_link_edit_password_hint": "공유 비밀번호 입력", "shared_link_edit_submit_button": "링크 편집", "shared_link_error_server_url_fetch": "서버 URL을 불러올 수 없습니다.", - "shared_link_expires_day": "{}일 후 만료", - "shared_link_expires_days": "{}일 후 만료", - "shared_link_expires_hour": "{}시간 후 만료", - "shared_link_expires_hours": "{}시간 후 만료", - "shared_link_expires_minute": "{}분 후 만료", - "shared_link_expires_minutes": "{}분 후 만료", + "shared_link_expires_day": "{count}일 후 만료", + "shared_link_expires_days": "{count}일 후 만료", + "shared_link_expires_hour": "{count}시간 후 만료", + "shared_link_expires_hours": "{count}시간 후 만료", + "shared_link_expires_minute": "{count}분 후 만료", + "shared_link_expires_minutes": "{count}분 후 만료", "shared_link_expires_never": "만료되지 않음", - "shared_link_expires_second": "{}초 후 만료", - "shared_link_expires_seconds": "{}초 후 만료", + "shared_link_expires_second": "{count}초 후 만료", + "shared_link_expires_seconds": "{count}초 후 만료", "shared_link_individual_shared": "개인 공유", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "공유 링크 관리", @@ -1737,13 +1753,14 @@ "stop_sharing_photos_with_user": "이 사용자와 사진 공유 중단", "storage": "저장 공간", "storage_label": "스토리지 레이블", + "storage_quota": "스토리지 할당량", "storage_usage": "{available} 중 {used} 사용", "submit": "확인", "suggestions": "추천", "sunrise_on_the_beach": "이미지에 존재하는 사물 검색", "support": "지원", "support_and_feedback": "지원 & 제안", - "support_third_party_description": "Immich가 서드파티 패키지로 설치 되었습니다. 링크를 눌러 먼저 패키지 문제인지 확인해 보세요.", + "support_third_party_description": "서드파티 패키지를 이용하여 Immich가 설치된 것으로 보입니다. 현재 발생하는 문제는 해당 패키지가 원인일 수 있으므로, 먼저 아래 링크를 통해 패키지 개발자에게 문의해주세요.", "swap_merge_direction": "병합 방향 변경", "sync": "동기화", "sync_albums": "앨범 동기화", @@ -1763,7 +1780,7 @@ "theme_selection": "테마 설정", "theme_selection_description": "브라우저 및 시스템 기본 설정에 따라 라이트 모드와 다크 모드를 자동으로 설정", "theme_setting_asset_list_storage_indicator_title": "타일에 서버 동기화 상태 표시", - "theme_setting_asset_list_tiles_per_row_title": "한 줄에 표시할 항목 수 ({})", + "theme_setting_asset_list_tiles_per_row_title": "한 줄에 표시할 항목 수 ({count})", "theme_setting_colorful_interface_subtitle": "배경에 대표 색상을 적용합니다.", "theme_setting_colorful_interface_title": "미려한 인터페이스", "theme_setting_image_viewer_quality_subtitle": "상세 보기 이미지 품질 조정", @@ -1798,13 +1815,15 @@ "trash_no_results_message": "삭제된 사진과 동영상이 여기에 표시됩니다.", "trash_page_delete_all": "모두 삭제", "trash_page_empty_trash_dialog_content": "휴지통을 비우시겠습니까? 휴지통에 있는 모든 항목이 Immich에서 영구적으로 제거됩니다.", - "trash_page_info": "휴지통으로 이동된 항목은 {}일 후 영구적으로 삭제됩니다.", + "trash_page_info": "휴지통으로 이동된 항목은 {days}일 후 영구적으로 삭제됩니다.", "trash_page_no_assets": "휴지통이 비어 있음", "trash_page_restore_all": "모두 복원", "trash_page_select_assets_btn": "항목 선택", - "trash_page_title": "휴지통 ({})", + "trash_page_title": "휴지통 ({count})", "trashed_items_will_be_permanently_deleted_after": "휴지통으로 이동된 항목은 {days, plural, one {#일} other {#일}} 후 영구적으로 삭제됩니다.", "type": "형식", + "unable_to_change_pin_code": "PIN 코드를 변경할 수 없음", + "unable_to_setup_pin_code": "PIN 코드를 설정할 수 없음", "unarchive": "보관함에서 제거", "unarchived_count": "보관함에서 항목 {count, plural, other {#개}} 제거됨", "unfavorite": "즐겨찾기 해제", @@ -1840,7 +1859,7 @@ "upload_status_errors": "오류", "upload_status_uploaded": "완료", "upload_success": "업로드가 완료되었습니다. 업로드된 항목을 보려면 페이지를 새로고침하세요.", - "upload_to_immich": "Immich에 업로드 ({})", + "upload_to_immich": "Immich에 업로드 ({count})", "uploading": "업로드 중", "url": "URL", "usage": "사용량", @@ -1849,6 +1868,8 @@ "user": "사용자", "user_id": "사용자 ID", "user_liked": "{user}님이 {type, select, photo {이 사진을} video {이 동영상을} asset {이 항목을} other {이 항목을}} 좋아합니다.", + "user_pin_code_settings": "PIN 코드", + "user_pin_code_settings_description": "PIN 코드 관리", "user_purchase_settings": "구매", "user_purchase_settings_description": "구매 및 제품 키 관리", "user_role_set": "{user}님에게 {role} 역할을 설정했습니다.", diff --git a/i18n/lt.json b/i18n/lt.json index b279d88f08..4a3973e5c8 100644 --- a/i18n/lt.json +++ b/i18n/lt.json @@ -38,12 +38,13 @@ "authentication_settings_disable_all": "Ar tikrai norite išjungti visus prisijungimo būdus? Prisijungimas bus visiškai išjungtas.", "authentication_settings_reenable": "Norėdami vėl įjungti, naudokite Serverio komandą.", "background_task_job": "Foninės užduotys", - "backup_database": "Duomenų bazės atsarginė kopija", - "backup_database_enable_description": "Įgalinti duomenų bazės atsarginė kopijas", - "backup_keep_last_amount": "Išsaugomų ankstesnių atsarginių duomenų bazės kopijų skaičius", - "backup_settings": "Atsarginės kopijos nustatymai", - "backup_settings_description": "Tvarkyti duomenų bazės atsarginės kopijos nustatymus", + "backup_database": "Sukurti duomenų bazės išklotinę", + "backup_database_enable_description": "Įgalinti duomenų bazės išklotinės", + "backup_keep_last_amount": "Išsaugomų ankstesnių duomenų bazės išklotinių skaičius", + "backup_settings": "Duomenų bazės išklotinių nustatymai", + "backup_settings_description": "Tvarkyti duomenų bazės išklotinės nustatymus. Pastaba: Šie darbai nėra stebimi ir jums nebus pranešta apie nesėkmę.", "check_all": "Pažymėti viską", + "cleanup": "Valymas", "cleared_jobs": "Išvalyti darbai: {job}", "config_set_by_file": "Konfigūracija nustatyta pagal konfigūracinį failą", "confirm_delete_library": "Ar tikrai norite ištrinti {library} biblioteką?", @@ -51,6 +52,7 @@ "confirm_email_below": "Patvirtinimui įveskite \"{email}\" žemiau", "confirm_reprocess_all_faces": "Ar tikrai norite iš naujo apdoroti visus veidus? Tai taip pat ištrins įvardytus asmenis.", "confirm_user_password_reset": "Ar tikrai norite iš naujo nustatyti {user} slaptažodį?", + "confirm_user_pin_code_reset": "Ar tikrai norite iš naujo nustatyti {user} PIN kodą?", "create_job": "Sukurti darbą", "cron_expression": "Cron išraiška", "cron_expression_description": "Nustatyti skanavimo intervalą naudojant cron formatą. Norėdami gauti daugiau informacijos žiūrėkite Crontab Guru", @@ -1203,7 +1205,7 @@ "public_share": "", "purchase_account_info": "Rėmėjas", "purchase_activated_subtitle": "Dėkojame, kad remiate Immich ir atviro kodo programinę įrangą", - "purchase_activated_time": "Suaktyvinta {date, date}", + "purchase_activated_time": "Suaktyvinta {date}", "purchase_activated_title": "Jūsų raktas sėkmingai aktyvuotas", "purchase_button_activate": "Aktyvuoti", "purchase_button_buy": "Pirkti", @@ -1566,31 +1568,35 @@ "unlinked_oauth_account": "Atsieta OAuth paskyra", "unnamed_album_delete_confirmation": "Ar tikrai norite ištrinti šį albumą?", "unsaved_change": "Neišsaugoti pakeitimai", - "unselect_all": "", + "unselect_all": "Atšaukti visų pasirinkimą", "unselect_all_duplicates": "Atžymėti visus dublikatus", "unstack": "Išgrupuoti", "unstacked_assets_count": "{count, plural, one {Išgrupuotas # elementas} few {Išgrupuoti # elementai} other {Išgrupuota # elementų}}", "untracked_files": "Nesekami failai", "untracked_files_decription": "Šie failai aplikacijos nesekami. Jie galėjo atsirasti dėl nepavykusio perkėlimo, nutraukto įkėlimo ar palikti per klaidą", "up_next": "", + "updated_at": "Atnaujintas", "updated_password": "Slaptažodis atnaujintas", "upload": "Įkelti", - "upload_concurrency": "", - "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", - "upload_dialog_title": "Upload Asset", + "upload_concurrency": "Įkėlimo lygiagretumas", + "upload_dialog_info": "Ar norite sukurti pasirinkto(-ų) turinio(-ų) atsarginę kopiją serveryje?", + "upload_dialog_title": "Įkelti turinį", "upload_errors": "Įkėlimas įvyko su {count, plural, one {# klaida} few {# klaidomis} other {# klaidų}}, norėdami pamatyti naujai įkeltus elementus perkraukite puslapį.", "upload_progress": "Liko {remaining, number} - Apdorota {processed, number}/{total, number}", "upload_status_duplicates": "Dublikatai", "upload_status_errors": "Klaidos", "upload_status_uploaded": "Įkelta", "upload_success": "Įkėlimas pavyko, norėdami pamatyti naujai įkeltus elementus perkraukite puslapį.", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", + "upload_to_immich": "Įkelti į Immich ({count})", + "uploading": "Įkeliama", "url": "URL", - "usage": "", - "use_current_connection": "use current connection", + "usage": "Naudojymas", + "use_current_connection": "naudoti dabartinį ryšį", "user": "Naudotojas", + "user_has_been_deleted": "Šis naudotojas buvo ištrintas.", "user_id": "Naudotojo ID", + "user_pin_code_settings": "PIN kodas", + "user_pin_code_settings_description": "Tvarkykite savo PIN kodą", "user_usage_detail": "", "user_usage_stats": "Paskyros naudojimo statistika", "user_usage_stats_description": "Žiūrėti paskyros naudojimo statistiką", diff --git a/i18n/lv.json b/i18n/lv.json index 6dfb9cdb67..1d46e697b6 100644 --- a/i18n/lv.json +++ b/i18n/lv.json @@ -8,13 +8,13 @@ "actions": "Darbības", "active": "Aktīvs", "activity": "Aktivitāte", - "activity_changed": "Aktivitāte ir", + "activity_changed": "Aktivitāte ir {enabled, select, true {iespējota} other {atspējota}}", "add": "Pievienot", "add_a_description": "Pievienot aprakstu", "add_a_location": "Pievienot atrašanās vietu", "add_a_name": "Pievienot vārdu", "add_a_title": "Pievienot virsrakstu", - "add_endpoint": "Add endpoint", + "add_endpoint": "Pievienot galapunktu", "add_exclusion_pattern": "Pievienot izslēgšanas šablonu", "add_import_path": "Pievienot importa ceļu", "add_location": "Pievienot lokāciju", @@ -171,13 +171,13 @@ "repair_all": "Salabot visu", "require_password_change_on_login": "Pieprasīt lietotājam mainīt paroli pēc pirmās pieteikšanās", "scanning_library": "Skenē bibliotēku", - "search_jobs": "Meklēt uzdevumus...", + "search_jobs": "Meklēt uzdevumus…", "server_external_domain_settings": "", "server_external_domain_settings_description": "", "server_settings": "Servera iestatījumi", "server_settings_description": "Servera iestatījumu pārvaldība", - "server_welcome_message": "", - "server_welcome_message_description": "", + "server_welcome_message": "Sveiciena ziņa", + "server_welcome_message_description": "Ziņojums, kas tiek parādīts pieslēgšanās lapā.", "sidecar_job_description": "", "slideshow_duration_description": "", "smart_search_job_description": "", @@ -187,13 +187,15 @@ "storage_template_hash_verification_enabled_description": "", "storage_template_migration": "Krātuves veidņu migrācija", "storage_template_migration_job": "Krātuves veidņu migrācijas uzdevums", + "storage_template_path_length": "Aptuvenais ceļa garuma ierobežojums: {length, number}/{limit, number}", "storage_template_settings": "Krātuves veidne", "storage_template_settings_description": "", "system_settings": "Sistēmas iestatījumi", + "template_email_preview": "Priekšskatījums", "template_email_settings_description": "Pielāgotu e-pasta paziņojumu veidņu pārvaldība", "template_settings_description": "Pielāgotu paziņojumu veidņu pārvaldība", "theme_custom_css_settings": "Pielāgots CSS", - "theme_custom_css_settings_description": "", + "theme_custom_css_settings_description": "Cascading Style Sheets ļauj pielāgot Immich izskatu.", "theme_settings": "", "theme_settings_description": "Immich tīmekļa saskarnes pielāgojumu pārvaldība", "thumbnail_generation_job": "Sīktēlu ģenerēšana", @@ -274,16 +276,19 @@ "admin_password": "Administratora parole", "administration": "Administrēšana", "advanced": "Papildu", - "advanced_settings_log_level_title": "Žurnalēšanas līmenis: {}", + "advanced_settings_log_level_title": "Žurnalēšanas līmenis: {level}", "advanced_settings_prefer_remote_subtitle": "Dažās ierīcēs sīktēli no ierīcē esošajiem resursiem tiek ielādēti ļoti lēni. Aktivizējiet šo iestatījumu, lai tā vietā ielādētu attālus attēlus.", "advanced_settings_prefer_remote_title": "Dot priekšroku attāliem attēliem", "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", + "advanced_settings_proxy_headers_title": "Starpniekservera galvenes", "advanced_settings_self_signed_ssl_subtitle": "Izlaiž servera galapunkta SSL sertifikātu verifikāciju. Nepieciešams pašparakstītajiem sertifikātiem.", "advanced_settings_self_signed_ssl_title": "Atļaut pašparakstītus SSL sertifikātus", "advanced_settings_tile_subtitle": "Lietotāja papildu iestatījumi", "advanced_settings_troubleshooting_subtitle": "Iespējot papildu aktīvus problēmu novēršanai", "advanced_settings_troubleshooting_title": "Problēmas novēršana", + "age_months": "Vecums {months, plural, zero {# mēnešu} one {# mēnesis} other {# mēneši}}", + "age_year_months": "Vecums 1 gads, {months, plural, zero {# mēnešu} one {# mēnesis} other {# mēneši}}", + "age_years": "{years, plural, zero {# gadu} one {# gads} other {# gadi}}", "album_added": "Albums pievienots", "album_added_notification_setting_description": "", "album_cover_updated": "Albuma attēls atjaunināts", @@ -295,9 +300,9 @@ "album_options": "", "album_remove_user": "Noņemt lietotāju?", "album_thumbnail_card_item": "1 vienums", - "album_thumbnail_card_items": "{} vienumi", - "album_thumbnail_card_shared": "· Koplietots", - "album_thumbnail_shared_by": "Kopīgoja {}", + "album_thumbnail_card_items": "{count} vienumi", + "album_thumbnail_card_shared": " · Kopīgots", + "album_thumbnail_shared_by": "Kopīgoja {user}", "album_updated": "Albums atjaunināts", "album_updated_setting_description": "", "album_user_left": "Pameta {album}", @@ -331,13 +336,13 @@ "archive": "Arhīvs", "archive_or_unarchive_photo": "", "archive_page_no_archived_assets": "Nav atrasts neviens arhivēts aktīvs", - "archive_page_title": "Arhīvs ({})", + "archive_page_title": "Arhīvs ({count})", "archive_size": "Arhīva izmērs", - "archived": "Archived", + "archived": "Arhivēts", "are_these_the_same_person": "Vai šī ir tā pati persona?", "asset_action_delete_err_read_only": "Nevar dzēst read only aktīvu(-s), notiek izlaišana", "asset_action_share_err_offline": "Nevar iegūt bezsaistes aktīvu(-s), notiek izlaišana", - "asset_adding_to_album": "Pievieno albumam...", + "asset_adding_to_album": "Pievieno albumam…", "asset_list_group_by_sub_title": "Grupēt pēc", "asset_list_layout_settings_dynamic_layout_title": "Dinamiskais izkārtojums", "asset_list_layout_settings_group_automatically": "Automātiski", @@ -348,7 +353,7 @@ "asset_list_settings_title": "Fotorežģis", "asset_offline": "", "asset_restored_successfully": "Asset restored successfully", - "asset_uploading": "Augšupielādē...", + "asset_uploading": "Augšupielādē…", "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", "asset_viewer_settings_title": "Aktīvu Skatītājs", "assets": "aktīvi", @@ -360,11 +365,11 @@ "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "authorized_devices": "Autorizētās ierīces", "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", + "automatic_endpoint_switching_title": "Automātiska URL pārslēgšana", "back": "Atpakaļ", "background_location_permission": "Background location permission", "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", - "backup_album_selection_page_albums_device": "Albumi ierīcē ({})", + "backup_album_selection_page_albums_device": "Albumi ierīcē ({count})", "backup_album_selection_page_albums_tap": "Pieskarieties, lai iekļautu, veiciet dubultskārienu, lai izslēgtu", "backup_album_selection_page_assets_scatter": "Aktīvi var būt izmētāti pa vairākiem albumiem. Tādējādi dublēšanas procesā albumus var iekļaut vai neiekļaut.", "backup_album_selection_page_select_albums": "Atlasīt albumus", @@ -373,11 +378,11 @@ "backup_all": "Viss", "backup_background_service_backup_failed_message": "Neizdevās dublēt līdzekļus. Notiek atkārtota mēģināšana…", "backup_background_service_connection_failed_message": "Neizdevās izveidot savienojumu ar serveri. Notiek atkārtota mēģināšana…", - "backup_background_service_current_upload_notification": "Notiek {} augšupielāde", + "backup_background_service_current_upload_notification": "Notiek {filename} augšupielāde", "backup_background_service_default_notification": "Notiek jaunu aktīvu meklēšana…", "backup_background_service_error_title": "Dublēšanas kļūda", "backup_background_service_in_progress_notification": "Notiek aktīvu dublēšana…", - "backup_background_service_upload_failure_notification": "Neizdevās augšupielādēt {}", + "backup_background_service_upload_failure_notification": "Neizdevās augšupielādēt {filename}", "backup_controller_page_albums": "Dublējuma Albumi", "backup_controller_page_background_app_refresh_disabled_content": "Iespējojiet fona aplikācijas atsvaidzināšanu sadaļā Iestatījumi > Vispārīgi > Fona Aplikācijas Atsvaidzināšana, lai izmantotu fona dublēšanu.", "backup_controller_page_background_app_refresh_disabled_title": "Fona aplikācijas atsvaidzināšana atspējota", @@ -388,7 +393,7 @@ "backup_controller_page_background_battery_info_title": "Akumulatora optimizācija", "backup_controller_page_background_charging": "Tikai uzlādes laikā", "backup_controller_page_background_configure_error": "Neizdevās konfigurēt fona pakalpojumu", - "backup_controller_page_background_delay": "Aizkavēt jaunu līdzekļu dublēšanu: {}", + "backup_controller_page_background_delay": "Aizkavēt jaunu līdzekļu dublēšanu: {duration}", "backup_controller_page_background_description": "Ieslēdziet fona pakalpojumu, lai automātiski dublētu visus jaunos aktīvus, neatverot programmu", "backup_controller_page_background_is_off": "Automātiskā fona dublēšana ir izslēgta", "backup_controller_page_background_is_on": "Automātiskā fona dublēšana ir ieslēgta", @@ -398,12 +403,12 @@ "backup_controller_page_backup": "Dublēšana", "backup_controller_page_backup_selected": "Atlasīts: ", "backup_controller_page_backup_sub": "Dublētie Fotoattēli un videoklipi", - "backup_controller_page_created": "Izveidots: {}", + "backup_controller_page_created": "Izveidots: {date}", "backup_controller_page_desc_backup": "Ieslēdziet priekšplāna dublēšanu, lai, atverot programmu, serverī automātiski augšupielādētu jaunus aktīvus.", "backup_controller_page_excluded": "Izņemot: ", - "backup_controller_page_failed": "Neizdevās ({})", - "backup_controller_page_filename": "Faila nosaukums: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Neizdevās ({count})", + "backup_controller_page_filename": "Faila nosaukums: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Dublējuma Informācija", "backup_controller_page_none_selected": "Neviens nav atlasīts", "backup_controller_page_remainder": "Atlikums", @@ -412,7 +417,7 @@ "backup_controller_page_start_backup": "Sākt Dublēšanu", "backup_controller_page_status_off": "Automātiskā priekšplāna dublēšana ir izslēgta", "backup_controller_page_status_on": "Automātiskā priekšplāna dublēšana ir ieslēgta", - "backup_controller_page_storage_format": "{} no {} tiek izmantots", + "backup_controller_page_storage_format": "{used} no {total} tiek izmantots", "backup_controller_page_to_backup": "Dublējamie albumi", "backup_controller_page_total_sub": "Visi unikālie fotoattēli un videoklipi no izvēlētajiem albumiem", "backup_controller_page_turn_off": "Izslēgt priekšplāna dublēšanu", @@ -433,21 +438,21 @@ "bugs_and_feature_requests": "Kļūdas un funkciju pieprasījumi", "build": "Būvējums", "build_image": "Būvējuma attēls", - "cache_settings_album_thumbnails": "Bibliotēkas lapu sīktēli ({} aktīvi)", + "cache_settings_album_thumbnails": "Bibliotēkas lapu sīktēli ({count} faili)", "cache_settings_clear_cache_button": "Iztīrīt kešatmiņu", "cache_settings_clear_cache_button_title": "Iztīra aplikācijas kešatmiņu. Tas būtiski ietekmēs lietotnes veiktspēju, līdz kešatmiņa būs pārbūvēta.", "cache_settings_duplicated_assets_clear_button": "NOTĪRĪT", "cache_settings_duplicated_assets_subtitle": "Fotoattēli un videoklipi, kurus lietotne ir iekļāvusi melnajā sarakstā", - "cache_settings_duplicated_assets_title": "Dublicētie Aktīvi ({})", - "cache_settings_image_cache_size": "Attēlu kešatmiņas lielums ({} aktīvi)", + "cache_settings_duplicated_assets_title": "Dublicētie faili ({count})", + "cache_settings_image_cache_size": "Attēlu kešatmiņas lielums ({count} faili)", "cache_settings_statistics_album": "Bibliotēkas sīktēli", - "cache_settings_statistics_assets": "{} aktīvi ({})", + "cache_settings_statistics_assets": "{count} faili ({size})", "cache_settings_statistics_full": "Pilni attēli", "cache_settings_statistics_shared": "Koplietojamo albumu sīktēli", "cache_settings_statistics_thumbnail": "Sīktēli", "cache_settings_statistics_title": "Kešatmiņas lietojums", "cache_settings_subtitle": "Kontrolēt Immich mobilās lietotnes kešdarbi", - "cache_settings_thumbnail_size": "Sīktēlu keša lielums ({} aktīvi)", + "cache_settings_thumbnail_size": "Sīktēlu kešatmiņas izmērs ({count} faili)", "cache_settings_tile_subtitle": "Kontrolēt lokālās krātuves uzvedību", "cache_settings_tile_title": "Lokālā Krātuve", "cache_settings_title": "Kešdarbes iestatījumi", @@ -471,6 +476,7 @@ "change_password_form_new_password": "Jauna Parole", "change_password_form_password_mismatch": "Paroles nesakrīt", "change_password_form_reenter_new_password": "Atkārtoti ievadīt jaunu paroli", + "change_pin_code": "Nomainīt PIN kodu", "change_your_password": "", "changed_visibility_successfully": "", "check_corrupt_asset_backup": "Check for corrupt asset backups", @@ -505,11 +511,12 @@ "completed": "Completed", "confirm": "Apstiprināt", "confirm_admin_password": "", + "confirm_new_pin_code": "Apstiprināt jauno PIN kodu", "confirm_password": "Apstiprināt paroli", "contain": "", "context": "Konteksts", "continue": "Turpināt", - "control_bottom_app_bar_album_info_shared": "{} vienumi · Koplietoti", + "control_bottom_app_bar_album_info_shared": "{count} vienumi · Koplietoti", "control_bottom_app_bar_create_new_album": "Izveidot jaunu albumu", "control_bottom_app_bar_delete_from_immich": "Dzēst no Immich", "control_bottom_app_bar_delete_from_local": "Dzēst no ierīces", @@ -519,7 +526,7 @@ "control_bottom_app_bar_share_to": "Kopīgot Uz", "control_bottom_app_bar_trash_from_immich": "Pārvietot uz Atkritni", "copied_image_to_clipboard": "", - "copy_error": "", + "copy_error": "Kopēšanas kļūda", "copy_file_path": "", "copy_image": "", "copy_link": "", @@ -532,11 +539,11 @@ "create": "Izveidot", "create_album": "Izveidot albumu", "create_album_page_untitled": "Bez nosaukuma", - "create_library": "", + "create_library": "Izveidot bibliotēku", "create_link": "Izveidot saiti", "create_link_to_share": "Izveidot kopīgošanas saiti", "create_new": "CREATE NEW", - "create_new_person": "", + "create_new_person": "Izveidot jaunu personu", "create_new_user": "Izveidot jaunu lietotāju", "create_shared_album_page_share_add_assets": "PIEVIENOT AKTĪVUS", "create_shared_album_page_share_select_photos": "Fotoattēlu Izvēle", @@ -545,19 +552,21 @@ "crop": "Crop", "curated_object_page_title": "Lietas", "current_device": "", + "current_pin_code": "Esošais PIN kods", "current_server_address": "Current server address", "custom_locale": "", "custom_locale_description": "", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, gggg", "dark": "", - "date_after": "", + "date_after": "Datums pēc", "date_and_time": "Datums un Laiks", - "date_before": "", + "date_before": "Datums pirms", "date_format": "E, LLL d, g • h:mm a", "date_of_birth_saved": "Dzimšanas datums veiksmīgi saglabāts", "date_range": "Datumu diapazons", - "day": "", + "day": "Diena", + "deduplication_criteria_1": "Attēla izmērs baitos", "default_locale": "", "default_locale_description": "", "delete": "Dzēst", @@ -568,26 +577,29 @@ "delete_dialog_alert_remote": "Šie vienumi tiks neatgriezeniski dzēsti no Immich servera.", "delete_dialog_ok_force": "Tā pat dzēst", "delete_dialog_title": "Neatgriezeniski Dzēst", - "delete_key": "", - "delete_library": "", - "delete_link": "", + "delete_face": "Dzēst seju", + "delete_key": "Dzēst atslēgu", + "delete_library": "Dzēst bibliotēku", + "delete_link": "Dzēst saiti", "delete_local_dialog_ok_backed_up_only": "Dzēst tikai Dublētos", "delete_local_dialog_ok_force": "Tā pat dzēst", + "delete_others": "Dzēst citus", "delete_shared_link": "Dzēst Kopīgošanas saiti", "delete_shared_link_dialog_title": "Dzēst Kopīgošanas saiti", "delete_user": "Dzēst lietotāju", - "deleted_shared_link": "", + "deleted_shared_link": "Dzēst kopīgoto saiti", "description": "Apraksts", "description_input_hint_text": "Pievienot aprakstu...", "description_input_submit_error": "Atjauninot aprakstu, radās kļūda; papildinformāciju skatiet žurnālā", "details": "INFORMĀCIJA", "direction": "Virziens", "disallow_edits": "", + "discord": "Discord", "discover": "", "dismiss_all_errors": "", "dismiss_error": "", "display_options": "", - "display_order": "", + "display_order": "Attēlošanas secība", "display_original_photos": "", "display_original_photos_setting_description": "", "documentation": "Dokumentācija", @@ -598,7 +610,7 @@ "download_enqueue": "Download enqueued", "download_error": "Download Error", "download_failed": "Download failed", - "download_filename": "file: {}", + "download_filename": "fails: {filename}", "download_finished": "Download finished", "download_notfound": "Download not found", "download_paused": "Download paused", @@ -608,31 +620,34 @@ "download_sucess": "Download success", "download_sucess_android": "The media has been downloaded to DCIM/Immich", "download_waiting_to_retry": "Waiting to retry", - "downloading": "", + "downloading": "Lejupielādē", + "downloading_asset_filename": "Lejupielādē failu {filename}", "downloading_media": "Downloading media", "duplicates": "Dublikāti", "duration": "", - "edit_album": "", + "edit": "Labot", + "edit_album": "Labot albumu", "edit_avatar": "", - "edit_date": "", - "edit_date_and_time": "", + "edit_date": "Labot datumu", + "edit_date_and_time": "Labot datumu un laiku", "edit_exclusion_pattern": "", - "edit_faces": "", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", + "edit_faces": "Labot sejas", + "edit_import_path": "Labot importa ceļu", + "edit_import_paths": "Labot importa ceļus", + "edit_key": "Labot atslēgu", "edit_link": "Rediģēt saiti", "edit_location": "Rediģēt Atrašanās Vietu", "edit_location_dialog_title": "Atrašanās vieta", "edit_name": "Rediģēt vārdu", - "edit_people": "", - "edit_title": "", + "edit_people": "Labot profilu", + "edit_title": "Labot nosaukumu", "edit_user": "Labot lietotāju", - "edited": "", - "editor": "", + "edited": "Labots", + "editor": "Redaktors", "editor_close_without_save_prompt": "Izmaiņas netiks saglabātas", "editor_close_without_save_title": "Aizvērt redaktoru?", "email": "E-pasts", + "email_notifications": "E-pasta paziņojumi", "empty_folder": "This folder is empty", "empty_trash": "Iztukšot atkritni", "enable": "", @@ -643,7 +658,7 @@ "error": "", "error_change_sort_album": "Failed to change album sort order", "error_loading_image": "", - "error_saving_image": "Error: {}", + "error_saving_image": "Kļūda: {error}", "errors": { "cant_get_faces": "Nevar iegūt sejas", "cant_search_people": "Neizdevās veikt peronu meklēšanu", @@ -701,10 +716,10 @@ "exif_bottom_sheet_location": "ATRAŠANĀS VIETA", "exif_bottom_sheet_people": "CILVĒKI", "exif_bottom_sheet_person_add_person": "Pievienot vārdu", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", + "exif_bottom_sheet_person_age": "Vecums {age}", + "exif_bottom_sheet_person_age_months": "Vecums {months} mēneši", + "exif_bottom_sheet_person_age_year_months": "Vecums 1 gads, {months} mēneši", + "exif_bottom_sheet_person_age_years": "Vecums {years}", "exit_slideshow": "Iziet no slīdrādes", "expand_all": "", "experimental_settings_new_asset_list_subtitle": "Izstrādes posmā", @@ -875,8 +890,8 @@ "manage_your_devices": "Pieslēgto ierīču pārvaldība", "manage_your_oauth_connection": "OAuth savienojumu pārvaldība", "map": "Karte", - "map_assets_in_bound": "{} fotoattēls", - "map_assets_in_bounds": "{} fotoattēli", + "map_assets_in_bound": "{count} fotoattēls", + "map_assets_in_bounds": "{count} fotoattēli", "map_cannot_get_user_location": "Nevar iegūt lietotāja atrašanās vietu", "map_location_dialog_yes": "Jā", "map_location_picker_page_use_location": "Izvēlēties šo atrašanās vietu", @@ -890,9 +905,9 @@ "map_settings": "Kartes Iestatījumi", "map_settings_dark_mode": "Tumšais režīms", "map_settings_date_range_option_day": "Pēdējās 24 stundas", - "map_settings_date_range_option_days": "Pēdējās {} dienas", + "map_settings_date_range_option_days": "Pēdējās {days} dienas", "map_settings_date_range_option_year": "Pēdējo gadu", - "map_settings_date_range_option_years": "Pēdējos {} gadus", + "map_settings_date_range_option_years": "Pēdējie {years} gadi", "map_settings_dialog_title": "Kartes Iestatījumi", "map_settings_include_show_archived": "Iekļaut Arhivētos", "map_settings_include_show_partners": "Iekļaut Partnerus", @@ -908,7 +923,7 @@ "memories_start_over": "Sākt no jauna", "memories_swipe_to_close": "Pavelciet uz augšu, lai aizvērtu", "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", + "memories_years_ago": "Pirms {years} gadiem", "memory": "Atmiņa", "menu": "Izvēlne", "merge": "Apvienot", @@ -923,6 +938,7 @@ "month": "Mēnesis", "monthly_title_text_date_format": "MMMM g", "more": "Vairāk", + "moved_to_library": "Pārvietoja {count, plural, one {# failu} other {# failus}} uz bibliotēku", "moved_to_trash": "Pārvietots uz atkritni", "multiselect_grid_edit_date_time_err_read_only": "Nevar rediģēt read only aktīva(-u) datumu, notiek izlaišana", "multiselect_grid_edit_gps_err_read_only": "Nevar rediģēt atrašanās vietu read only aktīva(-u) datumu, notiek izlaišana", @@ -936,13 +952,14 @@ "new_api_key": "Jauna API atslēga", "new_password": "Jaunā parole", "new_person": "Jauna persona", + "new_pin_code": "Jaunais PIN kods", "new_user_created": "Izveidots jauns lietotājs", "new_version_available": "PIEEJAMA JAUNA VERSIJA", "newest_first": "", "next": "Nākošais", "next_memory": "Nākamā atmiņa", "no": "Nē", - "no_albums_message": "", + "no_albums_message": "Izveido albumu, lai organizētu savas fotogrāfijas un video", "no_archived_assets_message": "", "no_assets_message": "NOKLIKŠĶINIET, LAI AUGŠUPIELĀDĒTU SAVU PIRMO FOTOATTĒLU", "no_assets_to_show": "Nav uzrādāmo aktīvu", @@ -952,6 +969,7 @@ "no_favorites_message": "", "no_libraries_message": "", "no_name": "Nav nosaukuma", + "no_notifications": "Nav paziņojumu", "no_places": "Nav atrašanās vietu", "no_results": "Nav rezultātu", "no_results_description": "Izmēģiniet sinonīmu vai vispārīgāku atslēgvārdu", @@ -980,11 +998,13 @@ "options": "Iestatījumi", "or": "vai", "organize_your_library": "", + "original": "oriģināls", "other": "Citi", "other_devices": "Citas ierīces", "other_variables": "Citi mainīgie", "owned": "Īpašumā", "owner": "Īpašnieks", + "partner_can_access": "{partner} var piekļūt", "partner_list_user_photos": "{user} fotoattēli", "partner_list_view_all": "Apskatīt visu", "partner_page_empty_message": "Jūsu fotogrāfijas pagaidām nav kopīgotas ar nevienu partneri.", @@ -992,9 +1012,9 @@ "partner_page_partner_add_failed": "Neizdevās pievienot partneri", "partner_page_select_partner": "Izvēlēties partneri", "partner_page_shared_to_title": "Kopīgots uz", - "partner_page_stop_sharing_content": "{} vairs nevarēs piekļūt jūsu fotoattēliem.", + "partner_page_stop_sharing_content": "{partner} vairs nevarēs piekļūt jūsu fotoattēliem.", "partner_sharing": "", - "partners": "", + "partners": "Partneri", "password": "Parole", "password_does_not_match": "Parole nesakrīt", "password_required": "", @@ -1004,7 +1024,7 @@ "hours": "", "years": "" }, - "path": "", + "path": "Ceļš", "pattern": "", "pause": "", "pause_memories": "", @@ -1024,8 +1044,9 @@ "permission_onboarding_permission_granted": "Atļauja piešķirta! Jūs esat gatavi darbam.", "permission_onboarding_permission_limited": "Atļauja ierobežota. Lai atļautu Immich dublēšanu un varētu pārvaldīt visu galeriju kolekciju, sadaļā Iestatījumi piešķiriet fotoattēlu un video atļaujas.", "permission_onboarding_request": "Immich nepieciešama atļauja skatīt jūsu fotoattēlus un videoklipus.", + "person": "Persona", "photos": "Fotoattēli", - "photos_from_previous_years": "", + "photos_from_previous_years": "Fotogrāfijas no iepriekšējiem gadiem", "pick_a_location": "", "place": "", "places": "Vietas", @@ -1033,15 +1054,17 @@ "play_memories": "", "play_motion_photo": "", "play_or_pause_video": "", - "port": "", + "port": "Ports", "preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_title": "Iestatījumi", "preset": "", - "preview": "", + "preview": "Priekšskatījums", "previous": "", "previous_memory": "", "previous_or_next_photo": "", "primary": "", + "privacy": "Privātums", + "profile": "Profils", "profile_drawer_app_logs": "Žurnāli", "profile_drawer_client_out_of_date_major": "Mobilā Aplikācija ir novecojusi. Lūdzu atjaunojiet to uz jaunāko lielo versiju", "profile_drawer_client_out_of_date_minor": "Mobilā Aplikācija ir novecojusi. Lūdzu atjaunojiet to uz jaunāko mazo versiju", @@ -1188,7 +1211,7 @@ "set_profile_picture": "", "set_slideshow_to_fullscreen": "", "setting_image_viewer_help": "Detaļu skatītājs vispirms ielādē mazo sīktēlu, pēc tam ielādē vidēja lieluma priekšskatījumu (ja iespējots), visbeidzot ielādē oriģinālu (ja iespējots).", - "setting_image_viewer_original_subtitle": "Iespējojiet sākotnējā pilnas izšķirtspējas attēla (liels!) ielādi. Atspējot lai samazinātu datu lietojumu (gan tīklā, gan ierīces kešatmiņā).", + "setting_image_viewer_original_subtitle": "Iespējot sākotnējā pilnas izšķirtspējas attēla (liels!) ielādi. Atspējot, lai samazinātu datu lietojumu (gan tīklā, gan ierīces kešatmiņā).", "setting_image_viewer_original_title": "Ielādēt oriģinālo attēlu", "setting_image_viewer_preview_subtitle": "Iespējojiet vidējas izšķirtspējas attēla ielādēšanu. Atspējojiet vai nu tiešu oriģināla ielādi, vai izmantojiet tikai sīktēlu.", "setting_image_viewer_preview_title": "Ielādēt priekšskatījuma attēlu", @@ -1196,12 +1219,12 @@ "setting_languages_apply": "Lietot", "setting_languages_subtitle": "Change the app's language", "setting_languages_title": "Valodas", - "setting_notifications_notify_failures_grace_period": "Paziņot par fona dublēšanas kļūmēm: {}", - "setting_notifications_notify_hours": "{} stundas", + "setting_notifications_notify_failures_grace_period": "Paziņot par fona dublēšanas kļūmēm: {duration}", + "setting_notifications_notify_hours": "{count} stundas", "setting_notifications_notify_immediately": "nekavējoties", - "setting_notifications_notify_minutes": "{} minūtes", + "setting_notifications_notify_minutes": "{count} minūtes", "setting_notifications_notify_never": "nekad", - "setting_notifications_notify_seconds": "{} sekundes", + "setting_notifications_notify_seconds": "{count} sekundes", "setting_notifications_single_progress_subtitle": "Detalizēta augšupielādes progresa informācija par katru aktīvu", "setting_notifications_single_progress_title": "Rādīt fona dublējuma detalizēto progresu", "setting_notifications_subtitle": "Paziņojumu preferenču pielāgošana", @@ -1213,9 +1236,10 @@ "settings": "Iestatījumi", "settings_require_restart": "Lūdzu, restartējiet Immich, lai lietotu šo iestatījumu", "settings_saved": "", + "setup_pin_code": "Uzstādīt PIN kodu", "share": "Kopīgot", "share_add_photos": "Pievienot fotoattēlus", - "share_assets_selected": "{} izvēlēti", + "share_assets_selected": "{count} izvēlēti", "share_dialog_preparing": "Notiek sagatavošana...", "shared": "Kopīgots", "shared_album_activities_input_disable": "Komentāri atslēgti", @@ -1227,32 +1251,32 @@ "shared_album_section_people_title": "CILVĒKI", "shared_by": "", "shared_by_you": "", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", + "shared_intent_upload_button_progress_text": "Augšupielādēti {current} / {total}", "shared_link_app_bar_title": "Kopīgotas Saites", "shared_link_clipboard_copied_massage": "Ievietots starpliktuvē", - "shared_link_clipboard_text": "Saite: {}\nParole: {}", + "shared_link_clipboard_text": "Saite: {link}\nParole: {password}", "shared_link_create_error": "Kļūda izveidojot kopīgošanas saiti", "shared_link_edit_description_hint": "Ievadiet kopīgojuma aprakstu", "shared_link_edit_expire_after_option_day": "1 diena", - "shared_link_edit_expire_after_option_days": "{} dienas", + "shared_link_edit_expire_after_option_days": "{count} dienas", "shared_link_edit_expire_after_option_hour": "1 stunda", - "shared_link_edit_expire_after_option_hours": "{} stundas", + "shared_link_edit_expire_after_option_hours": "{count} stundas", "shared_link_edit_expire_after_option_minute": "1 minūte", - "shared_link_edit_expire_after_option_minutes": "{} minūtes", - "shared_link_edit_expire_after_option_months": "{} mēneši", - "shared_link_edit_expire_after_option_year": "{} gads", + "shared_link_edit_expire_after_option_minutes": "{count} minūtes", + "shared_link_edit_expire_after_option_months": "{count} mēneši", + "shared_link_edit_expire_after_option_year": "{count} gads", "shared_link_edit_password_hint": "Ierakstīt kopīgojuma paroli", "shared_link_edit_submit_button": "Atjaunināt saiti", "shared_link_error_server_url_fetch": "Nevarēja ienest servera URL", - "shared_link_expires_day": "Derīguma termiņš beigsies pēc {} dienas", - "shared_link_expires_days": "Derīguma termiņš beigsies pēc {} dienām", - "shared_link_expires_hour": "Derīguma termiņš beigsies pēc {} stundas", - "shared_link_expires_hours": "Derīguma termiņš beigsies pēc {} stundām", - "shared_link_expires_minute": "Derīguma termiņš beigsies pēc {} minūtes", - "shared_link_expires_minutes": "Derīguma termiņš beidzas pēc {} minūtēm", + "shared_link_expires_day": "Derīguma termiņš beigsies pēc {count} dienas", + "shared_link_expires_days": "Derīguma termiņš beigsies pēc {count} dienām", + "shared_link_expires_hour": "Derīguma termiņš beigsies pēc {count} stundas", + "shared_link_expires_hours": "Derīguma termiņš beigsies pēc {count} stundām", + "shared_link_expires_minute": "Derīguma termiņš beigsies pēc {count} minūtes", + "shared_link_expires_minutes": "Derīguma termiņš beidzas pēc {count} minūtēm", "shared_link_expires_never": "Derīguma termiņš beigsies ∞", - "shared_link_expires_second": "Derīguma termiņš beigsies pēc {} sekundes", - "shared_link_expires_seconds": "Derīguma termiņš beidzas pēc {} sekundēm", + "shared_link_expires_second": "Derīguma termiņš beigsies pēc {count} sekundes", + "shared_link_expires_seconds": "Derīguma termiņš beidzas pēc {count} sekundēm", "shared_link_individual_shared": "Individuāli kopīgots", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Pārvaldīt Kopīgotās saites", @@ -1323,7 +1347,7 @@ "theme_selection": "", "theme_selection_description": "", "theme_setting_asset_list_storage_indicator_title": "Rādīt krātuves indikatoru uz aktīvu elementiem", - "theme_setting_asset_list_tiles_per_row_title": "Aktīvu skaits rindā ({})", + "theme_setting_asset_list_tiles_per_row_title": "Failu skaits rindā ({count})", "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_image_viewer_quality_subtitle": "Attēlu skatītāja detaļu kvalitātes pielāgošana", @@ -1349,12 +1373,14 @@ "trash_no_results_message": "", "trash_page_delete_all": "Dzēst Visu", "trash_page_empty_trash_dialog_content": "Vai vēlaties iztukšot savus izmestos aktīvus? Tie tiks neatgriezeniski izņemti no Immich", - "trash_page_info": "Atkritnes vienumi tiks neatgriezeniski dzēsti pēc {} dienām", + "trash_page_info": "Atkritnes vienumi tiks neatgriezeniski dzēsti pēc {days} dienām", "trash_page_no_assets": "Atkritnē nav aktīvu", "trash_page_restore_all": "Atjaunot Visu", "trash_page_select_assets_btn": "Atlasīt aktīvus", - "trash_page_title": "Atkritne ({})", + "trash_page_title": "Atkritne ({count})", "type": "", + "unable_to_change_pin_code": "Neizdevās nomainīt PIN kodu", + "unable_to_setup_pin_code": "Neizdevās uzstādīt PIN kodu", "unarchive": "Atarhivēt", "unfavorite": "Noņemt no izlases", "unhide_person": "Atcelt personas slēpšanu", @@ -1377,13 +1403,14 @@ "upload_status_duplicates": "Dublikāti", "upload_status_errors": "Kļūdas", "upload_status_uploaded": "Augšupielādēts", - "upload_to_immich": "Upload to Immich ({})", + "upload_to_immich": "Augšupielādēt Immich ({count})", "uploading": "Uploading", "url": "", "usage": "Lietojums", "use_current_connection": "use current connection", "user": "Lietotājs", "user_id": "Lietotāja ID", + "user_pin_code_settings": "PIN kods", "user_purchase_settings_description": "Pirkuma pārvaldība", "user_usage_detail": "Informācija par lietotāju lietojumu", "username": "Lietotājvārds", @@ -1396,7 +1423,7 @@ "version_announcement_message": "Sveiki! Ir pieejama jauna Immich versija. Lūdzu, veltiet laiku, lai izlasītu laidiena piezīmes un pārliecinātos, ka jūsu iestatījumi ir atjaunināti, lai novērstu jebkādu nepareizu konfigurāciju, jo īpaši, ja izmantojat WatchTower vai citu mehānismu, kas automātiski atjaunina jūsu Immich instanci.", "version_announcement_overlay_release_notes": "informācija par laidienu", "version_announcement_overlay_text_1": "Sveiks draugs, ir jauns izlaidums no", - "version_announcement_overlay_text_2": "lūdzu, veltiet laiku, lai apmeklētu", + "version_announcement_overlay_text_2": "lūdzu, veltiet laiku, lai apmeklētu ", "version_announcement_overlay_text_3": " un pārliecinieties, vai docker-compose un .env iestatījumi ir atjaunināti, lai novērstu jebkādas nepareizas konfigurācijas, īpaši, ja izmantojat WatchTower vai mehānismu, kas automātiski veic servera lietojumprogrammas atjaunināšanu.", "version_announcement_overlay_title": "Pieejama jauna servera versija 🎉", "version_history": "Versiju vēsture", diff --git a/i18n/nb_NO.json b/i18n/nb_NO.json index 1291f0dfef..b4e9814215 100644 --- a/i18n/nb_NO.json +++ b/i18n/nb_NO.json @@ -192,6 +192,7 @@ "oauth_auto_register": "Automatisk registrering", "oauth_auto_register_description": "Registrer automatisk nye brukere etter innlogging med OAuth", "oauth_button_text": "Knappetekst", + "oauth_client_secret_description": "Kreves hvis PKCE (Proof Key for Code Exchange) ikke støttes av OAuth-leverandøren", "oauth_enable_description": "Logg inn med OAuth", "oauth_mobile_redirect_uri": "Mobil omdirigerings-URI", "oauth_mobile_redirect_uri_override": "Mobil omdirigerings-URI overstyring", @@ -848,10 +849,12 @@ "failed_to_keep_this_delete_others": "Feilet med å beholde dette bilde og slette de andre", "failed_to_load_asset": "Feilet med å laste bilder", "failed_to_load_assets": "Feilet med å laste bilde", + "failed_to_load_notifications": "Kunne ikke laste inn varsler", "failed_to_load_people": "Feilen med å laste mennesker", "failed_to_remove_product_key": "Feilet med å ta bort produkt nøkkel", "failed_to_stack_assets": "Feilet med å stable bilder", "failed_to_unstack_assets": "Feilet med å avstable bilder", + "failed_to_update_notification_status": "Kunne ikke oppdatere varslingsstatusen", "import_path_already_exists": "Denne importstien eksisterer allerede.", "incorrect_email_or_password": "Feil epost eller passord", "paths_validation_failed": "{paths, plural, one {# sti} other {# sti}} mislyktes validering", @@ -1194,6 +1197,9 @@ "map_settings_only_show_favorites": "Vis kun favoritter", "map_settings_theme_settings": "Karttema", "map_zoom_to_see_photos": "Zoom ut for å se bilder", + "mark_all_as_read": "Merk alle som lest", + "mark_as_read": "Merk som lest", + "marked_all_as_read": "Merket alle som lest", "matches": "Samsvarende", "media_type": "Mediatype", "memories": "Minner", @@ -1220,6 +1226,8 @@ "month": "Måned", "monthly_title_text_date_format": "MMMM y", "more": "Mer", + "moved_to_archive": "Flyttet {count, plural, one {# asset} other {# assets}} til arkivet", + "moved_to_library": "Flyttet {count, plural, one {# asset} other {# assets}} til biblioteket", "moved_to_trash": "Flyttet til papirkurven", "multiselect_grid_edit_date_time_err_read_only": "Kan ikke endre dato på objekt(er) med kun lese-rettigheter, hopper over", "multiselect_grid_edit_gps_err_read_only": "Kan ikke endre lokasjon på objekt(er) med kun lese-rettigheter, hopper over", @@ -1252,6 +1260,8 @@ "no_favorites_message": "Legg til favoritter for å raskt finne dine beste bilder og videoer", "no_libraries_message": "Opprett et eksternt bibliotek for å se bildene og videoene dine", "no_name": "Ingen navn", + "no_notifications": "Ingen varsler", + "no_people_found": "Ingen samsvarende personer funnet", "no_places": "Ingen steder", "no_results": "Ingen resultater", "no_results_description": "Prøv et synonym eller mer generelt søkeord", @@ -1382,7 +1392,7 @@ "public_share": "Offentlig deling", "purchase_account_info": "Støttespiller", "purchase_activated_subtitle": "Takk for at du støtter Immich og åpen kildekode programvare", - "purchase_activated_time": "Aktiver den {date, date}", + "purchase_activated_time": "Aktiver den {date}", "purchase_activated_title": "Du produktnøkkel har vellyket blitt aktivert", "purchase_button_activate": "Aktiver", "purchase_button_buy": "Kjøp", @@ -1563,6 +1573,7 @@ "select_keep_all": "Velg beholde alle", "select_library_owner": "Velg bibliotekseier", "select_new_face": "Velg nytt ansikt", + "select_person_to_tag": "Velg en person å tagge", "select_photos": "Velg bilder", "select_trash_all": "Velg å flytte alt til papirkurven", "select_user_for_sharing_page_err_album": "Feilet ved oppretting av album", diff --git a/i18n/nl.json b/i18n/nl.json index 8a8f82cf33..ac4a3558ea 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "Weet je zeker dat je alle inlogmethoden wilt uitschakelen? Inloggen zal volledig worden uitgeschakeld.", "authentication_settings_reenable": "Gebruik een servercommando om opnieuw in te schakelen.", "background_task_job": "Achtergrondtaken", - "backup_database": "Backup Database", - "backup_database_enable_description": "Database back-ups activeren", - "backup_keep_last_amount": "Maximaal aantal back-ups om te bewaren", - "backup_settings": "Back-up instellingen", - "backup_settings_description": "Database back-up instellingen beheren", + "backup_database": "Maak database backup", + "backup_database_enable_description": "Database dumps activeren", + "backup_keep_last_amount": "Aantal back-ups om te bewaren", + "backup_settings": "Database dump instellingen", + "backup_settings_description": "Beheer database back-up instellingen. Noot: Deze taken worden niet bijgehouden en je wordt niet op de hoogte gesteld van een fout.", "check_all": "Controleer het logboek", "cleanup": "Opruimen", "cleared_jobs": "Taken gewist voor: {job}", @@ -53,6 +53,7 @@ "confirm_email_below": "Typ hieronder \"{email}\" ter bevestiging", "confirm_reprocess_all_faces": "Weet je zeker dat je alle gezichten opnieuw wilt verwerken? Hiermee worden ook alle mensen gewist.", "confirm_user_password_reset": "Weet u zeker dat je het wachtwoord van {user} wilt resetten?", + "confirm_user_pin_code_reset": "Weet je zeker dat je de PIN code van {user} wilt resetten?", "create_job": "Taak maken", "cron_expression": "Cron expressie", "cron_expression_description": "Stel de scaninterval in met het cron-formaat. Voor meer informatie kun je kijken naar bijvoorbeeld Crontab Guru", @@ -192,6 +193,7 @@ "oauth_auto_register": "Automatisch registreren", "oauth_auto_register_description": "Nieuwe gebruikers automatisch registreren na inloggen met OAuth", "oauth_button_text": "Button tekst", + "oauth_client_secret_description": "Vereist als PKCE (Proof Key for Code Exchange) niet wordt ondersteund door de OAuth aanbieder", "oauth_enable_description": "Inloggen met OAuth", "oauth_mobile_redirect_uri": "Omleidings URI voor mobiel", "oauth_mobile_redirect_uri_override": "Omleidings URI voor mobiele app overschrijven", @@ -205,6 +207,8 @@ "oauth_storage_quota_claim_description": "Stel de opslaglimiet van de gebruiker automatisch in op de waarde van deze claim.", "oauth_storage_quota_default": "Standaard opslaglimiet (GiB)", "oauth_storage_quota_default_description": "Limiet in GiB die moet worden gebruikt als er geen claim is opgegeven (voer 0 in voor onbeperkt).", + "oauth_timeout": "Aanvraag timeout", + "oauth_timeout_description": "Time-out voor aanvragen in milliseconden", "offline_paths": "Offline paden", "offline_paths_description": "Deze resultaten kunnen het gevolg zijn van het handmatig verwijderen van bestanden die geen deel uitmaken van een externe bibliotheek.", "password_enable_description": "Inloggen met e-mailadres en wachtwoord", @@ -366,13 +370,14 @@ "advanced": "Geavanceerd", "advanced_settings_enable_alternate_media_filter_subtitle": "Gebruik deze optie om media te filteren tijdens de synchronisatie op basis van alternatieve criteria. Gebruik dit enkel als de app problemen heeft met het detecteren van albums.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTEEL] Gebruik een alternatieve album synchronisatie filter", - "advanced_settings_log_level_title": "Log niveau: {}", + "advanced_settings_log_level_title": "Log niveau: {level}", "advanced_settings_prefer_remote_subtitle": "Sommige apparaten zijn traag met het laden van afbeeldingen die lokaal zijn opgeslagen op het apparaat. Activeer deze instelling om in plaats daarvan externe afbeeldingen te laden.", "advanced_settings_prefer_remote_title": "Externe afbeeldingen laden", "advanced_settings_proxy_headers_subtitle": "Definieer proxy headers die Immich bij elk netwerkverzoek moet verzenden", "advanced_settings_proxy_headers_title": "Proxy headers", "advanced_settings_self_signed_ssl_subtitle": "Slaat SSL-certificaatverificatie voor de connectie met de server over. Deze optie is vereist voor zelfondertekende certificaten", "advanced_settings_self_signed_ssl_title": "Zelfondertekende SSL-certificaten toestaan", + "advanced_settings_sync_remote_deletions_subtitle": "Automatisch bestanden verwijderen of herstellen op dit apparaat als die actie op het web is ondernomen", "advanced_settings_sync_remote_deletions_title": "Synchroniseer verwijderingen op afstand [EXPERIMENTEEL]", "advanced_settings_tile_subtitle": "Geavanceerde gebruikersinstellingen", "advanced_settings_troubleshooting_subtitle": "Schakel extra functies voor probleemoplossing in ", @@ -396,9 +401,9 @@ "album_remove_user_confirmation": "Weet je zeker dat je {user} wilt verwijderen?", "album_share_no_users": "Het lijkt erop dat je dit album met alle gebruikers hebt gedeeld, of dat je geen gebruikers hebt om mee te delen.", "album_thumbnail_card_item": "1 item", - "album_thumbnail_card_items": "{} items", + "album_thumbnail_card_items": "{count} items", "album_thumbnail_card_shared": " · Gedeeld", - "album_thumbnail_shared_by": "Gedeeld door {}", + "album_thumbnail_shared_by": "Gedeeld door {user}", "album_updated": "Album bijgewerkt", "album_updated_setting_description": "Ontvang een e-mailmelding wanneer een gedeeld album nieuwe assets heeft", "album_user_left": "{album} verlaten", @@ -436,7 +441,7 @@ "archive": "Archief", "archive_or_unarchive_photo": "Foto archiveren of uit het archief halen", "archive_page_no_archived_assets": "Geen gearchiveerde assets gevonden", - "archive_page_title": "Archief ({})", + "archive_page_title": "Archief ({count})", "archive_size": "Archiefgrootte", "archive_size_description": "Configureer de archiefgrootte voor downloads (in GiB)", "archived": "Gearchiveerd", @@ -473,27 +478,27 @@ "assets_added_to_album_count": "{count, plural, one {# asset} other {# assets}} aan het album toegevoegd", "assets_added_to_name_count": "{count, plural, one {# asset} other {# assets}} toegevoegd aan {hasName, select, true {{name}} other {nieuw album}}", "assets_count": "{count, plural, one {# asset} other {# assets}}", - "assets_deleted_permanently": "{} asset(s) permanent verwijderd", - "assets_deleted_permanently_from_server": "{} asset(s) permanent verwijderd van de Immich server", + "assets_deleted_permanently": "{count} asset(s) permanent verwijderd", + "assets_deleted_permanently_from_server": "{count} asset(s) permanent verwijderd van de Immich server", "assets_moved_to_trash_count": "{count, plural, one {# asset} other {# assets}} verplaatst naar prullenbak", "assets_permanently_deleted_count": "{count, plural, one {# asset} other {# assets}} permanent verwijderd", "assets_removed_count": "{count, plural, one {# asset} other {# assets}} verwijderd", - "assets_removed_permanently_from_device": "{} asset(s) permanent verwijderd van je apparaat", + "assets_removed_permanently_from_device": "{count} asset(s) permanent verwijderd van je apparaat", "assets_restore_confirmation": "Weet je zeker dat je alle verwijderde assets wilt herstellen? Je kunt deze actie niet ongedaan maken! Offline assets kunnen op deze manier niet worden hersteld.", "assets_restored_count": "{count, plural, one {# asset} other {# assets}} hersteld", - "assets_restored_successfully": "{} asset(s) succesvol hersteld", - "assets_trashed": "{} asset(s) naar de prullenbak verplaatst", + "assets_restored_successfully": "{count} asset(s) succesvol hersteld", + "assets_trashed": "{count} asset(s) naar de prullenbak verplaatst", "assets_trashed_count": "{count, plural, one {# asset} other {# assets}} naar prullenbak verplaatst", - "assets_trashed_from_server": "{} asset(s) naar de prullenbak verplaatst op de Immich server", + "assets_trashed_from_server": "{count} asset(s) naar de prullenbak verplaatst op de Immich server", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets waren}} al onderdeel van het album", "authorized_devices": "Geautoriseerde apparaten", - "automatic_endpoint_switching_subtitle": "Verbind lokaal bij het opgegeven wifi-netwerk en gebruik anders de externe url", + "automatic_endpoint_switching_subtitle": "Maak een lokale verbinding bij het opgegeven WiFi-netwerk en gebruik in andere gevallen de externe URL", "automatic_endpoint_switching_title": "Automatische serverwissel", "back": "Terug", "back_close_deselect": "Terug, sluiten of deselecteren", "background_location_permission": "Achtergrond locatie toestemming", - "background_location_permission_content": "Om van netwerk te wisselen terwijl de app op de achtergrond draait, heeft Immich *altijd* toegang tot de exacte locatie nodig om de naam van het wifi-netwerk te kunnen lezen", - "backup_album_selection_page_albums_device": "Albums op apparaat ({})", + "background_location_permission_content": "Om van netwerk te wisselen terwijl de app op de achtergrond draait, heeft Immich *altijd* toegang tot de exacte locatie nodig om de naam van het WiFi-netwerk te kunnen lezen", + "backup_album_selection_page_albums_device": "Albums op apparaat ({count})", "backup_album_selection_page_albums_tap": "Tik om in te voegen, dubbel tik om uit te sluiten", "backup_album_selection_page_assets_scatter": "Assets kunnen over verschillende albums verdeeld zijn, dus albums kunnen inbegrepen of uitgesloten zijn van het backup proces.", "backup_album_selection_page_select_albums": "Albums selecteren", @@ -502,11 +507,11 @@ "backup_all": "Alle", "backup_background_service_backup_failed_message": "Fout bij back-uppen assets. Opnieuw proberen…", "backup_background_service_connection_failed_message": "Fout bij verbinden server. Opnieuw proberen…", - "backup_background_service_current_upload_notification": "Uploaden {}", + "backup_background_service_current_upload_notification": "{filename} aan het uploaden...", "backup_background_service_default_notification": "Controleren op nieuwe assets…", "backup_background_service_error_title": "Backupfout", "backup_background_service_in_progress_notification": "Back-up van assets maken…", - "backup_background_service_upload_failure_notification": "Fout bij upload {}", + "backup_background_service_upload_failure_notification": "Fout bij het uploaden van {filename}", "backup_controller_page_albums": "Back-up albums", "backup_controller_page_background_app_refresh_disabled_content": "Schakel verversen op de achtergrond in via Instellingen > Algemeen > Ververs op achtergrond, om back-ups op de achtergrond te maken.", "backup_controller_page_background_app_refresh_disabled_title": "Verversen op achtergrond uitgeschakeld", @@ -517,7 +522,7 @@ "backup_controller_page_background_battery_info_title": "Batterijoptimalisaties", "backup_controller_page_background_charging": "Alleen tijdens opladen", "backup_controller_page_background_configure_error": "Achtergrondserviceconfiguratie mislukt", - "backup_controller_page_background_delay": "Back-upvertraging nieuwe assets: {}", + "backup_controller_page_background_delay": "Back-upvertraging voor nieuwe assets: {duration}", "backup_controller_page_background_description": "Schakel de achtergrondservice in om automatisch een back-up te maken van nieuwe assets zonder de app te hoeven openen", "backup_controller_page_background_is_off": "Automatische achtergrond back-up staat uit", "backup_controller_page_background_is_on": "Automatische achtergrond back-up staat aan", @@ -527,12 +532,12 @@ "backup_controller_page_backup": "Back-up", "backup_controller_page_backup_selected": "Geselecteerd: ", "backup_controller_page_backup_sub": "Geback-upte foto's en video's", - "backup_controller_page_created": "Gemaakt op: {}", + "backup_controller_page_created": "Gemaakt op: {date}", "backup_controller_page_desc_backup": "Schakel back-up op de voorgrond in om automatisch nieuwe assets naar de server te uploaden bij het openen van de app.", "backup_controller_page_excluded": "Uitgezonderd: ", - "backup_controller_page_failed": "Mislukt ({})", - "backup_controller_page_filename": "Bestandsnaam: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Mislukt ({count})", + "backup_controller_page_filename": "Bestandsnaam: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Back-up informatie", "backup_controller_page_none_selected": "Geen geselecteerd", "backup_controller_page_remainder": "Resterend", @@ -541,7 +546,7 @@ "backup_controller_page_start_backup": "Back-up uitvoeren", "backup_controller_page_status_off": "Automatische back-up op de voorgrond staat uit", "backup_controller_page_status_on": "Automatische back-up op de voorgrond staat aan", - "backup_controller_page_storage_format": "{} van {} gebruikt", + "backup_controller_page_storage_format": "{used} van {total} gebruikt", "backup_controller_page_to_backup": "Albums om een back-up van te maken", "backup_controller_page_total_sub": "Alle unieke foto's en video's uit geselecteerde albums", "backup_controller_page_turn_off": "Back-up op de voorgrond uitzetten", @@ -566,21 +571,21 @@ "bulk_keep_duplicates_confirmation": "Weet je zeker dat je {count, plural, one {# duplicate asset} other {# duplicate assets}} wilt behouden? Dit zal alle groepen met duplicaten oplossen zonder iets te verwijderen.", "bulk_trash_duplicates_confirmation": "Weet je zeker dat je {count, plural, one {# duplicate asset} other {# duplicate assets}} in bulk naar de prullenbak wilt verplaatsen? Dit zal de grootste asset van elke groep behouden en alle andere duplicaten naar de prullenbak verplaatsen.", "buy": "Immich kopen", - "cache_settings_album_thumbnails": "Thumbnails bibliotheekpagina ({} assets)", + "cache_settings_album_thumbnails": "Thumbnails bibliotheekpagina ({count} assets)", "cache_settings_clear_cache_button": "Cache wissen", "cache_settings_clear_cache_button_title": "Wist de cache van de app. Dit zal de presentaties van de app aanzienlijk beïnvloeden totdat de cache opnieuw is opgebouwd.", "cache_settings_duplicated_assets_clear_button": "MAAK VRIJ", "cache_settings_duplicated_assets_subtitle": "Foto's en video's op de zwarte lijst van de app", - "cache_settings_duplicated_assets_title": "Gedupliceerde assets ({})", - "cache_settings_image_cache_size": "Grootte afbeeldingscache ({} assets)", + "cache_settings_duplicated_assets_title": "Gedupliceerde assets ({count})", + "cache_settings_image_cache_size": "Grootte afbeeldingscache ({count} assets)", "cache_settings_statistics_album": "Bibliotheekthumbnails", - "cache_settings_statistics_assets": "{} assets ({})", + "cache_settings_statistics_assets": "{count} assets ({size})", "cache_settings_statistics_full": "Volledige afbeeldingen", "cache_settings_statistics_shared": "Gedeeld-albumthumbnails", "cache_settings_statistics_thumbnail": "Thumbnails", "cache_settings_statistics_title": "Cachegebruik", "cache_settings_subtitle": "Beheer het cachegedrag van de Immich app", - "cache_settings_thumbnail_size": "Thumbnail-cachegrootte ({} assets)", + "cache_settings_thumbnail_size": "Thumbnail-cachegrootte ({count} assets)", "cache_settings_tile_subtitle": "Beheer het gedrag van lokale opslag", "cache_settings_tile_title": "Lokale opslag", "cache_settings_title": "Cache-instellingen", @@ -606,12 +611,13 @@ "change_password_form_new_password": "Nieuw wachtwoord", "change_password_form_password_mismatch": "Wachtwoorden komen niet overeen", "change_password_form_reenter_new_password": "Vul het wachtwoord opnieuw in", + "change_pin_code": "Wijzig PIN code", "change_your_password": "Wijzig je wachtwoord", "changed_visibility_successfully": "Zichtbaarheid succesvol gewijzigd", "check_all": "Controleer alle", "check_corrupt_asset_backup": "Controleer op corrupte back-ups van assets", "check_corrupt_asset_backup_button": "Controle uitvoeren", - "check_corrupt_asset_backup_description": "Voer deze controle alleen uit via wifi en nadat alle assets zijn geback-upt. De procedure kan een paar minuten duren.", + "check_corrupt_asset_backup_description": "Voer deze controle alleen uit via WiFi en nadat alle assets zijn geback-upt. De procedure kan een paar minuten duren.", "check_logs": "Controleer logboek", "choose_matching_people_to_merge": "Kies overeenkomende mensen om samen te voegen", "city": "Stad", @@ -646,11 +652,12 @@ "confirm_delete_face": "Weet je zeker dat je {name} gezicht wilt verwijderen uit de asset?", "confirm_delete_shared_link": "Weet je zeker dat je deze gedeelde link wilt verwijderen?", "confirm_keep_this_delete_others": "Alle andere assets in de stack worden verwijderd, behalve deze. Weet je zeker dat je wilt doorgaan?", + "confirm_new_pin_code": "Bevestig nieuwe PIN code", "confirm_password": "Bevestig wachtwoord", "contain": "Bevat", "context": "Context", "continue": "Doorgaan", - "control_bottom_app_bar_album_info_shared": "{} items · Gedeeld", + "control_bottom_app_bar_album_info_shared": "{count} items · Gedeeld", "control_bottom_app_bar_create_new_album": "Nieuw album maken", "control_bottom_app_bar_delete_from_immich": "Verwijderen van Immich", "control_bottom_app_bar_delete_from_local": "Verwijderen van apparaat", @@ -691,6 +698,7 @@ "crop": "Bijsnijden", "curated_object_page_title": "Dingen", "current_device": "Huidig apparaat", + "current_pin_code": "Huidige PIN code", "current_server_address": "Huidige serveradres", "custom_locale": "Aangepaste landinstelling", "custom_locale_description": "Formatteer datums en getallen op basis van de taal en de regio", @@ -759,7 +767,7 @@ "download_enqueue": "Download in wachtrij", "download_error": "Fout bij downloaden", "download_failed": "Download mislukt", - "download_filename": "bestand: {}", + "download_filename": "bestand: {filename}", "download_finished": "Download voltooid", "download_include_embedded_motion_videos": "Ingesloten video's", "download_include_embedded_motion_videos_description": "Voeg video's toe die ingesloten zijn in bewegende foto's als een apart bestand", @@ -810,12 +818,12 @@ "enabled": "Ingeschakeld", "end_date": "Einddatum", "enqueued": "In de wachtrij", - "enter_wifi_name": "Voer de WiFi naam in", + "enter_wifi_name": "Voer de WiFi-naam in", "error": "Fout", "error_change_sort_album": "Sorteervolgorde van album wijzigen mislukt", "error_delete_face": "Fout bij verwijderen gezicht uit asset", "error_loading_image": "Fout bij laden afbeelding", - "error_saving_image": "Fout: {}", + "error_saving_image": "Fout: {error}", "error_title": "Fout - Er is iets misgegaan", "errors": { "cannot_navigate_next_asset": "Kan niet naar de volgende asset navigeren", @@ -845,10 +853,12 @@ "failed_to_keep_this_delete_others": "Het is niet gelukt om dit asset te behouden en de andere assets te verwijderen", "failed_to_load_asset": "Kan asset niet laden", "failed_to_load_assets": "Kan assets niet laden", + "failed_to_load_notifications": "Kon meldingen niet laden", "failed_to_load_people": "Kan mensen niet laden", "failed_to_remove_product_key": "Er is een fout opgetreden bij het verwijderen van de licentiesleutel", "failed_to_stack_assets": "Fout bij stapelen van assets", "failed_to_unstack_assets": "Fout bij ontstapelen van assets", + "failed_to_update_notification_status": "Kon notificatie status niet updaten", "import_path_already_exists": "Dit import-pad bestaat al.", "incorrect_email_or_password": "Onjuist e-mailadres of wachtwoord", "paths_validation_failed": "validatie van {paths, plural, one {# pad} other {# paden}} mislukt", @@ -916,6 +926,7 @@ "unable_to_remove_reaction": "Kan reactie niet verwijderen", "unable_to_repair_items": "Kan items niet repareren", "unable_to_reset_password": "Kan wachtwoord niet resetten", + "unable_to_reset_pin_code": "Kan PIN code niet resetten", "unable_to_resolve_duplicate": "Kan duplicaat niet oplossen", "unable_to_restore_assets": "Kan assets niet herstellen", "unable_to_restore_trash": "Kan niet herstellen uit prullenbak", @@ -949,10 +960,10 @@ "exif_bottom_sheet_location": "LOCATIE", "exif_bottom_sheet_people": "MENSEN", "exif_bottom_sheet_person_add_person": "Naam toevoegen", - "exif_bottom_sheet_person_age": "Leeftijd {}", - "exif_bottom_sheet_person_age_months": "Leeftijd {} maanden", - "exif_bottom_sheet_person_age_year_months": "Leeftijd 1 jaar, {} maanden", - "exif_bottom_sheet_person_age_years": "Leeftijd {}", + "exif_bottom_sheet_person_age": "Leeftijd {age}", + "exif_bottom_sheet_person_age_months": "Leeftijd {months} maanden", + "exif_bottom_sheet_person_age_year_months": "Leeftijd 1 jaar, {months} maanden", + "exif_bottom_sheet_person_age_years": "Leeftijd {years}", "exit_slideshow": "Diavoorstelling sluiten", "expand_all": "Alles uitvouwen", "experimental_settings_new_asset_list_subtitle": "Werk in uitvoering", @@ -970,7 +981,7 @@ "external": "Extern", "external_libraries": "Externe bibliotheken", "external_network": "Extern netwerk", - "external_network_sheet_info": "Als je niet verbonden bent met het opgegeven wifi-netwerk, maakt de app verbinding met de server via de eerst bereikbare URL in de onderstaande lijst, van boven naar beneden", + "external_network_sheet_info": "Als je niet verbonden bent met het opgegeven WiFi-netwerk, maakt de app verbinding met de server via de eerst bereikbare URL in de onderstaande lijst, van boven naar beneden", "face_unassigned": "Niet toegewezen", "failed": "Mislukt", "failed_to_load_assets": "Kan assets niet laden", @@ -988,6 +999,7 @@ "filetype": "Bestandstype", "filter": "Filter", "filter_people": "Filter op mensen", + "filter_places": "Filter locaties", "find_them_fast": "Vind ze snel op naam door te zoeken", "fix_incorrect_match": "Onjuiste overeenkomst corrigeren", "folder": "Map", @@ -997,7 +1009,7 @@ "forward": "Vooruit", "general": "Algemeen", "get_help": "Krijg hulp", - "get_wifiname_error": "Kon de Wi-Fi naam niet ophalen. Zorg ervoor dat je de benodigde machtigingen hebt verleend en verbonden bent met een Wi-Fi-netwerk", + "get_wifiname_error": "Kon de WiFi-naam niet ophalen. Zorg ervoor dat je de benodigde machtigingen hebt verleend en verbonden bent met een WiFi-netwerk", "getting_started": "Aan de slag", "go_back": "Ga terug", "go_to_folder": "Ga naar map", @@ -1114,9 +1126,9 @@ "loading": "Laden", "loading_search_results_failed": "Laden van zoekresultaten mislukt", "local_network": "Lokaal netwerk", - "local_network_sheet_info": "De app maakt verbinding met de server via deze URL wanneer het opgegeven wifi-netwerk wordt gebruikt", + "local_network_sheet_info": "De app maakt verbinding met de server via deze URL wanneer het opgegeven WiFi-netwerk wordt gebruikt", "location_permission": "Locatie toestemming", - "location_permission_content": "Om de functie voor automatische serverwissel te gebruiken, heeft Immich toegang tot de exacte locatie nodig om de naam van het huidige wifi-netwerk te kunnen bepalen.", + "location_permission_content": "Om de functie voor automatische serverwissel te gebruiken, heeft Immich toegang tot de exacte locatie nodig om de naam van het huidige WiFi-netwerk te kunnen bepalen.", "location_picker_choose_on_map": "Kies op kaart", "location_picker_latitude_error": "Voer een geldige breedtegraad in", "location_picker_latitude_hint": "Voer hier je breedtegraad in", @@ -1166,8 +1178,8 @@ "manage_your_devices": "Beheer je ingelogde apparaten", "manage_your_oauth_connection": "Beheer je OAuth koppeling", "map": "Kaart", - "map_assets_in_bound": "{} foto", - "map_assets_in_bounds": "{} foto's", + "map_assets_in_bound": "{count} foto", + "map_assets_in_bounds": "{count} foto's", "map_cannot_get_user_location": "Kan locatie van de gebruiker niet ophalen", "map_location_dialog_yes": "Ja", "map_location_picker_page_use_location": "Gebruik deze locatie", @@ -1181,15 +1193,18 @@ "map_settings": "Kaartinstellingen", "map_settings_dark_mode": "Donkere modus", "map_settings_date_range_option_day": "Afgelopen 24 uur", - "map_settings_date_range_option_days": "Afgelopen {} dagen", + "map_settings_date_range_option_days": "Afgelopen {days} dagen", "map_settings_date_range_option_year": "Afgelopen jaar", - "map_settings_date_range_option_years": "Afgelopen {} jaar", + "map_settings_date_range_option_years": "Afgelopen {years} jaar", "map_settings_dialog_title": "Kaart Instellingen", "map_settings_include_show_archived": "Toon gearchiveerde", "map_settings_include_show_partners": "Inclusief partners", "map_settings_only_show_favorites": "Toon enkel favorieten", "map_settings_theme_settings": "Kaart thema", "map_zoom_to_see_photos": "Zoom uit om foto's te zien", + "mark_all_as_read": "Alles markeren als gelezen", + "mark_as_read": "Markeren als gelezen", + "marked_all_as_read": "Allen gemarkeerd als gelezen", "matches": "Overeenkomsten", "media_type": "Mediatype", "memories": "Herinneringen", @@ -1199,7 +1214,7 @@ "memories_start_over": "Opnieuw beginnen", "memories_swipe_to_close": "Swipe omhoog om te sluiten", "memories_year_ago": "Een jaar geleden", - "memories_years_ago": "{} jaar geleden", + "memories_years_ago": "{years} jaar geleden", "memory": "Herinnering", "memory_lane_title": "Herinneringen {title}", "menu": "Menu", @@ -1216,6 +1231,8 @@ "month": "Maand", "monthly_title_text_date_format": "MMMM y", "more": "Meer", + "moved_to_archive": "{count, plural, one {# asset} other {# assets}} verplaatst naar archief", + "moved_to_library": "{count, plural, one {# asset} other {# assets}} verplaatst naar bibliotheek", "moved_to_trash": "Naar de prullenbak verplaatst", "multiselect_grid_edit_date_time_err_read_only": "Kan datum van alleen-lezen asset(s) niet wijzigen, overslaan", "multiselect_grid_edit_gps_err_read_only": "Kan locatie van alleen-lezen asset(s) niet wijzigen, overslaan", @@ -1230,6 +1247,7 @@ "new_api_key": "Nieuwe API key", "new_password": "Nieuw wachtwoord", "new_person": "Nieuw persoon", + "new_pin_code": "Nieuwe PIN code", "new_user_created": "Nieuwe gebruiker aangemaakt", "new_version_available": "NIEUWE VERSIE BESCHIKBAAR", "newest_first": "Nieuwste eerst", @@ -1248,6 +1266,8 @@ "no_favorites_message": "Voeg favorieten toe om snel je beste foto's en video's te vinden", "no_libraries_message": "Maak een externe bibliotheek om je foto's en video's te bekijken", "no_name": "Geen naam", + "no_notifications": "Geen notificaties", + "no_people_found": "Geen mensen gevonden", "no_places": "Geen plaatsen", "no_results": "Geen resultaten", "no_results_description": "Probeer een synoniem of een algemener zoekwoord", @@ -1278,6 +1298,7 @@ "onboarding_welcome_user": "Welkom, {user}", "online": "Online", "only_favorites": "Alleen favorieten", + "open": "Openen", "open_in_map_view": "Openen in kaartweergave", "open_in_openstreetmap": "Openen in OpenStreetMap", "open_the_search_filters": "Open de zoekfilters", @@ -1301,7 +1322,7 @@ "partner_page_partner_add_failed": "Partner toevoegen mislukt", "partner_page_select_partner": "Selecteer partner", "partner_page_shared_to_title": "Gedeeld met", - "partner_page_stop_sharing_content": "{} zal geen toegang meer hebben tot je fotos's.", + "partner_page_stop_sharing_content": "{partner} zal geen toegang meer hebben tot je fotos's.", "partner_sharing": "Delen met partner", "partners": "Partners", "password": "Wachtwoord", @@ -1347,6 +1368,9 @@ "photos_count": "{count, plural, one {{count, number} foto} other {{count, number} foto's}}", "photos_from_previous_years": "Foto's van voorgaande jaren", "pick_a_location": "Kies een locatie", + "pin_code_changed_successfully": "PIN code succesvol gewijzigd", + "pin_code_reset_successfully": "PIN code succesvol gereset", + "pin_code_setup_successfully": "PIN code succesvol ingesteld", "place": "Plaats", "places": "Plaatsen", "places_count": "{count, plural, one {{count, number} Plaats} other {{count, number} Plaatsen}}", @@ -1377,7 +1401,7 @@ "public_share": "Openbare deellink", "purchase_account_info": "Supporter", "purchase_activated_subtitle": "Bedankt voor het ondersteunen van Immich en open-source software", - "purchase_activated_time": "Geactiveerd op {date, date}", + "purchase_activated_time": "Geactiveerd op {date}", "purchase_activated_title": "Je licentiesleutel is succesvol geactiveerd", "purchase_button_activate": "Activeren", "purchase_button_buy": "Kopen", @@ -1422,6 +1446,8 @@ "recent_searches": "Recente zoekopdrachten", "recently_added": "Onlangs toegevoegd", "recently_added_page_title": "Recent toegevoegd", + "recently_taken": "Recent genomen", + "recently_taken_page_title": "Recent Genomen", "refresh": "Vernieuwen", "refresh_encoded_videos": "Vernieuw gecodeerde video's", "refresh_faces": "Vernieuw gezichten", @@ -1464,6 +1490,7 @@ "reset": "Resetten", "reset_password": "Wachtwoord resetten", "reset_people_visibility": "Zichtbaarheid mensen resetten", + "reset_pin_code": "Reset PIN code", "reset_to_default": "Resetten naar standaard", "resolve_duplicates": "Duplicaten oplossen", "resolved_all_duplicates": "Alle duplicaten verwerkt", @@ -1556,6 +1583,7 @@ "select_keep_all": "Selecteer alles behouden", "select_library_owner": "Selecteer bibliotheekeigenaar", "select_new_face": "Selecteer nieuw gezicht", + "select_person_to_tag": "Selecteer een persoon om te taggen", "select_photos": "Selecteer foto's", "select_trash_all": "Selecteer alles naar prullenbak verplaatsen", "select_user_for_sharing_page_err_album": "Album aanmaken mislukt", @@ -1586,12 +1614,12 @@ "setting_languages_apply": "Toepassen", "setting_languages_subtitle": "Wijzig de taal van de app", "setting_languages_title": "Taal", - "setting_notifications_notify_failures_grace_period": "Fouten van de achtergrond back-up melden: {}", - "setting_notifications_notify_hours": "{} uur", + "setting_notifications_notify_failures_grace_period": "Fouten van de achtergrond back-up melden: {duration}", + "setting_notifications_notify_hours": "{count} uur", "setting_notifications_notify_immediately": "meteen", - "setting_notifications_notify_minutes": "{} minuten", + "setting_notifications_notify_minutes": "{count} minuten", "setting_notifications_notify_never": "nooit", - "setting_notifications_notify_seconds": "{} seconden", + "setting_notifications_notify_seconds": "{count} seconden", "setting_notifications_single_progress_subtitle": "Gedetailleerde informatie over de uploadvoortgang per asset", "setting_notifications_single_progress_title": "Gedetailleerde informatie over achtergrond back-ups tonen", "setting_notifications_subtitle": "Voorkeuren voor meldingen beheren", @@ -1603,9 +1631,10 @@ "settings": "Instellingen", "settings_require_restart": "Start Immich opnieuw op om deze instelling toe te passen", "settings_saved": "Instellingen opgeslagen", + "setup_pin_code": "Stel een PIN code in", "share": "Delen", "share_add_photos": "Foto's toevoegen", - "share_assets_selected": "{} geselecteerd", + "share_assets_selected": "{count} geselecteerd", "share_dialog_preparing": "Voorbereiden...", "shared": "Gedeeld", "shared_album_activities_input_disable": "Reactie is uitgeschakeld", @@ -1619,32 +1648,32 @@ "shared_by_user": "Gedeeld door {user}", "shared_by_you": "Gedeeld door jou", "shared_from_partner": "Foto's van {partner}", - "shared_intent_upload_button_progress_text": "{} / {} geüpload", + "shared_intent_upload_button_progress_text": "{current} / {total} geüpload", "shared_link_app_bar_title": "Gedeelde links", "shared_link_clipboard_copied_massage": "Gekopieerd naar klembord", - "shared_link_clipboard_text": "Link: {}\nWachtwoord: {}", + "shared_link_clipboard_text": "Link: {link}\nWachtwoord: {password}", "shared_link_create_error": "Fout bij het maken van een gedeelde link", "shared_link_edit_description_hint": "Voer beschrijving voor de gedeelde link in", "shared_link_edit_expire_after_option_day": "1 dag", - "shared_link_edit_expire_after_option_days": "{} dagen", + "shared_link_edit_expire_after_option_days": "{count} dagen", "shared_link_edit_expire_after_option_hour": "1 uur", - "shared_link_edit_expire_after_option_hours": "{} uren", + "shared_link_edit_expire_after_option_hours": "{count} uren", "shared_link_edit_expire_after_option_minute": "1 minuut", - "shared_link_edit_expire_after_option_minutes": "{} minuten", - "shared_link_edit_expire_after_option_months": "{} maanden", - "shared_link_edit_expire_after_option_year": "{} jaar", + "shared_link_edit_expire_after_option_minutes": "{count} minuten", + "shared_link_edit_expire_after_option_months": "{count} maanden", + "shared_link_edit_expire_after_option_year": "{count} jaar", "shared_link_edit_password_hint": "Voer wachtwoord voor de gedeelde link in", "shared_link_edit_submit_button": "Link bijwerken", "shared_link_error_server_url_fetch": "Kan de server url niet ophalen", - "shared_link_expires_day": "Verloopt over {} dag", - "shared_link_expires_days": "Verloopt over {} dagen", - "shared_link_expires_hour": "Verloopt over {} uur", - "shared_link_expires_hours": "Verloopt over {} uur", - "shared_link_expires_minute": "Verloopt over {} minuut", - "shared_link_expires_minutes": "Verloopt over {} minuten", + "shared_link_expires_day": "Verloopt over {count} dag", + "shared_link_expires_days": "Verloopt over {count} dagen", + "shared_link_expires_hour": "Verloopt over {count} uur", + "shared_link_expires_hours": "Verloopt over {count} uur", + "shared_link_expires_minute": "Verloopt over {count} minuut", + "shared_link_expires_minutes": "Verloopt over {count} minuten", "shared_link_expires_never": "Verloopt ∞", - "shared_link_expires_second": "Verloopt over {} seconde", - "shared_link_expires_seconds": "Verloopt over {} seconden", + "shared_link_expires_second": "Verloopt over {count} seconde", + "shared_link_expires_seconds": "Verloopt over {count} seconden", "shared_link_individual_shared": "Individueel gedeeld", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Beheer gedeelde links", @@ -1745,7 +1774,7 @@ "theme_selection": "Thema selectie", "theme_selection_description": "Stel het thema automatisch in op licht of donker op basis van de systeemvoorkeuren van je browser", "theme_setting_asset_list_storage_indicator_title": "Toon opslag indicator bij de asset tegels", - "theme_setting_asset_list_tiles_per_row_title": "Aantal assets per rij ({})", + "theme_setting_asset_list_tiles_per_row_title": "Aantal assets per rij ({count})", "theme_setting_colorful_interface_subtitle": "Pas primaire kleuren toe op achtergronden.", "theme_setting_colorful_interface_title": "Kleurrijke interface", "theme_setting_image_viewer_quality_subtitle": "De kwaliteit van de gedetailleerde-fotoweergave aanpassen", @@ -1780,13 +1809,15 @@ "trash_no_results_message": "Hier verschijnen foto's en video's die in de prullenbak zijn geplaatst.", "trash_page_delete_all": "Verwijder alle", "trash_page_empty_trash_dialog_content": "Wil je de prullenbak leegmaken? Deze items worden permanent verwijderd van Immich", - "trash_page_info": "Verwijderde items worden permanent verwijderd na {} dagen", + "trash_page_info": "Verwijderde items worden permanent verwijderd na {days} dagen", "trash_page_no_assets": "Geen verwijderde assets", "trash_page_restore_all": "Herstel alle", "trash_page_select_assets_btn": "Selecteer assets", - "trash_page_title": "Prullenbak ({})", + "trash_page_title": "Prullenbak ({count})", "trashed_items_will_be_permanently_deleted_after": "Items in de prullenbak worden na {days, plural, one {# dag} other {# dagen}} permanent verwijderd.", "type": "Type", + "unable_to_change_pin_code": "PIN code kan niet gewijzigd worden", + "unable_to_setup_pin_code": "PIN code kan niet ingesteld worden", "unarchive": "Herstellen uit archief", "unarchived_count": "{count, plural, other {# verwijderd uit archief}}", "unfavorite": "Verwijderen uit favorieten", @@ -1822,7 +1853,7 @@ "upload_status_errors": "Fouten", "upload_status_uploaded": "Geüpload", "upload_success": "Uploaden gelukt, vernieuw de pagina om de nieuwe assets te zien.", - "upload_to_immich": "Uploaden naar Immich ({})", + "upload_to_immich": "Uploaden naar Immich ({count})", "uploading": "Aan het uploaden", "url": "URL", "usage": "Gebruik", @@ -1831,6 +1862,8 @@ "user": "Gebruiker", "user_id": "Gebruikers ID", "user_liked": "{user} heeft {type, select, photo {deze foto} video {deze video} asset {deze asset} other {dit}} geliket", + "user_pin_code_settings": "PIN Code", + "user_pin_code_settings_description": "Beheer je PIN code", "user_purchase_settings": "Kopen", "user_purchase_settings_description": "Beheer je aankoop", "user_role_set": "{user} instellen als {role}", @@ -1868,6 +1901,7 @@ "view_name": "Bekijken", "view_next_asset": "Bekijk volgende asset", "view_previous_asset": "Bekijk vorige asset", + "view_qr_code": "QR-code bekijken", "view_stack": "Bekijk stapel", "viewer_remove_from_stack": "Verwijder van Stapel", "viewer_stack_use_as_main_asset": "Gebruik als Hoofd Asset", @@ -1878,11 +1912,11 @@ "week": "Week", "welcome": "Welkom", "welcome_to_immich": "Welkom bij Immich", - "wifi_name": "WiFi naam", + "wifi_name": "WiFi-naam", "year": "Jaar", "years_ago": "{years, plural, one {# jaar} other {# jaar}} geleden", "yes": "Ja", "you_dont_have_any_shared_links": "Je hebt geen gedeelde links", - "your_wifi_name": "Je WiFi naam", + "your_wifi_name": "Je WiFi-naam", "zoom_image": "Inzoomen" } diff --git a/i18n/pl.json b/i18n/pl.json index f45865e89d..a6048b3bf9 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -1,5 +1,5 @@ { - "about": "O", + "about": "O aplikacji", "account": "Konto", "account_settings": "Ustawienia konta", "acknowledge": "Zrozumiałem/łam", @@ -53,6 +53,7 @@ "confirm_email_below": "Aby potwierdzić, wpisz \"{email}\" poniżej", "confirm_reprocess_all_faces": "Czy na pewno chcesz ponownie przetworzyć wszystkie twarze? Spowoduje to utratę nazwanych osób.", "confirm_user_password_reset": "Czy na pewno chcesz zresetować hasło użytkownika {user}?", + "confirm_user_pin_code_reset": "Czy jesteś pewny, że chcesz zresetować kod pin dla użytkownika {user}?", "create_job": "Utwórz zadanie", "cron_expression": "Wyrażenie Cron", "cron_expression_description": "Ustaw interwał skanowania przy pomocy formatu Cron'a. Po więcej informacji na temat formatu Cron zobacz . Crontab Guru", @@ -96,8 +97,8 @@ "job_settings": "Ustawienia Zadań", "job_settings_description": "Zarządzaj współbieżnością zadań", "job_status": "Status Zadań", - "jobs_delayed": "{jobCount, plural, other {# oczekujących}}", - "jobs_failed": "{jobCount, plural, other {# nieudane}}", + "jobs_delayed": "{jobCount, plural, one {# oczekujący} few {# oczekujące} other {# oczekujących}}", + "jobs_failed": "{jobCount, plural, one {# nieudany} few {# nieudane} other {# nieudanych}}", "library_created": "Utworzono bibliotekę: {library}", "library_deleted": "Biblioteka usunięta", "library_import_path_description": "Określ folder do załadowania plików. Ten folder, łącznie z podfolderami, zostanie przeskanowany w poszukiwaniu obrazów i filmów.", @@ -348,6 +349,7 @@ "user_delete_delay_settings_description": "Liczba dni po usunięciu, po której następuje trwałe usunięcie konta użytkownika i zasobów. Zadanie usuwania użytkowników jest uruchamiane o północy w celu sprawdzenia, czy użytkownicy są gotowi do usunięcia. Zmiany tego ustawienia zostaną sprawdzone przy następnym wykonaniu.", "user_delete_immediately": "Konto {user} i powiązane zasoby zostaną zakolejkowane do natychmiastowego usunięcia.", "user_delete_immediately_checkbox": "Umieść użytkownika i zasoby w kolejce do natychmiastowego usunięcia", + "user_details": "Szczegóły Użytkownika", "user_management": "Zarządzenie Użytkownikami", "user_password_has_been_reset": "Hasło użytkownika zostało zresetowane:", "user_password_reset_description": "Proszę przekazać tymczasowe hasło użytkownikowi i poinformuj o konieczności jego zmiany przy najbliższym logowaniu.", @@ -369,7 +371,7 @@ "advanced": "Zaawansowane", "advanced_settings_enable_alternate_media_filter_subtitle": "Użyj tej opcji do filtrowania mediów podczas synchronizacji alternatywnych kryteriów. Używaj tylko wtedy gdy aplikacja ma problemy z wykrywaniem wszystkich albumów.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERYMENTALNE] Użyj alternatywnego filtra synchronizacji albumu", - "advanced_settings_log_level_title": "Poziom szczegółowości dziennika: {name}", + "advanced_settings_log_level_title": "Poziom szczegółowości dziennika: {level}", "advanced_settings_prefer_remote_subtitle": "Niektóre urządzenia bardzo wolno ładują miniatury z zasobów na urządzeniu. Aktywuj to ustawienie, aby ładować zdalne obrazy.", "advanced_settings_prefer_remote_title": "Preferuj obrazy zdalne", "advanced_settings_proxy_headers_subtitle": "Zdefiniuj nagłówki proxy, które Immich powinien wysyłać z każdym żądaniem sieciowym", @@ -400,9 +402,9 @@ "album_remove_user_confirmation": "Na pewno chcesz usunąć {user}?", "album_share_no_users": "Wygląda na to, że ten album albo udostępniono wszystkim użytkownikom, albo nie ma komu go udostępnić.", "album_thumbnail_card_item": "1 pozycja", - "album_thumbnail_card_items": "{album_thumbnail_card_items} pozycje", + "album_thumbnail_card_items": "{count} plików", "album_thumbnail_card_shared": " · Udostępniony", - "album_thumbnail_shared_by": "Udostępnione przez {album_thumbnail_shared_by}", + "album_thumbnail_shared_by": "Udostępnione przez {user}", "album_updated": "Album zaktualizowany", "album_updated_setting_description": "Otrzymaj powiadomienie e-mail, gdy do udostępnionego Ci albumu zostaną dodane nowe zasoby", "album_user_left": "Opuszczono {album}", @@ -440,7 +442,7 @@ "archive": "Archiwum", "archive_or_unarchive_photo": "Dodaj lub usuń zasób z archiwum", "archive_page_no_archived_assets": "Nie znaleziono zarchiwizowanych zasobów", - "archive_page_title": "Archiwum ({archive_page_title})", + "archive_page_title": "Archiwum {count}", "archive_size": "Rozmiar archiwum", "archive_size_description": "Podziel pobierane pliki na więcej niż jedno archiwum, jeżeli rozmiar archiwum przekroczy tę wartość w GiB", "archived": "Zarchiwizowane", @@ -497,7 +499,7 @@ "back_close_deselect": "Wróć, zamknij lub odznacz", "background_location_permission": "Uprawnienia do lokalizacji w tle", "background_location_permission_content": "Aby móc przełączać sieć podczas pracy w tle, Immich musi *zawsze* mieć dostęp do dokładnej lokalizacji, aby aplikacja mogła odczytać nazwę sieci Wi-Fi", - "backup_album_selection_page_albums_device": "Albumy na urządzeniu ({number})", + "backup_album_selection_page_albums_device": "Albumy na urządzeniu ({count})", "backup_album_selection_page_albums_tap": "Stuknij, aby włączyć, stuknij dwukrotnie, aby wykluczyć", "backup_album_selection_page_assets_scatter": "Pliki mogą być rozproszone w wielu albumach. Dzięki temu albumy mogą być włączane lub wyłączane podczas procesu tworzenia kopii zapasowej.", "backup_album_selection_page_select_albums": "Zaznacz albumy", @@ -506,11 +508,11 @@ "backup_all": "Wszystkie", "backup_background_service_backup_failed_message": "Nie udało się wykonać kopii zapasowej zasobów. Ponowna próba…", "backup_background_service_connection_failed_message": "Nie udało się połączyć z serwerem. Ponowna próba…", - "backup_background_service_current_upload_notification": "Wysyłanie {backup_background_service_current_upload_notification}", + "backup_background_service_current_upload_notification": "Wysyłanie {filename}", "backup_background_service_default_notification": "Sprawdzanie nowych zasobów…", "backup_background_service_error_title": "Błąd kopii zapasowej", "backup_background_service_in_progress_notification": "Tworzenie kopii zapasowej twoich zasobów…", - "backup_background_service_upload_failure_notification": "Błąd przesyłania {backup_background_service_upload_failure_notification}", + "backup_background_service_upload_failure_notification": "Błąd przesyłania {filename}", "backup_controller_page_albums": "Kopia Zapasowa albumów", "backup_controller_page_background_app_refresh_disabled_content": "Włącz odświeżanie aplikacji w tle w Ustawienia > Ogólne > Odświeżanie aplikacji w tle, aby móc korzystać z kopii zapasowej w tle.", "backup_controller_page_background_app_refresh_disabled_title": "Odświeżanie aplikacji w tle wyłączone", @@ -521,7 +523,7 @@ "backup_controller_page_background_battery_info_title": "Optymalizacja Baterii", "backup_controller_page_background_charging": "Tylko podczas ładowania", "backup_controller_page_background_configure_error": "Nie udało się skonfigurować usługi w tle", - "backup_controller_page_background_delay": "Opóźnienie tworzenia kopii zapasowych nowych zasobów: {backup_controller_page_background_delay}", + "backup_controller_page_background_delay": "Opóźnienie tworzenia kopii zapasowych nowych zasobów: {duration}", "backup_controller_page_background_description": "Włącz usługę w tle, aby automatycznie tworzyć kopie zapasowe wszelkich nowych zasobów bez konieczności otwierania aplikacji", "backup_controller_page_background_is_off": "Automatyczna kopia zapasowa w tle jest wyłączona", "backup_controller_page_background_is_on": "Automatyczna kopia zapasowa w tle jest włączona", @@ -531,12 +533,12 @@ "backup_controller_page_backup": "Kopia Zapasowa", "backup_controller_page_backup_selected": "Zaznaczone: ", "backup_controller_page_backup_sub": "Tworzenie kopii zapasowych zdjęć i filmów", - "backup_controller_page_created": "Utworzono dnia: {backup_controller_page_created}", + "backup_controller_page_created": "Utworzono dnia: {date}", "backup_controller_page_desc_backup": "Włącz kopię zapasową, aby automatycznie przesyłać nowe zasoby na serwer.", "backup_controller_page_excluded": "Wykluczone: ", - "backup_controller_page_failed": "Nieudane ({backup_controller_page_failed})", - "backup_controller_page_filename": "Nazwa pliku: {backup_controller_page_filename} [{backup_controller_page_filename}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Nieudane ({count})", + "backup_controller_page_filename": "Nazwa pliku: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informacje o kopii zapasowej", "backup_controller_page_none_selected": "Brak wybranych", "backup_controller_page_remainder": "Reszta", @@ -545,7 +547,7 @@ "backup_controller_page_start_backup": "Rozpocznij Kopię Zapasową", "backup_controller_page_status_off": "Kopia Zapasowa jest wyłaczona", "backup_controller_page_status_on": "Kopia Zapasowa jest włączona", - "backup_controller_page_storage_format": "{used} z {available} wykorzystanych", + "backup_controller_page_storage_format": "{used} z {total} wykorzystanych", "backup_controller_page_to_backup": "Albumy z Kopią Zapasową", "backup_controller_page_total_sub": "Wszystkie unikalne zdjęcia i filmy z wybranych albumów", "backup_controller_page_turn_off": "Wyłącz Kopię Zapasową", @@ -575,10 +577,10 @@ "cache_settings_clear_cache_button_title": "Czyści pamięć podręczną aplikacji. Wpłynie to znacząco na wydajność aplikacji, dopóki pamięć podręczna nie zostanie odbudowana.", "cache_settings_duplicated_assets_clear_button": "WYCZYŚĆ", "cache_settings_duplicated_assets_subtitle": "Zdjęcia i filmy umieszczone na czarnej liście aplikacji", - "cache_settings_duplicated_assets_title": "Zduplikowane zasoby ({number})", + "cache_settings_duplicated_assets_title": "Zduplikowane zasoby ({count})", "cache_settings_image_cache_size": "Rozmiar pamięci podręcznej obrazów ({count, plural, one {# zasób} few {# zasoby} other {# zasobów}})", "cache_settings_statistics_album": "Biblioteka miniatur", - "cache_settings_statistics_assets": "{} zasoby ({})", + "cache_settings_statistics_assets": "{count} zasoby ({size})", "cache_settings_statistics_full": "Pełne Zdjęcia", "cache_settings_statistics_shared": "Udostępnione miniatury albumów", "cache_settings_statistics_thumbnail": "Miniatury", @@ -610,6 +612,7 @@ "change_password_form_new_password": "Nowe Hasło", "change_password_form_password_mismatch": "Hasła nie są zgodne", "change_password_form_reenter_new_password": "Wprowadź ponownie Nowe Hasło", + "change_pin_code": "Zmień kod PIN", "change_your_password": "Zmień swoje hasło", "changed_visibility_successfully": "Pomyślnie zmieniono widoczność", "check_all": "Zaznacz wszystko", @@ -650,6 +653,7 @@ "confirm_delete_face": "Czy na pewno chcesz usunąć twarz {name} z zasobów?", "confirm_delete_shared_link": "Czy na pewno chcesz usunąć ten udostępniony link?", "confirm_keep_this_delete_others": "Wszystkie inne zasoby zostaną usunięte poza tym zasobem. Czy jesteś pewien, że chcesz kontynuować?", + "confirm_new_pin_code": "Potwierdź nowy kod PIN", "confirm_password": "Potwierdź hasło", "contain": "Zawiera", "context": "Kontekst", @@ -692,9 +696,11 @@ "create_tag_description": "Stwórz nową etykietę. Dla etykiet zagnieżdżonych, wprowadź pełną ścieżkę etykiety zawierającą ukośniki.", "create_user": "Stwórz użytkownika", "created": "Utworzono", + "created_at": "Utworzony", "crop": "Przytnij", "curated_object_page_title": "Rzeczy", "current_device": "Obecne urządzenie", + "current_pin_code": "Aktualny kod PIN", "current_server_address": "Aktualny adres serwera", "custom_locale": "Niestandardowy Region", "custom_locale_description": "Formatuj daty i liczby na podstawie języka i regionu", @@ -763,7 +769,7 @@ "download_enqueue": "Pobieranie w kolejce", "download_error": "Błąd pobierania", "download_failed": "Pobieranie nieudane", - "download_filename": "plik: {}", + "download_filename": "plik: {filename}", "download_finished": "Pobieranie zakończone", "download_include_embedded_motion_videos": "Pobierz filmy ruchomych zdjęć", "download_include_embedded_motion_videos_description": "Dołącz filmy osadzone w ruchomych zdjęciach jako oddzielny plik", @@ -807,6 +813,7 @@ "editor_crop_tool_h2_aspect_ratios": "Proporcje obrazu", "editor_crop_tool_h2_rotation": "Obrót", "email": "E-mail", + "email_notifications": "Powiadomienia e-mail", "empty_folder": "Ten folder jest pusty", "empty_trash": "Opróżnij kosz", "empty_trash_confirmation": "Czy na pewno chcesz opróżnić kosz? Spowoduje to trwałe usunięcie wszystkich zasobów znajdujących się w koszu z Immich.\nNie można cofnąć tej operacji!", @@ -819,7 +826,7 @@ "error_change_sort_album": "Nie udało się zmienić kolejności sortowania albumów", "error_delete_face": "Wystąpił błąd podczas usuwania twarzy z zasobów", "error_loading_image": "Błąd podczas ładowania zdjęcia", - "error_saving_image": "Błąd: {}", + "error_saving_image": "Błąd: {error}", "error_title": "Błąd - Coś poszło nie tak", "errors": { "cannot_navigate_next_asset": "Nie można przejść do następnego zasobu", @@ -921,7 +928,8 @@ "unable_to_remove_partner": "Nie można usunąć partnerów", "unable_to_remove_reaction": "Usunięcie reakcji nie powiodło się", "unable_to_repair_items": "Naprawianie elementów nie powiodło się", - "unable_to_reset_password": "Nie można resetować hasła", + "unable_to_reset_password": "Zresetowanie hasła nie powiodło się", + "unable_to_reset_pin_code": "Zresetowanie kodu PIN nie powiodło się", "unable_to_resolve_duplicate": "Usuwanie duplikatów nie powiodło się", "unable_to_restore_assets": "Przywracanie zasobów nie powiodło się", "unable_to_restore_trash": "Przywracanie zasobów z kosza nie powiodło się", @@ -955,10 +963,10 @@ "exif_bottom_sheet_location": "LOKALIZACJA", "exif_bottom_sheet_people": "LUDZIE", "exif_bottom_sheet_person_add_person": "Dodaj nazwę", - "exif_bottom_sheet_person_age": "Wiek {count, plural, one {# rok} few {# lata} other {# lat}}", + "exif_bottom_sheet_person_age": "Wiek {age}", "exif_bottom_sheet_person_age_months": "Wiek {months, plural, one {# miesiąc} few {# miesiące} other {# miesięcy}}", "exif_bottom_sheet_person_age_year_months": "Wiek 1 rok, {months, plural, one {# miesiąc} few {# miesiące} other {# miesięcy}}", - "exif_bottom_sheet_person_age_years": "Wiek {years, plural, one {# rok} few {# lata} other {# lat}}", + "exif_bottom_sheet_person_age_years": "Wiek {years, plural, few {# lata} other {# lat}}", "exit_slideshow": "Zamknij Pokaz Slajdów", "expand_all": "Rozwiń wszystko", "experimental_settings_new_asset_list_subtitle": "Praca w toku", @@ -1048,6 +1056,7 @@ "home_page_upload_err_limit": "Można przesłać maksymalnie 30 zasobów jednocześnie, pomijanie", "host": "Host", "hour": "Godzina", + "id": "ID", "ignore_icloud_photos": "Ignoruj zdjęcia w iCloud", "ignore_icloud_photos_description": "Zdjęcia przechowywane w usłudze iCloud nie zostaną przesłane na serwer Immich", "image": "Zdjęcie", @@ -1173,8 +1182,8 @@ "manage_your_devices": "Zarządzaj swoimi zalogowanymi urządzeniami", "manage_your_oauth_connection": "Zarządzaj swoim połączeniem OAuth", "map": "Mapa", - "map_assets_in_bound": "{count, plural, one {# zdjęcie}}", - "map_assets_in_bounds": "{count, plural, one {# zdjęcie} few {# zdjęcia} other {# zdjęć}}", + "map_assets_in_bound": "{count} zdjęcie", + "map_assets_in_bounds": "{count, plural, few {# zdjęcia} other {# zdjęć}}", "map_cannot_get_user_location": "Nie można uzyskać lokalizacji użytkownika", "map_location_dialog_yes": "Tak", "map_location_picker_page_use_location": "Użyj tej lokalizacji", @@ -1188,9 +1197,9 @@ "map_settings": "Ustawienia mapy", "map_settings_dark_mode": "Tryb ciemny", "map_settings_date_range_option_day": "Ostatnie 24 godziny", - "map_settings_date_range_option_days": "{count, plural, one {Poprzedni dzień} other {Minione # dni}}", - "map_settings_date_range_option_year": "Poprzedni rok", - "map_settings_date_range_option_years": "{count, plural, one {Poprzedni rok} few {Minione # lata} other {Minione # lat}}", + "map_settings_date_range_option_days": "Minione {days} dni", + "map_settings_date_range_option_year": "Miniony rok", + "map_settings_date_range_option_years": "Minione {count, plural, few {# lata} other {# lat}}", "map_settings_dialog_title": "Ustawienia mapy", "map_settings_include_show_archived": "Uwzględnij zarchiwizowane", "map_settings_include_show_partners": "Uwzględnij partnerów", @@ -1209,7 +1218,7 @@ "memories_start_over": "Zacznij od nowa", "memories_swipe_to_close": "Przesuń w górę, aby zamknąć", "memories_year_ago": "Rok temu", - "memories_years_ago": "{count, plural, few {# lata temu} other {# lat temu}}", + "memories_years_ago": "{years, plural, one {rok temu} few {# lata temu} other {# lat temu}}", "memory": "Pamięć", "memory_lane_title": "Aleja Wspomnień {title}", "menu": "Menu", @@ -1218,7 +1227,7 @@ "merge_people_limit": "Możesz łączyć maksymalnie 5 twarzy naraz", "merge_people_prompt": "Czy chcesz połączyć te osoby? Ta czynność jest nieodwracalna.", "merge_people_successfully": "Pomyślnie złączono osoby", - "merged_people_count": "Połączono {count, plural, one {# osobę} other {# osób}}", + "merged_people_count": "Połączono {count, plural, one {# osobę} few {# osoby} other {# osób}}", "minimize": "Zminimalizuj", "minute": "Minuta", "missing": "Brakujące", @@ -1242,6 +1251,7 @@ "new_api_key": "Nowy Klucz API", "new_password": "Nowe hasło", "new_person": "Nowa osoba", + "new_pin_code": "Nowy kod PIN", "new_user_created": "Pomyślnie stworzono nowego użytkownika", "new_version_available": "NOWA WERSJA DOSTĘPNA", "newest_first": "Od najnowszych", @@ -1316,7 +1326,7 @@ "partner_page_partner_add_failed": "Nie udało się dodać partnera", "partner_page_select_partner": "Wybierz partnera", "partner_page_shared_to_title": "Udostępniono", - "partner_page_stop_sharing_content": "{user} nie będzie już mieć dostępu do twoich zdjęć.", + "partner_page_stop_sharing_content": "{partner} nie będzie już mieć dostępu do twoich zdjęć.", "partner_sharing": "Dzielenie z Partnerami", "partners": "Partnerzy", "password": "Hasło", @@ -1362,6 +1372,9 @@ "photos_count": "{count, plural, one {{count, number} Zdjęcie} few {{count, number} Zdjęcia} other {{count, number} Zdjęć}}", "photos_from_previous_years": "Zdjęcia z ubiegłych lat", "pick_a_location": "Oznacz lokalizację", + "pin_code_changed_successfully": "Pomyślnie zmieniono kod PIN", + "pin_code_reset_successfully": "Pomyślnie zresetowano kod PIN", + "pin_code_setup_successfully": "Pomyślnie ustawiono kod PIN", "place": "Miejsce", "places": "Miejsca", "places_count": "{count, plural, one {{count, number} Miejsce} few {{count, number} Miejsca}other {{count, number} Miejsc}}", @@ -1379,6 +1392,7 @@ "previous_or_next_photo": "Poprzednie lub następne zdjęcie", "primary": "Główny", "privacy": "Prywatność", + "profile": "Profil", "profile_drawer_app_logs": "Logi", "profile_drawer_client_out_of_date_major": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej wersji głównej.", "profile_drawer_client_out_of_date_minor": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej wersji dodatkowej.", @@ -1392,7 +1406,7 @@ "public_share": "Udostępnienie publiczne", "purchase_account_info": "Wspierający", "purchase_activated_subtitle": "Dziękuję za wspieranie Immich i oprogramowania open-source", - "purchase_activated_time": "Aktywowane dnia {date, date}", + "purchase_activated_time": "Aktywowane dnia {date}", "purchase_activated_title": "Twój klucz został pomyślnie aktywowany", "purchase_button_activate": "Aktywuj", "purchase_button_buy": "Kup", @@ -1481,6 +1495,7 @@ "reset": "Reset", "reset_password": "Resetuj hasło", "reset_people_visibility": "Zresetuj widoczność osób", + "reset_pin_code": "Zresetuj kod PIN", "reset_to_default": "Przywróć ustawienia domyślne", "resolve_duplicates": "Rozwiąż problemy z duplikatami", "resolved_all_duplicates": "Rozwiązano wszystkie duplikaty", @@ -1604,7 +1619,7 @@ "setting_languages_apply": "Zastosuj", "setting_languages_subtitle": "Zmień język aplikacji", "setting_languages_title": "Języki", - "setting_notifications_notify_failures_grace_period": "Powiadomienie o awariach kopii zapasowych w tle: {}", + "setting_notifications_notify_failures_grace_period": "Powiadomienie o awariach kopii zapasowych w tle: {duration}", "setting_notifications_notify_hours": "{count, plural, one {# godzina} few {# godziny} other {# godzin}}", "setting_notifications_notify_immediately": "natychmiast", "setting_notifications_notify_minutes": "{count, plural, one {# minuta} few {# minuty} other {# minut}}", @@ -1621,9 +1636,10 @@ "settings": "Ustawienia", "settings_require_restart": "Aby zastosować to ustawienie, uruchom ponownie Immich", "settings_saved": "Ustawienia zapisane", + "setup_pin_code": "Ustaw kod PIN", "share": "Udostępnij", "share_add_photos": "Dodaj zdjęcia", - "share_assets_selected": "{count, plural, one {# wybrane} few {# wybrane} other {# wybranych}}", + "share_assets_selected": "Wybrano {count}", "share_dialog_preparing": "Przygotowywanie…", "shared": "Udostępnione", "shared_album_activities_input_disable": "Komentarz jest wyłączony", @@ -1637,31 +1653,31 @@ "shared_by_user": "Udostępnione przez {user}", "shared_by_you": "Udostępnione przez ciebie", "shared_from_partner": "Zdjęcia od {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Przesłano", + "shared_intent_upload_button_progress_text": "{current} / {total} Przesłano", "shared_link_app_bar_title": "Udostępnione linki", "shared_link_clipboard_copied_massage": "Skopiowane do schowka", - "shared_link_clipboard_text": "Link: {}\nHasło: {}", + "shared_link_clipboard_text": "Link: {link}\nHasło: {password}", "shared_link_create_error": "Błąd podczas tworzenia linka do udostępnienia", "shared_link_edit_description_hint": "Wprowadź opis udostępnienia", "shared_link_edit_expire_after_option_day": "1 dniu", - "shared_link_edit_expire_after_option_days": "{count, plural, one {# dniu} other {# dniach}}", + "shared_link_edit_expire_after_option_days": "{count} dniach", "shared_link_edit_expire_after_option_hour": "1 godzinie", - "shared_link_edit_expire_after_option_hours": "{count, plural, one {# godzinie} other {# godzinach}}", + "shared_link_edit_expire_after_option_hours": "{count} godzinach", "shared_link_edit_expire_after_option_minute": "1 minucie", - "shared_link_edit_expire_after_option_minutes": "{count, plural, one {# minucie} other {# minutach}}", - "shared_link_edit_expire_after_option_months": "{count, plural, one {# miesiącu} other {# miesiącach}}", + "shared_link_edit_expire_after_option_minutes": "{count} minutach", + "shared_link_edit_expire_after_option_months": "{count} miesiącach", "shared_link_edit_expire_after_option_year": "{count, plural, one {# roku} other {# latach}}", "shared_link_edit_password_hint": "Wprowadź hasło udostępniania", "shared_link_edit_submit_button": "Aktualizuj link", "shared_link_error_server_url_fetch": "Nie można pobrać adresu URL serwera", - "shared_link_expires_day": "Wygasa za {count, plural, one {# dzień}}", - "shared_link_expires_days": "Wygasa za {count, plural, one {# dzień} other {# dni}}", - "shared_link_expires_hour": "Wygasa za {count, plural, one {# godzinę}}", + "shared_link_expires_day": "Wygasa za {count} dzień", + "shared_link_expires_days": "Wygasa za {count} dni", + "shared_link_expires_hour": "Wygasa za {count} godzinę", "shared_link_expires_hours": "Wygasa za {count, plural, one {# godzinę} few {# godziny} other {# godzin}}", - "shared_link_expires_minute": "Wygasa za {count, plural, one {# minutę}}", + "shared_link_expires_minute": "Wygasa za {count} minutę", "shared_link_expires_minutes": "Wygasa za {count, plural, one {# minutę} few {# minuty} other {# minut}}", "shared_link_expires_never": "Wygasa ∞", - "shared_link_expires_second": "Wygasa za {count, plural, one {# sekundę}}", + "shared_link_expires_second": "Wygasa za {count} sekundę", "shared_link_expires_seconds": "Wygasa za {count, plural, one {# sekundę} few {# sekundy} other {# sekund}}", "shared_link_individual_shared": "Indywidualnie udostępnione", "shared_link_info_chip_metadata": "EXIF", @@ -1737,6 +1753,7 @@ "stop_sharing_photos_with_user": "Przestań udostępniać zdjęcia temu użytkownikowi", "storage": "Przestrzeń dyskowa", "storage_label": "Etykieta magazynu", + "storage_quota": "Limit pamięci", "storage_usage": "{used} z {available} użyte", "submit": "Zatwierdź", "suggestions": "Sugestie", @@ -1763,7 +1780,7 @@ "theme_selection": "Wybór motywu", "theme_selection_description": "Automatycznie zmień motyw na jasny lub ciemny zależnie od ustawień przeglądarki", "theme_setting_asset_list_storage_indicator_title": "Pokaż wskaźnik przechowywania na kafelkach zasobów", - "theme_setting_asset_list_tiles_per_row_title": "Liczba zasobów w wierszu ({})", + "theme_setting_asset_list_tiles_per_row_title": "Liczba zasobów w wierszu ({count})", "theme_setting_colorful_interface_subtitle": "Zastosuj kolor podstawowy do powierzchni tła.", "theme_setting_colorful_interface_title": "Kolorowy interfejs", "theme_setting_image_viewer_quality_subtitle": "Dostosuj jakość podglądu szczegółowości", @@ -1783,7 +1800,7 @@ "to_archive": "Archiwum", "to_change_password": "Zmień hasło", "to_favorite": "Dodaj do ulubionych", - "to_login": "Login", + "to_login": "Logowanie", "to_parent": "Idź do rodzica", "to_trash": "Kosz", "toggle_settings": "Przełącz ustawienia", @@ -1791,22 +1808,24 @@ "total": "Całkowity", "total_usage": "Całkowite wykorzystanie", "trash": "Kosz", - "trash_all": "Usuń wszystko", + "trash_all": "Usuń wszystkie", "trash_count": "Kosz {count, number}", "trash_delete_asset": "Kosz/Usuń zasób", "trash_emptied": "Opróżnione śmieci", "trash_no_results_message": "Tu znajdziesz wyrzucone zdjęcia i filmy.", "trash_page_delete_all": "Usuń wszystko", "trash_page_empty_trash_dialog_content": "Czy chcesz opróżnić swoje usunięte zasoby? Przedmioty te zostaną trwale usunięte z Immich", - "trash_page_info": "Elementy przeniesione do kosza zostaną trwale usunięte po {count, plural, one {# dniu} other {# dniach}}", + "trash_page_info": "Elementy przeniesione do kosza zostaną trwale usunięte po {days, plural, one {# dniu} other {# dniach}}", "trash_page_no_assets": "Brak usuniętych zasobów", "trash_page_restore_all": "Przywrócić wszystkie", "trash_page_select_assets_btn": "Wybierz zasoby", - "trash_page_title": "Kosz ({})", - "trashed_items_will_be_permanently_deleted_after": "Wyrzucone zasoby zostaną trwale usunięte po {days, plural, one {jednym dniu} other {{days, number} dniach}}.", + "trash_page_title": "Kosz ({count})", + "trashed_items_will_be_permanently_deleted_after": "Wyrzucone zasoby zostaną trwale usunięte po {days, plural, one {jednym dniu} other {# dniach}}.", "type": "Typ", + "unable_to_change_pin_code": "Nie można zmienić kodu PIN", + "unable_to_setup_pin_code": "Nie można ustawić kodu PIN", "unarchive": "Cofnij archiwizację", - "unarchived_count": "{count, plural, other {Niezarchiwizowane #}}", + "unarchived_count": "{count, plural, one {# cofnięta archiwizacja} few {# cofnięte archiwizacje} other {# cofniętych archiwizacji}}", "unfavorite": "Usuń z ulubionych", "unhide_person": "Przywróć osobę", "unknown": "Nieznany", @@ -1823,32 +1842,36 @@ "unsaved_change": "Niezapisana zmiana", "unselect_all": "Odznacz wszystko", "unselect_all_duplicates": "Odznacz wszystkie duplikaty", - "unstack": "Usuń stos", - "unstacked_assets_count": "Nieułożone {count, plural, one {# zasób} other{# zasoby}}", + "unstack": "Rozłóż stos", + "unstacked_assets_count": "{count, plural, one {Rozłożony # zasób} few {Rozłożone # zasoby} other {Rozłożonych # zasobów}}", "untracked_files": "Nieśledzone pliki", "untracked_files_decription": "Pliki te nie są śledzone przez aplikację. Mogą być wynikiem nieudanych przeniesień, przerwanego przesyłania lub pozostawienia z powodu błędu", "up_next": "Do następnego", + "updated_at": "Zaktualizowany", "updated_password": "Pomyślnie zaktualizowano hasło", "upload": "Prześlij", "upload_concurrency": "Współbieżność wysyłania", "upload_dialog_info": "Czy chcesz wykonać kopię zapasową wybranych zasobów na serwerze?", "upload_dialog_title": "Prześlij Zasób", - "upload_errors": "Przesyłanie zakończone z {count, plural, one {# błąd} other {# błędy}}. Odśwież stronę, aby zobaczyć nowo przesłane zasoby.", + "upload_errors": "Przesyłanie zakończone z {count, plural, one {# błędem} other {# błędami}}. Odśwież stronę, aby zobaczyć nowo przesłane zasoby.", "upload_progress": "Pozostałe {remaining, number} - Przetworzone {processed, number}/{total, number}", - "upload_skipped_duplicates": "Pominięte {count, plural, one {# zduplikowany zasób} other {# zduplikowane zasoby}}", + "upload_skipped_duplicates": "Pominięto {count, plural, one {# zduplikowany zasób} few {# zduplikowane zasoby} other {# zduplikowanych zasobów}}", "upload_status_duplicates": "Duplikaty", "upload_status_errors": "Błędy", "upload_status_uploaded": "Przesłano", "upload_success": "Przesyłanie powiodło się, odśwież stronę, aby zobaczyć nowo przesłane zasoby.", - "upload_to_immich": "Prześlij do Immich ({})", + "upload_to_immich": "Prześlij do Immich ({count})", "uploading": "Przesyłanie", "url": "URL", "usage": "Użycie", "use_current_connection": "użyj bieżącego połączenia", "use_custom_date_range": "Zamiast tego użyj niestandardowego zakresu dat", "user": "Użytkownik", + "user_has_been_deleted": "Ten użytkownik został usunięty.", "user_id": "ID użytkownika", "user_liked": "{user} polubił {type, select, photo {to zdjęcie} video {to wideo} asset {ten zasób} other {to}}", + "user_pin_code_settings": "Kod PIN", + "user_pin_code_settings_description": "Zarządzaj swoim kodem PIN", "user_purchase_settings": "Zakup", "user_purchase_settings_description": "Zarządzaj swoim zakupem", "user_role_set": "Ustaw {user} jako {role}", @@ -1863,7 +1886,7 @@ "variables": "Zmienne", "version": "Wersja", "version_announcement_closing": "Twój przyjaciel Aleks", - "version_announcement_message": "Witaj! Dostępna jest nowa wersja Immich. Poświęć trochę czasu na zapoznanie się z informacjami o wydaniu, aby upewnić się, że twoja konfiguracja jest aktualna, aby uniknąć błędów, szczególnie jeśli używasz WatchTower lub jakiegokolwiek mechanizmu odpowiedzialnego za automatyczne aktualizowanie Immich.", + "version_announcement_message": "Witaj! Dostępna jest nowa wersja Immich. Poświęć trochę czasu na zapoznanie się z informacjami o wydaniu, aby upewnić się, że ustawienia twojej instalacji są aktualne i zapobiec błędnym konfiguracjom. Szczególnie jeśli używasz WatchTower lub jakiegokolwiek mechanizmu odpowiedzialnego za automatyczne aktualizowanie Immich.", "version_announcement_overlay_release_notes": "informacje o wydaniu", "version_announcement_overlay_text_1": "Cześć przyjacielu, jest nowe wydanie", "version_announcement_overlay_text_2": "prosimy o poświęcenie czasu na odwiedzenie ", @@ -1890,8 +1913,8 @@ "view_stack": "Zobacz Ułożenie", "viewer_remove_from_stack": "Usuń ze stosu", "viewer_stack_use_as_main_asset": "Użyj jako głównego zasobu", - "viewer_unstack": "Usuń stos", - "visibility_changed": "Zmieniono widoczność dla {count, plural, one {# osoba} other {# osoby}}", + "viewer_unstack": "Rozłóż Stos", + "visibility_changed": "Zmieniono widoczność dla {count, plural, one {# osoby} other {# osób}}", "waiting": "Oczekiwanie", "warning": "Ostrzeżenie", "week": "Tydzień", diff --git a/i18n/pt.json b/i18n/pt.json index 5f97f65a25..690eca2e5f 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -53,6 +53,7 @@ "confirm_email_below": "Para confirmar, escreva \"{email}\" abaixo", "confirm_reprocess_all_faces": "Tem a certeza de que deseja reprocessar todos os rostos? Isto também limpará os nomes das pessoas.", "confirm_user_password_reset": "Tem a certeza de que deseja redefinir a palavra-passe de {user}?", + "confirm_user_pin_code_reset": "Tem a certeza de que quer repor o código PIN de {user}?", "create_job": "Criar tarefa", "cron_expression": "Expressão Cron", "cron_expression_description": "Definir o intervalo de análise utilizando o formato Cron. Para mais informações, por favor veja o Crontab Guru", @@ -192,6 +193,7 @@ "oauth_auto_register": "Registo automático", "oauth_auto_register_description": "Registar automaticamente novos utilizadores após iniciarem sessão com o OAuth", "oauth_button_text": "Texto do botão", + "oauth_client_secret_description": "Obrigatório se PKCE (Proof Key for Code Exchange) não for suportado pelo provedor OAuth", "oauth_enable_description": "Iniciar sessão com o OAuth", "oauth_mobile_redirect_uri": "URI de redirecionamento móvel", "oauth_mobile_redirect_uri_override": "Substituição de URI de redirecionamento móvel", @@ -205,6 +207,8 @@ "oauth_storage_quota_claim_description": "Definir automaticamente a quota de armazenamento do utilizador para o valor desta declaração.", "oauth_storage_quota_default": "Quota de armazenamento padrão (GiB)", "oauth_storage_quota_default_description": "Quota em GiB a ser usada quando nenhuma reivindicação for fornecida (insira 0 para quota ilimitada).", + "oauth_timeout": "Tempo Limite de Requisição", + "oauth_timeout_description": "Tempo limite para requisições, em milissegundos", "offline_paths": "Caminhos Offline", "offline_paths_description": "Estes resultados podem ser devidos à eliminação manual de ficheiros que não fazem parte de uma biblioteca externa.", "password_enable_description": "Iniciar sessão com e-mail e palavra-passe", @@ -345,6 +349,7 @@ "user_delete_delay_settings_description": "Número de dias após a remoção para excluir permanentemente a conta e os ficheiros de um utilizador. A tarefa de eliminação de utilizadores é executada à meia-noite para verificar utilizadores que estão prontos para eliminação. As alterações a esta definição serão avaliadas na próxima execução.", "user_delete_immediately": "A conta e os ficheiros de {user} serão colocados em fila para eliminação permanente de imediato.", "user_delete_immediately_checkbox": "Adicionar utilizador e ficheiros à fila para eliminação imediata", + "user_details": "Detalhes do utilizador", "user_management": "Gestão de utilizadores", "user_password_has_been_reset": "A palavra-passe do utilizador foi redefinida:", "user_password_reset_description": "Por favor forneça a palavra-passe temporária ao utilizador e informe-o(a) de que será necessário alterá-la próximo início de sessão.", @@ -366,7 +371,7 @@ "advanced": "Avançado", "advanced_settings_enable_alternate_media_filter_subtitle": "Utilize esta definição para filtrar ficheiros durante a sincronização baseada em critérios alternativos. Utilize apenas se a aplicação estiver com problemas a detetar todos os álbuns.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Utilizar um filtro alternativo de sincronização de álbuns em dispositivos", - "advanced_settings_log_level_title": "Nível de registo: {}", + "advanced_settings_log_level_title": "Nível de registo: {level}", "advanced_settings_prefer_remote_subtitle": "Alguns dispositivos são extremamente lentos para carregar miniaturas da memória. Ative esta opção para preferir imagens do servidor.", "advanced_settings_prefer_remote_title": "Preferir imagens do servidor", "advanced_settings_proxy_headers_subtitle": "Defina os cabeçalhos do proxy que o Immich deve enviar em todas comunicações com a rede", @@ -397,9 +402,9 @@ "album_remove_user_confirmation": "Tem a certeza de que quer remover {user}?", "album_share_no_users": "Parece que tem este álbum partilhado com todos os utilizadores ou que não existem utilizadores com quem o partilhar.", "album_thumbnail_card_item": "1 arquivo", - "album_thumbnail_card_items": "{} ficheiros", + "album_thumbnail_card_items": "{count} ficheiros", "album_thumbnail_card_shared": " · Compartilhado", - "album_thumbnail_shared_by": "Partilhado por {}", + "album_thumbnail_shared_by": "Partilhado por {user}", "album_updated": "Álbum atualizado", "album_updated_setting_description": "Receber uma notificação por e-mail quando um álbum partilhado tiver novos ficheiros", "album_user_left": "Saíu do {album}", @@ -437,7 +442,7 @@ "archive": "Arquivo", "archive_or_unarchive_photo": "Arquivar ou desarquivar foto", "archive_page_no_archived_assets": "Nenhum arquivo encontrado", - "archive_page_title": "Arquivo ({})", + "archive_page_title": "Arquivo ({count})", "archive_size": "Tamanho do arquivo", "archive_size_description": "Configure o tamanho do arquivo para transferências (em GiB)", "archived": "Arquivado", @@ -474,18 +479,18 @@ "assets_added_to_album_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}} ao álbum", "assets_added_to_name_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}} a {hasName, select, true {{name}} other {novo álbum}}", "assets_count": "{count, plural, one {# ficheiro} other {# ficheiros}}", - "assets_deleted_permanently": "{} ficheiro(s) eliminado(s) permanentemente", - "assets_deleted_permanently_from_server": "{} ficheiro(s) eliminado(s) permanentemente do servidor Immich", + "assets_deleted_permanently": "{count} ficheiro(s) eliminado(s) permanentemente", + "assets_deleted_permanently_from_server": "{count} ficheiro(s) eliminado(s) permanentemente do servidor Immich", "assets_moved_to_trash_count": "{count, plural, one {# ficheiro movido} other {# ficheiros movidos}} para a reciclagem", "assets_permanently_deleted_count": "{count, plural, one {# ficheiro} other {# ficheiros}} eliminados permanentemente", "assets_removed_count": "{count, plural, one {# ficheiro eliminado} other {# ficheiros eliminados}}", - "assets_removed_permanently_from_device": "{} ficheiro(s) removido(s) permanentemente do seu dispositivo", + "assets_removed_permanently_from_device": "{count} ficheiro(s) removido(s) permanentemente do seu dispositivo", "assets_restore_confirmation": "Tem a certeza de que quer recuperar todos os ficheiros apagados? Não é possível anular esta ação! Tenha em conta de que quaisquer ficheiros indisponíveis não podem ser restaurados desta forma.", "assets_restored_count": "{count, plural, one {# ficheiro restaurado} other {# ficheiros restaurados}}", - "assets_restored_successfully": "{} ficheiro(s) restaurados com sucesso", - "assets_trashed": "{} ficheiro(s) enviado(s) para a reciclagem", + "assets_restored_successfully": "{count} ficheiro(s) restaurados com sucesso", + "assets_trashed": "{count} ficheiro(s) enviado(s) para a reciclagem", "assets_trashed_count": "{count, plural, one {# ficheiro enviado} other {# ficheiros enviados}} para a reciclagem", - "assets_trashed_from_server": "{} ficheiro(s) do servidor Immich foi/foram enviados para a reciclagem", + "assets_trashed_from_server": "{count} ficheiro(s) do servidor Immich foi/foram enviados para a reciclagem", "assets_were_part_of_album_count": "{count, plural, one {O ficheiro já fazia} other {Os ficheiros já faziam}} parte do álbum", "authorized_devices": "Dispositivos Autorizados", "automatic_endpoint_switching_subtitle": "Conecte-se localmente quando estiver em uma rede uma Wi-Fi específica e use conexões alternativas em outras redes", @@ -494,7 +499,7 @@ "back_close_deselect": "Voltar, fechar ou desmarcar", "background_location_permission": "Permissão de localização em segundo plano", "background_location_permission_content": "Para que seja possível trocar a URL quando estiver executando em segundo plano, o Immich deve *sempre* ter a permissão de localização precisa para que o aplicativo consiga ler o nome da rede Wi-Fi", - "backup_album_selection_page_albums_device": "Álbuns no dispositivo ({})", + "backup_album_selection_page_albums_device": "Álbuns no dispositivo ({count})", "backup_album_selection_page_albums_tap": "Toque para incluir, duplo toque para excluir", "backup_album_selection_page_assets_scatter": "Os arquivos podem estar espalhados em vários álbuns. Assim, os álbuns podem ser incluídos ou excluídos durante o processo de backup.", "backup_album_selection_page_select_albums": "Selecione Álbuns", @@ -502,14 +507,14 @@ "backup_album_selection_page_total_assets": "Total de arquivos únicos", "backup_all": "Tudo", "backup_background_service_backup_failed_message": "Falha ao fazer backup dos arquivos. Tentando novamente…", - "backup_background_service_connection_failed_message": "Falha na conexão com o servidor. Tentando novamente...", - "backup_background_service_current_upload_notification": "A enviar {}", + "backup_background_service_connection_failed_message": "Falha na ligação ao servidor. A tentar de novo…", + "backup_background_service_current_upload_notification": "A enviar {filename}", "backup_background_service_default_notification": "Verificando novos arquivos…", "backup_background_service_error_title": "Erro de backup", "backup_background_service_in_progress_notification": "Fazendo backup dos arquivos…", - "backup_background_service_upload_failure_notification": "Ocorreu um erro ao enviar {}", + "backup_background_service_upload_failure_notification": "Ocorreu um erro ao enviar {filename}", "backup_controller_page_albums": "Backup Álbuns", - "backup_controller_page_background_app_refresh_disabled_content": "Para utilizar o backup em segundo plano, ative a atualização da aplicação em segundo plano em Configurações > Geral > Atualização do app em segundo plano ", + "backup_controller_page_background_app_refresh_disabled_content": "Para utilizar a cópia de segurança em segundo plano, ative a atualização da aplicação em segundo plano em Sefinições > Geral > Atualização da aplicação em segundo plano.", "backup_controller_page_background_app_refresh_disabled_title": "Atualização do app em segundo plano desativada", "backup_controller_page_background_app_refresh_enable_button_text": "Ir para as configurações", "backup_controller_page_background_battery_info_link": "Mostre-me como", @@ -518,22 +523,22 @@ "backup_controller_page_background_battery_info_title": "Otimizações de bateria", "backup_controller_page_background_charging": "Apenas enquanto carrega a bateria", "backup_controller_page_background_configure_error": "Falha ao configurar o serviço em segundo plano", - "backup_controller_page_background_delay": "Atrasar a cópia de segurança de novos ficheiros: {}", + "backup_controller_page_background_delay": "Atraso da cópia de segurança de novos ficheiros: {duration}", "backup_controller_page_background_description": "Ative o serviço em segundo plano para fazer backup automático de novos arquivos sem precisar abrir o aplicativo", "backup_controller_page_background_is_off": "O backup automático em segundo plano está desativado", "backup_controller_page_background_is_on": "O backup automático em segundo plano está ativado", "backup_controller_page_background_turn_off": "Desativar o serviço em segundo plano", "backup_controller_page_background_turn_on": "Ativar o serviço em segundo plano", - "backup_controller_page_background_wifi": "Apenas no WiFi", + "backup_controller_page_background_wifi": "Apenas em Wi-Fi", "backup_controller_page_backup": "Backup", "backup_controller_page_backup_selected": "Selecionado: ", "backup_controller_page_backup_sub": "Fotos e vídeos salvos em backup", - "backup_controller_page_created": "Criado em: {}", + "backup_controller_page_created": "Criado em: {date}", "backup_controller_page_desc_backup": "Ative o backup para enviar automáticamente novos arquivos para o servidor.", "backup_controller_page_excluded": "Eliminado: ", - "backup_controller_page_failed": "Falhou ({})", - "backup_controller_page_filename": "Nome do ficheiro: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Falhou ({count})", + "backup_controller_page_filename": "Nome do ficheiro: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informações do backup", "backup_controller_page_none_selected": "Nenhum selecionado", "backup_controller_page_remainder": "Restante", @@ -542,7 +547,7 @@ "backup_controller_page_start_backup": "Iniciar Backup", "backup_controller_page_status_off": "Backup automático desativado", "backup_controller_page_status_on": "Backup automático ativado", - "backup_controller_page_storage_format": "{} de {} utilizado", + "backup_controller_page_storage_format": "{used} de {total} utilizado", "backup_controller_page_to_backup": "Álbuns para fazer backup", "backup_controller_page_total_sub": "Todas as fotos e vídeos dos álbuns selecionados", "backup_controller_page_turn_off": "Desativar backup", @@ -567,21 +572,21 @@ "bulk_keep_duplicates_confirmation": "Tem a certeza de que deseja manter {count, plural, one {# ficheiro duplicado} other {# ficheiros duplicados}}? Isto resolverá todos os grupos duplicados sem eliminar nada.", "bulk_trash_duplicates_confirmation": "Tem a certeza de que deseja mover para a reciclagem {count, plural, one {# ficheiro duplicado} other {# ficheiros duplicados}}? Isto manterá o maior ficheiro de cada grupo e irá mover para a reciclagem todos os outros duplicados.", "buy": "Comprar Immich", - "cache_settings_album_thumbnails": "Miniaturas da página da biblioteca ({} ficheiros)", + "cache_settings_album_thumbnails": "Miniaturas da página da biblioteca ({count} ficheiros)", "cache_settings_clear_cache_button": "Limpar cache", "cache_settings_clear_cache_button_title": "Limpa o cache do aplicativo. Isso afetará significativamente o desempenho do aplicativo até que o cache seja reconstruído.", "cache_settings_duplicated_assets_clear_button": "LIMPAR", "cache_settings_duplicated_assets_subtitle": "Fotos e vídeos que estão na lista negra da aplicação", - "cache_settings_duplicated_assets_title": "Ficheiros duplicados ({})", - "cache_settings_image_cache_size": "Tamanho da cache de imagem ({} ficheiros)", + "cache_settings_duplicated_assets_title": "Ficheiros duplicados ({count})", + "cache_settings_image_cache_size": "Tamanho da cache de imagem ({count} ficheiros)", "cache_settings_statistics_album": "Miniaturas da biblioteca", - "cache_settings_statistics_assets": "{} ficheiros ({})", + "cache_settings_statistics_assets": "{count} ficheiros ({size})", "cache_settings_statistics_full": "Imagens completas", "cache_settings_statistics_shared": "Miniaturas de álbuns compartilhados", "cache_settings_statistics_thumbnail": "Miniaturas", "cache_settings_statistics_title": "Uso de cache", "cache_settings_subtitle": "Controle o comportamento de cache do aplicativo Immich", - "cache_settings_thumbnail_size": "Tamanho da cache das miniaturas ({} ficheiros)", + "cache_settings_thumbnail_size": "Tamanho da cache das miniaturas ({count} ficheiros)", "cache_settings_tile_subtitle": "Controlar o comportamento do armazenamento local", "cache_settings_tile_title": "Armazenamento local", "cache_settings_title": "Configurações de cache", @@ -607,6 +612,7 @@ "change_password_form_new_password": "Nova senha", "change_password_form_password_mismatch": "As senhas não estão iguais", "change_password_form_reenter_new_password": "Confirme a nova senha", + "change_pin_code": "Alterar código PIN", "change_your_password": "Alterar a sua palavra-passe", "changed_visibility_successfully": "Visibilidade alterada com sucesso", "check_all": "Verificar tudo", @@ -647,17 +653,18 @@ "confirm_delete_face": "Tem a certeza de que deseja remover o rosto de {name} deste ficheiro?", "confirm_delete_shared_link": "Tem a certeza de que deseja eliminar este link partilhado?", "confirm_keep_this_delete_others": "Todos os outros ficheiros na pilha serão eliminados, exceto este ficheiro. Tem a certeza de que deseja continuar?", + "confirm_new_pin_code": "Confirmar novo código PIN", "confirm_password": "Confirmar a palavra-passe", "contain": "Ajustar", "context": "Contexto", "continue": "Continuar", - "control_bottom_app_bar_album_info_shared": "{} ficheiros · Partilhado", + "control_bottom_app_bar_album_info_shared": "{count} ficheiros · Partilhado", "control_bottom_app_bar_create_new_album": "Criar novo álbum", "control_bottom_app_bar_delete_from_immich": "Excluir do Immich", "control_bottom_app_bar_delete_from_local": "Excluir do dispositivo", "control_bottom_app_bar_edit_location": "Editar Localização", "control_bottom_app_bar_edit_time": "Editar Data & Hora", - "control_bottom_app_bar_share_link": "Share Link", + "control_bottom_app_bar_share_link": "Partilhar ligação", "control_bottom_app_bar_share_to": "Compartilhar com", "control_bottom_app_bar_trash_from_immich": "Mover para a lixeira", "copied_image_to_clipboard": "Imagem copiada para a área de transferência.", @@ -689,9 +696,11 @@ "create_tag_description": "Criar uma nova etiqueta. Para etiquetas compostas, introduza o caminho completo, incluindo as barras.", "create_user": "Criar utilizador", "created": "Criado", + "created_at": "Criado a", "crop": "Cortar", "curated_object_page_title": "Objetos", "current_device": "Dispositivo atual", + "current_pin_code": "Código PIN atual", "current_server_address": "Endereço atual do servidor", "custom_locale": "Localização Personalizada", "custom_locale_description": "Formatar datas e números baseados na língua e na região", @@ -760,7 +769,7 @@ "download_enqueue": "Na fila", "download_error": "Erro ao baixar", "download_failed": "Falha", - "download_filename": "ficheiro: {}", + "download_filename": "ficheiro: {filename}", "download_finished": "Concluído", "download_include_embedded_motion_videos": "Vídeos incorporados", "download_include_embedded_motion_videos_description": "Incluir vídeos incorporados em fotos em movimento como um ficheiro separado", @@ -804,19 +813,20 @@ "editor_crop_tool_h2_aspect_ratios": "Relação de aspeto", "editor_crop_tool_h2_rotation": "Rotação", "email": "E-mail", - "empty_folder": "This folder is empty", + "email_notifications": "Notificações por e-mail", + "empty_folder": "Esta pasta está vazia", "empty_trash": "Esvaziar reciclagem", "empty_trash_confirmation": "Tem a certeza de que deseja esvaziar a reciclagem? Isto removerá todos os ficheiros da reciclagem do Immich permanentemente.\nNão é possível anular esta ação!", "enable": "Ativar", "enabled": "Ativado", "end_date": "Data final", "enqueued": "Na fila", - "enter_wifi_name": "Digite o nome do Wi-Fi", + "enter_wifi_name": "Escreva o nome da rede Wi-Fi", "error": "Erro", "error_change_sort_album": "Falha ao mudar a ordem de exibição", "error_delete_face": "Falha ao remover rosto do ficheiro", "error_loading_image": "Erro ao carregar a imagem", - "error_saving_image": "Erro: {}", + "error_saving_image": "Erro: {error}", "error_title": "Erro - Algo correu mal", "errors": { "cannot_navigate_next_asset": "Não foi possível navegar para o próximo ficheiro", @@ -846,10 +856,12 @@ "failed_to_keep_this_delete_others": "Ocorreu um erro ao manter este ficheiro e eliminar os outros", "failed_to_load_asset": "Não foi possível ler o ficheiro", "failed_to_load_assets": "Não foi possível ler ficheiros", + "failed_to_load_notifications": "Ocorreu um erro ao carregar notificações", "failed_to_load_people": "Não foi possível carregar pessoas", "failed_to_remove_product_key": "Não foi possível remover chave de produto", "failed_to_stack_assets": "Não foi possível empilhar os ficheiros", "failed_to_unstack_assets": "Não foi possível desempilhar ficheiros", + "failed_to_update_notification_status": "Ocorreu um erro ao atualizar o estado das notificações", "import_path_already_exists": "Este caminho de importação já existe.", "incorrect_email_or_password": "Email ou palavra-passe incorretos", "paths_validation_failed": "A validação de {paths, plural, one {# caminho falhou} other {# caminhos falharam}}", @@ -917,6 +929,7 @@ "unable_to_remove_reaction": "Não foi possível remover a reação", "unable_to_repair_items": "Não foi possível reparar os itens", "unable_to_reset_password": "Não foi possível redefinir a palavra-passe", + "unable_to_reset_pin_code": "Não foi possível repor o código PIN", "unable_to_resolve_duplicate": "Não foi possível resolver as duplicidades", "unable_to_restore_assets": "Não foi possível restaurar ficheiros", "unable_to_restore_trash": "Não foi possível restaurar itens da reciclagem", @@ -950,10 +963,10 @@ "exif_bottom_sheet_location": "LOCALIZAÇÃO", "exif_bottom_sheet_people": "PESSOAS", "exif_bottom_sheet_person_add_person": "Adicionar nome", - "exif_bottom_sheet_person_age": "Idade {}", - "exif_bottom_sheet_person_age_months": "Idade {} meses", - "exif_bottom_sheet_person_age_year_months": "Idade 1 ano, {} meses", - "exif_bottom_sheet_person_age_years": "Idade {}", + "exif_bottom_sheet_person_age": "Idade {age}", + "exif_bottom_sheet_person_age_months": "Idade {months} meses", + "exif_bottom_sheet_person_age_year_months": "Idade 1 ano, {months} meses", + "exif_bottom_sheet_person_age_years": "Idade {years}", "exit_slideshow": "Sair da apresentação", "expand_all": "Expandir tudo", "experimental_settings_new_asset_list_subtitle": "Trabalho em andamento", @@ -971,11 +984,11 @@ "external": "Externo", "external_libraries": "Bibliotecas externas", "external_network": "Rede externa", - "external_network_sheet_info": "Quando não estiver ligado à rede Wi-Fi especificada, a aplicação irá ligar-se utilizando o primeiro URL abaixo que conseguir aceder, a começar do topo da lista para baixo.", + "external_network_sheet_info": "Quando não estiver ligado à rede Wi-Fi especificada, a aplicação irá ligar-se utilizando o primeiro URL abaixo que conseguir aceder, a começar do topo da lista para baixo", "face_unassigned": "Sem atribuição", "failed": "Falhou", "failed_to_load_assets": "Falha ao carregar ficheiros", - "failed_to_load_folder": "Failed to load folder", + "failed_to_load_folder": "Ocorreu um erro ao carregar a pasta", "favorite": "Favorito", "favorite_or_unfavorite_photo": "Marcar ou desmarcar a foto como favorita", "favorites": "Favoritos", @@ -992,8 +1005,8 @@ "filter_places": "Filtrar lugares", "find_them_fast": "Encontre-as mais rapidamente pelo nome numa pesquisa", "fix_incorrect_match": "Corrigir correspondência incorreta", - "folder": "Folder", - "folder_not_found": "Folder not found", + "folder": "Pasta", + "folder_not_found": "Pasta não encontrada", "folders": "Pastas", "folders_feature_description": "Navegar na vista de pastas por fotos e vídeos no sistema de ficheiros", "forward": "Para a frente", @@ -1038,11 +1051,12 @@ "home_page_delete_remote_err_local": "Foram selecionados arquivos locais para excluir remotamente, ignorando", "home_page_favorite_err_local": "Ainda não é possível adicionar recursos locais favoritos, ignorando", "home_page_favorite_err_partner": "Ainda não é possível marcar arquivos do parceiro como favoritos, ignorando", - "home_page_first_time_notice": "Se é a primeira vez que utiliza o aplicativo, certifique-se de marcar um ou mais álbuns do dispositivo para backup, assim a linha do tempo será preenchida com as fotos e vídeos.", + "home_page_first_time_notice": "Se é a primeira vez que utiliza a aplicação, certifique-se de que marca pelo menos um álbum do dispositivo para cópia de segurança, para a linha do tempo poder ser preenchida com fotos e vídeos", "home_page_share_err_local": "Não é possível compartilhar arquivos locais com um link, ignorando", "home_page_upload_err_limit": "Só é possível enviar 30 arquivos por vez, ignorando", "host": "Host", "hour": "Hora", + "id": "ID", "ignore_icloud_photos": "ignorar fotos no iCloud", "ignore_icloud_photos_description": "Fotos que estão armazenadas no iCloud não serão carregadas para o servidor do Immich", "image": "Imagem", @@ -1118,7 +1132,7 @@ "local_network": "Rede local", "local_network_sheet_info": "O aplicativo irá se conectar ao servidor através desta URL quando estiver na rede Wi-Fi especificada", "location_permission": "Permissão de localização", - "location_permission_content": "Para utilizar a função de troca automática de URL, é necessário a permissão de localização precisa, para que seja possível ler o nome da rede Wi-Fi.", + "location_permission_content": "Para utilizar a função de troca automática de URL, o Immich necessita da permissão de localização exata, para que seja possível ler o nome da rede Wi-Fi atual", "location_picker_choose_on_map": "Escolha no mapa", "location_picker_latitude_error": "Digite uma latitude válida", "location_picker_latitude_hint": "Digite a latitude", @@ -1168,8 +1182,8 @@ "manage_your_devices": "Gerir os seus dispositivos com sessão iniciada", "manage_your_oauth_connection": "Gerir a sua ligação ao OAuth", "map": "Mapa", - "map_assets_in_bound": "{} foto", - "map_assets_in_bounds": "{} fotos", + "map_assets_in_bound": "{count} foto", + "map_assets_in_bounds": "{count} fotos", "map_cannot_get_user_location": "Impossível obter a sua localização", "map_location_dialog_yes": "Sim", "map_location_picker_page_use_location": "Utilizar esta localização", @@ -1183,25 +1197,28 @@ "map_settings": "Definições do mapa", "map_settings_dark_mode": "Modo escuro", "map_settings_date_range_option_day": "Últimas 24 horas", - "map_settings_date_range_option_days": "Últimos {} dias", + "map_settings_date_range_option_days": "Últimos {days} dias", "map_settings_date_range_option_year": "Último ano", - "map_settings_date_range_option_years": "Últimos {} anos", + "map_settings_date_range_option_years": "Últimos {years} anos", "map_settings_dialog_title": "Configurações do mapa", "map_settings_include_show_archived": "Incluir arquivados", "map_settings_include_show_partners": "Incluir parceiros", "map_settings_only_show_favorites": "Mostrar apenas favoritos", "map_settings_theme_settings": "Tema do mapa", "map_zoom_to_see_photos": "Diminua o zoom para ver mais fotos", + "mark_all_as_read": "Marcar tudo como lido", + "mark_as_read": "Marcar como lido", + "marked_all_as_read": "Tudo marcado como lido", "matches": "Correspondências", "media_type": "Tipo de média", "memories": "Memórias", "memories_all_caught_up": "Finalizamos por hoje", - "memories_check_back_tomorrow": "Volte amanhã para ver mais lembranças ", + "memories_check_back_tomorrow": "Volte amanhã para ver mais memórias", "memories_setting_description": "Gerir o que vê nas suas memórias", "memories_start_over": "Ver de novo", "memories_swipe_to_close": "Deslize para cima para fechar", "memories_year_ago": "Um ano atrás", - "memories_years_ago": "Há {} anos atrás", + "memories_years_ago": "Há {years} anos atrás", "memory": "Memória", "memory_lane_title": "Memórias {title}", "menu": "Menu", @@ -1218,6 +1235,8 @@ "month": "Mês", "monthly_title_text_date_format": "MMMM y", "more": "Mais", + "moved_to_archive": "{count, plural, one {Foi movido # ficheiro} other {Foram movidos # ficheiros}} para o arquivo", + "moved_to_library": "{count, plural, one {Foi movido # ficheiro} other {Foram movidos # ficheiros}} para a biblioteca", "moved_to_trash": "Enviado para a reciclagem", "multiselect_grid_edit_date_time_err_read_only": "Não é possível editar a data de arquivo só leitura, ignorando", "multiselect_grid_edit_gps_err_read_only": "Não é possível editar a localização de arquivo só leitura, ignorando", @@ -1232,6 +1251,7 @@ "new_api_key": "Nova Chave de API", "new_password": "Nova palavra-passe", "new_person": "Nova Pessoa", + "new_pin_code": "Novo código PIN", "new_user_created": "Novo utilizador criado", "new_version_available": "NOVA VERSÃO DISPONÍVEL", "newest_first": "Mais recente primeiro", @@ -1250,6 +1270,8 @@ "no_favorites_message": "Adicione aos favoritos para encontrar as suas melhores fotos e vídeos rapidamente", "no_libraries_message": "Crie uma biblioteca externa para ver as suas fotos e vídeos", "no_name": "Sem nome", + "no_notifications": "Sem notificações", + "no_people_found": "Nenhuma pessoa encontrada", "no_places": "Sem lugares", "no_results": "Sem resultados", "no_results_description": "Tente um sinónimo ou uma palavra-chave mais comum", @@ -1259,7 +1281,7 @@ "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o Rótulo de Armazenamento a ficheiros carregados anteriormente, execute o", "notes": "Notas", "notification_permission_dialog_content": "Para ativar as notificações, vá em Configurações e selecione permitir.", - "notification_permission_list_tile_content": "Dar permissões para ativar notificações", + "notification_permission_list_tile_content": "Conceder permissões para ativar notificações.", "notification_permission_list_tile_enable_button": "Ativar notificações", "notification_permission_list_tile_title": "Permissão de notificações", "notification_toggle_setting_description": "Ativar notificações por e-mail", @@ -1304,7 +1326,7 @@ "partner_page_partner_add_failed": "Falha ao adicionar parceiro", "partner_page_select_partner": "Selecionar parceiro", "partner_page_shared_to_title": "Compartilhar com", - "partner_page_stop_sharing_content": "{} irá deixar de ter acesso às suas fotos.", + "partner_page_stop_sharing_content": "{partner} irá deixar de ter acesso às suas fotos.", "partner_sharing": "Partilha com Parceiro", "partners": "Parceiros", "password": "Palavra-passe", @@ -1350,6 +1372,9 @@ "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", "photos_from_previous_years": "Fotos de anos anteriores", "pick_a_location": "Selecione uma localização", + "pin_code_changed_successfully": "Código PIN alterado com sucesso", + "pin_code_reset_successfully": "Código PIN reposto com sucesso", + "pin_code_setup_successfully": "Código PIN configurado com sucesso", "place": "Lugar", "places": "Lugares", "places_count": "{count, plural, one {{count, number} Lugar} other {{count, number} Lugares}}", @@ -1367,7 +1392,8 @@ "previous_or_next_photo": "Foto anterior ou próxima", "primary": "Primário", "privacy": "Privacidade", - "profile_drawer_app_logs": "Logs", + "profile": "Perfil", + "profile_drawer_app_logs": "Registo", "profile_drawer_client_out_of_date_major": "O aplicativo está desatualizado. Por favor, atualize para a versão mais recente.", "profile_drawer_client_out_of_date_minor": "O aplicativo está desatualizado. Por favor, atualize para a versão mais recente.", "profile_drawer_client_server_up_to_date": "Cliente e Servidor atualizados", @@ -1380,7 +1406,7 @@ "public_share": "Partilhar Publicamente", "purchase_account_info": "Apoiante", "purchase_activated_subtitle": "Agradecemos por apoiar o Immich e software de código aberto", - "purchase_activated_time": "Ativado em {date, date}", + "purchase_activated_time": "Ativado em {date}", "purchase_activated_title": "A sua chave foi ativada com sucesso", "purchase_button_activate": "Ativar", "purchase_button_buy": "Comprar", @@ -1425,6 +1451,8 @@ "recent_searches": "Pesquisas recentes", "recently_added": "Adicionados Recentemente", "recently_added_page_title": "Adicionado recentemente", + "recently_taken": "Tirada recentemente", + "recently_taken_page_title": "Tiradas recentemente", "refresh": "Atualizar", "refresh_encoded_videos": "Atualizar vídeos codificados", "refresh_faces": "Atualizar rostos", @@ -1467,6 +1495,7 @@ "reset": "Redefinir", "reset_password": "Redefinir palavra-passe", "reset_people_visibility": "Redefinir pessoas ocultas", + "reset_pin_code": "Repor código PIN", "reset_to_default": "Repor predefinições", "resolve_duplicates": "Resolver itens duplicados", "resolved_all_duplicates": "Todos os itens duplicados resolvidos", @@ -1528,7 +1557,7 @@ "search_page_no_places": "Nenhuma informação de local disponível", "search_page_screenshots": "Capturas de tela", "search_page_search_photos_videos": "Pesquise suas fotos e vídeos", - "search_page_selfies": "Selfies", + "search_page_selfies": "Auto-retratos (Selfies)", "search_page_things": "Objetos", "search_page_view_all_button": "Ver tudo", "search_page_your_activity": "A sua atividade", @@ -1539,7 +1568,7 @@ "search_result_page_new_search_hint": "Nova Pesquisa", "search_settings": "Definições de pesquisa", "search_state": "Pesquisar estado/distrito...", - "search_suggestion_list_smart_search_hint_1": "A pesquisa inteligente está ativada por padrão. Para pesquisar metadados, utilize a sintaxe", + "search_suggestion_list_smart_search_hint_1": "A pesquisa inteligente está ativada por omissão. Para pesquisar por metadados, utilize a sintaxe ", "search_suggestion_list_smart_search_hint_2": "m:a-sua-pesquisa", "search_tags": "Pesquisar etiquetas...", "search_timezone": "Pesquisar fuso horário...", @@ -1559,6 +1588,7 @@ "select_keep_all": "Selecionar manter todos", "select_library_owner": "Selecionar o dono da biblioteca", "select_new_face": "Selecionar novo rosto", + "select_person_to_tag": "Selecione uma pessoa para etiquetar", "select_photos": "Selecionar fotos", "select_trash_all": "Selecionar todos para reciclagem", "select_user_for_sharing_page_err_album": "Falha ao criar o álbum", @@ -1589,12 +1619,12 @@ "setting_languages_apply": "Aplicar", "setting_languages_subtitle": "Alterar o idioma do aplicativo", "setting_languages_title": "Idioma", - "setting_notifications_notify_failures_grace_period": "Notificar erros da cópia de segurança em segundo plano: {}", - "setting_notifications_notify_hours": "{} horas", + "setting_notifications_notify_failures_grace_period": "Notificar erros da cópia de segurança em segundo plano: {duration}", + "setting_notifications_notify_hours": "{count} horas", "setting_notifications_notify_immediately": "imediatamente", - "setting_notifications_notify_minutes": "{} minutos", + "setting_notifications_notify_minutes": "{count} minutos", "setting_notifications_notify_never": "Nunca", - "setting_notifications_notify_seconds": "{} segundos", + "setting_notifications_notify_seconds": "{count} segundos", "setting_notifications_single_progress_subtitle": "Informações detalhadas sobre o progresso do envio por arquivo", "setting_notifications_single_progress_title": "Mostrar progresso detalhado do backup em segundo plano", "setting_notifications_subtitle": "Ajuste as preferências de notificação", @@ -1606,9 +1636,10 @@ "settings": "Definições", "settings_require_restart": "Reinicie o Immich para aplicar essa configuração", "settings_saved": "Definições guardadas", + "setup_pin_code": "Configurar um código PIN", "share": "Partilhar", "share_add_photos": "Adicionar fotos", - "share_assets_selected": "{} selecionado", + "share_assets_selected": "{count} selecionados", "share_dialog_preparing": "Preparando...", "shared": "Partilhado", "shared_album_activities_input_disable": "Comentários desativados", @@ -1622,32 +1653,32 @@ "shared_by_user": "Partilhado por {user}", "shared_by_you": "Partilhado por si", "shared_from_partner": "Fotos de {partner}", - "shared_intent_upload_button_progress_text": "Enviados {} de {}", + "shared_intent_upload_button_progress_text": "Enviados {current} de {total}", "shared_link_app_bar_title": "Links compartilhados", "shared_link_clipboard_copied_massage": "Copiado para a área de transferência", - "shared_link_clipboard_text": "Link: {}\nPalavra-passe: {}", + "shared_link_clipboard_text": "Ligação: {link}\nPalavra-passe: {password}", "shared_link_create_error": "Erro ao criar o link compartilhado", "shared_link_edit_description_hint": "Digite a descrição do compartilhamento", "shared_link_edit_expire_after_option_day": "1 dia", - "shared_link_edit_expire_after_option_days": "{} dias", + "shared_link_edit_expire_after_option_days": "{count} dias", "shared_link_edit_expire_after_option_hour": "1 hora", - "shared_link_edit_expire_after_option_hours": "{} horas", + "shared_link_edit_expire_after_option_hours": "{count} horas", "shared_link_edit_expire_after_option_minute": "1 minuto", - "shared_link_edit_expire_after_option_minutes": "{} minutos", - "shared_link_edit_expire_after_option_months": "{} meses", - "shared_link_edit_expire_after_option_year": "{} ano", + "shared_link_edit_expire_after_option_minutes": "{count} minutos", + "shared_link_edit_expire_after_option_months": "{count} meses", + "shared_link_edit_expire_after_option_year": "{count} ano", "shared_link_edit_password_hint": "Digite uma senha para proteger este link", "shared_link_edit_submit_button": "Atualizar link", "shared_link_error_server_url_fetch": "Erro ao abrir a URL do servidor", - "shared_link_expires_day": "Expira em {} dia", - "shared_link_expires_days": "Expira em {} dias", - "shared_link_expires_hour": "Expira em {} hora", - "shared_link_expires_hours": "Expira em {} horas", - "shared_link_expires_minute": "Expira em {} minuto", - "shared_link_expires_minutes": "Expira em {} minutos", + "shared_link_expires_day": "Expira em {count} dia", + "shared_link_expires_days": "Expira em {count} dias", + "shared_link_expires_hour": "Expira em {count} hora", + "shared_link_expires_hours": "Expira em {count} horas", + "shared_link_expires_minute": "Expira em {count} minuto", + "shared_link_expires_minutes": "Expira em {count} minutos", "shared_link_expires_never": "Expira ∞", - "shared_link_expires_second": "Expira em {} segundo", - "shared_link_expires_seconds": "Expira em {} segundos", + "shared_link_expires_second": "Expira em {count} segundo", + "shared_link_expires_seconds": "Expira em {count} segundos", "shared_link_individual_shared": "Compartilhamento único", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Gerenciar links compartilhados", @@ -1722,6 +1753,7 @@ "stop_sharing_photos_with_user": "Deixar de partilhar as fotos com este utilizador", "storage": "Espaço de armazenamento", "storage_label": "Rótulo de Armazenamento", + "storage_quota": "Quota de armazenamento", "storage_usage": "Utilizado {used} de {available}", "submit": "Enviar", "suggestions": "Sugestões", @@ -1748,12 +1780,12 @@ "theme_selection": "Selecionar tema", "theme_selection_description": "Definir automaticamente o tema como claro ou escuro com base na preferência do sistema do seu navegador", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de armazenamento na grade de fotos", - "theme_setting_asset_list_tiles_per_row_title": "Quantidade de ficheiros por linha ({})", - "theme_setting_colorful_interface_subtitle": "Aplica a cor primária ao fundo", + "theme_setting_asset_list_tiles_per_row_title": "Quantidade de ficheiros por linha ({count})", + "theme_setting_colorful_interface_subtitle": "Aplica a cor primária ao fundo.", "theme_setting_colorful_interface_title": "Interface colorida", "theme_setting_image_viewer_quality_subtitle": "Ajuste a qualidade do visualizador de imagens detalhadas", "theme_setting_image_viewer_quality_title": "Qualidade do visualizador de imagens", - "theme_setting_primary_color_subtitle": "Selecione a cor primária, usada nas ações principais e realces", + "theme_setting_primary_color_subtitle": "Selecione a cor primária, utilizada nas ações principais e nos realces.", "theme_setting_primary_color_title": "Cor primária", "theme_setting_system_primary_color_title": "Use a cor do sistema", "theme_setting_system_theme_switch": "Automático (Siga a configuração do sistema)", @@ -1783,13 +1815,15 @@ "trash_no_results_message": "Fotos e vídeos enviados para a reciclagem aparecem aqui.", "trash_page_delete_all": "Excluir tudo", "trash_page_empty_trash_dialog_content": "Deseja esvaziar a lixera? Estes arquivos serão apagados de forma permanente do Immich", - "trash_page_info": "Ficheiros na reciclagem irão ser eliminados permanentemente após {} dias", + "trash_page_info": "Ficheiros na reciclagem irão ser eliminados permanentemente após {days} dias", "trash_page_no_assets": "Lixeira vazia", "trash_page_restore_all": "Restaurar tudo", "trash_page_select_assets_btn": "Selecionar arquivos", - "trash_page_title": "Reciclagem ({})", + "trash_page_title": "Reciclagem ({count})", "trashed_items_will_be_permanently_deleted_after": "Os itens da reciclagem são eliminados permanentemente após {days, plural, one {# dia} other {# dias}}.", "type": "Tipo", + "unable_to_change_pin_code": "Não foi possível alterar o código PIN", + "unable_to_setup_pin_code": "Não foi possível configurar o código PIN", "unarchive": "Desarquivar", "unarchived_count": "{count, plural, other {Não arquivado #}}", "unfavorite": "Remover favorito", @@ -1813,6 +1847,7 @@ "untracked_files": "Ficheiros não monitorizados", "untracked_files_decription": "Estes ficheiros não são monitorizados pela aplicação. Podem ser resultados de falhas numa movimentação, carregamentos interrompidos, ou deixados para trás por causa de um problema", "up_next": "A seguir", + "updated_at": "Atualizado a", "updated_password": "Palavra-passe atualizada", "upload": "Carregar", "upload_concurrency": "Carregamentos em simultâneo", @@ -1825,7 +1860,7 @@ "upload_status_errors": "Erros", "upload_status_uploaded": "Enviado", "upload_success": "Carregamento realizado com sucesso, atualize a página para ver os novos ficheiros carregados.", - "upload_to_immich": "Enviar para o Immich ({})", + "upload_to_immich": "Enviar para o Immich ({count})", "uploading": "Enviando", "url": "URL", "usage": "Utilização", @@ -1834,6 +1869,8 @@ "user": "Utilizador", "user_id": "ID do utilizador", "user_liked": "{user} gostou {type, select, photo {desta fotografia} video {deste video} asset {deste ficheiro} other {disto}}", + "user_pin_code_settings": "Código PIN", + "user_pin_code_settings_description": "Gerir o seu código PIN", "user_purchase_settings": "Comprar", "user_purchase_settings_description": "Gerir a sua compra", "user_role_set": "Definir {user} como {role}", @@ -1852,7 +1889,7 @@ "version_announcement_overlay_release_notes": "notas da versão", "version_announcement_overlay_text_1": "Olá, há um novo lançamento de", "version_announcement_overlay_text_2": "por favor, Verifique com calma as ", - "version_announcement_overlay_text_3": "e certifique-se de que a configuração do docker-compose e do arquivo .env estejam atualizadas para evitar configurações incorretas, especialmente se utiliza o WatchTower ou qualquer outro mecanismo que faça atualização automática do servidor.", + "version_announcement_overlay_text_3": " e certifique-se de que a configuração do docker-compose e do ficheiro .env estejam atualizadas para evitar configurações incorretas, especialmente se utilizar o WatchTower ou qualquer outro mecanismo que faça atualização automática do servidor.", "version_announcement_overlay_title": "Nova versão do servidor disponível 🎉", "version_history": "Histórico de versões", "version_history_item": "Instalado {version} em {date}", @@ -1882,11 +1919,11 @@ "week": "Semana", "welcome": "Bem-vindo(a)", "welcome_to_immich": "Bem-vindo(a) ao Immich", - "wifi_name": "Nome do Wi-Fi", + "wifi_name": "Nome da rede Wi-Fi", "year": "Ano", "years_ago": "Há {years, plural, one {# ano} other {# anos}} atrás", "yes": "Sim", "you_dont_have_any_shared_links": "Não tem links partilhados", - "your_wifi_name": "Nome do seu Wi-Fi", + "your_wifi_name": "Nome da sua rede Wi-Fi", "zoom_image": "Ampliar/Reduzir imagem" } diff --git a/i18n/pt_BR.json b/i18n/pt_BR.json index b398c6ab28..bbafe81484 100644 --- a/i18n/pt_BR.json +++ b/i18n/pt_BR.json @@ -110,9 +110,9 @@ "library_watching_enable_description": "Observe bibliotecas externas para alterações de arquivos", "library_watching_settings": "Observação de biblioteca (EXPERIMENTAL)", "library_watching_settings_description": "Observe automaticamente os arquivos alterados", - "logging_enable_description": "Habilitar registro", + "logging_enable_description": "Habilitar logs", "logging_level_description": "Quando ativado, qual nível de log usar.", - "logging_settings": "Registros", + "logging_settings": "Logs", "machine_learning_clip_model": "Modelo CLIP", "machine_learning_clip_model_description": "O nome de um modelo CLIP listado aqui. Lembre-se de executar novamente a tarefa de 'Pesquisa Inteligente' para todas as imagens após alterar o modelo.", "machine_learning_duplicate_detection": "Detecção de duplicidade", @@ -143,7 +143,7 @@ "machine_learning_smart_search_enabled_description": "Se desativado, as imagens não serão codificadas para pesquisa inteligente.", "machine_learning_url_description": "A URL do servidor de aprendizado de máquina. Se mais de uma URL for fornecida, elas serão tentadas, uma de cada vez e na ordem indicada, até que uma responda com sucesso. Servidores que não responderem serão ignorados temporariamente até voltarem a estar conectados.", "manage_concurrency": "Gerenciar simultaneidade", - "manage_log_settings": "Gerenciar configurações de registro", + "manage_log_settings": "Gerenciar configurações de log", "map_dark_style": "Tema Escuro", "map_enable_description": "Ativar recursos do mapa", "map_gps_settings": "Mapa e Configurações de GPS", @@ -369,7 +369,7 @@ "advanced": "Avançado", "advanced_settings_enable_alternate_media_filter_subtitle": "Use esta opção para filtrar mídias durante a sincronização com base em critérios alternativos. Tente esta opção somente se o aplicativo estiver com problemas para detectar todos os álbuns.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Utilizar filtro alternativo de sincronização de álbum de dispositivo", - "advanced_settings_log_level_title": "Nível de log: {}", + "advanced_settings_log_level_title": "Nível de log: {level}", "advanced_settings_prefer_remote_subtitle": "Alguns dispositivos são extremamente lentos para carregar as miniaturas locais. Ative esta opção para preferir imagens do servidor.", "advanced_settings_prefer_remote_title": "Preferir imagens do servidor", "advanced_settings_proxy_headers_subtitle": "Defina os cabeçalhos do proxy que o Immich deve enviar em todas comunicações com a rede", @@ -400,9 +400,9 @@ "album_remove_user_confirmation": "Tem certeza de que deseja remover {user}?", "album_share_no_users": "Parece que você já compartilhou este álbum com todos os usuários ou não há nenhum usuário para compartilhar.", "album_thumbnail_card_item": "1 item", - "album_thumbnail_card_items": "{} itens", + "album_thumbnail_card_items": "{count} itens", "album_thumbnail_card_shared": " · Compartilhado", - "album_thumbnail_shared_by": "Compartilhado por {}", + "album_thumbnail_shared_by": "Compartilhado por {user}", "album_updated": "Álbum atualizado", "album_updated_setting_description": "Receba uma notificação por e-mail quando um álbum compartilhado tiver novos recursos", "album_user_left": "Saiu do álbum {album}", @@ -440,7 +440,7 @@ "archive": "Arquivados", "archive_or_unarchive_photo": "Arquivar ou desarquivar foto", "archive_page_no_archived_assets": "Nenhum arquivo encontrado", - "archive_page_title": "Arquivados ({})", + "archive_page_title": "Arquivados ({count})", "archive_size": "Tamanho do arquivo", "archive_size_description": "Configure o tamanho do arquivo para baixar (em GiB)", "archived": "Arquivado", @@ -477,18 +477,18 @@ "assets_added_to_album_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} ao álbum", "assets_added_to_name_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} {hasName, select, true {ao álbum {name}} other {em um novo álbum}}", "assets_count": "{count, plural, one {# arquivo} other {# arquivos}}", - "assets_deleted_permanently": "{} arquivo(s) deletado(s) permanentemente", - "assets_deleted_permanently_from_server": "{} arquivo(s) deletado(s) permanentemente do servidor Immich", + "assets_deleted_permanently": "{count} arquivo(s) deletado(s) permanentemente", + "assets_deleted_permanently_from_server": "{count} arquivo(s) deletado(s) permanentemente do servidor Immich", "assets_moved_to_trash_count": "{count, plural, one {# arquivo movido} other {# arquivos movidos}} para a lixeira", "assets_permanently_deleted_count": "{count, plural, one {# arquivo excluído permanentemente} other {# arquivos excluídos permanentemente}}", "assets_removed_count": "{count, plural, one {# arquivo removido} other {# arquivos removidos}}", - "assets_removed_permanently_from_device": "{} arquivo(s) removido(s) permanentemente do seu dispositivo", + "assets_removed_permanently_from_device": "{count} arquivo(s) removido(s) permanentemente do seu dispositivo", "assets_restore_confirmation": "Tem certeza de que deseja restaurar todos os seus arquivos na lixeira? Esta ação não pode ser desfeita! Nota: Arquivos externos não podem ser restaurados desta maneira.", "assets_restored_count": "{count, plural, one {# arquivo restaurado} other {# arquivos restaurados}}", - "assets_restored_successfully": "{} arquivo(s) restaurado(s)", - "assets_trashed": "{} arquivo enviado para a lixeira", + "assets_restored_successfully": "{count} arquivo(s) restaurado(s)", + "assets_trashed": "{count} arquivo enviado para a lixeira", "assets_trashed_count": "{count, plural, one {# arquivo movido para a lixeira} other {# arquivos movidos para a lixeira}}", - "assets_trashed_from_server": "{} arquivos foram enviados para a lixeira", + "assets_trashed_from_server": "{count} arquivos foram enviados para a lixeira", "assets_were_part_of_album_count": "{count, plural, one {O arquivo já faz} other {Os arquivos já fazem}} parte do álbum", "authorized_devices": "Dispositivos Autorizados", "automatic_endpoint_switching_subtitle": "Conecte-se localmente quando estiver em uma rede uma Wi-Fi específica e use conexões alternativas em outras redes", @@ -497,7 +497,7 @@ "back_close_deselect": "Voltar, fechar ou desmarcar", "background_location_permission": "Permissão de localização em segundo plano", "background_location_permission_content": "Para que seja possível trocar a URL quando estiver executando em segundo plano, o Immich deve *sempre* ter a permissão de localização precisa para que o aplicativo consiga ler o nome da rede Wi-Fi", - "backup_album_selection_page_albums_device": "Álbuns no dispositivo ({})", + "backup_album_selection_page_albums_device": "Álbuns no dispositivo ({count})", "backup_album_selection_page_albums_tap": "Toque para incluir, toque duas vezes para excluir", "backup_album_selection_page_assets_scatter": "Os recursos podem se espalhar por vários álbuns. Assim, os álbuns podem ser incluídos ou excluídos durante o processo de backup.", "backup_album_selection_page_select_albums": "Selecionar álbuns", @@ -506,13 +506,13 @@ "backup_all": "Todos", "backup_background_service_backup_failed_message": "Falha ao fazer backup. Tentando novamente…", "backup_background_service_connection_failed_message": "Falha na conexão com o servidor. Tentando novamente…", - "backup_background_service_current_upload_notification": "Enviando {}", - "backup_background_service_default_notification": "Checking for new assets…", + "backup_background_service_current_upload_notification": "Enviando {filename}", + "backup_background_service_default_notification": "Verificando se há novos arquivos…", "backup_background_service_error_title": "Erro no backup", "backup_background_service_in_progress_notification": "Fazendo backup de seus ativos…", - "backup_background_service_upload_failure_notification": "Falha ao enviar {}", + "backup_background_service_upload_failure_notification": "Falha ao enviar {filename}", "backup_controller_page_albums": "Álbuns de backup", - "backup_controller_page_background_app_refresh_disabled_content": "Para utilizar o backup em segundo plano, ative a atualização da aplicação em segundo plano em Configurações > Geral > Atualização em 2º plano", + "backup_controller_page_background_app_refresh_disabled_content": "Para utilizar o backup em segundo plano, ative a atualização da aplicação em segundo plano em Configurações > Geral > Atualização em 2º plano.", "backup_controller_page_background_app_refresh_disabled_title": "Atualização em 2º plano desativada", "backup_controller_page_background_app_refresh_enable_button_text": "Ir para as configurações", "backup_controller_page_background_battery_info_link": "Mostre-me como", @@ -521,7 +521,7 @@ "backup_controller_page_background_battery_info_title": "Otimizações de bateria", "backup_controller_page_background_charging": "Apenas durante o carregamento", "backup_controller_page_background_configure_error": "Falha ao configurar o serviço em segundo plano", - "backup_controller_page_background_delay": "Adiar backup de novos arquivos: {}", + "backup_controller_page_background_delay": "Adiar backup de novos arquivos: {duration}", "backup_controller_page_background_description": "Ative o serviço em segundo plano para fazer backup automático de novos ativos sem precisar abrir o aplicativo", "backup_controller_page_background_is_off": "O backup automático em segundo plano está desativado", "backup_controller_page_background_is_on": "O backup automático em segundo plano está ativado", @@ -531,12 +531,12 @@ "backup_controller_page_backup": "Backup", "backup_controller_page_backup_selected": "Selecionado: ", "backup_controller_page_backup_sub": "Backup de fotos e vídeos", - "backup_controller_page_created": "Criado em: {}", + "backup_controller_page_created": "Criado em: {date}", "backup_controller_page_desc_backup": "Ative o backup para carregar automaticamente novos ativos no servidor.", "backup_controller_page_excluded": "Excluído: ", - "backup_controller_page_failed": "Falhou ({})", - "backup_controller_page_filename": "Nome do arquivo: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Falhou ({count})", + "backup_controller_page_filename": "Nome do arquivo: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informações de backup", "backup_controller_page_none_selected": "Nenhum selecionado", "backup_controller_page_remainder": "Restante", @@ -545,7 +545,7 @@ "backup_controller_page_start_backup": "Iniciar backup", "backup_controller_page_status_off": "O backup está desativado", "backup_controller_page_status_on": "O backup está ativado", - "backup_controller_page_storage_format": "{} de {} usados", + "backup_controller_page_storage_format": "{used} de {total} usados", "backup_controller_page_to_backup": "Álbuns para backup", "backup_controller_page_total_sub": "Todas as fotos e vídeos únicos dos álbuns selecionados", "backup_controller_page_turn_off": "Desativar o backup", @@ -570,21 +570,21 @@ "bulk_keep_duplicates_confirmation": "Tem certeza de que deseja manter {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso resolverá todos os grupos duplicados sem excluir nada.", "bulk_trash_duplicates_confirmation": "Tem a certeza de que deseja mover para a lixeira {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso manterá o maior arquivo de cada grupo e moverá para a lixeira todas as outras duplicidades.", "buy": "Comprar o Immich", - "cache_settings_album_thumbnails": "Miniaturas da biblioteca ({} arquivos)", + "cache_settings_album_thumbnails": "Miniaturas da biblioteca ({count} arquivos)", "cache_settings_clear_cache_button": "Limpar o cache", "cache_settings_clear_cache_button_title": "Limpa o cache do aplicativo. Isso afetará significativamente o desempenho do aplicativo até que o cache seja reconstruído.", "cache_settings_duplicated_assets_clear_button": "LIMPAR", "cache_settings_duplicated_assets_subtitle": "Fotos e vídeos que são bloqueados pelo app", - "cache_settings_duplicated_assets_title": "Arquivos duplicados ({})", - "cache_settings_image_cache_size": "Tamanho do cache de imagens ({} arquivos)", + "cache_settings_duplicated_assets_title": "Arquivos duplicados ({count})", + "cache_settings_image_cache_size": "Tamanho do cache de imagens ({count} arquivos)", "cache_settings_statistics_album": "Miniaturas da biblioteca", - "cache_settings_statistics_assets": "{} arquivos ({})", + "cache_settings_statistics_assets": "{count} arquivos ({size})", "cache_settings_statistics_full": "Imagens completas", "cache_settings_statistics_shared": "Miniaturas de álbuns compartilhados", "cache_settings_statistics_thumbnail": "Miniaturas", "cache_settings_statistics_title": "Uso do cache", "cache_settings_subtitle": "Controle o comportamento de cache do aplicativo Immich", - "cache_settings_thumbnail_size": "Tamanho do cache de miniaturas ({} arquivos)", + "cache_settings_thumbnail_size": "Tamanho do cache de miniaturas ({count} arquivos)", "cache_settings_tile_subtitle": "Controle o comportamento do armazenamento local", "cache_settings_tile_title": "Armazenamento Local", "cache_settings_title": "Configurações de cache", @@ -616,7 +616,7 @@ "check_corrupt_asset_backup": "Verifique se há backups corrompidos", "check_corrupt_asset_backup_button": "Verificar", "check_corrupt_asset_backup_description": "Execute esta verificação somente em uma rede Wi-Fi e quando o backup de todos os arquivos já estiver concluído. O processo demora alguns minutos.", - "check_logs": "Verificar registros", + "check_logs": "Ver logs", "choose_matching_people_to_merge": "Escolha pessoas correspondentes para mesclar", "city": "Cidade", "clear": "Limpar", @@ -654,7 +654,7 @@ "contain": "Caber", "context": "Contexto", "continue": "Continuar", - "control_bottom_app_bar_album_info_shared": "{} arquivos · Compartilhado", + "control_bottom_app_bar_album_info_shared": "{count} arquivos · Compartilhado", "control_bottom_app_bar_create_new_album": "Criar novo álbum", "control_bottom_app_bar_delete_from_immich": "Excluir do Immich", "control_bottom_app_bar_delete_from_local": "Excluir do dispositivo", @@ -698,13 +698,13 @@ "current_server_address": "Endereço atual do servidor", "custom_locale": "Localização Customizada", "custom_locale_description": "Formatar datas e números baseados na linguagem e região", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", + "daily_title_text_date": "E, dd MMM", + "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Escuro", "date_after": "Data após", "date_and_time": "Data e Hora", "date_before": "Data antes", - "date_format": "E, LLL d, y • h:mm a", + "date_format": "E, d LLL, y • h:mm a", "date_of_birth_saved": "Data de nascimento salvo com sucesso", "date_range": "Intervalo de datas", "day": "Dia", @@ -763,7 +763,7 @@ "download_enqueue": "Na fila", "download_error": "Erro ao baixar", "download_failed": "Falha", - "download_filename": "arquivo: {}", + "download_filename": "arquivo: {filename}", "download_finished": "Concluído", "download_include_embedded_motion_videos": "Vídeos inclusos", "download_include_embedded_motion_videos_description": "Baixar os vídeos inclusos de uma foto em movimento em um arquivo separado", @@ -819,7 +819,7 @@ "error_change_sort_album": "Falha ao alterar a ordem de exibição", "error_delete_face": "Erro ao remover face do arquivo", "error_loading_image": "Erro ao carregar a página", - "error_saving_image": "Erro: {}", + "error_saving_image": "Erro: {error}", "error_title": "Erro - Algo deu errado", "errors": { "cannot_navigate_next_asset": "Não foi possível navegar para o próximo arquivo", @@ -955,10 +955,10 @@ "exif_bottom_sheet_location": "LOCALIZAÇÃO", "exif_bottom_sheet_people": "PESSOAS", "exif_bottom_sheet_person_add_person": "Adicionar nome", - "exif_bottom_sheet_person_age": "Idade {}", - "exif_bottom_sheet_person_age_months": "Idade {} meses", - "exif_bottom_sheet_person_age_year_months": "Idade 1 ano, {} meses", - "exif_bottom_sheet_person_age_years": "Idade {}", + "exif_bottom_sheet_person_age": "Idade {age}", + "exif_bottom_sheet_person_age_months": "Idade {months} meses", + "exif_bottom_sheet_person_age_year_months": "Idade 1 ano, {months} meses", + "exif_bottom_sheet_person_age_years": "Idade {years}", "exit_slideshow": "Sair da apresentação", "expand_all": "Expandir tudo", "experimental_settings_new_asset_list_subtitle": "Em andamento", @@ -976,7 +976,7 @@ "external": "Externo", "external_libraries": "Bibliotecas externas", "external_network": "Rede externa", - "external_network_sheet_info": "Quando não estiver na rede Wi-Fi especificada, o aplicativo irá se conectar usando a primeira URL abaixo que obtiver sucesso, começando do topo da lista para baixo.", + "external_network_sheet_info": "Quando não estiver na rede Wi-Fi especificada, o aplicativo irá se conectar usando a primeira URL abaixo que obtiver sucesso, começando do topo da lista para baixo", "face_unassigned": "Sem nome", "failed": "Falhou", "failed_to_load_assets": "Falha ao carregar arquivos", @@ -1043,7 +1043,7 @@ "home_page_delete_remote_err_local": "Foram selecionados arquivos locais para excluir remotamente, ignorando", "home_page_favorite_err_local": "Ainda não é possível adicionar arquivos locais aos favoritos, ignorando", "home_page_favorite_err_partner": "Ainda não é possível marcar arquivos do parceiro como favoritos, ignorando", - "home_page_first_time_notice": "Se é a primeira vez que utiliza o aplicativo, certifique-se de marcar um ou mais álbuns do dispositivo para backup, assim a linha do tempo será preenchida com as fotos e vídeos.", + "home_page_first_time_notice": "Se é a primeira vez que utiliza o aplicativo, certifique-se de marcar um ou mais álbuns do dispositivo para backup, assim a linha do tempo será preenchida com as fotos e vídeos", "home_page_share_err_local": "Não é possível compartilhar arquivos locais com um link, ignorando", "home_page_upload_err_limit": "Só é possível enviar 30 arquivos de cada vez, ignorando", "host": "Host", @@ -1123,7 +1123,7 @@ "local_network": "Rede local", "local_network_sheet_info": "O aplicativo irá se conectar ao servidor através desta URL quando estiver na rede Wi-Fi especificada", "location_permission": "Permissão de localização", - "location_permission_content": "Para utilizar a função de troca automática de URL, é necessário a permissão de localização precisa, para que seja possível ler o nome da rede Wi-Fi.", + "location_permission_content": "Para utilizar a função de troca automática de URL é necessário a permissão de localização precisa, para que seja possível ler o nome da rede Wi-Fi", "location_picker_choose_on_map": "Escolha no mapa", "location_picker_latitude_error": "Digite uma latitude válida", "location_picker_latitude_hint": "Digite a latitude", @@ -1138,13 +1138,13 @@ "login_form_api_exception": "Erro de API. Verifique a URL do servidor e tente novamente.", "login_form_back_button_text": "Voltar", "login_form_email_hint": "youremail@email.com", - "login_form_endpoint_hint": "http://your-server-ip:port", - "login_form_endpoint_url": "Server Endpoint URL", - "login_form_err_http": "Please specify http:// or https://", + "login_form_endpoint_hint": "http://ip-do-seu-servidor:porta", + "login_form_endpoint_url": "URL do servidor", + "login_form_err_http": "Por favor especifique http:// ou https://", "login_form_err_invalid_email": "E-mail inválido", "login_form_err_invalid_url": "URL Inválida", - "login_form_err_leading_whitespace": "Leading whitespace", - "login_form_err_trailing_whitespace": "Trailing whitespace", + "login_form_err_leading_whitespace": "Há um espaço em branco no início", + "login_form_err_trailing_whitespace": "Há um espaço em branco no fim", "login_form_failed_get_oauth_server_config": "Erro de login com OAuth, verifique a URL do servidor", "login_form_failed_get_oauth_server_disable": "O recurso OAuth não está disponível neste servidor", "login_form_failed_login": "Erro ao fazer login, verifique a url do servidor, e-mail e senha", @@ -1173,8 +1173,8 @@ "manage_your_devices": "Gerenciar seus dispositivos logados", "manage_your_oauth_connection": "Gerenciar sua conexão OAuth", "map": "Mapa", - "map_assets_in_bound": "{} foto", - "map_assets_in_bounds": "{} fotos", + "map_assets_in_bound": "{count} foto", + "map_assets_in_bounds": "{count} fotos", "map_cannot_get_user_location": "Não foi possível obter a sua localização", "map_location_dialog_yes": "Sim", "map_location_picker_page_use_location": "Use esta localização", @@ -1188,9 +1188,9 @@ "map_settings": "Definições do mapa", "map_settings_dark_mode": "Modo escuro", "map_settings_date_range_option_day": "Últimas 24 horas", - "map_settings_date_range_option_days": "Últimos {} dias", + "map_settings_date_range_option_days": "Últimos {days} dias", "map_settings_date_range_option_year": "Último ano", - "map_settings_date_range_option_years": "Últimos {} anos", + "map_settings_date_range_option_years": "Últimos {years} anos", "map_settings_dialog_title": "Configurações do mapa", "map_settings_include_show_archived": "Incluir arquivados", "map_settings_include_show_partners": "Incluir parceiros", @@ -1209,7 +1209,7 @@ "memories_start_over": "Ver de novo", "memories_swipe_to_close": "Deslize para cima para fechar", "memories_year_ago": "Um ano atrás", - "memories_years_ago": "{} anos atrás", + "memories_years_ago": "{years} anos atrás", "memory": "Memória", "memory_lane_title": "Trilha das Recordações {title}", "menu": "Menu", @@ -1316,7 +1316,7 @@ "partner_page_partner_add_failed": "Falha ao adicionar parceiro", "partner_page_select_partner": "Selecione o parceiro", "partner_page_shared_to_title": "Compartilhado com", - "partner_page_stop_sharing_content": "{} não poderá mais acessar as suas fotos.", + "partner_page_stop_sharing_content": "{partner} não poderá mais acessar as suas fotos.", "partner_sharing": "Compartilhamento com Parceiro", "partners": "Parceiros", "password": "Senha", @@ -1392,7 +1392,7 @@ "public_share": "Compartilhar Publicamente", "purchase_account_info": "Contribuidor", "purchase_activated_subtitle": "Obrigado(a) por apoiar o Immich e programas de código aberto", - "purchase_activated_time": "Ativado em {date, date}", + "purchase_activated_time": "Ativado em {date}", "purchase_activated_title": "Sua chave foi ativada com sucesso", "purchase_button_activate": "Ativar", "purchase_button_buy": "Comprar", @@ -1604,12 +1604,12 @@ "setting_languages_apply": "Aplicar", "setting_languages_subtitle": "Alterar o idioma do aplicativo", "setting_languages_title": "Idiomas", - "setting_notifications_notify_failures_grace_period": "Notifique falhas de backup em segundo plano: {}", - "setting_notifications_notify_hours": "{} horas", + "setting_notifications_notify_failures_grace_period": "Notifique falhas de backup em segundo plano: {duration}", + "setting_notifications_notify_hours": "{count} horas", "setting_notifications_notify_immediately": "imediatamente", - "setting_notifications_notify_minutes": "{} minutos", + "setting_notifications_notify_minutes": "{count} minutos", "setting_notifications_notify_never": "nunca", - "setting_notifications_notify_seconds": "{} segundos", + "setting_notifications_notify_seconds": "{count} segundos", "setting_notifications_single_progress_subtitle": "Informações detalhadas sobre o progresso do envio por arquivo", "setting_notifications_single_progress_title": "Mostrar detalhes do progresso do backup em segundo plano", "setting_notifications_subtitle": "Ajuste suas preferências de notificação", @@ -1623,7 +1623,7 @@ "settings_saved": "Configurações salvas", "share": "Compartilhar", "share_add_photos": "Adicionar fotos", - "share_assets_selected": "{} selecionado", + "share_assets_selected": "{count} selecionado", "share_dialog_preparing": "Preparando...", "shared": "Compartilhado", "shared_album_activities_input_disable": "Comentários desativados", @@ -1637,32 +1637,32 @@ "shared_by_user": "Compartilhado por {user}", "shared_by_you": "Compartilhado por você", "shared_from_partner": "Fotos de {partner}", - "shared_intent_upload_button_progress_text": "Enviados {} de {}", + "shared_intent_upload_button_progress_text": "Enviados {current} de {total}", "shared_link_app_bar_title": "Links compartilhados", "shared_link_clipboard_copied_massage": "Copiado para a área de transferência", - "shared_link_clipboard_text": "Link: {}\nSenha: {}", + "shared_link_clipboard_text": "Link: {link}\nSenha: {password}", "shared_link_create_error": "Erro ao criar o link compartilhado", "shared_link_edit_description_hint": "Digite a descrição do compartilhamento", "shared_link_edit_expire_after_option_day": "1 dia", - "shared_link_edit_expire_after_option_days": "{} dias", + "shared_link_edit_expire_after_option_days": "{count} dias", "shared_link_edit_expire_after_option_hour": "1 hora", - "shared_link_edit_expire_after_option_hours": "{} horas", + "shared_link_edit_expire_after_option_hours": "{count} horas", "shared_link_edit_expire_after_option_minute": "1 minuto", - "shared_link_edit_expire_after_option_minutes": "{} minutos", - "shared_link_edit_expire_after_option_months": "{} meses", - "shared_link_edit_expire_after_option_year": "{} ano", + "shared_link_edit_expire_after_option_minutes": "{count} minutos", + "shared_link_edit_expire_after_option_months": "{count} meses", + "shared_link_edit_expire_after_option_year": "{count} ano", "shared_link_edit_password_hint": "Digite uma senha para proteger este link", "shared_link_edit_submit_button": "Atualizar link", "shared_link_error_server_url_fetch": "Erro ao abrir a URL do servidor", - "shared_link_expires_day": "Expira em {} dia", - "shared_link_expires_days": "Expira em {} dias", - "shared_link_expires_hour": "Expira em {} hora", - "shared_link_expires_hours": "Expira em {} horas", - "shared_link_expires_minute": "Expira em {} minuto", - "shared_link_expires_minutes": "Expira em {} minutos", + "shared_link_expires_day": "Expira em {count} dia", + "shared_link_expires_days": "Expira em {count} dias", + "shared_link_expires_hour": "Expira em {count} hora", + "shared_link_expires_hours": "Expira em {count} horas", + "shared_link_expires_minute": "Expira em {count} minuto", + "shared_link_expires_minutes": "Expira em {count} minutos", "shared_link_expires_never": "Expira em ∞", - "shared_link_expires_second": "Expira em {} segundo", - "shared_link_expires_seconds": "Expira em {} segundos", + "shared_link_expires_second": "Expira em {count} segundo", + "shared_link_expires_seconds": "Expira em {count} segundos", "shared_link_individual_shared": "Compartilhado Individualmente", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Gerenciar links compartilhados", @@ -1763,8 +1763,8 @@ "theme_selection": "Selecionar tema", "theme_selection_description": "Defina automaticamente o tema como claro ou escuro com base na preferência do sistema do seu navegador", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de armazenamento na grade de fotos", - "theme_setting_asset_list_tiles_per_row_title": "Quantidade de arquivos por linha ({})", - "theme_setting_colorful_interface_subtitle": "Aplica a cor primária ao fundo", + "theme_setting_asset_list_tiles_per_row_title": "Quantidade de arquivos por linha ({count})", + "theme_setting_colorful_interface_subtitle": "Aplica a cor primária ao fundo.", "theme_setting_colorful_interface_title": "Interface colorida", "theme_setting_image_viewer_quality_subtitle": "Ajuste a qualidade de imagens detalhadas do visualizador", "theme_setting_image_viewer_quality_title": "Qualidade das imagens do visualizador", @@ -1798,11 +1798,11 @@ "trash_no_results_message": "Fotos e vídeos enviados para o lixo aparecem aqui.", "trash_page_delete_all": "Excluir tudo", "trash_page_empty_trash_dialog_content": "Deseja esvaziar a lixera? Estes arquivos serão apagados de forma permanente do Immich", - "trash_page_info": "Os itens da lixeira são excluídos de forma permanente após {} dias", + "trash_page_info": "Os itens da lixeira são excluídos de forma permanente após {days} dias", "trash_page_no_assets": "Lixeira vazia", "trash_page_restore_all": "Restaurar tudo", "trash_page_select_assets_btn": "Selecionar arquivos", - "trash_page_title": "Lixeira ({})", + "trash_page_title": "Lixeira ({count})", "trashed_items_will_be_permanently_deleted_after": "Os itens da lixeira serão deletados permanentemente após {days, plural, one {# dia} other {# dias}}.", "type": "Tipo", "unarchive": "Desarquivar", @@ -1840,7 +1840,7 @@ "upload_status_errors": "Erros", "upload_status_uploaded": "Carregado", "upload_success": "Carregado com sucesso, atualize a página para ver os novos arquivos.", - "upload_to_immich": "Enviar para o Immich ({})", + "upload_to_immich": "Enviar para o Immich ({count})", "uploading": "Enviando", "url": "URL", "usage": "Uso", diff --git a/i18n/ro.json b/i18n/ro.json index 80f5ed4650..b8c5eed46c 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -1380,7 +1380,7 @@ "public_share": "Distribuire Publică", "purchase_account_info": "Suporter", "purchase_activated_subtitle": "Vă mulțumim că susțineți Immich și software-ul open-source", - "purchase_activated_time": "Activat pe data de {date, date}", + "purchase_activated_time": "Activat pe data de {date}", "purchase_activated_title": "Cheia dvs. a fost activată cu succes", "purchase_button_activate": "Activați", "purchase_button_buy": "Cumpărați", diff --git a/i18n/ru.json b/i18n/ru.json index bc230a3d29..9b925ad903 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -52,7 +52,8 @@ "confirm_delete_library_assets": "Вы уверены, что хотите удалить эту библиотеку? Это безвозвратно удалит {count, plural, one {# объект} many {# объектов} other {# объекта}} из Immich. Файлы останутся на диске.", "confirm_email_below": "Чтобы подтвердить, введите \"{email}\" ниже", "confirm_reprocess_all_faces": "Вы уверены, что хотите повторно определить все лица? Будут также удалены имена со всех лиц.", - "confirm_user_password_reset": "Вы уверены, что хотите сбросить пароль пользователя {user}?", + "confirm_user_password_reset": "Вы действительно хотите сбросить пароль пользователя {user}?", + "confirm_user_pin_code_reset": "Вы действительно хотите сбросить PIN-код пользователя {user}?", "create_job": "Создать задание", "cron_expression": "Расписание (выражение планировщика cron)", "cron_expression_description": "Частота и время выполнения задания в формате планировщика cron. Воспользуйтесь при необходимости визуальным редактором Crontab Guru", @@ -348,6 +349,7 @@ "user_delete_delay_settings_description": "Срок в днях, по истечение которого происходит окончательное удаление учетной записи пользователя и его ресурсов. Задача по удалению пользователей выполняется в полночь. Изменения этой настройки будут учтены при следующем запуске задачи.", "user_delete_immediately": "Аккаунт и файлы пользователя {user} будут немедленно поставлены в очередь для окончательного удаления.", "user_delete_immediately_checkbox": "Поместить пользователя и его файлы в очередь для немедленного удаления", + "user_details": "Данные пользователя", "user_management": "Управление пользователями", "user_password_has_been_reset": "Пароль пользователя был сброшен:", "user_password_reset_description": "Пожалуйста, предоставьте временный пароль пользователю и сообщите ему, что при следующем входе в систему пароль нужно будет изменить.", @@ -369,7 +371,7 @@ "advanced": "Расширенные", "advanced_settings_enable_alternate_media_filter_subtitle": "Используйте этот параметр для фильтрации медиафайлов во время синхронизации на основе альтернативных критериев. Пробуйте только в том случае, если у вас есть проблемы с обнаружением приложением всех альбомов.", "advanced_settings_enable_alternate_media_filter_title": "[ЭКСПЕРИМЕНТАЛЬНО] Использование фильтра синхронизации альбомов альтернативных устройств", - "advanced_settings_log_level_title": "Уровень логирования: {}", + "advanced_settings_log_level_title": "Уровень логирования: {level}", "advanced_settings_prefer_remote_subtitle": "Некоторые устройства очень медленно загружают локальные изображения. Активируйте эту настройку, чтобы изображения всегда загружались с сервера.", "advanced_settings_prefer_remote_title": "Предпочитать фото на сервере", "advanced_settings_proxy_headers_subtitle": "Определите заголовки прокси-сервера, которые Immich должен отправлять с каждым сетевым запросом", @@ -400,9 +402,9 @@ "album_remove_user_confirmation": "Вы уверены, что хотите удалить пользователя {user}?", "album_share_no_users": "Похоже, вы поделились этим альбомом со всеми пользователями или у вас нет пользователей, с которыми можно поделиться.", "album_thumbnail_card_item": "1 элемент", - "album_thumbnail_card_items": "{} элементов", + "album_thumbnail_card_items": "{count} элементов", "album_thumbnail_card_shared": " · Общий", - "album_thumbnail_shared_by": "Поделился {}", + "album_thumbnail_shared_by": "Поделился пользователь {user}", "album_updated": "Альбом обновлён", "album_updated_setting_description": "Получать уведомление по электронной почте при добавлении новых ресурсов в общий альбом", "album_user_left": "Вы покинули {album}", @@ -440,7 +442,7 @@ "archive": "Архив", "archive_or_unarchive_photo": "Архивировать или разархивировать фото", "archive_page_no_archived_assets": "В архиве сейчас пусто", - "archive_page_title": "Архив ({})", + "archive_page_title": "Архив ({count})", "archive_size": "Размер архива", "archive_size_description": "Настройка размера архива для скачивания (в GiB)", "archived": "Архив", @@ -477,18 +479,18 @@ "assets_added_to_album_count": "В альбом {count, plural, one {добавлен # объект} many {добавлено # объектов} other {добавлено # объекта}}", "assets_added_to_name_count": "{count, plural, one {# объект добавлен} many {# объектов добавлено} other {# объекта добавлено}} в {hasName, select, true {альбом {name}} other {новый альбом}}", "assets_count": "{count, plural, one {# объект} many {# объектов} other {# объекта}}", - "assets_deleted_permanently": "{} объект(ы) удален(ы) навсегда", - "assets_deleted_permanently_from_server": "{} объект(ы) удален(ы) навсегда с сервера Immich", + "assets_deleted_permanently": "{count} объект(ов) удалено навсегда", + "assets_deleted_permanently_from_server": "{count} объект(ов) навсегда удалено с сервера Immich", "assets_moved_to_trash_count": "{count, plural, one {# объект перемещён} many {# объектов перемещено} other {# объекта перемещено}} в корзину", "assets_permanently_deleted_count": "{count, plural, one {# объект удалён} many {# объектов удалено} other {# объекта удалено}} навсегда", "assets_removed_count": "{count, plural, one {# объект удалён} many {# объектов удалено} other {# объекта удалено}}", - "assets_removed_permanently_from_device": "{} объект(ы) удален(ы) навсегда с вашего устройства", + "assets_removed_permanently_from_device": "{count} объект(ов) навсегда удалено с вашего устройства", "assets_restore_confirmation": "Вы уверены, что хотите восстановить все объекты из корзины? Это действие нельзя отменить! Обратите внимание, что любые оффлайн-объекты не могут быть восстановлены таким способом.", "assets_restored_count": "{count, plural, one {# объект восстановлен} many {# объектов восстановлено} other {# объекта восстановлено}}", - "assets_restored_successfully": "{} объект(ы) успешно восстановлен(ы)", - "assets_trashed": "{} объект(ы) помещен(ы) в корзину", + "assets_restored_successfully": "{count} объект(ов) успешно восстановлено", + "assets_trashed": "{count} объект(ов) помещено в корзину", "assets_trashed_count": "{count, plural, one {# объект перемещён} many {# объектов перемещено} other {# объекта перемещено}} в корзину", - "assets_trashed_from_server": "{} объект(ы) помещен(ы) в корзину на сервере Immich", + "assets_trashed_from_server": "{count} объект(ов) помещено в корзину на сервере Immich", "assets_were_part_of_album_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}} уже в альбоме", "authorized_devices": "Разрешенные устройства", "automatic_endpoint_switching_subtitle": "Подключаться локально по выбранной сети и использовать альтернативные адреса в ином случае", @@ -497,7 +499,7 @@ "back_close_deselect": "Назад, закрыть или отменить выбор", "background_location_permission": "Доступ к местоположению в фоне", "background_location_permission_content": "Чтобы считывать имя Wi-Fi сети в фоне, приложению *всегда* необходим доступ к точному местоположению устройства", - "backup_album_selection_page_albums_device": "Альбомы на устройстве ({})", + "backup_album_selection_page_albums_device": "Альбомы на устройстве ({count})", "backup_album_selection_page_albums_tap": "Нажмите, чтобы включить, дважды, чтобы исключить", "backup_album_selection_page_assets_scatter": "Ваши изображения и видео могут находиться в разных альбомах. Вы можете выбрать, какие альбомы включить, а какие исключить из резервного копирования.", "backup_album_selection_page_select_albums": "Выбор альбомов", @@ -506,11 +508,11 @@ "backup_all": "Все", "backup_background_service_backup_failed_message": "Не удалось выполнить резервное копирование. Повторная попытка…", "backup_background_service_connection_failed_message": "Не удалось подключиться к серверу. Повторная попытка…", - "backup_background_service_current_upload_notification": "Загружается {}", + "backup_background_service_current_upload_notification": "Загружается {filename}", "backup_background_service_default_notification": "Поиск новых объектов…", "backup_background_service_error_title": "Ошибка резервного копирования", "backup_background_service_in_progress_notification": "Резервное копирование ваших объектов…", - "backup_background_service_upload_failure_notification": "Ошибка загрузки {}", + "backup_background_service_upload_failure_notification": "Ошибка загрузки {filename}", "backup_controller_page_albums": "Резервное копирование альбомов", "backup_controller_page_background_app_refresh_disabled_content": "Включите фоновое обновление приложения в Настройки > Общие > Фоновое обновление приложений, чтобы использовать фоновое резервное копирование.", "backup_controller_page_background_app_refresh_disabled_title": "Фоновое обновление отключено", @@ -521,7 +523,7 @@ "backup_controller_page_background_battery_info_title": "Оптимизация батареи", "backup_controller_page_background_charging": "Только во время зарядки", "backup_controller_page_background_configure_error": "Не удалось настроить фоновую службу", - "backup_controller_page_background_delay": "Отложить резервное копирование новых объектов: {}", + "backup_controller_page_background_delay": "Отложить резервное копирование новых объектов: {duration}", "backup_controller_page_background_description": "Включите фоновую службу для автоматического резервного копирования любых новых объектов без необходимости открывать приложение", "backup_controller_page_background_is_off": "Автоматическое резервное копирование в фоновом режиме отключено", "backup_controller_page_background_is_on": "Автоматическое резервное копирование в фоновом режиме включено", @@ -531,12 +533,12 @@ "backup_controller_page_backup": "Резервное копирование", "backup_controller_page_backup_selected": "Выбрано: ", "backup_controller_page_backup_sub": "Загруженные фото и видео", - "backup_controller_page_created": "Создано: {}", + "backup_controller_page_created": "Создано: {date}", "backup_controller_page_desc_backup": "Включите резервное копирование в активном режиме, чтобы автоматически загружать новые объекты при открытии приложения.", "backup_controller_page_excluded": "Исключены: ", - "backup_controller_page_failed": "Неудачных ({})", - "backup_controller_page_filename": "Имя файла: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Неудачных ({count})", + "backup_controller_page_filename": "Имя файла: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Информация о резервном копировании", "backup_controller_page_none_selected": "Ничего не выбрано", "backup_controller_page_remainder": "Осталось", @@ -545,7 +547,7 @@ "backup_controller_page_start_backup": "Начать резервное копирование", "backup_controller_page_status_off": "Автоматическое резервное копирование в активном режиме выключено", "backup_controller_page_status_on": "Автоматическое резервное копирование в активном режиме включено", - "backup_controller_page_storage_format": "{} из {} использовано", + "backup_controller_page_storage_format": "{used} из {total} использовано", "backup_controller_page_to_backup": "Альбомы для резервного копирования", "backup_controller_page_total_sub": "Все уникальные фото и видео из выбранных альбомов", "backup_controller_page_turn_off": "Выключить", @@ -570,21 +572,21 @@ "bulk_keep_duplicates_confirmation": "Вы уверены, что хотите оставить {count, plural, one {# дублирующийся объект} many {# дублирующихся объектов} other {# дублирующихся объекта}}? Это сохранит все дубликаты.", "bulk_trash_duplicates_confirmation": "Вы уверены, что хотите переместить в корзину {count, plural, one {# дублирующийся объект} many {# дублирующихся объектов} other {# дублирующихся объекта}}? Будет сохранён самый большой файл в каждой группе, а его дубликаты перемещены в корзину.", "buy": "Приобретение лицензии Immich", - "cache_settings_album_thumbnails": "Миниатюры страниц библиотеки ({} объектов)", + "cache_settings_album_thumbnails": "Миниатюры страниц библиотеки ({count} объектов)", "cache_settings_clear_cache_button": "Очистить кэш", "cache_settings_clear_cache_button_title": "Очищает кэш приложения. Это негативно повлияет на производительность, пока кэш не будет создан заново.", "cache_settings_duplicated_assets_clear_button": "ОЧИСТИТЬ", "cache_settings_duplicated_assets_subtitle": "Фото и видео, занесенные приложением в черный список", - "cache_settings_duplicated_assets_title": "Дублирующиеся объекты ({})", - "cache_settings_image_cache_size": "Размер кэша изображений ({} объектов)", + "cache_settings_duplicated_assets_title": "Дублирующиеся объекты ({count})", + "cache_settings_image_cache_size": "Размер кэша изображений ({count} объектов)", "cache_settings_statistics_album": "Миниатюры библиотеки", - "cache_settings_statistics_assets": "{} объектов ({})", + "cache_settings_statistics_assets": "{count} объектов ({size})", "cache_settings_statistics_full": "Полные изображения", "cache_settings_statistics_shared": "Миниатюры общих альбомов", "cache_settings_statistics_thumbnail": "Миниатюры", "cache_settings_statistics_title": "Размер кэша", "cache_settings_subtitle": "Управление кэшированием мобильного приложения", - "cache_settings_thumbnail_size": "Размер кэша миниатюр ({} объектов)", + "cache_settings_thumbnail_size": "Размер кэша миниатюр ({count} объектов)", "cache_settings_tile_subtitle": "Управление локальным хранилищем", "cache_settings_tile_title": "Локальное хранилище", "cache_settings_title": "Настройки кэширования", @@ -610,6 +612,7 @@ "change_password_form_new_password": "Новый пароль", "change_password_form_password_mismatch": "Пароли не совпадают", "change_password_form_reenter_new_password": "Повторно введите новый пароль", + "change_pin_code": "Изменение PIN-кода", "change_your_password": "Изменить свой пароль", "changed_visibility_successfully": "Видимость успешно изменена", "check_all": "Выбрать всё", @@ -650,11 +653,12 @@ "confirm_delete_face": "Вы хотите удалить лицо человека {name} из объекта?", "confirm_delete_shared_link": "Вы уверены, что хотите удалить эту публичную ссылку?", "confirm_keep_this_delete_others": "Все остальные объекты в группе будут удалены, кроме этого объекта. Вы уверены, что хотите продолжить?", + "confirm_new_pin_code": "Подтвердите новый PIN-код", "confirm_password": "Подтвердите пароль", "contain": "Вместить", "context": "Контекст", "continue": "Продолжить", - "control_bottom_app_bar_album_info_shared": "{} элементов · Общий", + "control_bottom_app_bar_album_info_shared": "{count} элементов · Общий", "control_bottom_app_bar_create_new_album": "Создать альбом", "control_bottom_app_bar_delete_from_immich": "Удалить из Immich", "control_bottom_app_bar_delete_from_local": "Удалить с устройства", @@ -692,9 +696,11 @@ "create_tag_description": "Создайте новый тег. Для вложенных тегов введите полный путь к тегу, включая слэши.", "create_user": "Создать пользователя", "created": "Создан", + "created_at": "Создан", "crop": "Обрезать", "curated_object_page_title": "Предметы", "current_device": "Текущее устройство", + "current_pin_code": "Текущий PIN-код", "current_server_address": "Текущий адрес сервера", "custom_locale": "Пользовательский регион", "custom_locale_description": "Форматирование дат и чисел в зависимости от языка и региона", @@ -763,7 +769,7 @@ "download_enqueue": "Загрузка в очереди", "download_error": "Ошибка загрузки", "download_failed": "Загрузка не удалась", - "download_filename": "файл: {}", + "download_filename": "файл: {filename}", "download_finished": "Загрузка окончена", "download_include_embedded_motion_videos": "Встроенные видео", "download_include_embedded_motion_videos_description": "Включить видео, встроенные в живые фото, в виде отдельного файла", @@ -807,6 +813,7 @@ "editor_crop_tool_h2_aspect_ratios": "Соотношения сторон", "editor_crop_tool_h2_rotation": "Вращение", "email": "Электронная почта", + "email_notifications": "Уведомления по электронной почте", "empty_folder": "Пустая папка", "empty_trash": "Очистить корзину", "empty_trash_confirmation": "Вы уверены, что хотите очистить корзину? Все объекты в корзине будут навсегда удалены из Immich.\nВы не сможете отменить это действие!", @@ -819,7 +826,7 @@ "error_change_sort_album": "Не удалось изменить порядок сортировки альбома", "error_delete_face": "Ошибка при удалении лица из объекта", "error_loading_image": "Ошибка при загрузке изображения", - "error_saving_image": "Ошибка: {}", + "error_saving_image": "Ошибка: {error}", "error_title": "Ошибка - Что-то пошло не так", "errors": { "cannot_navigate_next_asset": "Не удалось перейти к следующему объекту", @@ -922,6 +929,7 @@ "unable_to_remove_reaction": "Не удается удалить реакцию", "unable_to_repair_items": "Не удалось восстановить элементы", "unable_to_reset_password": "Не удается сбросить пароль", + "unable_to_reset_pin_code": "Ошибка при сбросе PIN-кода", "unable_to_resolve_duplicate": "Не удалось разрешить дубликат", "unable_to_restore_assets": "Не удалось восстановить объекты", "unable_to_restore_trash": "Не удается восстановить содержимое", @@ -955,10 +963,10 @@ "exif_bottom_sheet_location": "МЕСТО", "exif_bottom_sheet_people": "ЛЮДИ", "exif_bottom_sheet_person_add_person": "Добавить имя", - "exif_bottom_sheet_person_age": "Возраст {}", - "exif_bottom_sheet_person_age_months": "Возраст {} месяцев", - "exif_bottom_sheet_person_age_year_months": "Возраст 1 год, {} месяцев", - "exif_bottom_sheet_person_age_years": "Возраст {}", + "exif_bottom_sheet_person_age": "Возраст {age}", + "exif_bottom_sheet_person_age_months": "Возраст {months} месяцев", + "exif_bottom_sheet_person_age_year_months": "Возраст 1 год, {months} месяцев", + "exif_bottom_sheet_person_age_years": "Возраст {years}", "exit_slideshow": "Выйти из слайд-шоу", "expand_all": "Развернуть всё", "experimental_settings_new_asset_list_subtitle": "В разработке", @@ -1048,6 +1056,7 @@ "home_page_upload_err_limit": "Вы можете загрузить максимум 30 файлов за раз, пропуск", "host": "Хост", "hour": "Час", + "id": "ID", "ignore_icloud_photos": "Пропускать файлы из iCloud", "ignore_icloud_photos_description": "Не загружать файлы в Immich, если они хранятся в iCloud", "image": "Изображения", @@ -1173,8 +1182,8 @@ "manage_your_devices": "Управление устройствами, с помощью которых осуществлялся доступ к системе", "manage_your_oauth_connection": "Настройки подключённого OAuth", "map": "Карта", - "map_assets_in_bound": "{} фото", - "map_assets_in_bounds": "{} фото", + "map_assets_in_bound": "{count} фото", + "map_assets_in_bounds": "{count} фото", "map_cannot_get_user_location": "Невозможно получить местоположение пользователя", "map_location_dialog_yes": "Да", "map_location_picker_page_use_location": "Это местоположение", @@ -1188,9 +1197,9 @@ "map_settings": "Настройки карты", "map_settings_dark_mode": "Темный режим", "map_settings_date_range_option_day": "24 часа", - "map_settings_date_range_option_days": "Последние {} дней", + "map_settings_date_range_option_days": "Последние {days} дней", "map_settings_date_range_option_year": "Год", - "map_settings_date_range_option_years": "{} года", + "map_settings_date_range_option_years": "{years} года", "map_settings_dialog_title": "Настройки карты", "map_settings_include_show_archived": "Отображать архивированное", "map_settings_include_show_partners": "Отображать медиа партнера", @@ -1209,7 +1218,7 @@ "memories_start_over": "Начать заново", "memories_swipe_to_close": "Смахните вверх, чтобы закрыть", "memories_year_ago": "Год назад", - "memories_years_ago": "Лет назад: {}", + "memories_years_ago": "{years, plural, one {# год} many {# лет} other {# года}} назад", "memory": "Память", "memory_lane_title": "Воспоминание {title}", "menu": "Меню", @@ -1242,6 +1251,7 @@ "new_api_key": "Новый API-ключ", "new_password": "Новый пароль", "new_person": "Новый человек", + "new_pin_code": "Новый PIN-код", "new_user_created": "Новый пользователь создан", "new_version_available": "ДОСТУПНА НОВАЯ ВЕРСИЯ", "newest_first": "Сначала новые", @@ -1316,7 +1326,7 @@ "partner_page_partner_add_failed": "Не удалось добавить партнёра", "partner_page_select_partner": "Выбрать партнёра", "partner_page_shared_to_title": "Поделиться с...", - "partner_page_stop_sharing_content": "{} больше не сможет получить доступ к вашим фотографиям.", + "partner_page_stop_sharing_content": "Пользователь {partner} больше не сможет получить доступ к вашим фото.", "partner_sharing": "Совместное использование", "partners": "Партнёры", "password": "Пароль", @@ -1362,6 +1372,9 @@ "photos_count": "{count, plural, one {{count, number} фото} other {{count, number} фото}}", "photos_from_previous_years": "Фотографии прошлых лет в этот день", "pick_a_location": "Выбрать местоположение", + "pin_code_changed_successfully": "PIN-код успешно изменён", + "pin_code_reset_successfully": "PIN-код сброшен", + "pin_code_setup_successfully": "PIN-код успешно установлен", "place": "Места", "places": "Места", "places_count": "{count, plural, one {{count, number} место} many {{count, number} мест} other {{count, number} места}}", @@ -1379,6 +1392,7 @@ "previous_or_next_photo": "Предыдущая или следующая фотография", "primary": "Главное", "privacy": "Конфиденциальность", + "profile": "Профиль", "profile_drawer_app_logs": "Журнал", "profile_drawer_client_out_of_date_major": "Версия мобильного приложения устарела. Пожалуйста, обновите его.", "profile_drawer_client_out_of_date_minor": "Версия мобильного приложения устарела. Пожалуйста, обновите его.", @@ -1392,7 +1406,7 @@ "public_share": "Публичный доступ", "purchase_account_info": "Поддержка", "purchase_activated_subtitle": "Благодарим вас за поддержку Immich и программного обеспечения с открытым исходным кодом", - "purchase_activated_time": "Активировано на {date, date}", + "purchase_activated_time": "Активировано {date}", "purchase_activated_title": "Ваш ключ успешно активирован", "purchase_button_activate": "Активировать", "purchase_button_buy": "Купить", @@ -1421,7 +1435,7 @@ "purchase_server_description_1": "Для всего сервера", "purchase_server_description_2": "Состояние поддержки", "purchase_server_title": "Сервер", - "purchase_settings_server_activated": "Ключ продукта сервера управляется администратором", + "purchase_settings_server_activated": "Ключом продукта управляет администратор сервера", "rating": "Рейтинг звёзд", "rating_clear": "Очистить рейтинг", "rating_count": "{count, plural, one {# звезда} many {# звезд} other {# звезды}}", @@ -1481,6 +1495,7 @@ "reset": "Сброс", "reset_password": "Сброс пароля", "reset_people_visibility": "Восстановить видимость людей", + "reset_pin_code": "Сбросить PIN-код", "reset_to_default": "Восстановление значений по умолчанию", "resolve_duplicates": "Устранить дубликаты", "resolved_all_duplicates": "Все дубликаты устранены", @@ -1604,12 +1619,12 @@ "setting_languages_apply": "Применить", "setting_languages_subtitle": "Изменить язык приложения", "setting_languages_title": "Язык", - "setting_notifications_notify_failures_grace_period": "Уведомлять об ошибках фонового резервного копирования: {}", - "setting_notifications_notify_hours": "{} ч.", + "setting_notifications_notify_failures_grace_period": "Уведомлять об ошибках фонового резервного копирования: {duration}", + "setting_notifications_notify_hours": "{count} ч.", "setting_notifications_notify_immediately": "немедленно", - "setting_notifications_notify_minutes": "{} мин.", + "setting_notifications_notify_minutes": "{count} мин.", "setting_notifications_notify_never": "никогда", - "setting_notifications_notify_seconds": "{} сек.", + "setting_notifications_notify_seconds": "{count} сек.", "setting_notifications_single_progress_subtitle": "Подробная информация о ходе загрузки для каждого объекта", "setting_notifications_single_progress_title": "Показать ход выполнения фонового резервного копирования", "setting_notifications_subtitle": "Настройка параметров уведомлений", @@ -1621,9 +1636,10 @@ "settings": "Настройки", "settings_require_restart": "Пожалуйста, перезапустите приложение, чтобы изменения вступили в силу", "settings_saved": "Настройки сохранены", + "setup_pin_code": "Создание PIN-кода", "share": "Поделиться", "share_add_photos": "Добавить фото", - "share_assets_selected": "{} выбрано", + "share_assets_selected": "{count} выбрано", "share_dialog_preparing": "Подготовка...", "shared": "Общиe", "shared_album_activities_input_disable": "Комментарии отключены", @@ -1637,32 +1653,32 @@ "shared_by_user": "Владелец: {user}", "shared_by_you": "Вы поделились", "shared_from_partner": "Фото от {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Загружено", + "shared_intent_upload_button_progress_text": "{current} / {total} Загружено", "shared_link_app_bar_title": "Публичные ссылки", "shared_link_clipboard_copied_massage": "Скопировано в буфер обмена", - "shared_link_clipboard_text": "Ссылка: {}\nПароль: {}", + "shared_link_clipboard_text": "Ссылка: {link}\nПароль: {password}", "shared_link_create_error": "Ошибка при создании публичной ссылки", "shared_link_edit_description_hint": "Введите описание публичного доступа", "shared_link_edit_expire_after_option_day": "1 день", - "shared_link_edit_expire_after_option_days": "{} дней", + "shared_link_edit_expire_after_option_days": "{count} дней", "shared_link_edit_expire_after_option_hour": "1 час", - "shared_link_edit_expire_after_option_hours": "{} часов", + "shared_link_edit_expire_after_option_hours": "{count} часов", "shared_link_edit_expire_after_option_minute": "1 минуту", - "shared_link_edit_expire_after_option_minutes": "{} минут", - "shared_link_edit_expire_after_option_months": "{} месяцев", - "shared_link_edit_expire_after_option_year": "{} лет", + "shared_link_edit_expire_after_option_minutes": "{count} минут", + "shared_link_edit_expire_after_option_months": "{count} месяцев", + "shared_link_edit_expire_after_option_year": "{count} лет", "shared_link_edit_password_hint": "Введите пароль для публичного доступа", "shared_link_edit_submit_button": "Обновить ссылку", "shared_link_error_server_url_fetch": "Невозможно запросить URL с сервера", - "shared_link_expires_day": "Истекает через {} день", - "shared_link_expires_days": "Истекает через {} дней", - "shared_link_expires_hour": "Истекает через {} час", - "shared_link_expires_hours": "Истекает через {} часов", - "shared_link_expires_minute": "Истекает через {} минуту", - "shared_link_expires_minutes": "Истекает через {} минут", + "shared_link_expires_day": "Истечёт через {count} день", + "shared_link_expires_days": "Истечёт через {count} дней", + "shared_link_expires_hour": "Истечёт через {count} час", + "shared_link_expires_hours": "Истечёт через {count} часов", + "shared_link_expires_minute": "Истечёт через {count} минуту", + "shared_link_expires_minutes": "Истечёт через {count} минут", "shared_link_expires_never": "Вечная ссылка", - "shared_link_expires_second": "Истекает через {} секунду", - "shared_link_expires_seconds": "Истекает через {} секунд", + "shared_link_expires_second": "Истечёт через {count} секунду", + "shared_link_expires_seconds": "Истечёт через {count} секунд", "shared_link_individual_shared": "Индивидуальный общий доступ", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Управление публичными ссылками", @@ -1737,6 +1753,7 @@ "stop_sharing_photos_with_user": "Прекратить делиться своими фотографиями с этим пользователем", "storage": "Хранилище", "storage_label": "Тег хранилища", + "storage_quota": "Квота хранилища", "storage_usage": "{used} из {available} доступных", "submit": "Подтвердить", "suggestions": "Предложения", @@ -1763,7 +1780,7 @@ "theme_selection": "Выбор темы", "theme_selection_description": "Автоматически устанавливать тему в зависимости от системных настроек вашего браузера", "theme_setting_asset_list_storage_indicator_title": "Показать индикатор хранилища на плитках объектов", - "theme_setting_asset_list_tiles_per_row_title": "Количество объектов в строке ({})", + "theme_setting_asset_list_tiles_per_row_title": "Количество объектов в строке ({count})", "theme_setting_colorful_interface_subtitle": "Добавить оттенок к фону.", "theme_setting_colorful_interface_title": "Цвет фона", "theme_setting_image_viewer_quality_subtitle": "Настройка качества просмотра изображения", @@ -1798,13 +1815,15 @@ "trash_no_results_message": "Здесь будут отображаться удалённые фотографии и видео.", "trash_page_delete_all": "Удалить все", "trash_page_empty_trash_dialog_content": "Очистить корзину? Объекты в ней будут навсегда удалены из Immich.", - "trash_page_info": "Объекты в корзине будут окончательно удалены через {} дней", + "trash_page_info": "Объекты в корзине будут окончательно удалены через {days} дней", "trash_page_no_assets": "Корзина пуста", "trash_page_restore_all": "Восстановить все", "trash_page_select_assets_btn": "Выбранные объекты", - "trash_page_title": "Корзина ({})", + "trash_page_title": "Корзина ({count})", "trashed_items_will_be_permanently_deleted_after": "Элементы в корзине будут автоматически удалены через {days, plural, one {# день} other {# дней}}.", "type": "Тип", + "unable_to_change_pin_code": "Ошибка при изменении PIN-кода", + "unable_to_setup_pin_code": "Ошибка при создании PIN-кода", "unarchive": "Восстановить", "unarchived_count": "{count, plural, one {# объект возвращён} many {# объектов возвращено} other {# объекта возвращено}} из архива", "unfavorite": "Удалить из избранного", @@ -1828,6 +1847,7 @@ "untracked_files": "НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ", "untracked_files_decription": "Приложение не отслеживает эти файлы. Они могут быть результатом неудачных перемещений, прерванных загрузок или пропущены из-за ошибки", "up_next": "Следующее", + "updated_at": "Обновлён", "updated_password": "Пароль обновлён", "upload": "Загрузить", "upload_concurrency": "Параллельность загрузки", @@ -1840,15 +1860,18 @@ "upload_status_errors": "Ошибки", "upload_status_uploaded": "Загружено", "upload_success": "Загрузка прошла успешно. Обновите страницу, чтобы увидеть новые объекты.", - "upload_to_immich": "Загрузка в Immich ({})", + "upload_to_immich": "Загрузка в Immich ({count})", "uploading": "Загружается", "url": "URL", "usage": "Использование", "use_current_connection": "Использовать текущее подключение", "use_custom_date_range": "Использовать пользовательский диапазон дат", "user": "Пользователь", + "user_has_been_deleted": "Пользователь был удалён.", "user_id": "ID пользователя", "user_liked": "{user} отметил(а) {type, select, photo {это фото} video {это видео} asset {этот ресурс} other {этот альбом}}", + "user_pin_code_settings": "PIN-код", + "user_pin_code_settings_description": "Создание, изменение или удаление персонального PIN-кода для доступа к защищённым объектам", "user_purchase_settings": "Покупка", "user_purchase_settings_description": "Управление покупкой", "user_role_set": "Установить {user} в качестве {role}", diff --git a/i18n/sk.json b/i18n/sk.json index 912669f596..0b4c9b6b28 100644 --- a/i18n/sk.json +++ b/i18n/sk.json @@ -1368,7 +1368,7 @@ "public_share": "Verejné zdieľanie", "purchase_account_info": "Podporovateľ", "purchase_activated_subtitle": "Ďakujeme za podporu Immich a softvéru s otvorenými zdrojákmi", - "purchase_activated_time": "Aktivované {date, date}", + "purchase_activated_time": "Aktivované {date}", "purchase_activated_title": "Váš kľúč je úspešne aktivovaný", "purchase_button_activate": "Aktivovať", "purchase_button_buy": "Kúpiť", diff --git a/i18n/sl.json b/i18n/sl.json index 19e31871a6..22fa9b2ef7 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -53,6 +53,7 @@ "confirm_email_below": "Za potrditev vnesite \"{email}\" spodaj", "confirm_reprocess_all_faces": "Ali ste prepričani, da želite znova obdelati vse obraze? S tem boste počistili tudi že imenovane osebe.", "confirm_user_password_reset": "Ali ste prepričani, da želite ponastaviti geslo uporabnika {user}?", + "confirm_user_pin_code_reset": "Ali ste prepričani, da želite ponastaviti PIN kodo uporabnika {user}?", "create_job": "Ustvari opravilo", "cron_expression": "Nastavitveni izraz Cron", "cron_expression_description": "Nastavite interval skeniranja z uporabo zapisa cron. Za več informacij poglej npr. Crontab Guru", @@ -192,6 +193,7 @@ "oauth_auto_register": "Samodejna registracija", "oauth_auto_register_description": "Samodejna registracija novih uporabnikov po prijavi z OAuth", "oauth_button_text": "Besedilo gumba", + "oauth_client_secret_description": "Zahtevano, če ponudnik OAuth ne podpira PKCE (Proof Key for Code Exchange)", "oauth_enable_description": "Prijava z OAuth", "oauth_mobile_redirect_uri": "Mobilni preusmeritveni URI", "oauth_mobile_redirect_uri_override": "Preglasitev URI preusmeritve za mobilne naprave", @@ -205,6 +207,8 @@ "oauth_storage_quota_claim_description": "Samodejno nastavi uporabnikovo kvoto shranjevanja na vrednost tega zahtevka.", "oauth_storage_quota_default": "Privzeta kvota za shranjevanje (GiB)", "oauth_storage_quota_default_description": "Kvota v GiB, ki se uporabi, ko ni predložen noben zahtevek (vnesite 0 za neomejeno kvoto).", + "oauth_timeout": "Časovna omejitev zahteve", + "oauth_timeout_description": "Časovna omejitev za zahteve v milisekundah", "offline_paths": "Poti brez povezave", "offline_paths_description": "Ti rezultati so morda posledica ročnega brisanja datotek, ki niso del zunanje knjižnice.", "password_enable_description": "Prijava z e-pošto in geslom", @@ -289,7 +293,7 @@ "transcoding_bitrate_description": "Videoposnetki, ki presegajo največjo bitno hitrost ali niso v sprejemljivem formatu", "transcoding_codecs_learn_more": "Če želite izvedeti več o tukaj uporabljeni terminologiji, glejte dokumentacijo FFmpeg za kodek H.264, kodek HEVC in VP9 kodek.", "transcoding_constant_quality_mode": "Način stalne kakovosti", - "transcoding_constant_quality_mode_description": "ICQ je boljši od CQP, vendar nekatere naprave za pospeševanje strojne opreme ne podpirajo tega načina. Če nastavite to možnost, bo pri uporabi kodiranja na podlagi kakovosti izbran izbran način. NVENC ga ignorira, ker ne podpira ICQ.", + "transcoding_constant_quality_mode_description": "ICQ je boljši od CQP, vendar nekatere naprave za pospeševanje strojne opreme ne podpirajo tega načina. Če nastavite to možnost, bo pri uporabi kodiranja na podlagi kakovosti izbran način. NVENC ga ignorira, ker ne podpira ICQ.", "transcoding_constant_rate_factor": "Faktor konstantne stopnje (-crf)", "transcoding_constant_rate_factor_description": "Raven kakovosti videa. Tipične vrednosti so 23 za H.264, 28 za HEVC, 31 za VP9 in 35 za AV1. Nižje je boljše, vendar ustvarja večje datoteke.", "transcoding_disabled_description": "Ne prekodirajte nobenih videoposnetkov, lahko prekine predvajanje na nekaterih odjemalcih", @@ -345,6 +349,7 @@ "user_delete_delay_settings_description": "Število dni po odstranitvi za trajno brisanje uporabnikovega računa in sredstev. Opravilo za brisanje uporabnikov se izvaja ob polnoči, da se preveri, ali so uporabniki pripravljeni na izbris. Spremembe te nastavitve bodo ovrednotene pri naslednji izvedbi.", "user_delete_immediately": "Račun in sredstva uporabnika {user} bodo v čakalni vrsti za trajno brisanje takoj.", "user_delete_immediately_checkbox": "Uporabnika in sredstva postavite v čakalno vrsto za takojšnje brisanje", + "user_details": "Podrobnosti o uporabniku", "user_management": "Upravljanje uporabnikov", "user_password_has_been_reset": "Geslo uporabnika je bilo ponastavljeno:", "user_password_reset_description": "Uporabniku posredujte začasno geslo in ga obvestite, da bo moral ob naslednji prijavi spremeniti geslo.", @@ -366,7 +371,7 @@ "advanced": "Napredno", "advanced_settings_enable_alternate_media_filter_subtitle": "Uporabite to možnost za filtriranje medijev med sinhronizacijo na podlagi alternativnih meril. To poskusite le, če imate težave z aplikacijo, ki zaznava vse albume.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTALNO] Uporabite alternativni filter za sinhronizacijo albuma v napravi", - "advanced_settings_log_level_title": "Nivo dnevnika: {}", + "advanced_settings_log_level_title": "Nivo dnevnika: {level}", "advanced_settings_prefer_remote_subtitle": "Nekatere naprave zelo počasi nalagajo sličice iz sredstev v napravi. Aktivirajte to nastavitev, če želite namesto tega naložiti oddaljene slike.", "advanced_settings_prefer_remote_title": "Uporabi raje oddaljene slike", "advanced_settings_proxy_headers_subtitle": "Določi proxy glavo, ki jo naj Immich pošlje ob vsaki mrežni zahtevi", @@ -397,9 +402,9 @@ "album_remove_user_confirmation": "Ali ste prepričani, da želite odstraniti {user}?", "album_share_no_users": "Videti je, da ste ta album dali v skupno rabo z vsemi uporabniki ali pa nimate nobenega uporabnika, s katerim bi ga lahko delili.", "album_thumbnail_card_item": "1 element", - "album_thumbnail_card_items": "{} elementov", + "album_thumbnail_card_items": "{count} elementov", "album_thumbnail_card_shared": " · V skupni rabi", - "album_thumbnail_shared_by": "Delil {}", + "album_thumbnail_shared_by": "Delil {user}", "album_updated": "Album posodobljen", "album_updated_setting_description": "Prejmite e-poštno obvestilo, ko ima album v skupni rabi nova sredstva", "album_user_left": "Zapustil {album}", @@ -437,7 +442,7 @@ "archive": "Arhiv", "archive_or_unarchive_photo": "Arhivirajte ali odstranite fotografijo iz arhiva", "archive_page_no_archived_assets": "Ni arhiviranih sredstev", - "archive_page_title": "Arhiv ({})", + "archive_page_title": "Arhiv ({count})", "archive_size": "Velikost arhiva", "archive_size_description": "Konfigurirajte velikost arhiva za prenose (v GiB)", "archived": "Arhivirano", @@ -474,18 +479,18 @@ "assets_added_to_album_count": "Dodano {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v album", "assets_added_to_name_count": "Dodano {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v {hasName, select, true {{name}} other {new album}}", "assets_count": "{count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", - "assets_deleted_permanently": "trajno izrisana sredstva {}", - "assets_deleted_permanently_from_server": "trajno izbrisana sredstva iz strežnika Immich {}", + "assets_deleted_permanently": "trajno izrisana sredstva {count}", + "assets_deleted_permanently_from_server": "trajno izbrisana sredstva iz strežnika Immich {count}", "assets_moved_to_trash_count": "Premaknjeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v smetnjak", "assets_permanently_deleted_count": "Trajno izbrisano {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", "assets_removed_count": "Odstranjeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", - "assets_removed_permanently_from_device": "trajno odstranjena sredstva iz naprave {}", + "assets_removed_permanently_from_device": "trajno odstranjena sredstva iz naprave {count}", "assets_restore_confirmation": "Ali ste prepričani, da želite obnoviti vsa sredstva, ki ste jih odstranili? Tega dejanja ne morete razveljaviti! Upoštevajte, da sredstev brez povezave ni mogoče obnoviti na ta način.", "assets_restored_count": "Obnovljeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", - "assets_restored_successfully": "uspešno obnovljena sredstva {}", - "assets_trashed": "sredstva v smetnjaku {}", + "assets_restored_successfully": "uspešno obnovljena sredstva {count}", + "assets_trashed": "sredstva v smetnjaku {count}", "assets_trashed_count": "V smetnjak {count, plural, one {# sredstvo} other {# sredstva}}", - "assets_trashed_from_server": "sredstva iz strežnika Immich v smetnjaku {}", + "assets_trashed_from_server": "sredstva iz strežnika Immich v smetnjaku {count}", "assets_were_part_of_album_count": "{count, plural, one {sredstvo je} two {sredstvi sta} few {sredstva so} other {sredstev je}} že del albuma", "authorized_devices": "Pooblaščene naprave", "automatic_endpoint_switching_subtitle": "Povežite se lokalno prek določenega omrežja Wi-Fi, ko je na voljo, in uporabite druge povezave drugje", @@ -494,7 +499,7 @@ "back_close_deselect": "Nazaj, zaprite ali prekličite izbiro", "background_location_permission": "Dovoljenje za iskanje lokacije v ozadju", "background_location_permission_content": "Ko deluje v ozadju mora imeti Immich za zamenjavo omrežij, *vedno* dostop do natančne lokacije, da lahko aplikacija prebere ime omrežja Wi-Fi", - "backup_album_selection_page_albums_device": "Albumi v napravi ({number})", + "backup_album_selection_page_albums_device": "Albumi v napravi ({count})", "backup_album_selection_page_albums_tap": "Tapnite za vključitev, dvakrat tapnite za izključitev", "backup_album_selection_page_assets_scatter": "Sredstva so lahko razpršena po več albumih. Tako je mogoče med postopkom varnostnega kopiranja albume vključiti ali izključiti.", "backup_album_selection_page_select_albums": "Izberi albume", @@ -503,11 +508,11 @@ "backup_all": "Vse", "backup_background_service_backup_failed_message": "Varnostno kopiranje sredstev ni uspelo. Ponovno poskušam…", "backup_background_service_connection_failed_message": "Povezava s strežnikom ni uspela. Ponovno poskušam…", - "backup_background_service_current_upload_notification": "Nalagam {}", + "backup_background_service_current_upload_notification": "Nalagam {filename}", "backup_background_service_default_notification": "Preverjam za novimi sredstvi…", "backup_background_service_error_title": "Napaka varnostnega kopiranja", "backup_background_service_in_progress_notification": "Varnostno kopiranje vaših sredstev…", - "backup_background_service_upload_failure_notification": "Nalaganje {} ni uspelo", + "backup_background_service_upload_failure_notification": "Nalaganje {filename} ni uspelo", "backup_controller_page_albums": "Varnostno kopiranje albumov", "backup_controller_page_background_app_refresh_disabled_content": "Omogočite osveževanje aplikacij v ozadju v Nastavitve > Splošno > Osvežitev aplikacij v ozadju, če želite uporabiti varnostno kopiranje v ozadju.", "backup_controller_page_background_app_refresh_disabled_title": "Osveževanje aplikacije v ozadju je onemogočeno", @@ -518,7 +523,7 @@ "backup_controller_page_background_battery_info_title": "Optimizacije baterije", "backup_controller_page_background_charging": "Samo med polnjenjem", "backup_controller_page_background_configure_error": "Storitve v ozadju ni bilo mogoče nastaviti", - "backup_controller_page_background_delay": "Zakasni varnostno kopiranje novih sredstev: {}", + "backup_controller_page_background_delay": "Zakasni varnostno kopiranje novih sredstev: {duration}", "backup_controller_page_background_description": "Vklopite storitev v ozadju za samodejno varnostno kopiranje novih sredstev, ne da bi morali odpreti aplikacijo", "backup_controller_page_background_is_off": "Samodejno varnostno kopiranje v ozadju je izklopljeno", "backup_controller_page_background_is_on": "Samodejno varnostno kopiranje v ozadju je vklopljeno", @@ -528,12 +533,12 @@ "backup_controller_page_backup": "Varnostna kopija", "backup_controller_page_backup_selected": "Izbrano: ", "backup_controller_page_backup_sub": "Varnostno kopirane fotografije in videoposnetki", - "backup_controller_page_created": "Ustvarjeno: {}", + "backup_controller_page_created": "Ustvarjeno: {date}", "backup_controller_page_desc_backup": "Vklopite varnostno kopiranje v ospredju za samodejno nalaganje novih sredstev na strežnik, ko odprete aplikacijo.", "backup_controller_page_excluded": "Izključeno: ", - "backup_controller_page_failed": "Neuspešno ({})", - "backup_controller_page_filename": "Ime datoteke: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Neuspešno ({count})", + "backup_controller_page_filename": "Ime datoteke: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Informacija o varnostnem kopiranju", "backup_controller_page_none_selected": "Noben izbran", "backup_controller_page_remainder": "Ostanek", @@ -542,7 +547,7 @@ "backup_controller_page_start_backup": "Zaženi varnostno kopiranje", "backup_controller_page_status_off": "Samodejno varnostno kopiranje v ospredju je izklopljeno", "backup_controller_page_status_on": "Samodejno varnostno kopiranje v ospredju je vklopljeno", - "backup_controller_page_storage_format": "Uporabljeno {} od {}", + "backup_controller_page_storage_format": "Uporabljeno {used} od {total}", "backup_controller_page_to_backup": "Albumi, ki bodo varnostno kopirani", "backup_controller_page_total_sub": "Vse edinstvene fotografije in videi iz izbranih albumov", "backup_controller_page_turn_off": "Izklopite varnostno kopiranje v ospredju", @@ -567,21 +572,21 @@ "bulk_keep_duplicates_confirmation": "Ali ste prepričani, da želite obdržati {count, plural, one {# dvojnik} two {# dvojnika} few {# dvojnike} other {# dvojnikov}}? S tem boste razrešili vse podvojene skupine, ne da bi karkoli izbrisali.", "bulk_trash_duplicates_confirmation": "Ali ste prepričani, da želite množično vreči v smetnjak {count, plural, one {# dvojnik} two {# dvojnika} few {# dvojnike} other {# dvojnikov}}? S tem boste obdržali največje sredstvo vsake skupine in odstranili vse druge dvojnike.", "buy": "Kupi Immich", - "cache_settings_album_thumbnails": "Sličice strani knjižnice ({} sredstev)", + "cache_settings_album_thumbnails": "Sličice strani knjižnice ({count} sredstev)", "cache_settings_clear_cache_button": "Počisti predpomnilnik", "cache_settings_clear_cache_button_title": "Počisti predpomnilnik aplikacije. To bo znatno vplivalo na delovanje aplikacije, dokler se predpomnilnik ne obnovi.", "cache_settings_duplicated_assets_clear_button": "POČISTI", "cache_settings_duplicated_assets_subtitle": "Fotografije in videoposnetki, ki jih je aplikacija uvrstila na črni seznam", - "cache_settings_duplicated_assets_title": "Podvojena sredstva ({})", - "cache_settings_image_cache_size": "Velikost predpomnilnika slik ({} sredstev)", + "cache_settings_duplicated_assets_title": "Podvojena sredstva ({count})", + "cache_settings_image_cache_size": "Velikost predpomnilnika slik ({count} sredstev)", "cache_settings_statistics_album": "Sličice knjižnice", - "cache_settings_statistics_assets": "{} sredstva ({})", + "cache_settings_statistics_assets": "{count} sredstva ({size})", "cache_settings_statistics_full": "Izvirne slike", "cache_settings_statistics_shared": "Sličice albuma v skupni rabi", "cache_settings_statistics_thumbnail": "Sličice", "cache_settings_statistics_title": "Uporaba predpomnilnika", "cache_settings_subtitle": "Nadzirajte delovanje predpomnjenja mobilne aplikacije Immich", - "cache_settings_thumbnail_size": "Velikost predpomnilnika sličic ({} sredstev)", + "cache_settings_thumbnail_size": "Velikost predpomnilnika sličic ({count} sredstev)", "cache_settings_tile_subtitle": "Nadzoruj vedenje lokalnega shranjevanja", "cache_settings_tile_title": "Lokalna shramba", "cache_settings_title": "Nastavitve predpomnjenja", @@ -607,6 +612,7 @@ "change_password_form_new_password": "Novo geslo", "change_password_form_password_mismatch": "Gesli se ne ujemata", "change_password_form_reenter_new_password": "Znova vnesi novo geslo", + "change_pin_code": "Spremeni PIN kodo", "change_your_password": "Spremenite geslo", "changed_visibility_successfully": "Uspešno spremenjena vidnost", "check_all": "Označite vse", @@ -647,16 +653,17 @@ "confirm_delete_face": "Ali ste prepričani, da želite izbrisati obraz osebe {name} iz sredstva?", "confirm_delete_shared_link": "Ali ste prepričani, da želite izbrisati to skupno povezavo?", "confirm_keep_this_delete_others": "Vsa druga sredstva v skladu bodo izbrisana, razen tega sredstva. Ste prepričani, da želite nadaljevati?", + "confirm_new_pin_code": "Potrdi novo PIN kodo", "confirm_password": "Potrdi geslo", "contain": "Vsebuje", "context": "Kontekst", "continue": "Nadaljuj", - "control_bottom_app_bar_album_info_shared": "{} elementov · V skupni rabi", + "control_bottom_app_bar_album_info_shared": "{count} elementov · V skupni rabi", "control_bottom_app_bar_create_new_album": "Ustvari nov album", "control_bottom_app_bar_delete_from_immich": "Izbriši iz Immicha", "control_bottom_app_bar_delete_from_local": "Izbriši iz naprave", "control_bottom_app_bar_edit_location": "Uredi lokacijo", - "control_bottom_app_bar_edit_time": "Uredi datum in uro", + "control_bottom_app_bar_edit_time": "Uredi datum & uro", "control_bottom_app_bar_share_link": "Deli povezavo", "control_bottom_app_bar_share_to": "Deli s/z", "control_bottom_app_bar_trash_from_immich": "Prestavi v smetnjak", @@ -689,9 +696,11 @@ "create_tag_description": "Ustvarite novo oznako. Za ugnezdene oznake vnesite celotno pot oznake, vključno s poševnicami.", "create_user": "Ustvari uporabnika", "created": "Ustvarjeno", + "created_at": "Ustvarjeno", "crop": "Obrezovanje", "curated_object_page_title": "Stvari", "current_device": "Trenutna naprava", + "current_pin_code": "Trenutna PIN koda", "current_server_address": "Trenutni naslov strežnika", "custom_locale": "Jezik po meri", "custom_locale_description": "Oblikujte datume in številke glede na jezik in regijo", @@ -760,7 +769,7 @@ "download_enqueue": "Prenos v čakalni vrsti", "download_error": "Napaka pri prenosu", "download_failed": "Prenos ni uspel", - "download_filename": "datoteka: {}", + "download_filename": "datoteka: {filename}", "download_finished": "Prenos zaključen", "download_include_embedded_motion_videos": "Vdelani videoposnetki", "download_include_embedded_motion_videos_description": "Videoposnetke, vdelane v fotografije gibanja, vključite kot ločeno datoteko", @@ -804,6 +813,7 @@ "editor_crop_tool_h2_aspect_ratios": "Razmerja stranic", "editor_crop_tool_h2_rotation": "Vrtenje", "email": "E-pošta", + "email_notifications": "Obvestila po e-pošti", "empty_folder": "Ta mapa je prazna", "empty_trash": "Izprazni smetnjak", "empty_trash_confirmation": "Ste prepričani, da želite izprazniti smetnjak? S tem boste iz Immicha trajno odstranili vsa sredstva v smetnjaku.\nTega dejanja ne morete razveljaviti!", @@ -811,12 +821,12 @@ "enabled": "Omogočeno", "end_date": "Končni datum", "enqueued": "V čakalni vrsti", - "enter_wifi_name": "Vnesi WiFi ime", + "enter_wifi_name": "Vnesi Wi-Fi ime", "error": "Napaka", "error_change_sort_album": "Vrstnega reda albuma ni bilo mogoče spremeniti", "error_delete_face": "Napaka pri brisanju obraza iz sredstva", "error_loading_image": "Napaka pri nalaganju slike", - "error_saving_image": "Napaka: {}", + "error_saving_image": "Napaka: {error}", "error_title": "Napaka - nekaj je šlo narobe", "errors": { "cannot_navigate_next_asset": "Ni mogoče krmariti do naslednjega sredstva", @@ -846,10 +856,12 @@ "failed_to_keep_this_delete_others": "Tega sredstva ni bilo mogoče obdržati in izbrisati ostalih sredstev", "failed_to_load_asset": "Sredstva ni bilo mogoče naložiti", "failed_to_load_assets": "Sredstev ni bilo mogoče naložiti", + "failed_to_load_notifications": "Nalaganje obvestil ni uspelo", "failed_to_load_people": "Oseb ni bilo mogoče naložiti", "failed_to_remove_product_key": "Ključa izdelka ni bilo mogoče odstraniti", "failed_to_stack_assets": "Zlaganje sredstev ni uspelo", "failed_to_unstack_assets": "Sredstev ni bilo mogoče razložiti", + "failed_to_update_notification_status": "Stanja obvestila ni bilo mogoče posodobiti", "import_path_already_exists": "Ta uvozna pot že obstaja.", "incorrect_email_or_password": "Napačen e-poštni naslov ali geslo", "paths_validation_failed": "{paths, plural, one {# pot ni bila uspešno preverjena} two {# poti nista bili uspešno preverjeni} few {# poti niso bile uspešno preverjene} other {# poti ni bilo uspešno preverjenih}}", @@ -917,6 +929,7 @@ "unable_to_remove_reaction": "Reakcije ni mogoče odstraniti", "unable_to_repair_items": "Elementov ni mogoče popraviti", "unable_to_reset_password": "Gesla ni mogoče ponastaviti", + "unable_to_reset_pin_code": "PIN kode ni mogoče ponastaviti", "unable_to_resolve_duplicate": "Dvojnika ni mogoče razrešiti", "unable_to_restore_assets": "Sredstev ni mogoče obnoviti", "unable_to_restore_trash": "Smetnjaka ni mogoče obnoviti", @@ -950,10 +963,10 @@ "exif_bottom_sheet_location": "LOKACIJA", "exif_bottom_sheet_people": "OSEBE", "exif_bottom_sheet_person_add_person": "Dodaj ime", - "exif_bottom_sheet_person_age": "Starost {count, plural, one {# leto} two {# leti} few {# leta} other {# let}}", - "exif_bottom_sheet_person_age_months": "Starost {months, plural, one {# mesec} two {# meseca} few {# mesece} other {# mesecev}}", - "exif_bottom_sheet_person_age_year_months": "Starost 1 leto, {months, plural, one {# mesec} two {# meseca} few {# mesece} other {# mesecev}}", - "exif_bottom_sheet_person_age_years": "Starost {years, plural, one {# leto} two {# leti} few {# leta} other {# let}}", + "exif_bottom_sheet_person_age": "Starost {age}", + "exif_bottom_sheet_person_age_months": "Staros {months} mesecev", + "exif_bottom_sheet_person_age_year_months": "Starost 1 leto, {months} mesecev", + "exif_bottom_sheet_person_age_years": "Starost {years}", "exit_slideshow": "Zapustite diaprojekcijo", "expand_all": "Razširi vse", "experimental_settings_new_asset_list_subtitle": "Delo v teku", @@ -1043,6 +1056,7 @@ "home_page_upload_err_limit": "Hkrati lahko naložite največ 30 sredstev, preskakujem", "host": "Gostitelj", "hour": "Ura", + "id": "ID", "ignore_icloud_photos": "Ignoriraj fotografije iCloud", "ignore_icloud_photos_description": "Fotografije, shranjene v iCloud, ne bodo naložene na strežnik Immich", "image": "Slika", @@ -1066,7 +1080,7 @@ "import_path": "Pot uvoza", "in_albums": "V {count, plural, one {# album} two {# albuma} few {# albume} other {# albumov}}", "in_archive": "V arhiv", - "include_archived": "Vključi arhivirane", + "include_archived": "Vključi arhivirano", "include_shared_albums": "Vključite skupne albume", "include_shared_partner_assets": "Vključite partnerjeva skupna sredstva", "individual_share": "Samostojna delitev", @@ -1168,8 +1182,8 @@ "manage_your_devices": "Upravljajte svoje prijavljene naprave", "manage_your_oauth_connection": "Upravljajte svojo OAuth povezavo", "map": "Zemljevid", - "map_assets_in_bound": "{} slika", - "map_assets_in_bounds": "{} slik", + "map_assets_in_bound": "{count} slika", + "map_assets_in_bounds": "{count} slik", "map_cannot_get_user_location": "Lokacije uporabnika ni mogoče dobiti", "map_location_dialog_yes": "Da", "map_location_picker_page_use_location": "Uporabi to lokacijo", @@ -1183,15 +1197,18 @@ "map_settings": "Nastavitve zemljevida", "map_settings_dark_mode": "Temni način", "map_settings_date_range_option_day": "Zadnjih 24 ur", - "map_settings_date_range_option_days": "Zadnjih {} dni", + "map_settings_date_range_option_days": "Zadnjih {days} dni", "map_settings_date_range_option_year": "Zadnje leto", - "map_settings_date_range_option_years": "Zadnjih {} let", + "map_settings_date_range_option_years": "Zadnjih {years} let", "map_settings_dialog_title": "Nastavitve zemljevida", "map_settings_include_show_archived": "Vključi arhivirane", "map_settings_include_show_partners": "Vključi partnerjeve", "map_settings_only_show_favorites": "Pokaži samo priljubljene", "map_settings_theme_settings": "Tema zemljevida", "map_zoom_to_see_photos": "Pomanjšajte za ogled fotografij", + "mark_all_as_read": "Označi vse kot prebrano", + "mark_as_read": "Označi kot prebrano", + "marked_all_as_read": "Označeno vse kot prebrano", "matches": "Ujemanja", "media_type": "Vrsta medija", "memories": "Spomini", @@ -1201,7 +1218,7 @@ "memories_start_over": "Začni od začetka", "memories_swipe_to_close": "Podrsaj gor za zapiranje", "memories_year_ago": "Leto dni nazaj", - "memories_years_ago": "{} let nazaj", + "memories_years_ago": "{years, plural, two {# leti} few {# leta} other {# let}} nazaj", "memory": "Spomin", "memory_lane_title": "Spominski trak {title}", "menu": "Meni", @@ -1218,6 +1235,8 @@ "month": "Mesec", "monthly_title_text_date_format": "MMMM y", "more": "Več", + "moved_to_archive": "Premaknjeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v arhiv", + "moved_to_library": "Premaknjeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v knjižnico", "moved_to_trash": "Premaknjeno v smetnjak", "multiselect_grid_edit_date_time_err_read_only": "Ni mogoče urediti datuma sredstev samo za branje, preskočim", "multiselect_grid_edit_gps_err_read_only": "Ni mogoče urediti lokacije sredstev samo za branje, preskočim", @@ -1232,6 +1251,7 @@ "new_api_key": "Nov API ključ", "new_password": "Novo geslo", "new_person": "Nova oseba", + "new_pin_code": "Nova PIN koda", "new_user_created": "Nov uporabnik ustvarjen", "new_version_available": "NA VOLJO JE NOVA RAZLIČICA", "newest_first": "Najprej najnovejše", @@ -1250,6 +1270,8 @@ "no_favorites_message": "Dodajte priljubljene, da hitreje najdete svoje najboljše slike in videoposnetke", "no_libraries_message": "Ustvarite zunanjo knjižnico za ogled svojih fotografij in videoposnetkov", "no_name": "Brez imena", + "no_notifications": "Ni obvestil", + "no_people_found": "Ni najdenih ustreznih oseb", "no_places": "Ni krajev", "no_results": "Brez rezultatov", "no_results_description": "Poskusite s sinonimom ali bolj splošno ključno besedo", @@ -1304,7 +1326,7 @@ "partner_page_partner_add_failed": "Partnerja ni bilo mogoče dodati", "partner_page_select_partner": "Izberi partnerja", "partner_page_shared_to_title": "V skupni rabi z", - "partner_page_stop_sharing_content": "{} ne bo imel več dostopa do vaših fotografij.", + "partner_page_stop_sharing_content": "{partner} ne bo imel več dostopa do vaših fotografij.", "partner_sharing": "Skupna raba s partnerjem", "partners": "Partnerji", "password": "Geslo", @@ -1350,6 +1372,9 @@ "photos_count": "{count, plural, one {{count, number} slika} two {{count, number} sliki} few {{count, number} slike} other {{count, number} slik}}", "photos_from_previous_years": "Fotografije iz prejšnjih let", "pick_a_location": "Izberi lokacijo", + "pin_code_changed_successfully": "PIN koda je bila uspešno spremenjena", + "pin_code_reset_successfully": "PIN koda je bila uspešno ponastavljena", + "pin_code_setup_successfully": "Uspešno nastavljena PIN koda", "place": "Lokacija", "places": "Lokacije", "places_count": "{count, plural, one {{count, number} kraj} two {{count, number} kraja} few {{count, number} kraji} other {{count, number} krajev}}", @@ -1367,6 +1392,7 @@ "previous_or_next_photo": "Prejšnja ali naslednja fotografija", "primary": "Primarni", "privacy": "Zasebnost", + "profile": "Profil", "profile_drawer_app_logs": "Dnevniki", "profile_drawer_client_out_of_date_major": "Mobilna aplikacija je zastarela. Posodobite na najnovejšo glavno različico.", "profile_drawer_client_out_of_date_minor": "Mobilna aplikacija je zastarela. Posodobite na najnovejšo manjšo različico.", @@ -1380,7 +1406,7 @@ "public_share": "Javno deljenje", "purchase_account_info": "Podpornik", "purchase_activated_subtitle": "Hvala, ker podpirate Immich in odprtokodno programsko opremo", - "purchase_activated_time": "Aktivirano {date, date}", + "purchase_activated_time": "Aktivirano {date}", "purchase_activated_title": "Vaš ključ je bil uspešno aktiviran", "purchase_button_activate": "Aktiviraj", "purchase_button_buy": "Kupi", @@ -1425,7 +1451,7 @@ "recent_searches": "Nedavna iskanja", "recently_added": "Nedavno dodano", "recently_added_page_title": "Nedavno dodano", - "recently_taken": "Nedavno uporabljen", + "recently_taken": "Nedavno posneto", "recently_taken_page_title": "Nedavno Uporabljen", "refresh": "Osveži", "refresh_encoded_videos": "Osveži kodirane videoposnetke", @@ -1469,6 +1495,7 @@ "reset": "Ponastavi", "reset_password": "Ponastavi geslo", "reset_people_visibility": "Ponastavi vidnost ljudi", + "reset_pin_code": "Ponastavi PIN kodo", "reset_to_default": "Ponastavi na privzeto", "resolve_duplicates": "Razreši dvojnike", "resolved_all_duplicates": "Razrešeni vsi dvojniki", @@ -1561,6 +1588,7 @@ "select_keep_all": "Izberi obdrži vse", "select_library_owner": "Izberi lastnika knjižnice", "select_new_face": "Izberi nov obraz", + "select_person_to_tag": "Izberite osebo, ki jo želite označiti", "select_photos": "Izberi fotografije", "select_trash_all": "Izberi vse v smetnjak", "select_user_for_sharing_page_err_album": "Albuma ni bilo mogoče ustvariti", @@ -1591,12 +1619,12 @@ "setting_languages_apply": "Uporabi", "setting_languages_subtitle": "Spremeni jezik aplikacije", "setting_languages_title": "Jeziki", - "setting_notifications_notify_failures_grace_period": "Obvesti o napakah varnostnega kopiranja v ozadju: {}", - "setting_notifications_notify_hours": "{} ur", + "setting_notifications_notify_failures_grace_period": "Obvesti o napakah varnostnega kopiranja v ozadju: {duration}", + "setting_notifications_notify_hours": "{count} ur", "setting_notifications_notify_immediately": "takoj", - "setting_notifications_notify_minutes": "{} minut", + "setting_notifications_notify_minutes": "{count} minut", "setting_notifications_notify_never": "nikoli", - "setting_notifications_notify_seconds": "{} sekund", + "setting_notifications_notify_seconds": "{count} sekund", "setting_notifications_single_progress_subtitle": "Podrobne informacije o napredku nalaganja po sredstvih", "setting_notifications_single_progress_title": "Pokaži napredek varnostnega kopiranja v ozadju", "setting_notifications_subtitle": "Prilagodite svoje nastavitve obvestil", @@ -1608,9 +1636,10 @@ "settings": "Nastavitve", "settings_require_restart": "Znova zaženite Immich, da uporabite to nastavitev", "settings_saved": "Nastavitve shranjene", + "setup_pin_code": "Nastavi PIN kodo", "share": "Deli", "share_add_photos": "Dodaj fotografije", - "share_assets_selected": "{} izbrano", + "share_assets_selected": "{count} izbrano", "share_dialog_preparing": "Priprava...", "shared": "V skupni rabi", "shared_album_activities_input_disable": "Komentiranje je onemogočeno", @@ -1624,32 +1653,32 @@ "shared_by_user": "Skupna raba s/z {user}", "shared_by_you": "Deliš", "shared_from_partner": "Fotografije od {partner}", - "shared_intent_upload_button_progress_text": "{} / {} naloženo", + "shared_intent_upload_button_progress_text": "{current} / {total} naloženo", "shared_link_app_bar_title": "Povezave v skupni rabi", "shared_link_clipboard_copied_massage": "Kopirano v odložišče", - "shared_link_clipboard_text": "Povezava: {}\nGeslo: {}", + "shared_link_clipboard_text": "Povezava: {link}\nGeslo: {password}", "shared_link_create_error": "Napaka pri ustvarjanju povezave skupne rabe", "shared_link_edit_description_hint": "Vnesi opis skupne rabe", "shared_link_edit_expire_after_option_day": "1 dan", - "shared_link_edit_expire_after_option_days": "{} dni", + "shared_link_edit_expire_after_option_days": "{count} dni", "shared_link_edit_expire_after_option_hour": "1 ura", - "shared_link_edit_expire_after_option_hours": "{} ur", + "shared_link_edit_expire_after_option_hours": "{count} ur", "shared_link_edit_expire_after_option_minute": "1 minuta", - "shared_link_edit_expire_after_option_minutes": "{} minut", - "shared_link_edit_expire_after_option_months": "{} mesecev", - "shared_link_edit_expire_after_option_year": "{} let", + "shared_link_edit_expire_after_option_minutes": "{count} minut", + "shared_link_edit_expire_after_option_months": "{count} mesecev", + "shared_link_edit_expire_after_option_year": "{count} let", "shared_link_edit_password_hint": "Vnesi geslo za skupno rabo", "shared_link_edit_submit_button": "Posodobi povezavo", "shared_link_error_server_url_fetch": "URL-ja strežnika ni mogoče pridobiti", - "shared_link_expires_day": "Poteče čez {} dan", - "shared_link_expires_days": "Poteče čez {} dni", - "shared_link_expires_hour": "Poteče čez {} uro", - "shared_link_expires_hours": "Poteče čez {} ur", - "shared_link_expires_minute": "Poteče čez {} minuto", - "shared_link_expires_minutes": "Poteče čez {} minut", + "shared_link_expires_day": "Poteče čez {count} dan", + "shared_link_expires_days": "Poteče čez {count} dni", + "shared_link_expires_hour": "Poteče čez {count} uro", + "shared_link_expires_hours": "Poteče čez {count} ur", + "shared_link_expires_minute": "Poteče čez {count} minuto", + "shared_link_expires_minutes": "Poteče čez {count} minut", "shared_link_expires_never": "Poteče ∞", - "shared_link_expires_second": "Poteče čez {} sekundo", - "shared_link_expires_seconds": "Poteče čez {} sekund", + "shared_link_expires_second": "Poteče čez {count} sekundo", + "shared_link_expires_seconds": "Poteče čez {count} sekund", "shared_link_individual_shared": "Individualno deljeno", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Upravljanje povezav v skupni rabi", @@ -1724,6 +1753,7 @@ "stop_sharing_photos_with_user": "Prenehaj deliti svoje fotografije s tem uporabnikom", "storage": "Prostor za shranjevanje", "storage_label": "Oznaka za shranjevanje", + "storage_quota": "Kvota shranjevanja", "storage_usage": "uporabljeno {used} od {available}", "submit": "Predloži", "suggestions": "Predlogi", @@ -1750,7 +1780,7 @@ "theme_selection": "Izbira teme", "theme_selection_description": "Samodejno nastavi temo na svetlo ali temno glede na sistemske nastavitve brskalnika", "theme_setting_asset_list_storage_indicator_title": "Pokaži indikator shrambe na ploščicah sredstev", - "theme_setting_asset_list_tiles_per_row_title": "Število sredstev na vrstico ({})", + "theme_setting_asset_list_tiles_per_row_title": "Število sredstev na vrstico ({count})", "theme_setting_colorful_interface_subtitle": "Nanesi primarno barvo na površine ozadja.", "theme_setting_colorful_interface_title": "Barvit vmesnik", "theme_setting_image_viewer_quality_subtitle": "Prilagodite kakovost podrobnega pregledovalnika slik", @@ -1785,13 +1815,15 @@ "trash_no_results_message": "Fotografije in videoposnetki, ki so v smetnjaku, bodo prikazani tukaj.", "trash_page_delete_all": "Izbriši vse", "trash_page_empty_trash_dialog_content": "Ali želite izprazniti svoja sredstva v smeti? Ti elementi bodo trajno odstranjeni iz Immicha", - "trash_page_info": "Elementi v smeteh bodo trajno izbrisani po {} dneh", + "trash_page_info": "Elementi v smeteh bodo trajno izbrisani po {days} dneh", "trash_page_no_assets": "Ni sredstev v smeteh", "trash_page_restore_all": "Obnovi vse", "trash_page_select_assets_btn": "Izberite sredstva", - "trash_page_title": "Smetnjak ({})", + "trash_page_title": "Smetnjak ({count})", "trashed_items_will_be_permanently_deleted_after": "Elementi v smetnjaku bodo trajno izbrisani po {days, plural, one {# dnevu} two {# dnevih} few {# dnevih} other {# dneh}}.", "type": "Vrsta", + "unable_to_change_pin_code": "PIN kode ni mogoče spremeniti", + "unable_to_setup_pin_code": "PIN kode ni mogoče nastaviti", "unarchive": "Odstrani iz arhiva", "unarchived_count": "{count, plural, other {nearhiviranih #}}", "unfavorite": "Odznači priljubljeno", @@ -1815,6 +1847,7 @@ "untracked_files": "Nesledene datoteke", "untracked_files_decription": "Tem datotekam aplikacija ne sledi. Lahko so posledica neuspelih premikov, prekinjenih ali zaostalih nalaganj zaradi hrošča", "up_next": "Naslednja", + "updated_at": "Posodobljeno", "updated_password": "Posodobljeno geslo", "upload": "Naloži", "upload_concurrency": "Sočasnost nalaganja", @@ -1827,15 +1860,18 @@ "upload_status_errors": "Napake", "upload_status_uploaded": "Naloženo", "upload_success": "Nalaganje je uspelo, osvežite stran, da vidite nova sredstva za nalaganje.", - "upload_to_immich": "Naloži v Immich ({})", + "upload_to_immich": "Naloži v Immich ({count})", "uploading": "Nalagam", "url": "URL", "usage": "Uporaba", "use_current_connection": "uporabi trenutno povezavo", "use_custom_date_range": "Namesto tega uporabite časovno obdobje po meri", "user": "Uporabnik", + "user_has_been_deleted": "Ta uporabnik je bil izbrisan.", "user_id": "ID uporabnika", "user_liked": "{user} je všeč {type, select, photo {ta fotografija} video {ta video} asset {to sredstvo} other {to}}", + "user_pin_code_settings": "PIN koda", + "user_pin_code_settings_description": "Upravljaj svojo PIN kodo", "user_purchase_settings": "Nakup", "user_purchase_settings_description": "Upravljajte svoj nakup", "user_role_set": "Nastavi {user} kot {role}", @@ -1884,11 +1920,11 @@ "week": "Teden", "welcome": "Dobrodošli", "welcome_to_immich": "Dobrodošli v Immich", - "wifi_name": "WiFi ime", + "wifi_name": "Wi-Fi ime", "year": "Leto", "years_ago": "{years, plural, one {# leto} two {# leti} few {# leta} other {# let}} nazaj", "yes": "Da", "you_dont_have_any_shared_links": "Nimate nobenih skupnih povezav", - "your_wifi_name": "Vaše ime WiFi", + "your_wifi_name": "Vaše ime Wi-Fi", "zoom_image": "Povečava slike" } diff --git a/i18n/sr_Cyrl.json b/i18n/sr_Cyrl.json index 34fe5084e6..a7d0e6e44d 100644 --- a/i18n/sr_Cyrl.json +++ b/i18n/sr_Cyrl.json @@ -1,14 +1,14 @@ { "about": "О апликацији", - "account": "Профил", + "account": "Налог", "account_settings": "Подешавања за Профил", "acknowledge": "Потврди", "action": "Поступак", - "action_common_update": "Update", + "action_common_update": "Упdate", "actions": "Поступци", "active": "Активни", "activity": "Активност", - "activity_changed": "Активност је {enabled, select, true {омогућена} other {oneмогућена}}", + "activity_changed": "Активност је {enabled, select, true {омогуц́ена} other {oneмогуц́ена}}", "add": "Додај", "add_a_description": "Додај опис", "add_a_location": "Додај Локацију", @@ -32,148 +32,149 @@ "added_to_favorites": "Додато у фаворите", "added_to_favorites_count": "Додато {count, number} у фаворите", "admin": { - "add_exclusion_pattern_description": "Додајте обрасце искључења. Кориштење *, ** и ? је подржано. Да бисте игнорисали све датотеке у било ком директоријуму под називом „Рав“, користите „**/Рав/**“. Да бисте игнорисали све датотеке које се завршавају на „.тиф“, користите „**/*.тиф“. Да бисте игнорисали апсолутну путању, користите „/path/to/ignore/**“.", - "asset_offline_description": "Ово екстерно библиотечко средство се више не налази на диску и премештено је у смеће. Ако је датотека премештена унутар библиотеке, проверите своју временску линију за ново одговарајуће средство. Да бисте вратили ово средство, уверите се да Иммицх може да приступи доле наведеној путањи датотеке и скенирајте библиотеку.", + "add_exclusion_pattern_description": "Додајте обрасце искључења. Кориштење *, ** и ? је подржано. Да бисте игнорисали све датотеке у било ком директоријуму под називом „Рав“, користите „**/Рав/**“. Да бисте игнорисали све датотеке које се завршавају на „.тиф“, користите „**/*.тиф“. Да бисте игнорисали апсолутну путању, користите „/патх/то/игноре/**“.", + "asset_offline_description": "Ово екстерно библиотечко средство се више не налази на диску и премештено је у смец́е. Ако је датотека премештена унутар библиотеке, проверите своју временску линију за ново одговарајуц́е средство. Да бисте вратили ово средство, уверите се да Immich може да приступи доле наведеној путањи датотеке и скенирајте библиотеку.", "authentication_settings": "Подешавања за аутентификацију", - "authentication_settings_description": "Управљајте лозинком, OAuth-om и другим подешавањима аутентификације", - "authentication_settings_disable_all": "Да ли сте сигурни да желите да oneмогућите све методе пријављивања? Пријава ће бити потпуно oneмогућена.", - "authentication_settings_reenable": "Да бисте поново омогућили, користите команду сервера.", + "authentication_settings_description": "Управљајте лозинком, OAuth-ом и другим подешавањима аутентификације", + "authentication_settings_disable_all": "Да ли сте сигурни да желите да oneмогуц́ите све методе пријављивања? Пријава ц́е бити потпуно oneмогуц́ена.", + "authentication_settings_reenable": "Да бисте поново омогуц́или, користите команду сервера.", "background_task_job": "Позадински задаци", - "backup_database": "Резервна копија базе података", - "backup_database_enable_description": "Омогућите резервне копије базе података", - "backup_keep_last_amount": "Количина претходних резервних копија за чување", - "backup_settings": "Подешавања резервне копије", - "backup_settings_description": "Управљајте поставкама резервне копије базе података", + "backup_database": "Креирајте резервну копију базе података", + "backup_database_enable_description": "Омогуц́и дампове базе података", + "backup_keep_last_amount": "Количина претходних дампова које треба задржати", + "backup_settings": "Подешавања дампа базе података", + "backup_settings_description": "Управљајте подешавањима дампа базе података. Напомена: Ови послови се не прате и нец́ете бити обавештени о неуспеху.", "check_all": "Провери све", - "cleanup": "Чишћење", - "cleared_jobs": "Очишћени послови за {job}", + "cleanup": "Чишц́ење", + "cleared_jobs": "Очишц́ени послови за: {job}", "config_set_by_file": "Конфигурацију тренутно поставља конфигурациони фајл", "confirm_delete_library": "Да ли стварно желите да избришете библиотеку {library} ?", - "confirm_delete_library_assets": "Да ли сте сигурни да желите да избришете ову библиотеку? Ово ће избрисати {count, plural, one {1 садржену датотеку} few {# садржене датотеке} other {# садржених датотека}} из Immich-a и акција се не може опозвати. Датотеке ће остати на диску.", + "confirm_delete_library_assets": "Да ли сте сигурни да желите да избришете ову библиотеку? Ово ц́е избрисати {count, plural, one {1 садржену датотеку} few {# садржене датотеке} other {# садржених датотека}} из Immich-a и акција се не може опозвати. Датотеке ц́е остати на диску.", "confirm_email_below": "Да бисте потврдили, унесите \"{email}\" испод", - "confirm_reprocess_all_faces": "Да ли сте сигурни да желите да поново обрадите сва лица? Ово ће такође обрисати именоване особе.", + "confirm_reprocess_all_faces": "Да ли сте сигурни да желите да поново обрадите сва лица? Ово ц́е такође обрисати именоване особе.", "confirm_user_password_reset": "Да ли сте сигурни да желите да ресетујете лозинку корисника {user}?", + "confirm_user_pin_code_reset": "Да ли сте сигурни да желите да ресетујете ПИН код корисника {user}?", "create_job": "Креирајте посао", - "cron_expression": "Cron израз (expression)", - "cron_expression_description": "Подесите интервал скенирања користећи cron формат. За више информација погледајте нпр. Crontab Guru", - "cron_expression_presets": "Предефинисана подешавања Cron израза (expression)", - "disable_login": "oneмогући пријаву", + "cron_expression": "Црон израз (еxпрессион)", + "cron_expression_description": "Подесите интервал скенирања користец́и црон формат. За више информација погледајте нпр. Цронтаб Гуру", + "cron_expression_presets": "Предефинисана подешавања Црон израза (еxпрессион)", + "disable_login": "Онемогуц́и пријаву", "duplicate_detection_job_description": "Покрените машинско учење на средствима да бисте открили сличне слике. Ослања се на паметну претрагу", - "exclusion_pattern_description": "Обрасци изузимања вам омогућавају да игноришете датотеке и фасцикле када скенирате библиотеку. Ово је корисно ако имате фасцикле које садрже датотеке које не желите да увезете, као што су RAW датотеке.", + "exclusion_pattern_description": "Обрасци изузимања вам омогуц́авају да игноришете датотеке и фасцикле када скенирате библиотеку. Ово је корисно ако имате фасцикле које садрже датотеке које не желите да увезете, као што су РАW датотеке.", "external_library_created_at": "Екстерна библиотека (направљена {date})", "external_library_management": "Управљање екстерним библиотекама", "face_detection": "Детекција лица", - "face_detection_description": "Откријте лица у датотекама помоћу машинског учења. За видео снимке се узима у обзир само сличица. „Освежи“ (поновно) обрађује све датотеке. „Ресетовање“ додатно брише све тренутне податке о лицу. „Недостају“ датотеке у реду које још нису обрађене. Откривена лица ће бити стављена у ред за препознавање лица након што се препознавање лица заврши, групишући их у постојеће или нове особе.", - "facial_recognition_job_description": "Група је детектовала лица и додала их постојећим људима. Овај корак се покреће након што је препознавање лица завршено. „Ресет“ (поновно) групише сва лица. „Недостају“ лица у редовима којима није додељена особа.", - "failed_job_command": "Команда {command} није успела за посао {job}", - "force_delete_user_warning": "УПОЗОРЕЊЕ: Ovo će odmah ukloniti korisnika i sve datoteke. Ovo se ne može opozvati i datoteke se ne mogu oporaviti.", + "face_detection_description": "Откријте лица у датотекама помоц́у машинског учења. За видео снимке се узима у обзир само сличица. „Освежи“ (поновно) обрађује све датотеке. „Ресетовање“ додатно брише све тренутне податке о лицу. „Недостају“ датотеке у реду које још нису обрађене. Откривена лица ц́е бити стављена у ред за препознавање лица након што се препознавање лица заврши, групишуц́и их у постојец́е или нове особе.", + "facial_recognition_job_description": "Група је детектовала лица и додала их постојец́им особама. Овај корак се покрец́е након што је препознавање лица завршено. „Ресетуј“ (поновно) групише сва лица. „Недостају“ лица у редовима којима није додељена особа.", + "failed_job_command": "Команда {command} није успела за посао: {job}", + "force_delete_user_warning": "УПОЗОРЕНЈЕ: Ово ц́е одмах уклонити корисника и све датотеке. Ово се не може опозвати и датотеке се не могу опоравити.", "forcing_refresh_library_files": "Принудно освежавање свих датотека библиотеке", "image_format": "Формат", - "image_format_description": "WebP производи мање датотеке од ЈПЕГ, али се спорије кодира.", - "image_fullsize_description": "Слика у пуној величини са огољеним метаподацима, користи се када је увећана", - "image_fullsize_enabled": "Омогућите генерисање слике у пуној величини", - "image_fullsize_enabled_description": "Генеришите слику пуне величине за формате који нису прилагођени вебу. Када је „Преферирај уграђени преглед“ омогућен, уграђени прегледи се користе директно без конверзије. Не утиче на формате прилагођене вебу као што је JPEG.", - "image_fullsize_quality_description": "Квалитет слике у пуној величини од 1-100. Више је боље, али производи веће датотеке.", + "image_format_description": "WебП производи мање датотеке од ЈПЕГ, али се спорије кодира.", + "image_fullsize_description": "Слика у пуној величини са огољеним метаподацима, користи се када је увец́ана", + "image_fullsize_enabled": "Омогуц́ите генерисање слике у пуној величини", + "image_fullsize_enabled_description": "Генеришите слику пуне величине за формате који нису прилагођени вебу. Када је „Преферирај уграђени преглед“ омогуц́ен, уграђени прегледи се користе директно без конверзије. Не утиче на формате прилагођене вебу као што је ЈПЕГ.", + "image_fullsize_quality_description": "Квалитет слике у пуној величини од 1-100. Више је боље, али производи вец́е датотеке.", "image_fullsize_title": "Подешавања слике у пуној величини", "image_prefer_embedded_preview": "Преферирајте уграђени преглед", - "image_prefer_embedded_preview_setting_description": "Користите уграђене прегледе у RAW фотографије као улаз за обраду слике када су доступне. Ово може да произведе прецизније боје за неке слике, али квалитет прегледа зависи од камере и слика може имати више неправилности компресије.", + "image_prefer_embedded_preview_setting_description": "Користите уграђене прегледе у РАW фотографије као улаз за обраду слике када су доступне. Ово може да произведе прецизније боје за неке слике, али квалитет прегледа зависи од камере и слика може имати више неправилности компресије.", "image_prefer_wide_gamut": "Преферирајте широк спектар", - "image_prefer_wide_gamut_setting_description": "Користите Display П3 за сличице. Ово боље чува живописност слика са широким просторима боја, али слике могу изгледати другачије на старим уређајима са старом верзијом претраживача. сРГБ слике се чувају као сРГБ да би се избегле промене боја.", + "image_prefer_wide_gamut_setting_description": "Користите Дисплаy П3 за сличице. Ово боље чува живописност слика са широким просторима боја, али слике могу изгледати другачије на старим уређајима са старом верзијом претраживача. сРГБ слике се чувају као сРГБ да би се избегле промене боја.", "image_preview_description": "Слика средње величине са уклоњеним метаподацима, која се користи приликом прегледа једног елемента и за машинско учење", - "image_preview_quality_description": "Квалитет прегледа од 1-100. Више је боље, али производи веће датотеке и може смањити одзив апликације. Постављање ниске вредности може утицати на квалитет машинског учења.", + "image_preview_quality_description": "Квалитет прегледа од 1-100. Више је боље, али производи вец́е датотеке и може смањити одзив апликације. Постављање ниске вредности може утицати на квалитет машинског учења.", "image_preview_title": "Подешавања прегледа", "image_quality": "Квалитет", "image_resolution": "Резолуција", - "image_resolution_description": "Веће резолуције могу да сачувају више детаља, али им је потребно више времена за кодирање, имају веће величине датотека и могу да смање одзив апликације.", + "image_resolution_description": "Вец́е резолуције могу да сачувају више детаља, али им је потребно више времена за кодирање, имају вец́е величине датотека и могу да смање одзив апликације.", "image_settings": "Подешавања слике", "image_settings_description": "Управљајте квалитетом и резолуцијом генерисаних слика", "image_thumbnail_description": "Мала сличица са огољеним метаподацима, која се користи приликом прегледа група фотографија као што је главна временска линија", - "image_thumbnail_quality_description": "Квалитет сличица од 1-100. Више је боље, али производи веће датотеке и може смањити одзив апликације.", + "image_thumbnail_quality_description": "Квалитет сличица од 1-100. Више је боље, али производи вец́е датотеке и може смањити одзив апликације.", "image_thumbnail_title": "Подешавања сличица", "job_concurrency": "{job} паралелност", "job_created": "Посао креиран", "job_not_concurrency_safe": "Овај посао није безбедан да буде паралелно активан.", "job_settings": "Подешавања посла", - "job_settings_description": "Управљајте паралелношћу послова", + "job_settings_description": "Управљајте паралелношц́у послова", "job_status": "Статус посла", - "jobs_delayed": "{jobCount, plural, other {# одложених}}", - "jobs_failed": "{jobCount, plural, other {# неуспешних}}", - "library_created": "Направљена библиотека {library}", + "jobs_delayed": "{jobCount, plural, one {# одложени} few {# одложена} other {# одложених}}", + "jobs_failed": "{jobCount, plural, one {# неуспешни} few {# неуспешна} other {# неуспешних}}", + "library_created": "Направљена библиотека: {library}", "library_deleted": "Библиотека је избрисана", - "library_import_path_description": "Одредите фасциклу за увоз. Ова фасцикла, укључујући подфасцикле, биће скенирана за слике и видео записе.", + "library_import_path_description": "Одредите фасциклу за увоз. Ова фасцикла, укључујуц́и подфасцикле, биц́е скенирана за слике и видео записе.", "library_scanning": "Периодично скенирање", "library_scanning_description": "Конфигуришите периодично скенирање библиотеке", - "library_scanning_enable_description": "Омогућите периодично скенирање библиотеке", + "library_scanning_enable_description": "Омогуц́ите периодично скенирање библиотеке", "library_settings": "Спољна библиотека", "library_settings_description": "Управљајте подешавањима спољне библиотеке", "library_tasks_description": "Обављај задатке библиотеке", "library_watching_enable_description": "Пратите спољне библиотеке за промене датотека", "library_watching_settings": "Надгледање библиотеке (ЕКСПЕРИМЕНТАЛНО)", "library_watching_settings_description": "Аутоматски пратите промењене датотеке", - "logging_enable_description": "Омогући евидентирање", - "logging_level_description": "Када је омогућено, који ниво евиденције користити.", + "logging_enable_description": "Омогуц́и евидентирање", + "logging_level_description": "Када је омогуц́ено, који ниво евиденције користити.", "logging_settings": "Евидентирање", "machine_learning_clip_model": "Модел ЦЛИП", "machine_learning_clip_model_description": "Назив ЦЛИП модела је наведен овде. Имајте на уму да морате поново да покренете посао „Паметно претраживање“ за све слике након промене модела.", "machine_learning_duplicate_detection": "Детекција дупликата", - "machine_learning_duplicate_detection_enabled": "Омогућите откривање дупликата", - "machine_learning_duplicate_detection_enabled_description": "Ако је oneмогућено, потпуно идентична средства ће и даље бити уклоњена.", + "machine_learning_duplicate_detection_enabled": "Омогуц́ите откривање дупликата", + "machine_learning_duplicate_detection_enabled_description": "Ако је oneмогуц́ено, потпуно идентична средства ц́е и даље бити уклоњена.", "machine_learning_duplicate_detection_setting_description": "Користите уграђен ЦЛИП да бисте пронашли вероватне дупликате", - "machine_learning_enabled": "Омогућите машинско учење", - "machine_learning_enabled_description": "Ако је oneмогућено, све функције МЛ ће бити oneмогућене без обзира на доле-наведена подешавања.", + "machine_learning_enabled": "Омогуц́ите машинско учење", + "machine_learning_enabled_description": "Ако је oneмогуц́ено, све функције МЛ ц́е бити oneмогуц́ене без обзира на доле-наведена подешавања.", "machine_learning_facial_recognition": "Препознавање лица", "machine_learning_facial_recognition_description": "Откривање, препознавање и груписање лица на сликама", "machine_learning_facial_recognition_model": "Модел за препознавање лица", - "machine_learning_facial_recognition_model_description": "Модели су наведени у опадајућем редоследу величине. Већи модели су спорији и користе више меморије, али дају боље резултате. Имајте на уму да морате поново да покренете задатак детекције лица за све слике након промене модела.", - "machine_learning_facial_recognition_setting": "Омогућите препознавање лица", - "machine_learning_facial_recognition_setting_description": "Ако је oneмогућено, слике неће бити кодиране за препознавање лица и неће попуњавати одељак Људи на страници Истражи.", + "machine_learning_facial_recognition_model_description": "Модели су наведени у опадајуц́ем редоследу величине. Вец́и модели су спорији и користе више меморије, али дају боље резултате. Имајте на уму да морате поново да покренете задатак детекције лица за све слике након промене модела.", + "machine_learning_facial_recognition_setting": "Омогуц́ите препознавање лица", + "machine_learning_facial_recognition_setting_description": "Ако је oneмогуц́ено, слике нец́е бити кодиране за препознавање лица и нец́е попуњавати одељак Људи на страници Истражи.", "machine_learning_max_detection_distance": "Максимална удаљеност детекције", - "machine_learning_max_detection_distance_description": "Максимално растојање између две слике да се сматрају дупликатима, у распону од 0,001-0,1. Веће вредности ће открити више дупликата, али могу довести до лажних позитивних резултата.", + "machine_learning_max_detection_distance_description": "Максимално растојање између две слике да се сматрају дупликатима, у распону од 0,001-0,1. Вец́е вредности ц́е открити више дупликата, али могу довести до лажних позитивних резултата.", "machine_learning_max_recognition_distance": "Максимална удаљеност препознавања", - "machine_learning_max_recognition_distance_description": "Максимална удаљеност између два лица која се сматра истом особом, у распону од 0-2. Смањење овог броја може спречити означавање две особе као исте особе, док повећање може спречити етикетирање исте особе као две различите особе. Имајте на уму да је лакше спојити две особе него поделити једну особу на двоје, па погрешите на страни нижег прага када је то могуће.", + "machine_learning_max_recognition_distance_description": "Максимална удаљеност између два лица која се сматра истом особом, у распону од 0-2. Смањење овог броја може спречити означавање две особе као исте особе, док повец́ање може спречити етикетирање исте особе као две различите особе. Имајте на уму да је лакше спојити две особе него поделити једну особу на двоје, па погрешите на страни нижег прага када је то могуц́е.", "machine_learning_min_detection_score": "Најмањи резултат детекције", - "machine_learning_min_detection_score_description": "Минимални резултат поузданости за лице које треба открити од 0-1. Ниже вредности ће открити више лица, али могу довести до лажних позитивних резултата.", + "machine_learning_min_detection_score_description": "Минимални резултат поузданости за лице које треба открити од 0-1. Ниже вредности ц́е открити више лица, али могу довести до лажних позитивних резултата.", "machine_learning_min_recognized_faces": "Најмање препознатих лица", - "machine_learning_min_recognized_faces_description": "Минимални број препознатих лица за креирање особе. Повећање овога чини препознавање лица прецизнијим по цену повећања шансе да лице није додељено особи.", + "machine_learning_min_recognized_faces_description": "Минимални број препознатих лица за креирање особе. Повец́ање овога чини препознавање лица прецизнијим по цену повец́ања шансе да лице није додељено особи.", "machine_learning_settings": "Подешавања машинског учења", "machine_learning_settings_description": "Управљајте функцијама и подешавањима машинског учења", "machine_learning_smart_search": "Паметна претрага", - "machine_learning_smart_search_description": "Потражите слике семантички користећи уграђени ЦЛИП", - "machine_learning_smart_search_enabled": "Омогућите паметну претрагу", - "machine_learning_smart_search_enabled_description": "Ако је oneмогућено, слике неће бити кодиране за паметну претрагу.", - "machine_learning_url_description": "УРЛ сервера за машинско учење. Ако је наведено више од једне УРЛ адресе, сваки сервер ће се покушавати један по један док један не одговори успешно, редом од првог до последњег. Сервери који не реагују биће привремено занемарени док се не врате на мрежу.", - "manage_concurrency": "Управљање паралелношћу", + "machine_learning_smart_search_description": "Потражите слике семантички користец́и уграђени ЦЛИП", + "machine_learning_smart_search_enabled": "Омогуц́ите паметну претрагу", + "machine_learning_smart_search_enabled_description": "Ако је oneмогуц́ено, слике нец́е бити кодиране за паметну претрагу.", + "machine_learning_url_description": "URL сервера за машинско учење. Ако је наведено више URL адреса, сваки сервер ц́е бити покушаван појединачно док не одговори успешно, редом од првог до последњег. Сервери који не одговоре биц́е привремено игнорисани док се поново не повежу са мрежом.", + "manage_concurrency": "Управљање паралелношц́у", "manage_log_settings": "Управљајте подешавањима евиденције", "map_dark_style": "Тамни стил", - "map_enable_description": "Омогућите карактеристике мапе", + "map_enable_description": "Омогуц́ите карактеристике мапе", "map_gps_settings": "Мап & ГПС подешавања", "map_gps_settings_description": "Управљајте поставкама мапе и ГПС-а (обрнуто геокодирање)", - "map_implications": "Функција мапе се ослања на екстерну услугу плочица (tiles.immich.cloud)", + "map_implications": "Функција мапе се ослања на екстерну услугу плочица (тилес.иммицх.цлоуд)", "map_light_style": "Светли стил", "map_manage_reverse_geocoding_settings": "Управљајте подешавањима Обрнуто геокодирање", "map_reverse_geocoding": "Обрнуто геокодирање", - "map_reverse_geocoding_enable_description": "Омогућите обрнуто геокодирање", + "map_reverse_geocoding_enable_description": "Омогуц́ите обрнуто геокодирање", "map_reverse_geocoding_settings": "Подешавања обрнутог геокодирања", "map_settings": "Подешавање мапе", "map_settings_description": "Управљајте подешавањима мапе", - "map_style_description": "УРЛ до style.json мапе тема изгледа", - "memory_cleanup_job": "Чишћење меморије", + "map_style_description": "URL до стyле.јсон мапе тема изгледа", + "memory_cleanup_job": "Чишц́ење меморије", "memory_generate_job": "Генерација меморије", "metadata_extraction_job": "Извод метаподатака", - "metadata_extraction_job_description": "Извуците информације о метаподацима из сваке датотеке, као што су GPS, лица и резолуција", - "metadata_faces_import_setting": "Омогући (enable) увоз лица", - "metadata_faces_import_setting_description": "Увезите лица из EXIF података слика и датотека са бочне траке", - "metadata_settings": "Подешавања метаподатака", + "metadata_extraction_job_description": "Извуците информације о метаподацима из сваке датотеке, као што су ГПС, лица и резолуција", + "metadata_faces_import_setting": "Омогућите (енабле) додавање лица", + "metadata_faces_import_setting_description": "Додајте лица из EXIF података слике и сличних метаподатака", + "metadata_settings": "Подешавање метаподатака", "metadata_settings_description": "Управљајте подешавањима метаподатака", "migration_job": "Миграције", "migration_job_description": "Пренесите сличице датотека и лица у најновију структуру директоријума", "no_paths_added": "Нема додатих путања", "no_pattern_added": "Није додат образац", - "note_apply_storage_label_previous_assets": "Напомена: Da biste primenili oznaku za skladištenje na prethodno otpremljena sredstva, pokrenite", - "note_cannot_be_changed_later": "НАПОМЕНА: Ovo se kasnije ne može promeniti!", + "note_apply_storage_label_previous_assets": "Напомена: Да бисте применили ознаку за складиштење на претходно отпремљена средства, покрените", + "note_cannot_be_changed_later": "НАПОМЕНА: Ово се касније не може променити!", "notification_email_from_address": "Са адресе", - "notification_email_from_address_description": "Адреса е-поште пошиљаоца, на пример: \"Immich foto server \"", - "notification_email_host_description": "Хост сервера е-поште (нпр. smtp.immich.app)", + "notification_email_from_address_description": "Адреса е-поште пошиљаоца, на пример: \"Immich фото сервер <нореплy@еxампле.цом>\"", + "notification_email_host_description": "Хост сервера е-поште (нпр. смтп.иммицх.апп)", "notification_email_ignore_certificate_errors": "Занемарите грешке сертификата", "notification_email_ignore_certificate_errors_description": "Игноришите грешке у валидацији ТЛС сертификата (не препоручује се)", "notification_email_password_description": "Лозинка за употребу при аутентификацији са сервером е-поште", @@ -184,18 +185,19 @@ "notification_email_test_email_failed": "Слање пробне е-поште није успело, проверите вредности", "notification_email_test_email_sent": "Пробна е-пошта је послата на {email}. Проверите своје пријемно сандуче.", "notification_email_username_description": "Корисничко име које се користи приликом аутентификације на серверу е-поште", - "notification_enable_email_notifications": "Омогућите обавештења путем е-поште", + "notification_enable_email_notifications": "Омогуц́ите обавештења путем е-поште", "notification_settings": "Подешавања обавештења", - "notification_settings_description": "Управљајте подешавањима обавештења, укључујући е-пошту", + "notification_settings_description": "Управљајте подешавањима обавештења, укључујуц́и е-пошту", "oauth_auto_launch": "Аутоматско покретање", "oauth_auto_launch_description": "Покрените OAuth ток пријављивања аутоматски након навигације на страницу за пријаву", "oauth_auto_register": "Аутоматска регистрација", - "oauth_auto_register_description": "Аутоматски региструјте нове кориснике након што се пријавите помоћу OAuth-a", + "oauth_auto_register_description": "Аутоматски региструјте нове кориснике након што се пријавите помоц́у OAuth-а", "oauth_button_text": "Текст дугмета", - "oauth_enable_description": "Пријавите се помоћу OAuth-a", + "oauth_client_secret_description": "Потребно ако OAuth провајдер не подржава ПКЦЕ (Прооф Кеy фор Цоде Еxцханге)", + "oauth_enable_description": "Пријавите се помоц́у OAuth-а", "oauth_mobile_redirect_uri": "УРИ за преусмеравање мобилних уређаја", "oauth_mobile_redirect_uri_override": "Замена УРИ-ја мобилног преусмеравања", - "oauth_mobile_redirect_uri_override_description": "Омогући када ОАuth добављач (provider) не дозвољава мобилни URI, као што је '{callback}'", + "oauth_mobile_redirect_uri_override_description": "Омогуц́и када OAuth добављач (провидер) не дозвољава мобилни УРИ, као што је '{цаллбацк}'", "oauth_settings": "ОАуторизација", "oauth_settings_description": "Управљајте подешавањима за пријаву са ОАуторизацијом", "oauth_settings_more_details": "За више детаља о овој функцији погледајте документе.", @@ -203,19 +205,21 @@ "oauth_storage_label_claim_description": "Аутоматски подесите ознаку за складиштење корисника на вредност овог захтева.", "oauth_storage_quota_claim": "Захтев за квоту складиштења", "oauth_storage_quota_claim_description": "Аутоматски подесите квоту меморијског простора корисника на вредност овог захтева.", - "oauth_storage_quota_default": "Подразумевана квота за складиштење (GiB)", + "oauth_storage_quota_default": "Подразумевана квота за складиштење (ГиБ)", "oauth_storage_quota_default_description": "Квота у ГиБ која се користи када нема потраживања (унесите 0 за неограничену квоту).", + "oauth_timeout": "Временско ограничење захтева", + "oauth_timeout_description": "Временско ограничење за захтеве у милисекундама", "offline_paths": "Ванмрежне путање", "offline_paths_description": "Ови резултати могу бити последица ручног брисања датотека које нису део спољне библиотеке.", - "password_enable_description": "Пријавите се помоћу е-поште и лозинке", + "password_enable_description": "Пријавите се помоц́у е-поште и лозинке", "password_settings": "Лозинка за пријаву", "password_settings_description": "Управљајте подешавањима за пријаву лозинком", "paths_validated_successfully": "Све путање су успешно потврђене", - "person_cleanup_job": "Чишћење особа", + "person_cleanup_job": "Чишц́ење особа", "quota_size_gib": "Величина квоте (ГиБ)", "refreshing_all_libraries": "Освежавање свих библиотека", "registration": "Регистрација администратора", - "registration_description": "Пошто сте први корисник на систему, бићете додељени као Админ и одговорни сте за административне задатке, а додатне кориснике ћете креирати ви.", + "registration_description": "Пошто сте први корисник на систему, биц́ете додељени као Админ и одговорни сте за административне задатке, а додатне кориснике ц́ете креирати ви.", "repair_all": "Поправи све", "repair_matched_items": "Поклапа се са {count, plural, one {1 ставком} few {# ставке} other {# ставки}}", "repaired_items": "{count, plural, one {Поправљена 1 ставка} few {Поправљене # ставке} other {Поправљене # ставки}}", @@ -226,9 +230,9 @@ "search_jobs": "Тражи послове…", "send_welcome_email": "Пошаљите е-пошту добродошлице", "server_external_domain_settings": "Екстерни домаин", - "server_external_domain_settings_description": "Домаин за јавне дељене везе, укључујући http(s)://", + "server_external_domain_settings_description": "Домаин за јавне дељене везе, укључујуц́и хттп(с)://", "server_public_users": "Јавни корисници", - "server_public_users_description": "Сви корисници (име и адреса е-поште) су наведени приликом додавања корисника у дељене албуме. Када је онемогућена, листа корисника ће бити доступна само администраторима.", + "server_public_users_description": "Сви корисници (име и адреса е-поште) су наведени приликом додавања корисника у дељене албуме. Када је oneмогуц́ена, листа корисника ц́е бити доступна само администраторима.", "server_settings": "Подешавања сервера", "server_settings_description": "Управљајте подешавањима сервера", "server_welcome_message": "Порука добродошлице", @@ -239,24 +243,24 @@ "smart_search_job_description": "Покрените машинско учење на датотекама да бисте подржали паметну претрагу", "storage_template_date_time_description": "Временска ознака креирања датотеке се користи за информације о датуму и времену", "storage_template_date_time_sample": "Пример времена {date}", - "storage_template_enable_description": "Омогући механизам за шаблone за складиштење", + "storage_template_enable_description": "Омогуц́и механизам за шаблone за складиштење", "storage_template_hash_verification_enabled": "Хеш верификација омогућена", - "storage_template_hash_verification_enabled_description": "Омогућава хеш верификацију, не oneмогућавајте ово осим ако нисте сигурни у последице", + "storage_template_hash_verification_enabled_description": "Омогуц́ава хеш верификацију, не oneмогуц́авајте ово осим ако нисте сигурни у последице", "storage_template_migration": "Миграција шаблона за складиштење", "storage_template_migration_description": "Примените тренутни {template} на претходно отпремљене елементе", - "storage_template_migration_info": "Промене шаблона ће се применити само на нове датотеке. Да бисте ретроактивно применили шаблон на претходно отпремљене датотеке, покрените {job}.", + "storage_template_migration_info": "Промене шаблона ц́е се применити само на нове датотеке. Да бисте ретроактивно применили шаблон на претходно отпремљене датотеке, покрените {job}.", "storage_template_migration_job": "Посао миграције складишта", "storage_template_more_details": "За више детаља о овој функцији погледајте Шаблон за складиште и његове импликације", - "storage_template_onboarding_description": "Када је омогућена, ова функција ће аутоматски организовати датотеке на основу шаблона који дефинише корисник. Због проблема са стабилношћу ова функција је подразумевано искључена. За више информација погледајте документацију.", + "storage_template_onboarding_description": "Када је омогуц́ена, ова функција ц́е аутоматски организовати датотеке на основу шаблона који дефинише корисник. Због проблема са стабилношц́у ова функција је подразумевано искључена. За више информација погледајте документацију.", "storage_template_path_length": "Приближно ограничење дужине путање: {length, number}/{limit, number}", "storage_template_settings": "Шаблон за складиштење", "storage_template_settings_description": "Управљајте структуром директоријума и именом датотеке средства за отпремање", "storage_template_user_label": "{label} је ознака за складиштење корисника", "system_settings": "Подешавања система", - "tag_cleanup_job": "Чишћење ознака (tags)", - "template_email_available_tags": "Можете да користите следеће променљиве у свом шаблону: {tags}", - "template_email_if_empty": "Ако је шаблон празан, користиће се подразумевана адреса е-поште.", - "template_email_invite_album": "Шаблон албума позива", + "tag_cleanup_job": "Чишц́ење ознака (tags)", + "template_email_available_tags": "Можете да користите следец́е променљиве у свом шаблону: {tags}", + "template_email_if_empty": "Ако је шаблон празан, користиц́е се подразумевана адреса е-поште.", + "template_email_invite_album": "Шаблон за позив у албум", "template_email_preview": "Преглед", "template_email_settings": "Шаблони е-поште", "template_email_settings_description": "Управљајте прилагођеним шаблонима обавештења путем е-поште", @@ -264,99 +268,100 @@ "template_email_welcome": "Шаблон е-поште добродошлице", "template_settings": "Шаблони обавештења", "template_settings_description": "Управљајте прилагођеним шаблонима за обавештења.", - "theme_custom_css_settings": "Прилагођени CSS", - "theme_custom_css_settings_description": "Каскадни листови стилова (CSS) омогућавају прилагођавање дизајна Immich-a.", + "theme_custom_css_settings": "Прилагођени ЦСС", + "theme_custom_css_settings_description": "Каскадни листови стилова (ЦСС) омогуц́авају прилагођавање дизајна Immich-a.", "theme_settings": "Подешавање тема", - "theme_settings_description": "Управљајте прилагођавањем Immich web интерфејса", + "theme_settings_description": "Управљајте прилагођавањем Immich wеб интерфејса", "these_files_matched_by_checksum": "Овим датотекама се подударају њихови контролни-збирови", "thumbnail_generation_job": "Генеришите сличице", - "thumbnail_generation_job_description": "Генеришите велике, мале и замућене сличице за свако средство, као и сличице за сваку особу", + "thumbnail_generation_job_description": "Генеришите велике, мале и замуц́ене сличице за свако средство, као и сличице за сваку особу", "transcoding_acceleration_api": "АПИ за убрзање", - "transcoding_acceleration_api_description": "АПИ који ће комуницирати са вашим уређајем да би убрзао транскодирање. Ово подешавање је 'најбољи напор': vraća se na softversko transkodiranje u slučaju neuspeha. VP9 može ili ne mora da radi u zavisnosti od vašeg hardvera.", - "transcoding_acceleration_nvenc": "НВЕНЦ (захтева NVIDIA ГПУ)", - "transcoding_acceleration_qsv": "Quick Sync (захтева Интел CPU 7. генерације или новији)", - "transcoding_acceleration_rkmpp": "RKMPP (само на Rockchip СОЦ-овима)", + "transcoding_acceleration_api_description": "АПИ који ц́е комуницирати са вашим уређајем да би убрзао транскодирање. Ово подешавање је 'најбољи напор': врац́а се на софтверско транскодирање у случају неуспеха. VP9 може или не мора да ради у зависности од вашег хардвера.", + "transcoding_acceleration_nvenc": "НВЕНЦ (захтева НВИДИА ГПУ)", + "transcoding_acceleration_qsv": "Qуицк Сyнц (захтева Интел CPU 7. генерације или новији)", + "transcoding_acceleration_rkmpp": "РКМПП (само на Роцкцхип СОЦ-овима)", "transcoding_acceleration_vaapi": "Видео акцелерација АПИ (ВААПИ)", - "transcoding_accepted_audio_codecs": "Прихваћени аудио кодеци", + "transcoding_accepted_audio_codecs": "Прихвац́ени аудио кодеци", "transcoding_accepted_audio_codecs_description": "Изаберите које аудио кодеке не треба транскодирати. Користи се само за одређене политике транскодирања.", - "transcoding_accepted_containers": "Прихваћени контејнери", - "transcoding_accepted_containers_description": "Изаберите који формати контејнера не морају да се ремуксују у МP4. Користи се само за одређене услове транскодирања.", - "transcoding_accepted_video_codecs": "Прихваћени видео кодеци", + "transcoding_accepted_containers": "Прихвац́ени контејнери", + "transcoding_accepted_containers_description": "Изаберите који формати контејнера не морају да се ремуксују у МП4. Користи се само за одређене услове транскодирања.", + "transcoding_accepted_video_codecs": "Прихвац́ени видео кодеци", "transcoding_accepted_video_codecs_description": "Изаберите које видео кодеке није потребно транскодирати. Користи се само за одређене политике транскодирања.", - "transcoding_advanced_options_description": "Опције које већина корисника не би требало да мењају", + "transcoding_advanced_options_description": "Опције које вец́ина корисника не би требало да мењају", "transcoding_audio_codec": "Аудио кодек", "transcoding_audio_codec_description": "Опус је опција највишег квалитета, али има лошију компатибилност са старим уређајима или софтвером.", - "transcoding_bitrate_description": "Видео снимци већи од максималне брзине преноса или нису у прихваћеном формату", - "transcoding_codecs_learn_more": "Да бисте сазнали више о терминологији која се овде користи, погледајте FFmpeg документацију за H.264 кодек, HEVC кодек и VP9 кодек.", + "transcoding_bitrate_description": "Видео снимци вец́и од максималне брзине преноса или нису у прихвац́еном формату", + "transcoding_codecs_learn_more": "Да бисте сазнали више о терминологији која се овде користи, погледајте ФФмпег документацију за H.264 кодек, HEVC кодек и VP9 кодек.", "transcoding_constant_quality_mode": "Режим константног квалитета", - "transcoding_constant_quality_mode_description": "ICQ је бољи од CQP-a, али неки уређаји за хардверско убрзање не подржавају овај режим. Подешавање ове опције ће преферирати наведени режим када се користи кодирање засновано на квалитету. НВЕНЦ игнорише јер не подржава ICQ.", + "transcoding_constant_quality_mode_description": "ИЦQ је бољи од ЦQП-а, али неки уређаји за хардверско убрзање не подржавају овај режим. Подешавање ове опције ц́е преферирати наведени режим када се користи кодирање засновано на квалитету. НВЕНЦ игнорише јер не подржава ИЦQ.", "transcoding_constant_rate_factor": "Фактор константне стопе (-црф)", - "transcoding_constant_rate_factor_description": "Ниво квалитета видеа. Типичне вредности су 23 за Х.264, 28 за ХЕВЦ, 31 за ВП9 и 35 за АВ1. Ниже је боље, али производи веће датотеке.", + "transcoding_constant_rate_factor_description": "Ниво квалитета видеа. Типичне вредности су 23 за H.264, 28 за HEVC, 31 за VP9 и 35 за АВ1. Ниже је боље, али производи вец́е датотеке.", "transcoding_disabled_description": "Немојте транскодирати ниједан видео, може прекинути репродукцију на неким клијентима", "transcoding_encoding_options": "Опције Кодирања", "transcoding_encoding_options_description": "Подесите кодеке, резолуцију, квалитет и друге опције за кодиране видео записе", "transcoding_hardware_acceleration": "Хардверско убрзање", - "transcoding_hardware_acceleration_description": "Екпериментално; много брже, али ће имати нижи квалитет при истој брзини преноса", + "transcoding_hardware_acceleration_description": "Екпериментално; много брже, али ц́е имати нижи квалитет при истој брзини преноса", "transcoding_hardware_decoding": "Хардверско декодирање", - "transcoding_hardware_decoding_setting_description": "Омогућава убрзање од краја до краја уместо да само убрзава кодирање. Можда неће радити на свим видео снимцима.", - "transcoding_hevc_codec": "ХЕВЦ кодек", + "transcoding_hardware_decoding_setting_description": "Омогуц́ава убрзање од краја до краја уместо да само убрзава кодирање. Можда нец́е радити на свим видео снимцима.", + "transcoding_hevc_codec": "HEVC кодек", "transcoding_max_b_frames": "Максимални Б-кадри", - "transcoding_max_b_frames_description": "Више вредности побољшавају ефикасност компресије, али успоравају кодирање. Можда није компатибилно са хардверским убрзањем на старијим уређајима. 0 oneмогућава Б-кадре, док -1 аутоматски поставља ову вредност.", + "transcoding_max_b_frames_description": "Више вредности побољшавају ефикасност компресије, али успоравају кодирање. Можда није компатибилно са хардверским убрзањем на старијим уређајима. 0 oneмогуц́ава Б-кадре, док -1 аутоматски поставља ову вредност.", "transcoding_max_bitrate": "Максимални битрате", - "transcoding_max_bitrate_description": "Подешавање максималног битрате-а може учинити величине датотека предвидљивијим уз мању цену квалитета. При 720п, типичне вредности су 2600к за ВП9 или ХЕВЦ, или 4500к за Х.264. oneмогућено ако је постављено на 0.", - "transcoding_max_keyframe_interval": "Максимални интервал keyframe-a", + "transcoding_max_bitrate_description": "Подешавање максималног битрате-а може учинити величине датотека предвидљивијим уз мању цену квалитета. При 720п, типичне вредности су 2600к за VP9 или HEVC, или 4500к за H.264. Онемогуц́ено ако је постављено на 0.", + "transcoding_max_keyframe_interval": "Максимални интервал кеyфраме-а", "transcoding_max_keyframe_interval_description": "Поставља максималну удаљеност кадрова између кључних кадрова. Ниже вредности погоршавају ефикасност компресије, али побољшавају време тражења и могу побољшати квалитет сцена са брзим кретањем. 0 аутоматски поставља ову вредност.", - "transcoding_optimal_description": "Видео снимци већи од циљне резолуције или нису у прихваћеном формату", + "transcoding_optimal_description": "Видео снимци вец́и од циљне резолуције или нису у прихвац́еном формату", "transcoding_policy": "Услови Транскодирања", "transcoding_policy_description": "Одреди кад да се транскодира видео", "transcoding_preferred_hardware_device": "Жељени хардверски уређај", - "transcoding_preferred_hardware_device_description": "Односи се само на ВААПИ и QSV. Поставља дри ноде који се користи за хардверско транскодирање.", + "transcoding_preferred_hardware_device_description": "Односи се само на ВААПИ и QСВ. Поставља дри ноде који се користи за хардверско транскодирање.", "transcoding_preset_preset": "Унапред подешена подешавања (-пресет)", - "transcoding_preset_preset_description": "Брзина компресије. Спорије унапред подешене вредности производе мање датотеке и повећавају квалитет када циљате одређену брзину преноса. ВП9 игнорише брзине изнад 'брже'.", + "transcoding_preset_preset_description": "Брзина компресије. Спорије унапред подешене вредности производе мање датотеке и повец́авају квалитет када циљате одређену брзину преноса. VP9 игнорише брзине изнад 'брже'.", "transcoding_reference_frames": "Референтни оквири (фрамес)", "transcoding_reference_frames_description": "Број оквира (фрамес) за референцу приликом компресије датог оквира. Више вредности побољшавају ефикасност компресије, али успоравају кодирање. 0 аутоматски поставља ову вредност.", - "transcoding_required_description": "Само видео снимци који нису у прихваћеном формату", + "transcoding_required_description": "Само видео снимци који нису у прихвац́еном формату", "transcoding_settings": "Подешавања видео транскодирања", "transcoding_settings_description": "Управљајте резолуцијом и информацијама о кодирању видео датотека", "transcoding_target_resolution": "Циљана резолуција", - "transcoding_target_resolution_description": "Веће резолуције могу да сачувају више детаља, али им је потребно више времена за кодирање, имају веће величине датотека и могу да смање брзину апликације.", - "transcoding_temporal_aq": "Временски (Темпорал) AQ", - "transcoding_temporal_aq_description": "Односи се само на НВЕНЦ. Повећава квалитет сцена са високим детаљима и ниским покретима. Можда није компатибилан са старијим уређајима.", - "transcoding_threads": "Нити (threads)", - "transcoding_threads_description": "Више вредности доводе до бржег кодирања, али остављају мање простора серверу за обраду других задатака док је активан. Ова вредност не би требало да буде већа од броја CPU језгара. Максимизира искоришћеност ако је подешено на 0.", - "transcoding_tone_mapping": "Мапирање (tone-mapping)", + "transcoding_target_resolution_description": "Вец́е резолуције могу да сачувају више детаља, али им је потребно више времена за кодирање, имају вец́е величине датотека и могу да смање брзину апликације.", + "transcoding_temporal_aq": "Временски (Темпорал) АQ", + "transcoding_temporal_aq_description": "Односи се само на НВЕНЦ. Повец́ава квалитет сцена са високим детаљима и ниским покретима. Можда није компатибилан са старијим уређајима.", + "transcoding_threads": "Нити (тхреадс)", + "transcoding_threads_description": "Више вредности доводе до бржег кодирања, али остављају мање простора серверу за обраду других задатака док је активан. Ова вредност не би требало да буде вец́а од броја CPU језгара. Максимизира искоришц́еност ако је подешено на 0.", + "transcoding_tone_mapping": "Мапирање (тone-маппинг)", "transcoding_tone_mapping_description": "Покушава да се сачува изглед ХДР видео записа када се конвертују у СДР. Сваки алгоритам прави различите компромисе за боју, детаље и осветљеност. Хабле чува детаље, Мобиус чува боју, а Раеинхард светлину.", "transcoding_transcode_policy": "Услови транскодирања", - "transcoding_transcode_policy_description": "Услови о томе када видео треба транскодирати. ХДР видео снимци ће увек бити транскодирани (осим ако је транскодирање oneмогућено).", + "transcoding_transcode_policy_description": "Услови о томе када видео треба транскодирати. ХДР видео снимци ц́е увек бити транскодирани (осим ако је транскодирање oneмогуц́ено).", "transcoding_two_pass_encoding": "Двопролазно кодирање", - "transcoding_two_pass_encoding_setting_description": "Транскодирајте у два пролаза да бисте произвели боље кодиране видео записе. Када је максимална брзина у битовима омогућена (потребна за рад са Х.264 и ХЕВЦ), овај режим користи опсег брзине у битовима заснован на максималној брзини (max битрате) и игнорише ЦРФ. За ВП9, ЦРФ се може користити ако је максимална брзина преноса oneмогућена.", + "transcoding_two_pass_encoding_setting_description": "Транскодирајте у два пролаза да бисте произвели боље кодиране видео записе. Када је максимална брзина у битовима омогуц́ена (потребна за рад са H.264 и HEVC), овај режим користи опсег брзине у битовима заснован на максималној брзини (маx битрате) и игнорише ЦРФ. За VP9, ЦРФ се може користити ако је максимална брзина преноса oneмогуц́ена.", "transcoding_video_codec": "Видео кодек", - "transcoding_video_codec_description": "ВП9 има високу ефикасност и web компатибилност, али му је потребно више времена за транскодирање. ХЕВЦ ради слично, али има нижу web компатибилност. Х.264 је широко компатибилан и брзо се транскодира, али производи много веће датотеке. АВ1 је најефикаснији кодек, али му недостаје подршка на старијим уређајима.", - "trash_enabled_description": "Омогућите функције Отпада", + "transcoding_video_codec_description": "VP9 има високу ефикасност и wеб компатибилност, али му је потребно више времена за транскодирање. HEVC ради слично, али има нижу wеб компатибилност. H.264 је широко компатибилан и брзо се транскодира, али производи много вец́е датотеке. АВ1 је најефикаснији кодек, али му недостаје подршка на старијим уређајима.", + "trash_enabled_description": "Омогуц́ите функције Отпада", "trash_number_of_days": "Број дана", "trash_number_of_days_description": "Број дана за држање датотека у отпаду пре него што их трајно уклоните", - "trash_settings": "Подешавања смећа", - "trash_settings_description": "Управљајте подешавањима смећа", - "untracked_files": "Непраћене датотеке", - "untracked_files_description": "Апликација не прати ове датотеке. one могу настати због неуспешних премештења, због прекинутих отпремања или као преостатак због грешке", - "user_cleanup_job": "Чишћење корисника", - "user_delete_delay": "Налог и датотеке {user} биће заказани за трајно брисање за {delay, plural, one {# дан} other {# дана}}.", + "trash_settings": "Подешавања смец́а", + "trash_settings_description": "Управљајте подешавањима смец́а", + "untracked_files": "Непрац́ене датотеке", + "untracked_files_description": "Апликација не прати ове датотеке. Оне могу настати због неуспешних премештења, због прекинутих отпремања или као преостатак због грешке", + "user_cleanup_job": "Чишц́ење корисника", + "user_delete_delay": "Налог и датотеке {user} биц́е заказани за трајно брисање за {delay, plural, one {# дан} other {# дана}}.", "user_delete_delay_settings": "Избриши уз кашњење", - "user_delete_delay_settings_description": "Број дана након уклањања за трајно брисање корисничког налога и датотека. Посао брисања корисника се покреће у поноћ да би се проверили корисници који су спремни за брисање. Промене ове поставке ће бити процењене при следећем извршењу.", - "user_delete_immediately": "Налог и датотеке {user} ће бити стављени на чекање за трајно брисање одмах.", + "user_delete_delay_settings_description": "Број дана након уклањања за трајно брисање корисничког налога и датотека. Посао брисања корисника се покрец́е у поноц́ да би се проверили корисници који су спремни за брисање. Промене ове поставке ц́е бити процењене при следец́ем извршењу.", + "user_delete_immediately": "Налог и датотеке {user} ц́е бити стављени на чекање за трајно брисање одмах.", "user_delete_immediately_checkbox": "Ставите корисника и датотеке у ред за тренутно брисање", + "user_details": "Детаљи корисника", "user_management": "Управљање корисницима", "user_password_has_been_reset": "Лозинка корисника је ресетована:", - "user_password_reset_description": "Молимо да доставите привремену лозинку кориснику и обавестите га да ће морати да промени лозинку приликом следећег пријављивања.", - "user_restore_description": "Налог {user} ће бити враћен.", + "user_password_reset_description": "Молимо да доставите привремену лозинку кориснику и обавестите га да ц́е морати да промени лозинку приликом следец́ег пријављивања.", + "user_restore_description": "Налог {user} ц́е бити врац́ен.", "user_restore_scheduled_removal": "Врати корисника - заказано уклањање за {date, date, лонг}", "user_settings": "Подешавања корисника", "user_settings_description": "Управљајте корисничким подешавањима", "user_successfully_removed": "Корисник {email} је успешно уклоњен.", - "version_check_enabled_description": "Омогућите проверу нових издања", - "version_check_implications": "Функција провере верзије се ослања на периодичну комуникацију са github.com", + "version_check_enabled_description": "Омогуц́ите проверу нових издања", + "version_check_implications": "Функција провере верзије се ослања на периодичну комуникацију са гитхуб.цом", "version_check_settings": "Провера верзије", - "version_check_settings_description": "Омогућите/oneмогућите обавештење о новој верзији", + "version_check_settings_description": "Омогуц́ите/oneмогуц́ите обавештење о новој верзији", "video_conversion_job": "Транскодирање видео записа", "video_conversion_job_description": "Транскодирајте видео записе за ширу компатибилност са прегледачима и уређајима" }, @@ -366,28 +371,28 @@ "advanced": "Напредно", "advanced_settings_enable_alternate_media_filter_subtitle": "Користите ову опцију за филтрирање медија током синхронизације на основу алтернативних критеријума. Покушајте ово само ако имате проблема са апликацијом да открије све албуме.", "advanced_settings_enable_alternate_media_filter_title": "[ЕКСПЕРИМЕНТАЛНО] Користите филтер за синхронизацију албума на алтернативном уређају", - "advanced_settings_log_level_title": "Ниво евиденције(log): {}", + "advanced_settings_log_level_title": "Ниво евиденције (лог): {level}", "advanced_settings_prefer_remote_subtitle": "Неки уређаји веома споро учитавају сличице са средстава на уређају. Активирајте ово подешавање да бисте уместо тога учитали удаљене слике.", "advanced_settings_prefer_remote_title": "Преферирајте удаљене слике", - "advanced_settings_proxy_headers_subtitle": "Дефинишите прокси заглавља које Имич треба да пошаље са сваким мрежним захтевом", - "advanced_settings_proxy_headers_title": "Прокси Хеадери (headers)", + "advanced_settings_proxy_headers_subtitle": "Дефинишите прокси заглавља које Immich треба да пошаље са сваким мрежним захтевом", + "advanced_settings_proxy_headers_title": "Прокси Хеадери (хеадерс)", "advanced_settings_self_signed_ssl_subtitle": "Прескаче верификацију SSL сертификата за крајњу тачку сервера. Обавезно за самопотписане сертификате.", - "advanced_settings_self_signed_ssl_title": "Дозволите самопотписане SSL сертификате", + "advanced_settings_self_signed_ssl_title": "Дозволи самопотписане SSL сертификате", "advanced_settings_sync_remote_deletions_subtitle": "Аутоматски избришите или вратите средство на овом уређају када се та радња предузме на вебу", "advanced_settings_sync_remote_deletions_title": "Синхронизујте удаљена брисања [ЕКСПЕРИМЕНТАЛНО]", - "advanced_settings_tile_subtitle": "Advanced user's settings", - "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", - "advanced_settings_troubleshooting_title": "Troubleshooting", - "age_months": "Starost{months, plural, one {# месец} other {# месеци}}", + "advanced_settings_tile_subtitle": "Напредна корисничка подешавања", + "advanced_settings_troubleshooting_subtitle": "Омогуц́ите додатне функције за решавање проблема", + "advanced_settings_troubleshooting_title": "Решавање проблема", + "age_months": "Старост{months, plural, one {# месец} other {# месеци}}", "age_year_months": "Старост 1 година, {months, plural, one {# месец} other {# месец(а/и)}}", "age_years": "{years, plural, other {Старост #}}", "album_added": "Албум додан", "album_added_notification_setting_description": "Прими обавештење е-поштом кад будеш додан у дељен албум", "album_cover_updated": "Омот албума ажуриран", "album_delete_confirmation": "Да ли стварно желите да избришете албум {album}?", - "album_delete_confirmation_description": "Ако се овај албум дели, други корисници више неће моћи да му приступе.", - "album_info_card_backup_album_excluded": "EXCLUDED", - "album_info_card_backup_album_included": "INCLUDED", + "album_delete_confirmation_description": "Ако се овај албум дели, други корисници више нец́е моц́и да му приступе.", + "album_info_card_backup_album_excluded": "ИСКЛЈУЧЕНО", + "album_info_card_backup_album_included": "УКЛЈУЧЕНО", "album_info_updated": "Информација албума ажурирана", "album_leave": "Напустити албум?", "album_leave_confirmation": "Да ли стварно желите да напустите {album}?", @@ -396,239 +401,240 @@ "album_remove_user": "Уклонити корисника?", "album_remove_user_confirmation": "Да ли сте сигурни да желите да уклоните {user}?", "album_share_no_users": "Изгледа да сте поделили овај албум са свим корисницима или да немате ниједног корисника са којим бисте делили.", - "album_thumbnail_card_item": "1 item", - "album_thumbnail_card_items": "{} ставке", - "album_thumbnail_card_shared": " · Shared", - "album_thumbnail_shared_by": "Дели {}", + "album_thumbnail_card_item": "1 ставка", + "album_thumbnail_card_items": "{count} ставки", + "album_thumbnail_card_shared": " Дељено", + "album_thumbnail_shared_by": "Дели {user}", "album_updated": "Албум ажуриран", "album_updated_setting_description": "Примите обавештење е-поштом када дељени албум има нова својства", "album_user_left": "Напустио/ла {album}", "album_user_removed": "Уклоњен {user}", - "album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?", - "album_viewer_appbar_share_err_delete": "Failed to delete album", - "album_viewer_appbar_share_err_leave": "Failed to leave album", - "album_viewer_appbar_share_err_remove": "There are problems in removing assets from album", - "album_viewer_appbar_share_err_title": "Failed to change album title", - "album_viewer_appbar_share_leave": "Leave album", - "album_viewer_appbar_share_to": "Share To", - "album_viewer_page_share_add_users": "Add users", + "album_viewer_appbar_delete_confirm": "Да ли сте сигурни да желите да избришете овај албум са свог налога?", + "album_viewer_appbar_share_err_delete": "Неуспешно брисање албума", + "album_viewer_appbar_share_err_leave": "Неуспешно излажење из албума", + "album_viewer_appbar_share_err_remove": "Проблеми са брисањем записа из албума", + "album_viewer_appbar_share_err_title": "Неуспешно мењање назива албума", + "album_viewer_appbar_share_leave": "Изађи из албума", + "album_viewer_appbar_share_to": "Подели са", + "album_viewer_page_share_add_users": "Додај кориснике", "album_with_link_access": "Нека свако ко има везу види фотографије и људе у овом албуму.", "albums": "Албуми", - "albums_count": "{count, plural, one {{count, number} Албум} few {{count, number} Албумa} other {{count, number} Албумa}}", + "albums_count": "{count, plural, one {{count, number} Албум} few {{count, number} Албуми} other {{count, number} Албуми}}", "all": "Све", "all_albums": "Сви албуми", "all_people": "Све особе", "all_videos": "Сви видео снимци", "allow_dark_mode": "Дозволи тамни режим", "allow_edits": "Дозволи уређење", - "allow_public_user_to_download": "Дозволите јавном кориснику да преузме (download-uje)", + "allow_public_user_to_download": "Дозволите јавном кориснику да преузме (доwнлоад-ује)", "allow_public_user_to_upload": "Дозволи јавном кориснику да отпреми (уплоад-ује)", - "alt_text_qr_code": "Слика QR кода", + "alt_text_qr_code": "Слика QР кода", "anti_clockwise": "У смеру супротном од казаљке на сату", - "api_key": "АПИ кључ (key)", - "api_key_description": "Ова вредност ће бити приказана само једном. Обавезно копирајте пре него што затворите прозор.", + "api_key": "АПИ кључ (кеy)", + "api_key_description": "Ова вредност ц́е бити приказана само једном. Обавезно копирајте пре него што затворите прозор.", "api_key_empty": "Име вашег АПИ кључа не би требало да буде празно", - "api_keys": "АПИ кључеви (keys)", - "app_bar_signout_dialog_content": "Are you sure you want to sign out?", - "app_bar_signout_dialog_ok": "Yes", - "app_bar_signout_dialog_title": "Sign out", + "api_keys": "АПИ кључеви (кеyс)", + "app_bar_signout_dialog_content": "Да ли сте сигурни да желите да се одјавите?", + "app_bar_signout_dialog_ok": "Да", + "app_bar_signout_dialog_title": "Одјавите се", "app_settings": "Подешавања апликације", "appears_in": "Појављује се у", "archive": "Архива", "archive_or_unarchive_photo": "Архивирајте или поништите архивирање фотографије", - "archive_page_no_archived_assets": "No archived assets found", - "archive_page_title": "Archive ({})", + "archive_page_no_archived_assets": "Нису пронађена архивирана средства", + "archive_page_title": "Архива ({count})", "archive_size": "Величина архиве", "archive_size_description": "Подеси величину архиве за преузимање (у ГиБ)", - "archived": "Arhivirano", + "archived": "Архивирано", "archived_count": "{count, plural, other {Архивирано #}}", "are_these_the_same_person": "Да ли су ово иста особа?", "are_you_sure_to_do_this": "Јесте ли сигурни да желите ово да урадите?", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", + "asset_action_delete_err_read_only": "Не могу да обришем елемент(е) само за читање, прескачем", + "asset_action_share_err_offline": "Није могуц́е преузети офлајн ресурс(е), прескачем", "asset_added_to_album": "Додато у албум", "asset_adding_to_album": "Додаје се у албум…", "asset_description_updated": "Опис датотеке је ажуриран", "asset_filename_is_offline": "Датотека {filename} је ван мреже (offline)", "asset_has_unassigned_faces": "Датотека има недодељена лица", "asset_hashing": "Хеширање…", - "asset_list_group_by_sub_title": "Group by", - "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", - "asset_list_layout_settings_group_automatically": "Automatic", - "asset_list_layout_settings_group_by": "Group assets by", - "asset_list_layout_settings_group_by_month_day": "Month + day", - "asset_list_layout_sub_title": "Layout", - "asset_list_settings_subtitle": "Photo grid layout settings", - "asset_list_settings_title": "Photo Grid", - "asset_offline": "Датотека одсутна (offline)", - "asset_offline_description": "Ова вањска датотека се више не налази на диску. Молимо контактирајте свог Имич администратора за помоћ.", - "asset_restored_successfully": "Asset restored successfully", + "asset_list_group_by_sub_title": "Групиши по", + "asset_list_layout_settings_dynamic_layout_title": "Динамични распоред", + "asset_list_layout_settings_group_automatically": "Аутоматски", + "asset_list_layout_settings_group_by": "Групиши записе по", + "asset_list_layout_settings_group_by_month_day": "Месец + Дан", + "asset_list_layout_sub_title": "Лаyоут", + "asset_list_settings_subtitle": "Опције за мрежни приказ фотографија", + "asset_list_settings_title": "Мрежни приказ фотографија", + "asset_offline": "Датотека одсутна", + "asset_offline_description": "Ова вањска датотека се више не налази на диску. Молимо контактирајте свог Immich администратора за помоц́.", + "asset_restored_successfully": "Имовина је успешно врац́ена", "asset_skipped": "Прескочено", "asset_skipped_in_trash": "У отпад", "asset_uploaded": "Отпремљено (Уплоадед)", "asset_uploading": "Отпремање…", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", - "asset_viewer_settings_title": "Asset Viewer", + "asset_viewer_settings_subtitle": "Управљајте подешавањима прегледача галерије", + "asset_viewer_settings_title": "Прегледач имовине", "assets": "Записи", "assets_added_count": "Додато {count, plural, one {# датотека} other {# датотека}}", "assets_added_to_album_count": "Додато је {count, plural, one {# датотека} other {# датотека}} у албум", - "assets_added_to_name_count": "Додато {count, plural, one {# датотека} other {# датотекa}} у {hasName, select, true {{name}} other {нови албум}}", + "assets_added_to_name_count": "Додато {count, plural, one {# датотека} other {# датотеке}} у {hasName, select, true {{name}} other {нови албум}}", "assets_count": "{count, plural, one {# датотека} few {# датотеке} other {# датотека}}", - "assets_deleted_permanently": "{} asset(s) deleted permanently", - "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_deleted_permanently": "{count} елемената трајно обрисано", + "assets_deleted_permanently_from_server": "{count} ресурс(а) трајно обрисан(а) са Immich сервера", "assets_moved_to_trash_count": "Премештено {count, plural, one {# датотека} few {# датотеке} other {# датотека}} у отпад", "assets_permanently_deleted_count": "Трајно избрисано {count, plural, one {# датотека} few {# датотеке} other {# датотека}}", "assets_removed_count": "Уклоњено {count, plural, one {# датотека} few {# датотеке} other {# датотека}}", - "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_removed_permanently_from_device": "{count} елемената трајно уклоњено са вашег уређаја", "assets_restore_confirmation": "Да ли сте сигурни да желите да вратите све своје датотеке које су у отпаду? Не можете поништити ову радњу! Имајте на уму да се ванмрежна средства не могу вратити на овај начин.", - "assets_restored_count": "Враћено {count, plural, one {# датотека} few {# датотеке} other {# датотека}}", - "assets_restored_successfully": "{} asset(s) restored successfully", - "assets_trashed": "{} asset(s) trashed", + "assets_restored_count": "Врац́ено {count, plural, one {# датотека} few {# датотеке} other {# датотека}}", + "assets_restored_successfully": "{count} елемената успешно врац́ено", + "assets_trashed": "{count} елемената је пребачено у отпад", "assets_trashed_count": "Бачено у отпад {count, plural, one {# датотека} few{# датотеке} other {# датотека}}", - "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", - "assets_were_part_of_album_count": "{count, plural, one {Датотека је} other {Датотеке су}} већ део албума", - "authorized_devices": "Овлашћени уређаји", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", + "assets_trashed_from_server": "{count} ресурс(а) обрисаних са Immich сервера", + "assets_were_part_of_album_count": "{count, plural, one {Датотека је} other {Датотеке су}} вец́ део албума", + "authorized_devices": "Овлашц́ени уређаји", + "automatic_endpoint_switching_subtitle": "Повежите се локално преко одређеног Wi-Fi-ја када је доступан и користите алтернативне везе на другим местима", + "automatic_endpoint_switching_title": "Аутоматска промена URL-ова", "back": "Назад", "back_close_deselect": "Назад, затворите или опозовите избор", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", - "backup_album_selection_page_albums_device": "Albums on device ({})", - "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", - "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", - "backup_album_selection_page_select_albums": "Select albums", - "backup_album_selection_page_selection_info": "Selection Info", - "backup_album_selection_page_total_assets": "Total unique assets", - "backup_all": "All", - "backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…", - "backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…", - "backup_background_service_current_upload_notification": "Uploading {}", - "backup_background_service_default_notification": "Checking for new assets…", - "backup_background_service_error_title": "Backup error", - "backup_background_service_in_progress_notification": "Backing up your assets…", - "backup_background_service_upload_failure_notification": "Failed to upload {}", - "backup_controller_page_albums": "Backup Albums", - "backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.", - "backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled", - "backup_controller_page_background_app_refresh_enable_button_text": "Go to settings", - "backup_controller_page_background_battery_info_link": "Show me how", - "backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.", - "backup_controller_page_background_battery_info_ok": "OK", - "backup_controller_page_background_battery_info_title": "Battery optimizations", - "backup_controller_page_background_charging": "Only while charging", - "backup_controller_page_background_configure_error": "Failed to configure the background service", - "backup_controller_page_background_delay": "Delay new assets backup: {}", - "backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app", - "backup_controller_page_background_is_off": "Automatic background backup is off", - "backup_controller_page_background_is_on": "Automatic background backup is on", - "backup_controller_page_background_turn_off": "Turn off background service", - "backup_controller_page_background_turn_on": "Turn on background service", - "backup_controller_page_background_wifi": "Only on WiFi", - "backup_controller_page_backup": "Backup", - "backup_controller_page_backup_selected": "Selected: ", - "backup_controller_page_backup_sub": "Backed up photos and videos", - "backup_controller_page_created": "Created on: {}", - "backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.", - "backup_controller_page_excluded": "Excluded: ", - "backup_controller_page_failed": "Failed ({})", - "backup_controller_page_filename": "File name: {} [{}]", - "backup_controller_page_id": "ID: {}", - "backup_controller_page_info": "Backup Information", - "backup_controller_page_none_selected": "None selected", - "backup_controller_page_remainder": "Remainder", - "backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection", - "backup_controller_page_server_storage": "Server Storage", - "backup_controller_page_start_backup": "Start Backup", - "backup_controller_page_status_off": "Automatic foreground backup is off", - "backup_controller_page_status_on": "Automatic foreground backup is on", - "backup_controller_page_storage_format": "{} of {} used", - "backup_controller_page_to_backup": "Albums to be backed up", - "backup_controller_page_total_sub": "All unique photos and videos from selected albums", - "backup_controller_page_turn_off": "Turn off foreground backup", - "backup_controller_page_turn_on": "Turn on foreground backup", - "backup_controller_page_uploading_file_info": "Uploading file info", - "backup_err_only_album": "Cannot remove the only album", - "backup_info_card_assets": "assets", - "backup_manual_cancelled": "Cancelled", - "backup_manual_in_progress": "Upload already in progress. Try after sometime", - "backup_manual_success": "Success", - "backup_manual_title": "Upload status", - "backup_options_page_title": "Backup options", - "backup_setting_subtitle": "Manage background and foreground upload settings", + "background_location_permission": "Дозвола за локацију у позадини", + "background_location_permission_content": "Да би се мењале мреже док се ради у позадини, Имих мора *увек* имати прецизан приступ локацији како би апликација могла да прочита име Wi-Fi мреже", + "backup_album_selection_page_albums_device": "Албума на уређају ({count})", + "backup_album_selection_page_albums_tap": "Додирни да укључиш, додирни двапут да искључиш", + "backup_album_selection_page_assets_scatter": "Записи се могу наћи у више различитих албума. Одатле албуми се могу укључити или искључити током процеса прављења позадинских копија.", + "backup_album_selection_page_select_albums": "Одабери албуме", + "backup_album_selection_page_selection_info": "Информације о селекцији", + "backup_album_selection_page_total_assets": "Укупно јединствених ***", + "backup_all": "Све", + "backup_background_service_backup_failed_message": "Прављење резервне копије елемената није успело. Покушава се поново…", + "backup_background_service_connection_failed_message": "Повезивање са сервером није успело. Покушавам поново…", + "backup_background_service_current_upload_notification": "Отпремање {filename}", + "backup_background_service_default_notification": "Проверавање нових записа…", + "backup_background_service_error_title": "Грешка у прављењу резервних копија", + "backup_background_service_in_progress_notification": "Прављење резервних копија записа…", + "backup_background_service_upload_failure_notification": "Неуспешно отпремљено: {filename}", + "backup_controller_page_albums": "Направи резервну копију албума", + "backup_controller_page_background_app_refresh_disabled_content": "Активирај позадинско освежавање у Опције > Генералне > Позадинско Освежавање како би направили резервне копије у позадини.", + "backup_controller_page_background_app_refresh_disabled_title": "Позадинско освежавање искључено", + "backup_controller_page_background_app_refresh_enable_button_text": "Иди у подешавања", + "backup_controller_page_background_battery_info_link": "Покажи ми како", + "backup_controller_page_background_battery_info_message": "За најпоузданије прављење резервних копија, угасите било коју опцију у оптимизацијама које би спречавале Immich са правилним радом.\n\nОвај поступак варира од уређаја до уређаја, проверите потребне кораке за Ваш уређај.", + "backup_controller_page_background_battery_info_ok": "ОК", + "backup_controller_page_background_battery_info_title": "Оптимизација Батерије", + "backup_controller_page_background_charging": "Само током пуњења", + "backup_controller_page_background_configure_error": "Неуспешно конфигурисање позадинског сервиса", + "backup_controller_page_background_delay": "Време између прављејна резервних копија записа: {duration}", + "backup_controller_page_background_description": "Укључи позадински сервис да аутоматски правиш резервне копије, без да отвараш апликацију", + "backup_controller_page_background_is_off": "Аутоматско прављење резервних копија у позадини је искључено", + "backup_controller_page_background_is_on": "Аутоматско прављење резервних копија у позадини је укључено", + "backup_controller_page_background_turn_off": "Искључи позадински сервис", + "backup_controller_page_background_turn_on": "Укључи позадински сервис", + "backup_controller_page_background_wifi": "Само на Wi-Fi", + "backup_controller_page_backup": "Направи резервну копију", + "backup_controller_page_backup_selected": "Одабрано: ", + "backup_controller_page_backup_sub": "Завршено прављење резервне копије фотографија и видеа", + "backup_controller_page_created": "Направљено:{date}", + "backup_controller_page_desc_backup": "Укључи прављење резервних копија у првом плану да аутоматски направите резервне копије када отворите апликацију.", + "backup_controller_page_excluded": "Искључено: ", + "backup_controller_page_failed": "Неуспешно ({count})", + "backup_controller_page_filename": "Име фајла: {filename} [{size}]", + "backup_controller_page_id": "ИД:{id}", + "backup_controller_page_info": "Информације", + "backup_controller_page_none_selected": "Ништа одабрано", + "backup_controller_page_remainder": "Подсетник", + "backup_controller_page_remainder_sub": "Остало фотографија и видеа да се отпреми од селекције", + "backup_controller_page_server_storage": "Простор на серверу", + "backup_controller_page_start_backup": "Покрени прављење резервне копије", + "backup_controller_page_status_off": "Аутоматско прављење резервних копија у првом плану је искључено", + "backup_controller_page_status_on": "Аутоматско прављење резервних копија у првом плану је укључено", + "backup_controller_page_storage_format": "{used} од {total} искоришћено", + "backup_controller_page_to_backup": "Албуми који ће се отпремити", + "backup_controller_page_total_sub": "Све јединствене фотографије и видеи из одабраних албума", + "backup_controller_page_turn_off": "Искључи прављење резервних копија у првом плану", + "backup_controller_page_turn_on": "Укључи прављење резервних копија у првом плану", + "backup_controller_page_uploading_file_info": "Отпремање својстава датотеке", + "backup_err_only_album": "Немогуће брисање јединог албума", + "backup_info_card_assets": "записи", + "backup_manual_cancelled": "Отказано", + "backup_manual_in_progress": "Отпремање је вец́ у току. Покушајте касније", + "backup_manual_success": "Успех", + "backup_manual_title": "Уплоад статус", + "backup_options_page_title": "Бацкуп оптионс", + "backup_setting_subtitle": "Управљајте подешавањима отпремања у позадини и предњем плану", "backward": "Уназад", "birthdate_saved": "Датум рођења успешно сачуван", "birthdate_set_description": "Датум рођења се користи да би се израчунале године ове особе у добу одређене фотографије.", - "blurred_background": "Замућена позадина", - "bugs_and_feature_requests": "Грешке и захтеви за функције", - "build": "Под-верзија (Build)", - "build_image": "Сагради (Буилд) имаге", - "bulk_delete_duplicates_confirmation": "Да ли сте сигурни да желите групно да избришете {count, plural, one {# дуплиран елеменат} few {# дуплирана елемента} other {# дуплираних елемената}}? Ово ће задржати највеће средство сваке групе и трајно избрисати све друге дупликате. Не можете поништити ову радњу!", - "bulk_keep_duplicates_confirmation": "Да ли сте сигурни да желите да задржите {count, plural, one {1 дуплирану датотеку} few {# дуплиране датотеке} other {# дуплираних датотека}}? Ово ће решити све дуплиране групе без брисања било чега.", - "bulk_trash_duplicates_confirmation": "Да ли сте сигурни да желите групно да одбаците {count, plural, one {1 дуплирану датотеку} few {# дуплиране датотеке} other {# дуплираних датотека}}? Ово ће задржати највећу датотеку сваке групе и одбацити све остале дупликате.", - "buy": "Купите лиценцу Имич-а", - "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", - "cache_settings_clear_cache_button": "Clear cache", - "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", - "cache_settings_duplicated_assets_clear_button": "CLEAR", - "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", - "cache_settings_duplicated_assets_title": "Duplicated Assets ({})", - "cache_settings_image_cache_size": "Image cache size ({} assets)", - "cache_settings_statistics_album": "Library thumbnails", - "cache_settings_statistics_assets": "{} assets ({})", - "cache_settings_statistics_full": "Full images", - "cache_settings_statistics_shared": "Shared album thumbnails", - "cache_settings_statistics_thumbnail": "Thumbnails", - "cache_settings_statistics_title": "Cache usage", - "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", - "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", - "cache_settings_tile_subtitle": "Control the local storage behaviour", - "cache_settings_tile_title": "Local Storage", - "cache_settings_title": "Caching Settings", + "blurred_background": "Замуц́ена позадина", + "bugs_and_feature_requests": "Грешке (бугс) и захтеви за функције", + "build": "Под-верзија (Буилд)", + "build_image": "Сагради (Буилд) image", + "bulk_delete_duplicates_confirmation": "Да ли сте сигурни да желите групно да избришете {count, plural, one {# дуплиран елеменат} few {# дуплирана елемента} other {# дуплираних елемената}}? Ово ц́е задржати највец́е средство сваке групе и трајно избрисати све друге дупликате. Не можете поништити ову радњу!", + "bulk_keep_duplicates_confirmation": "Да ли сте сигурни да желите да задржите {count, plural, one {1 дуплирану датотеку} few {# дуплиране датотеке} other {# дуплираних датотека}}? Ово ц́е решити све дуплиране групе без брисања било чега.", + "bulk_trash_duplicates_confirmation": "Да ли сте сигурни да желите групно да одбаците {count, plural, one {1 дуплирану датотеку} few {# дуплиране датотеке} other {# дуплираних датотека}}? Ово ц́е задржати највец́у датотеку сваке групе и одбацити све остале дупликате.", + "buy": "Купите лиценцу Immich-a", + "cache_settings_album_thumbnails": "Сличице на страници библиотеке ({count} assets)", + "cache_settings_clear_cache_button": "Обриши кеш меморију", + "cache_settings_clear_cache_button_title": "Ова опција брише кеш меморију апликације. Ово ће битно утицати на перформансе апликације док се кеш меморија не учита поново.", + "cache_settings_duplicated_assets_clear_button": "ЦЛЕАР", + "cache_settings_duplicated_assets_subtitle": "Фотографије и видео снимци које је апликација ставила на црну листу", + "cache_settings_duplicated_assets_title": "Дуплирани елементи ({count})", + "cache_settings_image_cache_size": "Величина кеш меморије слика ({count} assets)", + "cache_settings_statistics_album": "Минијатуре библиотека", + "cache_settings_statistics_assets": "{count} ставки ({size})", + "cache_settings_statistics_full": "Пуне слике", + "cache_settings_statistics_shared": "Минијатуре дељених албума", + "cache_settings_statistics_thumbnail": "Минијатуре", + "cache_settings_statistics_title": "Искоришћена кеш меморија", + "cache_settings_subtitle": "Контrole за кеш меморију мобилне апликације Immich", + "cache_settings_thumbnail_size": "Кеш меморија коју заузимају минијатуре ({count} ставки)", + "cache_settings_tile_subtitle": "Контролишите понашање локалног складиштења", + "cache_settings_tile_title": "Локална меморија", + "cache_settings_title": "Опције за кеширање", "camera": "Камера", "camera_brand": "Бренд камере", "camera_model": "Модел камере", "cancel": "Одустани", "cancel_search": "Откажи претрагу", - "canceled": "Canceled", + "canceled": "Отказано", "cannot_merge_people": "Не може спојити особе", "cannot_undo_this_action": "Не можете поништити ову радњу!", "cannot_update_the_description": "Не може ажурирати опис", "change_date": "Промени датум", - "change_display_order": "Change display order", + "change_display_order": "Промени редослед приказа", "change_expiration_time": "Промени време истека", "change_location": "Промени место", "change_name": "Промени име", "change_name_successfully": "Промени име успешно", "change_password": "Промени Лозинку", "change_password_description": "Ово је или први пут да се пријављујете на систем или је поднет захтев за промену лозинке. Унесите нову лозинку испод.", - "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", - "change_password_form_new_password": "New Password", - "change_password_form_password_mismatch": "Passwords do not match", - "change_password_form_reenter_new_password": "Re-enter New Password", + "change_password_form_confirm_password": "Поново унесите шифру", + "change_password_form_description": "Ћао, {name}\n\nОво је вероватно Ваше прво приступање систему, или је поднешен захтев за промену шифре. Молимо Вас, унесите нову шифру испод.", + "change_password_form_new_password": "Нова шифра", + "change_password_form_password_mismatch": "Шифре се не подударају", + "change_password_form_reenter_new_password": "Поново унесите нову шифру", + "change_pin_code": "Промена ПИН кода", "change_your_password": "Промени своју шифру", "changed_visibility_successfully": "Видљивост је успешно промењена", "check_all": "Штиклирати све", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", + "check_corrupt_asset_backup": "Проверите да ли постоје оштец́ене резервне копије имовине", + "check_corrupt_asset_backup_button": "Извршите проверу", + "check_corrupt_asset_backup_description": "Покрените ову проверу само преко Wi-Fi мреже и након што се направи резервна копија свих података. Поступак може потрајати неколико минута.", "check_logs": "Проверите дневнике (логс)", - "choose_matching_people_to_merge": "Изаберите одговарајуће особе за спајање", + "choose_matching_people_to_merge": "Изаберите одговарајуц́е особе за спајање", "city": "Град", "clear": "Јасно", "clear_all": "Избриши све", "clear_all_recent_searches": "Обришите све недавне претраге", "clear_message": "Обриши поруку", "clear_value": "Јасна вредност", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove_msg": "Client certificate is removed", - "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", + "client_cert_dialog_msg_confirm": "ОК", + "client_cert_enter_password": "Ентер Password", + "client_cert_import": "Импорт", + "client_cert_import_success_msg": "Сертификат клијента је увезен", + "client_cert_invalid_msg": "Неважец́а датотека сертификата или погрешна лозинка", + "client_cert_remove_msg": "Сертификат клијента је уклоњен", + "client_cert_subtitle": "Подржава само ПКЦС12 (.п12, .пфx) формат. Увоз/уклањање сертификата је доступно само пре пријаве", + "client_cert_title": "SSL клијентски сертификат", "clockwise": "У смеру казаљке", "close": "Затвори", "collapse": "Скупи", @@ -638,28 +644,29 @@ "comment_deleted": "Коментар обрисан", "comment_options": "Опције коментара", "comments_and_likes": "Коментари и лајкови", - "comments_are_disabled": "Коментари су oneмогућени", - "common_create_new_album": "Create new album", - "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.", - "completed": "Completed", - "confirm": "Потврдите", + "comments_are_disabled": "Коментари су oneмогуц́ени", + "common_create_new_album": "Креирај нови албум", + "common_server_error": "Молимо вас да проверите мрежну везу, уверите се да је сервер доступан и да су верзије апликација/сервера компатибилне.", + "completed": "Завршено", + "confirm": "Потврди", "confirm_admin_password": "Потврди Административну Лозинку", "confirm_delete_face": "Да ли сте сигурни да желите да избришете особу {name} из дела?", "confirm_delete_shared_link": "Да ли сте сигурни да желите да избришете овај дељени link?", - "confirm_keep_this_delete_others": "Свe осталe датотекe у групи ће бити избрисанe осим овe датотекe. Да ли сте сигурни да желите да наставите?", + "confirm_keep_this_delete_others": "Све остале датотеке у групи ц́е бити избрисане осим ове датотеке. Да ли сте сигурни да желите да наставите?", + "confirm_new_pin_code": "Потврдите нови ПИН код", "confirm_password": "Поново унеси шифру", "contain": "Обухвати", "context": "Контекст", "continue": "Настави", - "control_bottom_app_bar_album_info_shared": "{} items · Shared", - "control_bottom_app_bar_create_new_album": "Create new album", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", - "control_bottom_app_bar_edit_location": "Edit Location", - "control_bottom_app_bar_edit_time": "Edit Date & Time", - "control_bottom_app_bar_share_link": "Share Link", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", + "control_bottom_app_bar_album_info_shared": "{count} ствари подељено", + "control_bottom_app_bar_create_new_album": "Креирај нови албум", + "control_bottom_app_bar_delete_from_immich": "Обриши из Immich-a", + "control_bottom_app_bar_delete_from_local": "Обриши са уређаја", + "control_bottom_app_bar_edit_location": "Измени локацију", + "control_bottom_app_bar_edit_time": "Измени датум и време", + "control_bottom_app_bar_share_link": "Дели link", + "control_bottom_app_bar_share_to": "Подели са", + "control_bottom_app_bar_trash_from_immich": "Премести у отпад", "copied_image_to_clipboard": "Копирана слика у међуспремник (цлипбоард).", "copied_to_clipboard": "Копирано у међуспремник (цлипбоард)!", "copy_error": "Грешка при копирању", @@ -674,34 +681,36 @@ "covers": "Омоти", "create": "Направи", "create_album": "Направи албум", - "create_album_page_untitled": "Untitled", + "create_album_page_untitled": "Без наслова", "create_library": "Направи Библиотеку", "create_link": "Направи везу", "create_link_to_share": "Направи везу за дељење", "create_link_to_share_description": "Нека свако са везом види изабране фотографије", - "create_new": "CREATE NEW", + "create_new": "ЦРЕАТЕ НЕW", "create_new_person": "Направи нову особу", "create_new_person_hint": "Доделите изабране датотеке новој особи", "create_new_user": "Направи новог корисника", - "create_shared_album_page_share_add_assets": "ADD ASSETS", - "create_shared_album_page_share_select_photos": "Select Photos", + "create_shared_album_page_share_add_assets": "ДОДАЈ СРЕДСТВА", + "create_shared_album_page_share_select_photos": "Одабери фотографије", "create_tag": "Креирајте ознаку (tag)", - "create_tag_description": "Направите нову ознаку (tag). За угнежђене ознаке, унесите пуну путању ознаке укључујући косе црте.", + "create_tag_description": "Направите нову ознаку (tag). За угнежђене ознаке, унесите пуну путању ознаке укључујуц́и косе црте.", "create_user": "Направи корисника", "created": "Направљен", - "crop": "Crop", - "curated_object_page_title": "Things", + "created_at": "Креирано", + "crop": "Обрезивање", + "curated_object_page_title": "Ствари", "current_device": "Тренутни уређај", - "current_server_address": "Current server address", - "custom_locale": "Прилагођена локација (locale)", + "current_pin_code": "Тренутни ПИН код", + "current_server_address": "Тренутна адреса сервера", + "custom_locale": "Прилагођена локација (лоцале)", "custom_locale_description": "Форматирајте датуме и бројеве на основу језика и региона", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", + "daily_title_text_date": "Е дд МММ", + "daily_title_text_date_year": "Е дд МММ yyyy", "dark": "Тамно", "date_after": "Датум после", "date_and_time": "Датум и Време", "date_before": "Датум пре", - "date_format": "E, LLL d, y • h:mm a", + "date_format": "Е д ЛЛЛ y • Х:мм", "date_of_birth_saved": "Датум рођења успешно сачуван", "date_range": "Распон датума", "day": "Дан", @@ -710,38 +719,38 @@ "deduplication_criteria_2": "Број EXIF података", "deduplication_info": "Информације о дедупликацији", "deduplication_info_description": "Да бисмо аутоматски унапред одабрали датотеке и уклонили дупликате групно, гледамо:", - "default_locale": "Подразумевана локација (locale)", + "default_locale": "Подразумевана локација (лоцале)", "default_locale_description": "Форматирајте датуме и бројеве на основу локализације вашег претраживача", "delete": "Обриши", "delete_album": "Обриши албум", - "delete_api_key_prompt": "Да ли сте сигурни да желите да избришете овај АПИ кључ (key)?", - "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", - "delete_dialog_ok_force": "Delete Anyway", - "delete_dialog_title": "Delete Permanently", + "delete_api_key_prompt": "Да ли сте сигурни да желите да избришете овај АПИ кључ (кеy)?", + "delete_dialog_alert": "Ове ствари ће перманентно бити обрисане са Immich-a и Вашег уређаја", + "delete_dialog_alert_local": "Ове ставке ц́е бити трајно уклоњене са вашег уређаја, али ц́е и даље бити доступне на Immich серверу", + "delete_dialog_alert_local_non_backed_up": "Неке ставке нису резервно копиране на Immich-u и биц́е трајно уклоњене са вашег уређаја", + "delete_dialog_alert_remote": "Ове ставке ц́е бити трајно избрисане са Immich сервера", + "delete_dialog_ok_force": "Ипак обриши", + "delete_dialog_title": "Обриши перманентно", "delete_duplicates_confirmation": "Да ли сте сигурни да желите да трајно избришете ове дупликате?", "delete_face": "Избриши особу", "delete_key": "Избриши кључ", "delete_library": "Обриши библиотеку", "delete_link": "Обриши везу", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", + "delete_local_dialog_ok_backed_up_only": "Обриши само резервне копије", + "delete_local_dialog_ok_force": "Ипак обриши", "delete_others": "Избришите друге", "delete_shared_link": "Обриши дељену везу", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_title": "Обриши дељени link", "delete_tag": "Обриши ознаку (tag)", - "delete_tag_confirmation_prompt": "Да ли стварно желите да избришете ознаку (tag) {tagName}?", + "delete_tag_confirmation_prompt": "Да ли стварно желите да избришете ознаку {tagName}?", "delete_user": "Обриши корисника", "deleted_shared_link": "Обришена дељена веза", - "deletes_missing_assets": "Брише датотеке које недостају са диска", + "deletes_missing_assets": "Брише средства која недостају са диска", "description": "Опис", - "description_input_hint_text": "Add description...", - "description_input_submit_error": "Error updating description, check the log for more details", + "description_input_hint_text": "Адд десцриптион...", + "description_input_submit_error": "Грешка при ажурирању описа, проверите дневник за више детаља", "details": "Детаљи", "direction": "Смер", - "disabled": "oneмогућено", + "disabled": "Онемогуц́ено", "disallow_edits": "Забрани измене", "discord": "Дискорд", "discover": "Откријте", @@ -750,34 +759,34 @@ "display_options": "Опције приказа", "display_order": "Редослед приказа", "display_original_photos": "Прикажите оригиналне фотографије", - "display_original_photos_setting_description": "Радије приказујете оригиналну фотографију када глеdate материјал него сличице када је оригинално дело компатибилно са webom. Ово може довести до споријег приказа фотографија.", + "display_original_photos_setting_description": "Радије приказујете оригиналну фотографију када глеdate материјал него сличице када је оригинално дело компатибилно са wебом. Ово може довести до споријег приказа фотографија.", "do_not_show_again": "Не прикажи поново ову поруку", "documentation": "Документација", "done": "Урађено", "download": "Преузми", - "download_canceled": "Download canceled", - "download_complete": "Download complete", - "download_enqueue": "Download enqueued", - "download_error": "Download Error", - "download_failed": "Download failed", - "download_filename": "file: {}", - "download_finished": "Download finished", + "download_canceled": "Преузми отказано", + "download_complete": "Преузми завршено", + "download_enqueue": "Преузимање је стављено у ред", + "download_error": "Доwнлоад Еррор", + "download_failed": "Преузимање није успело", + "download_filename": "датотека: {filename}", + "download_finished": "Преузимање завршено", "download_include_embedded_motion_videos": "Уграђени видео снимци", "download_include_embedded_motion_videos_description": "Укључите видео записе уграђене у фотографије у покрету као засебну датотеку", - "download_notfound": "Download not found", - "download_paused": "Download paused", + "download_notfound": "Преузимање није пронађено", + "download_paused": "Преузимање је паузирано", "download_settings": "Преузимање", "download_settings_description": "Управљајте подешавањима везаним за преузимање датотека", - "download_started": "Download started", - "download_sucess": "Download success", - "download_sucess_android": "The media has been downloaded to DCIM/Immich", - "download_waiting_to_retry": "Waiting to retry", + "download_started": "Преузимање је започето", + "download_sucess": "Преузимање је успешно", + "download_sucess_android": "Медији су преузети на ДЦИМ/Immich", + "download_waiting_to_retry": "Чекање на поновни покушај", "downloading": "Преузимање у току", "downloading_asset_filename": "Преузимање датотеке {filename}", - "downloading_media": "Downloading media", + "downloading_media": "Преузимање медија", "drop_files_to_upload": "Убаците датотеке било где да их отпремите (уплоад-ујете)", "duplicates": "Дупликати", - "duplicates_description": "Разрешите сваку групу тако што ћете навести дупликате, ако их има", + "duplicates_description": "Разрешите сваку групу тако што ц́ете навести дупликате, ако их има", "duration": "Трајање", "edit": "Уреди", "edit_album": "Уреди албум", @@ -791,45 +800,46 @@ "edit_key": "Измени кључ", "edit_link": "Уреди везу", "edit_location": "Уреди локацију", - "edit_location_dialog_title": "Location", + "edit_location_dialog_title": "Локација", "edit_name": "Уреди име", "edit_people": "Уреди особе", "edit_tag": "Уреди ознаку (tag)", "edit_title": "Уреди титулу", "edit_user": "Уреди корисника", "edited": "Уређено", - "editor": "Urednik", - "editor_close_without_save_prompt": "Промене неће бити сачуване", + "editor": "Уредник", + "editor_close_without_save_prompt": "Промене нец́е бити сачуване", "editor_close_without_save_title": "Затворити уређивач?", - "editor_crop_tool_h2_aspect_ratios": "Пропорције (aspect ratios)", + "editor_crop_tool_h2_aspect_ratios": "Пропорције (аспецт ратиос)", "editor_crop_tool_h2_rotation": "Ротација", "email": "Е-пошта", - "empty_folder": "This folder is empty", - "empty_trash": "Испразните смеће", - "empty_trash_confirmation": "Да ли сте сигурни да желите да испразните смеће? Ово ће трајно уклонити све датотеке у смећу из Immich-a.\nNe можете поништити ову радњу!", - "enable": "Омогући (Енабле)", - "enabled": "Омогућено (enabled)", + "email_notifications": "Обавештења е-поштом", + "empty_folder": "Ова мапа је празна", + "empty_trash": "Испразните смец́е", + "empty_trash_confirmation": "Да ли сте сигурни да желите да испразните смец́е? Ово ц́е трајно уклонити све датотеке у смец́у из Immich-a.\nНе можете поништити ову радњу!", + "enable": "Омогуц́и (Енабле)", + "enabled": "Омогуц́ено (Енаблед)", "end_date": "Крајњи датум", - "enqueued": "Enqueued", - "enter_wifi_name": "Enter WiFi name", + "enqueued": "Стављено у ред", + "enter_wifi_name": "Унесите назив Wi-Fi мреже", "error": "Грешка", - "error_change_sort_album": "Failed to change album sort order", + "error_change_sort_album": "Промена редоследа сортирања албума није успела", "error_delete_face": "Грешка при брисању особе из дела", "error_loading_image": "Грешка при учитавању слике", - "error_saving_image": "Error: {}", + "error_saving_image": "Грешка: {error}", "error_title": "Грешка – Нешто је пошло наопако", "errors": { - "cannot_navigate_next_asset": "Није могуће доћи до следеће датотеке", - "cannot_navigate_previous_asset": "Није могуће доћи до претходне датотеке", - "cant_apply_changes": "Није могуће применити промене", - "cant_change_activity": "Није могуће {enabled, select, true {oneмогућити} other {омогућити}} активности", - "cant_change_asset_favorite": "Није могуће променити фаворит за датотеку", - "cant_change_metadata_assets_count": "Није могуће променити метаподатке за {count, plural, one {# датотеку} other {# датотеке}}", + "cannot_navigate_next_asset": "Није могуц́е доц́и до следец́е датотеке", + "cannot_navigate_previous_asset": "Није могуц́е доц́и до претходне датотеке", + "cant_apply_changes": "Није могуц́е применити промене", + "cant_change_activity": "Није могуц́е {enabled, select, true {oneмогуц́ити} other {омогуц́ити}} активности", + "cant_change_asset_favorite": "Није могуц́е променити фаворит за датотеку", + "cant_change_metadata_assets_count": "Није могуц́е променити метаподатке за {count, plural, one {# датотеку} other {# датотеке}}", "cant_get_faces": "Не могу да нађем лица", "cant_get_number_of_comments": "Не могу добити број коментара", "cant_search_people": "Не могу претраживати особе", "cant_search_places": "Не могу претраживати места", - "cleared_jobs": "Очишћени послови за: {job}", + "cleared_jobs": "Очишц́ени послови за: {job}", "error_adding_assets_to_album": "Грешка при додавању датотека у албум", "error_adding_users_to_album": "Грешка при додавању корисника у албум", "error_deleting_shared_user": "Грешка при брисању дељеног корисника", @@ -837,7 +847,7 @@ "error_hiding_buy_button": "Грешка при скривању дугмета за куповину", "error_removing_assets_from_album": "Грешка при уклањању датотеке из албума, проверите конзолу за више детаља", "error_selecting_all_assets": "Грешка при избору свих датотека", - "exclusion_pattern_already_exists": "Овај образац искључења већ постоји.", + "exclusion_pattern_already_exists": "Овај образац искључења вец́ постоји.", "failed_job_command": "Команда {command} није успела за задатак: {job}", "failed_to_create_album": "Није могуће креирати албум", "failed_to_create_shared_link": "Прављење дељеног linkа није успело", @@ -846,180 +856,183 @@ "failed_to_keep_this_delete_others": "Није успело задржавање овог дела и брисање осталих датотека", "failed_to_load_asset": "Учитавање датотека није успело", "failed_to_load_assets": "Није успело учитавање датотека", + "failed_to_load_notifications": "Учитавање обавештења није успело", "failed_to_load_people": "Учитавање особа није успело", "failed_to_remove_product_key": "Уклањање кључа производа није успело", "failed_to_stack_assets": "Слагање датотека није успело", "failed_to_unstack_assets": "Расклапање датотека није успело", - "import_path_already_exists": "Ова путања увоза већ постоји.", + "failed_to_update_notification_status": "Ажурирање статуса обавештења није успело", + "import_path_already_exists": "Ова путања увоза вец́ постоји.", "incorrect_email_or_password": "Неисправан e-mail или лозинка", - "paths_validation_failed": "{paths, plural, one {# путања није прошла} few {# путање нису прошле} other {# путања нису прошле}} проверу ваљаности", - "profile_picture_transparent_pixels": "Слике профила не могу имати прозирне пикселе. Молимо увећајте и/или померите слику.", - "quota_higher_than_disk_size": "Поставили сте квоту већу од величине диска", - "repair_unable_to_check_items": "Није могуће проверити {count, select, one {ставку} other {ставке}}", - "unable_to_add_album_users": "Није могуће додати кориснике у албум", - "unable_to_add_assets_to_shared_link": "Није могуће додати елементе дељеној вези", - "unable_to_add_comment": "Није могуће додати коментар", - "unable_to_add_exclusion_pattern": "Није могуће додати образац изузимања", - "unable_to_add_import_path": "Није могуће додати путању за увоз", - "unable_to_add_partners": "Није могуће додати партнере", + "paths_validation_failed": "{paths, plural, one {# путања није прошла} other {# путањe нису прошле}} проверу ваљаности", + "profile_picture_transparent_pixels": "Слике профила не могу имати прозирне пикселе. Молимо увец́ајте и/или померите слику.", + "quota_higher_than_disk_size": "Поставили сте квоту вец́у од величине диска", + "repair_unable_to_check_items": "Није могуц́е проверити {count, select, one {ставку} other {ставке}}", + "unable_to_add_album_users": "Није могуц́е додати кориснике у албум", + "unable_to_add_assets_to_shared_link": "Није могуц́е додати елементе дељеној вези", + "unable_to_add_comment": "Није могуц́е додати коментар", + "unable_to_add_exclusion_pattern": "Није могуц́е додати образац изузимања", + "unable_to_add_import_path": "Није могуц́е додати путању за увоз", + "unable_to_add_partners": "Није могуц́е додати партнере", "unable_to_add_remove_archive": "Није могуће {archived, select, true {уклонити датотеке из} other {додати датотеке у}} архиву", - "unable_to_add_remove_favorites": "Није могуће {favorite, select, true {додати датотеке у} other {уклонити датотеке из}} фаворитa", + "unable_to_add_remove_favorites": "Није могуће {favorite, select, true {додати датотеке у} other {уклонити датотеке из}} фаворите", "unable_to_archive_unarchive": "Није могуће {archived, select, true {архивирати} other {де-архивирати}}", - "unable_to_change_album_user_role": "Није могуће променити улогу корисника албума", - "unable_to_change_date": "Није могуће променити датум", - "unable_to_change_favorite": "Није могуће променити фаворит за датотеку/е", - "unable_to_change_location": "Није могуће променити локацију", - "unable_to_change_password": "Није могуће променити лозинку", - "unable_to_change_visibility": "Није могуће променити видљивост за {count, plural, one {# особу} other {# особе}}", - "unable_to_complete_oauth_login": "Није могуће довршити OAuth пријаву", - "unable_to_connect": "Није могуће повезати се", - "unable_to_connect_to_server": "Немогуће је повезати се са сервером", - "unable_to_copy_to_clipboard": "Није могуће копирати у међуспремник (цлипбоард), проверите да ли приступате страници преко https-a", - "unable_to_create_admin_account": "Није могуће направити администраторски налог", - "unable_to_create_api_key": "Није могуће направити нови АПИ кључ (key)", - "unable_to_create_library": "Није могуће направити библиотеку", - "unable_to_create_user": "Није могуће креирати корисника", - "unable_to_delete_album": "Није могуће избрисати албум", - "unable_to_delete_asset": "Није могуће избрисати датотеке", + "unable_to_change_album_user_role": "Није могуц́е променити улогу корисника албума", + "unable_to_change_date": "Није могуц́е променити датум", + "unable_to_change_favorite": "Није могуц́е променити фаворит за датотеку/е", + "unable_to_change_location": "Није могуц́е променити локацију", + "unable_to_change_password": "Није могуц́е променити лозинку", + "unable_to_change_visibility": "Није могуц́е променити видљивост за {count, plural, one {# особу} other {# особе}}", + "unable_to_complete_oauth_login": "Није могуц́е довршити OAuth пријаву", + "unable_to_connect": "Није могуц́е повезати се", + "unable_to_connect_to_server": "Немогуц́е је повезати се са сервером", + "unable_to_copy_to_clipboard": "Није могуц́е копирати у међуспремник (цлипбоард), проверите да ли приступате страници преко хттпс-а", + "unable_to_create_admin_account": "Није могуц́е направити администраторски налог", + "unable_to_create_api_key": "Није могуц́е направити нови АПИ кључ (кеy)", + "unable_to_create_library": "Није могуц́е направити библиотеку", + "unable_to_create_user": "Није могуц́е креирати корисника", + "unable_to_delete_album": "Није могуц́е избрисати албум", + "unable_to_delete_asset": "Није могуц́е избрисати датотеке", "unable_to_delete_assets": "Грешка при брисању датотека", - "unable_to_delete_exclusion_pattern": "Није могуће избрисати образац изузимања", - "unable_to_delete_import_path": "Није могуће избрисати путању за увоз", - "unable_to_delete_shared_link": "Није могуће избрисати дељени link", - "unable_to_delete_user": "Није могуће избрисати корисника", - "unable_to_download_files": "Није могуће преузети датотеке", - "unable_to_edit_exclusion_pattern": "Није могуће изменити образац изузимања", - "unable_to_edit_import_path": "Није могуће изменити путању увоза", - "unable_to_empty_trash": "Није могуће испразнити отпад", - "unable_to_enter_fullscreen": "Није могуће отворити преко целог екрана", - "unable_to_exit_fullscreen": "Није могуће изаћи из целог екрана", - "unable_to_get_comments_number": "Није могуће добити број коментара", + "unable_to_delete_exclusion_pattern": "Није могуц́е избрисати образац изузимања", + "unable_to_delete_import_path": "Није могуц́е избрисати путању за увоз", + "unable_to_delete_shared_link": "Није могуц́е избрисати дељени link", + "unable_to_delete_user": "Није могуц́е избрисати корисника", + "unable_to_download_files": "Није могуц́е преузети датотеке", + "unable_to_edit_exclusion_pattern": "Није могуц́е изменити образац изузимања", + "unable_to_edit_import_path": "Није могуц́е изменити путању увоза", + "unable_to_empty_trash": "Није могуц́е испразнити отпад", + "unable_to_enter_fullscreen": "Није могуц́е отворити преко целог екрана", + "unable_to_exit_fullscreen": "Није могуц́е изац́и из целог екрана", + "unable_to_get_comments_number": "Није могуц́е добити број коментара", "unable_to_get_shared_link": "Преузимање дељене везе није успело", "unable_to_hide_person": "Није могуће сакрити особу", - "unable_to_link_motion_video": "Није могуће повезати (link) видео снимак", - "unable_to_link_oauth_account": "Није могуће повезати OAuth налог", - "unable_to_load_album": "Није могуће учитати албум", - "unable_to_load_asset_activity": "Није могуће учитати активност средстава", - "unable_to_load_items": "Није могуће учитати ставке", - "unable_to_load_liked_status": "Није могуће учитати статус свиђања", - "unable_to_log_out_all_devices": "Није могуће од‌јавити све уређаје", - "unable_to_log_out_device": "Није могуће од‌јавити уређај", - "unable_to_login_with_oauth": "Није могуће пријавити се помоћу OAuth-a", + "unable_to_link_motion_video": "Није могуће повезати видео са сликом", + "unable_to_link_oauth_account": "Није могуц́е повезати OAuth налог", + "unable_to_load_album": "Није могуц́е учитати албум", + "unable_to_load_asset_activity": "Није могуц́е учитати активност средстава", + "unable_to_load_items": "Није могуц́е учитати ставке", + "unable_to_load_liked_status": "Није могуц́е учитати статус свиђања", + "unable_to_log_out_all_devices": "Није могуц́е одјавити све уређаје", + "unable_to_log_out_device": "Није могуц́е одјавити уређај", + "unable_to_login_with_oauth": "Није могуц́е пријавити се помоц́у OAuth-а", "unable_to_play_video": "Није могуће пустити видео", - "unable_to_reassign_assets_existing_person": "Није могуће прерасподелити датотеке на {name, select, null {постојећу особу} other {{name}}}", - "unable_to_reassign_assets_new_person": "Није могуће пренети средства новој особи", + "unable_to_reassign_assets_existing_person": "Није могуц́е прерасподелити датотеке на {name, select, null {постојец́у особу} other {{name}}}", + "unable_to_reassign_assets_new_person": "Није могуц́е пренети средства новој особи", "unable_to_refresh_user": "Није могуће освежити корисника", "unable_to_remove_album_users": "Није могуће уклонити кориснике из албума", - "unable_to_remove_api_key": "Није могуће уклонити АПИ кључ (key)", - "unable_to_remove_assets_from_shared_link": "Није могуће уклонити елементе са дељеног linkа", + "unable_to_remove_api_key": "Није могуће уклонити АПИ кључ (кеy)", + "unable_to_remove_assets_from_shared_link": "Није могуц́е уклонити елементе са дељеног linkа", "unable_to_remove_deleted_assets": "Није могуће уклонити ванмрежне датотеке", "unable_to_remove_library": "Није могуће уклонити библиотеку", "unable_to_remove_partner": "Није могуће уклонити партнера", "unable_to_remove_reaction": "Није могуће уклонити реакцију", "unable_to_repair_items": "Није могуће поправити ставке", "unable_to_reset_password": "Није могуће ресетовати лозинку", + "unable_to_reset_pin_code": "Није могуц́е ресетовати ПИН код", "unable_to_resolve_duplicate": "Није могуће разрешити дупликат", - "unable_to_restore_assets": "Није могуће вратити датотеке", + "unable_to_restore_assets": "Није могуц́е вратити датотеке", "unable_to_restore_trash": "Није могуће повратити отпад", "unable_to_restore_user": "Није могуће повратити корисника", "unable_to_save_album": "Није могуће сачувати албум", - "unable_to_save_api_key": "Није могуће сачувати АПИ кључ (key)", - "unable_to_save_date_of_birth": "Није могуће сачувати датум рођења", + "unable_to_save_api_key": "Није могуће сачувати АПИ кључ (кеy)", + "unable_to_save_date_of_birth": "Није могуц́е сачувати датум рођења", "unable_to_save_name": "Није могуће сачувати име", "unable_to_save_profile": "Није могуће сачувати профил", "unable_to_save_settings": "Није могуће сачувати подешавања", "unable_to_scan_libraries": "Није могуће скенирати библиотеке", "unable_to_scan_library": "Није могуће скенирати библиотеку", - "unable_to_set_feature_photo": "Није могуће поставити истакнуту фотографију", + "unable_to_set_feature_photo": "Није могуц́е поставити истакнуту фотографију", "unable_to_set_profile_picture": "Није могуће поставити профилну слику", "unable_to_submit_job": "Није могуће предати задатак", - "unable_to_trash_asset": "Није могуће избацити материјал у отпад", + "unable_to_trash_asset": "Није могуц́е избацити материјал у отпад", "unable_to_unlink_account": "Није могуће раскинути профил", - "unable_to_unlink_motion_video": "Није могуће прекинути везу са видео снимком", - "unable_to_update_album_cover": "Није могуће ажурирати насловницу албума", - "unable_to_update_album_info": "Није могуће ажурирати информације о албуму", + "unable_to_unlink_motion_video": "Није могуће одвезати видео од слике", + "unable_to_update_album_cover": "Није могуц́е ажурирати насловницу албума", + "unable_to_update_album_info": "Није могуц́е ажурирати информације о албуму", "unable_to_update_library": "Није могуће ажурирати библиотеку", "unable_to_update_location": "Није могуће ажурирати локацију", "unable_to_update_settings": "Није могуће ажурирати подешавања", "unable_to_update_timeline_display_status": "Није могуће ажурирати статус приказа временске линије", "unable_to_update_user": "Није могуће ажурирати корисника", - "unable_to_upload_file": "Није могуће отпремити датотеку" + "unable_to_upload_file": "Није могуц́е отпремити датотеку" }, - "exif": "EXIF", - "exif_bottom_sheet_description": "Add Description...", - "exif_bottom_sheet_details": "DETAILS", - "exif_bottom_sheet_location": "LOCATION", - "exif_bottom_sheet_people": "PEOPLE", - "exif_bottom_sheet_person_add_person": "Add name", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", + "exif": "Exif", + "exif_bottom_sheet_description": "Додај опис...", + "exif_bottom_sheet_details": "ДЕТАЛЈИ", + "exif_bottom_sheet_location": "ЛОКАЦИЈА", + "exif_bottom_sheet_people": "ПЕОПЛЕ", + "exif_bottom_sheet_person_add_person": "Адд name", + "exif_bottom_sheet_person_age": "Старост {age}", + "exif_bottom_sheet_person_age_months": "Старост {months} месеци", + "exif_bottom_sheet_person_age_year_months": "Старост 1 година, {months} месеци", + "exif_bottom_sheet_person_age_years": "Старост {years}", "exit_slideshow": "Изађи из пројекције слајдова", "expand_all": "Прошири све", - "experimental_settings_new_asset_list_subtitle": "Work in progress", - "experimental_settings_new_asset_list_title": "Enable experimental photo grid", - "experimental_settings_subtitle": "Use at your own risk!", - "experimental_settings_title": "Experimental", + "experimental_settings_new_asset_list_subtitle": "У изради", + "experimental_settings_new_asset_list_title": "Активирај експериментални мрежни приказ фотографија", + "experimental_settings_subtitle": "Користити на сопствену одговорност!", + "experimental_settings_title": "Експериментално", "expire_after": "Да истекне након", "expired": "Истекло", "expires_date": "Истиче {date}", "explore": "Истражите", - "explorer": "Претраживач (Explorer)", + "explorer": "Претраживач (Еxплорер)", "export": "Извези", "export_as_json": "Извези ЈСОН", - "extension": "Екстензија (Extension)", + "extension": "Екстензија (Еxтенсион)", "external": "Спољашњи", "external_libraries": "Спољашње Библиотеке", - "external_network": "External network", - "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", + "external_network": "Спољна мрежа", + "external_network_sheet_info": "Када није на жељеној Wi-Fi мрежи, апликација ц́е се повезати са сервером преко прве од доле наведених URL адреса до којих може да дође, почевши од врха до дна", "face_unassigned": "Нераспоређени", - "failed": "Failed", - "failed_to_load_assets": "Учитавање средстава није успело", - "failed_to_load_folder": "Failed to load folder", + "failed": "Неуспешно", + "failed_to_load_assets": "Датотеке нису успешно учитане", + "failed_to_load_folder": "Учитавање фасцикле није успело", "favorite": "Фаворит", "favorite_or_unfavorite_photo": "Омиљена или неомиљена фотографија", "favorites": "Фаворити", - "favorites_page_no_favorites": "No favorite assets found", + "favorites_page_no_favorites": "Није пронађен ниједан омиљени материјал", "feature_photo_updated": "Главна фотографија је ажурирана", - "features": "Функције", + "features": "Функције (феатурес)", "features_setting_description": "Управљајте функцијама апликације", "file_name": "Назив документа", "file_name_or_extension": "Име датотеке или екстензија", "filename": "Име датотеке", "filetype": "Врста документа", - "filter": "Filter", + "filter": "Филтер", "filter_people": "Филтрирање особа", "filter_places": "Филтрирајте места", - "find_them_fast": "Брзо их пронађите по имену помоћу претраге", + "find_them_fast": "Брзо их пронађите по имену помоц́у претраге", "fix_incorrect_match": "Исправите нетачно подударање", - "folder": "Folder", - "folder_not_found": "Folder not found", - "folders": "Фасцикле (Folders)", - "folders_feature_description": "Прегледавање приказа фасцикле за фотографије и видео записе у систему датотека", + "folder": "Фасцикла", + "folder_not_found": "Фасцикла није пронађена", + "folders": "Фасцикле (Фолдерс)", + "folders_feature_description": "Прегледавање приказа фасцикле за фотографије и видео записа у систему датотека", "forward": "Напред", "general": "Генерално", - "get_help": "Нађи помоћ", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "get_help": "Нађи помоц́", + "get_wifiname_error": "Није могуц́е добити име Wi-Fi мреже. Уверите се да сте дали потребне дозволе и да сте повезани на Wi-Fi мрежу", "getting_started": "Почињем", "go_back": "Врати се", "go_to_folder": "Иди у фасциклу", "go_to_search": "Иди на претрагу", - "grant_permission": "Grant permission", + "grant_permission": "Дај дозволу", "group_albums_by": "Групни албуми по...", "group_country": "Група по држава", "group_no": "Без груписања", "group_owner": "Групирајте по власнику", "group_places_by": "Групирајте места по...", "group_year": "Групирајте по години", - "haptic_feedback_switch": "Enable haptic feedback", - "haptic_feedback_title": "Haptic Feedback", + "haptic_feedback_switch": "Омогуц́и хаптичку повратну информацију", + "haptic_feedback_title": "Хаптичке повратне информације", "has_quota": "Има квоту", - "header_settings_add_header_tip": "Add Header", - "header_settings_field_validator_msg": "Value cannot be empty", - "header_settings_header_name_input": "Header name", - "header_settings_header_value_input": "Header value", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", + "header_settings_add_header_tip": "Додај заглавље", + "header_settings_field_validator_msg": "Вредност не може бити празна", + "header_settings_header_name_input": "Назив заглавља", + "header_settings_header_value_input": "Вредност заглавља", + "headers_settings_tile_subtitle": "Дефинишите прокси заглавља која апликација треба да шаље са сваким мрежним захтевом", + "headers_settings_tile_title": "Прилагођени прокси заглавци", "hi_user": "Здраво {name} ({email})", "hide_all_people": "Сакриј све особе", "hide_gallery": "Сакриј галерију", @@ -1027,41 +1040,42 @@ "hide_password": "Сакриј лозинку", "hide_person": "Сакриј особу", "hide_unnamed_people": "Сакриј неименоване особе", - "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", - "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", - "home_page_add_to_album_success": "Added {added} assets to album {album}.", - "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", - "home_page_archive_err_local": "Can not archive local assets yet, skipping", - "home_page_archive_err_partner": "Can not archive partner assets, skipping", - "home_page_building_timeline": "Building the timeline", - "home_page_delete_err_partner": "Can not delete partner assets, skipping", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", - "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", - "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", - "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", - "home_page_share_err_local": "Can not share local assets via link, skipping", - "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", - "host": "Домаћин (Хост)", + "home_page_add_to_album_conflicts": "Додат {added} запис у албум {album}. {failed} записи су већ у албуму.", + "home_page_add_to_album_err_local": "Тренутно немогуће додати локалне записе у албуме, прескацу се", + "home_page_add_to_album_success": "Доdate {added} ставке у албум {album}.", + "home_page_album_err_partner": "Још увек није могуц́е додати партнерска средства у албум, прескачем", + "home_page_archive_err_local": "Још увек није могуц́е архивирати локалне ресурсе, прескачем", + "home_page_archive_err_partner": "Не могу да архивирам партнерску имовину, прескачем", + "home_page_building_timeline": "Креирање хронолошке линије", + "home_page_delete_err_partner": "Не могу да обришем партнерску имовину, прескачем", + "home_page_delete_remote_err_local": "Локална средства у обрисавању удаљеног избора, прескакање", + "home_page_favorite_err_local": "Тренутно није могуце додати локалне записе у фаворите, прескацу се", + "home_page_favorite_err_partner": "Још увек није могуц́е означити партнерске ресурсе као омиљене, прескачем", + "home_page_first_time_notice": "Ако је ово први пут да користите апликацију, молимо Вас да одаберете албуме које желите да сачувате", + "home_page_share_err_local": "Не могу да делим локалне ресурсе преко linkа, прескачем", + "home_page_upload_err_limit": "Можете отпремити највише 30 елемената истовремено, прескачуц́и", + "host": "Домац́ин (Хост)", "hour": "Сат", - "ignore_icloud_photos": "Ignore iCloud photos", - "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", + "id": "ИД", + "ignore_icloud_photos": "Игноришите иЦлоуд фотографије", + "ignore_icloud_photos_description": "Фотографије које су сачуване на иЦлоуд-у нец́е бити отпремљене на Immich сервер", "image": "Фотографија", - "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} снимљено {date}", - "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} снимљено {person1} {date}", - "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} снимили {person1} и {person2} {date}", - "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} снимили {person1}, {person2}, и {person3} {date}", - "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} снимили {person1}, {person2}, и {additionalCount, number} осталих {date}", - "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} {date}", - "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} са {person1} {date}", - "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} са {person1} и {person2} {date}", - "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} са {person1}, {person2}, и {person3} {date}", - "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} са {person1}, {person2}, и {additionalCount, number} других {date}", - "image_saved_successfully": "Image saved", - "image_viewer_page_state_provider_download_started": "Download Started", - "image_viewer_page_state_provider_download_success": "Download Success", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_alt_text_date": "{isVideo, select, true {Видео} other {Image}} снимљено {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Видео} other {Image}} снимљено са {person1} {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Видео} other {Image}} снимљено са {person1} и {person2} {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Видео} other {Image}} снимљено са {person1}, {person2} и {person3} {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Видео} other {Image}} снимљено са {person1}, {person2} и још {additionalCount, number} осталих {date}", + "image_alt_text_date_place": "{isVideo, select, true {Видео} other {Image}} снимљено у {city}, {country} {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Видео} other {Image}} снимљено у {city}, {country} са {person1} {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Видео} other {Image}} снимљено у {city}, {country} са {person1} и {person2} {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Видео} other {Image}} снимљеноу {city}, {country} са {person1}, {person2} и {person3} {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Видео} other {Image}} снимљено у {city}, {country} са {person1}, {person2} и још {additionalCount, number} других {date}", + "image_saved_successfully": "Слика је сачувана", + "image_viewer_page_state_provider_download_started": "Преузимање је започето", + "image_viewer_page_state_provider_download_success": "Преузимање Успешно", + "image_viewer_page_state_provider_share_error": "Грешка при дељењу", "immich_logo": "Лого Immich-a", - "immich_web_interface": "Web интерфејс Immich-a", + "immich_web_interface": "Wеб интерфејс Immich-a", "import_from_json": "Увези из ЈСОН-а", "import_path": "Путања увоза", "in_albums": "У {count, plural, one {# албуму} few {# албума} other {# албума}}", @@ -1078,8 +1092,8 @@ "night_at_midnight": "Свака ноћ у поноћ", "night_at_twoam": "Свака ноћ у 2ам" }, - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", + "invalid_date": "Неважец́и датум", + "invalid_date_format": "Неважец́и формат датума", "invite_people": "Позовите људе", "invite_to_album": "Позови на албум", "items_count": "{count, plural, one {# датотека} other {# датотека}}", @@ -1100,110 +1114,113 @@ "level": "Ниво", "library": "Библиотека", "library_options": "Опције библиотеке", - "library_page_device_albums": "Albums on Device", - "library_page_new_album": "New album", - "library_page_sort_asset_count": "Number of assets", - "library_page_sort_created": "Created date", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_title": "Album title", + "library_page_device_albums": "Албуми на уређају", + "library_page_new_album": "Нови албум", + "library_page_sort_asset_count": "Број средстава", + "library_page_sort_created": "Најновије креирано", + "library_page_sort_last_modified": "Последња измена", + "library_page_sort_title": "Назив албума", "light": "Светло", "like_deleted": "Лајкуј избрисано", "link_motion_video": "Направи везу за видео запис", "link_options": "Опције везе", - "link_to_oauth": "Веза до OAuth-a", + "link_to_oauth": "Веза до OAuth-а", "linked_oauth_account": "Повезани OAuth налог", "list": "Излистај", "loading": "Учитавање", "loading_search_results_failed": "Учитавање резултата претраге није успело", - "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", - "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", - "location_picker_choose_on_map": "Choose on map", - "location_picker_latitude_error": "Enter a valid latitude", - "location_picker_latitude_hint": "Enter your latitude here", - "location_picker_longitude_error": "Enter a valid longitude", - "location_picker_longitude_hint": "Enter your longitude here", + "local_network": "Лоцал нетwорк", + "local_network_sheet_info": "Апликација ц́е се повезати са сервером преко ове URL адресе када користи наведену Ви-Фи мрежу", + "location_permission": "Дозвола за локацију", + "location_permission_content": "Да би користио функцију аутоматског пребацивања, Immich-u је потребна прецизна дозвола за локацију како би могао да прочита назив тренутне Wi-Fi мреже", + "location_picker_choose_on_map": "Изаберите на мапи", + "location_picker_latitude_error": "Унесите важец́у географску ширину", + "location_picker_latitude_hint": "Унесите своју географску ширину овде", + "location_picker_longitude_error": "Унесите важец́у географску дужину", + "location_picker_longitude_hint": "Унесите своју географску дужину овде", "log_out": "Одјави се", "log_out_all_devices": "Одјавите се са свих уређаја", "logged_out_all_devices": "Одјављени су сви уређаји", "logged_out_device": "Одјављен уређај", "login": "Пријава", - "login_disabled": "Login has been disabled", - "login_form_api_exception": "API exception. Please check the server URL and try again.", - "login_form_back_button_text": "Back", - "login_form_email_hint": "youremail@email.com", - "login_form_endpoint_hint": "http://your-server-ip:port", - "login_form_endpoint_url": "Server Endpoint URL", - "login_form_err_http": "Please specify http:// or https://", - "login_form_err_invalid_email": "Invalid Email", - "login_form_err_invalid_url": "Invalid URL", - "login_form_err_leading_whitespace": "Leading whitespace", - "login_form_err_trailing_whitespace": "Trailing whitespace", - "login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL", - "login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server", - "login_form_failed_login": "Error logging you in, check server URL, email and password", - "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", - "login_form_password_hint": "password", - "login_form_save_login": "Stay logged in", - "login_form_server_empty": "Enter a server URL.", - "login_form_server_error": "Could not connect to server.", - "login_has_been_disabled": "Пријава је oneмогућена.", - "login_password_changed_error": "There was an error updating your password", - "login_password_changed_success": "Password updated successfully", - "logout_all_device_confirmation": "Да ли сте сигурни да желите да се од‌јавите са свих уређаја?", - "logout_this_device_confirmation": "Да ли сте сигурни да желите да се од‌јавите са овог уређаја?", + "login_disabled": "Пријава је oneмогуц́ена", + "login_form_api_exception": "Изузетак АПИ-ја. Молимо вас да проверите URL адресу сервера и покушате поново.", + "login_form_back_button_text": "Назад", + "login_form_email_hint": "вашemail@email.цом", + "login_form_endpoint_hint": "хттп://ип-вашег-сервера:порт", + "login_form_endpoint_url": "URL Сервера", + "login_form_err_http": "Допиши хттп:// или хттпс://", + "login_form_err_invalid_email": "Неважећи Емаил", + "login_form_err_invalid_url": "Не важећи link (URL)", + "login_form_err_leading_whitespace": "Размак испред", + "login_form_err_trailing_whitespace": "Размак иза", + "login_form_failed_get_oauth_server_config": "Евиденција грешака користећи OAuth, проверити серверски link (URL)", + "login_form_failed_get_oauth_server_disable": "OAuth опција није доступна на овом серверу", + "login_form_failed_login": "Неуспешна пријава, провери URL сервера, email и шифру", + "login_form_handshake_exception": "Дошло је до изузетка рукостискања са сервером. Омогуц́ите подршку за самопотписане сертификате у подешавањима ако користите самопотписани сертификат.", + "login_form_password_hint": "шифра", + "login_form_save_login": "Остани пријављен", + "login_form_server_empty": "Ентер а сервер URL.", + "login_form_server_error": "Није могуц́е повезати се са сервером.", + "login_has_been_disabled": "Пријава је oneмогуц́ена.", + "login_password_changed_error": "Дошло је до грешке приликом ажурирања лозинке", + "login_password_changed_success": "Лозинка је успешно ажурирана", + "logout_all_device_confirmation": "Да ли сте сигурни да желите да се одјавите са свих уређаја?", + "logout_this_device_confirmation": "Да ли сте сигурни да желите да се одјавите са овог уређаја?", "longitude": "Географска дужина", "look": "Погледај", "loop_videos": "Понављајте видео записе", - "loop_videos_description": "Омогућите за аутоматско понављање видео записа у прегледнику детаља.", - "main_branch_warning": "Употребљавате развојну верзију; строго препоручујемо употребу издате верзије!", + "loop_videos_description": "Омогуц́ите за аутоматско понављање видео записа у прегледнику детаља.", + "main_branch_warning": "Употребљавате развојну верзију; строго препоручујемо употребу изdate верзије!", "main_menu": "Главни мени", "make": "Креирај", "manage_shared_links": "Управљајте дељеним везама", "manage_sharing_with_partners": "Управљајте дељењем са партнерима", "manage_the_app_settings": "Управљајте подешавањима апликације", "manage_your_account": "Управљајте вашим профилом", - "manage_your_api_keys": "Управљајте АПИ кључевима (keys)", + "manage_your_api_keys": "Управљајте АПИ кључевима (кеyс)", "manage_your_devices": "Управљајте својим пријављеним уређајима", "manage_your_oauth_connection": "Управљајте својом OAuth везом", "map": "Мапа", - "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", - "map_cannot_get_user_location": "Cannot get user's location", - "map_location_dialog_yes": "Yes", - "map_location_picker_page_use_location": "Use this location", - "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", - "map_location_service_disabled_title": "Location Service disabled", + "map_assets_in_bound": "{count} фотографија", + "map_assets_in_bounds": "{count} фотографија", + "map_cannot_get_user_location": "Није могуц́е добити локацију корисника", + "map_location_dialog_yes": "Да", + "map_location_picker_page_use_location": "Користите ову локацију", + "map_location_service_disabled_content": "Услуга локације мора бити омогуц́ена да би се приказивала средства са ваше тренутне локације. Да ли желите да је сада омогуц́ите?", + "map_location_service_disabled_title": "Услуга локације је oneмогуц́ена", "map_marker_for_images": "Означивач на мапи за слике снимљене у {city}, {country}", "map_marker_with_image": "Маркер на мапи са сликом", - "map_no_assets_in_bounds": "No photos in this area", - "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", - "map_no_location_permission_title": "Location Permission denied", + "map_no_assets_in_bounds": "Нема фотографија у овој области", + "map_no_location_permission_content": "Потребна је дозвола за локацију да би се приказали ресурси са ваше тренутне локације. Да ли желите да је сада дозволите?", + "map_no_location_permission_title": "Дозвола за локацију је одбијена", "map_settings": "Подешавања мапе", - "map_settings_dark_mode": "Dark mode", - "map_settings_date_range_option_day": "Past 24 hours", - "map_settings_date_range_option_days": "Past {} days", - "map_settings_date_range_option_year": "Past year", - "map_settings_date_range_option_years": "Past {} years", - "map_settings_dialog_title": "Map Settings", - "map_settings_include_show_archived": "Include Archived", - "map_settings_include_show_partners": "Include Partners", - "map_settings_only_show_favorites": "Show Favorite Only", - "map_settings_theme_settings": "Map Theme", - "map_zoom_to_see_photos": "Zoom out to see photos", + "map_settings_dark_mode": "Тамни режим", + "map_settings_date_range_option_day": "Последња 24 сата", + "map_settings_date_range_option_days": "Претходних {days} дана", + "map_settings_date_range_option_year": "Прошла година", + "map_settings_date_range_option_years": "Протеклих {years} година", + "map_settings_dialog_title": "Подешавања Мапе", + "map_settings_include_show_archived": "Укључи архивирано", + "map_settings_include_show_partners": "Укључи партнере", + "map_settings_only_show_favorites": "Прикажи само омиљене", + "map_settings_theme_settings": "Тема мапе", + "map_zoom_to_see_photos": "Умањите да бисте видели фотографије", + "mark_all_as_read": "Означи све као прочитано", + "mark_as_read": "Означи као прочитано", + "marked_all_as_read": "Све је означено као прочитано", "matches": "Подударања", "media_type": "Врста медија", - "memories": "Сећања", - "memories_all_caught_up": "All caught up", - "memories_check_back_tomorrow": "Check back tomorrow for more memories", - "memories_setting_description": "Управљајте оним што видите у својим сећањима", - "memories_start_over": "Start Over", - "memories_swipe_to_close": "Swipe up to close", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", + "memories": "Сец́ања", + "memories_all_caught_up": "Све је ухвац́ено", + "memories_check_back_tomorrow": "Вратите се сутра за још успомена", + "memories_setting_description": "Управљајте оним што видите у својим сец́ањима", + "memories_start_over": "Почни испочетка", + "memories_swipe_to_close": "Превуците нагоре да бисте затворили", + "memories_year_ago": "Пре годину дана", + "memories_years_ago": "пре {years} година", "memory": "Меморија", - "memory_lane_title": "Трака сећања {title}", + "memory_lane_title": "Трака сец́ања {title}", "menu": "Мени", "merge": "Споји", "merge_people": "Споји особе", @@ -1216,72 +1233,77 @@ "missing": "Недостаје", "model": "Модел", "month": "Месец", - "monthly_title_text_date_format": "MMMM y", + "monthly_title_text_date_format": "ММММ y", "more": "Више", - "moved_to_trash": "Премештено у смеће", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", - "mute_memories": "Пригуши сећања", + "moved_to_archive": "Премештено {count, plural, one {# датотека} other {# датотеке}} у архиву", + "moved_to_library": "Премештено {count, plural, one {# датотека} other {# датотеке}} у библиотеку", + "moved_to_trash": "Премештено у смец́е", + "multiselect_grid_edit_date_time_err_read_only": "Не можете да измените датум елемената само за читање, прескачем", + "multiselect_grid_edit_gps_err_read_only": "Не могу да изменим локацију елемената само за читање, прескачем", + "mute_memories": "Пригуши сец́ања", "my_albums": "Моји албуми", "name": "Име", "name_or_nickname": "Име или надимак", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", + "networking_settings": "Умрежавање", + "networking_subtitle": "Управљајте подешавањима крајње тачке сервера", "never": "Никада", - "new_album": "Нови албум", - "new_api_key": "Нови АПИ кључ (key)", + "new_album": "Нови Албум", + "new_api_key": "Нови АПИ кључ (кеy)", "new_password": "Нова шифра", "new_person": "Нова особа", + "new_pin_code": "Нови ПИН код", "new_user_created": "Нови корисник је креиран", "new_version_available": "ДОСТУПНА НОВА ВЕРЗИЈА", "newest_first": "Најновије прво", "next": "Следеће", - "next_memory": "Следеће сећање", + "next_memory": "Следец́е сец́ање", "no": "Не", "no_albums_message": "Направите албум да бисте организовали своје фотографије и видео записе", "no_albums_with_name_yet": "Изгледа да још увек немате ниједан албум са овим именом.", "no_albums_yet": "Изгледа да још немате ниједан албум.", "no_archived_assets_message": "Архивирајте фотографије и видео записе да бисте их сакрили из приказа фотографија", "no_assets_message": "КЛИКНИТЕ ДА УПЛОАДИРАТЕ СВОЈУ ПРВУ ФОТОГРАФИЈУ", - "no_assets_to_show": "No assets to show", + "no_assets_to_show": "Нема елемената за приказ", "no_duplicates_found": "Није пронађен ниједан дупликат.", - "no_exif_info_available": "Нема доступних exif информација", + "no_exif_info_available": "Нема доступних еxиф информација", "no_explore_results_message": "Уплоадујте још фотографија да бисте истражили своју колекцију.", "no_favorites_message": "Поставите фаворите да бисте брзо нашли ваше најбоље слике и видео снимке", "no_libraries_message": "Направите спољну библиотеку да бисте видели своје фотографије и видео записе", "no_name": "Нема имена", + "no_notifications": "Нема обавештења", + "no_people_found": "Нису пронађени одговарајуц́и људи", "no_places": "Нема места", "no_results": "Нема резултата", "no_results_description": "Покушајте са синонимом или општијом кључном речи", "no_shared_albums_message": "Направите албум да бисте делили фотографије и видео записе са људима у вашој мрежи", "not_in_any_album": "Нема ни у једном албуму", - "not_selected": "Not selected", - "note_apply_storage_label_to_previously_uploaded assets": "Напомена: Да бисте применили ознаку за складиштење на претходно отпремљена средства, покрените", + "not_selected": "Није изабрано", + "note_apply_storage_label_to_previously_uploaded assets": "Напомена: Да бисте применили ознаку за складиштење на претходно уплоадиране датотеке, покрените", "notes": "Напомене", - "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", - "notification_permission_list_tile_content": "Grant permission to enable notifications.", - "notification_permission_list_tile_enable_button": "Enable Notifications", - "notification_permission_list_tile_title": "Notification Permission", - "notification_toggle_setting_description": "Омогућите обавештења путем е-поште", + "notification_permission_dialog_content": "Да би укљуцили нотификације, идите у Опције и одаберите Дозволи.", + "notification_permission_list_tile_content": "Дајте дозволу за омогуц́авање обавештења.", + "notification_permission_list_tile_enable_button": "Укључи Нотификације", + "notification_permission_list_tile_title": "Дозволе за нотификације", + "notification_toggle_setting_description": "Омогуц́ите обавештења путем е-поште", "notifications": "Нотификације", "notifications_setting_description": "Управљајте обавештењима", "oauth": "OAuth", - "official_immich_resources": "Званични Имич ресурси", - "offline": "Одсутан (Offline)", - "offline_paths": "Недоступне (Offline) путање", + "official_immich_resources": "Званични Immich ресурси", + "offline": "Одсутан (Оффлине)", + "offline_paths": "Недоступне (Оффлине) путање", "offline_paths_description": "Ови резултати могу бити последица ручног брисања датотека које нису део спољне библиотеке.", "ok": "Ок", "oldest_first": "Најстарије прво", - "on_this_device": "On this device", + "on_this_device": "На овом уређају", "onboarding": "Приступање (Онбоардинг)", - "onboarding_privacy_description": "Следеће (опционе) функције се ослањају на спољне услуге и могу се онемогућити у било ком тренутку у подешавањима администрације.", + "onboarding_privacy_description": "Следец́е (опциone) функције се ослањају на спољне услуге и могу се oneмогуц́ити у било ком тренутку у подешавањима администрације.", "onboarding_theme_description": "Изаберите тему боја за свој налог. Ово можете касније да промените у подешавањима.", "onboarding_welcome_description": "Хајде да подесимо вашу инстанцу са неким уобичајеним подешавањима.", "onboarding_welcome_user": "Добродошли, {user}", "online": "Доступан (Онлине)", "only_favorites": "Само фаворити", "open": "Отвори", - "open_in_map_view": "Отвори у приказу мапе", + "open_in_map_view": "Отворите у приказ карте", "open_in_openstreetmap": "Отворите у ОпенСтреетМап-у", "open_the_search_filters": "Отворите филтере за претрагу", "options": "Опције", @@ -1297,14 +1319,14 @@ "partner_can_access": "{partner} може да приступи", "partner_can_access_assets": "Све ваше фотографије и видео снимци осим оних у архивираним и избрисаним", "partner_can_access_location": "Локација на којој су ваше фотографије снимљене", - "partner_list_user_photos": "{user}'s photos", - "partner_list_view_all": "View all", - "partner_page_empty_message": "Your photos are not yet shared with any partner.", - "partner_page_no_more_users": "No more users to add", - "partner_page_partner_add_failed": "Failed to add partner", - "partner_page_select_partner": "Select partner", - "partner_page_shared_to_title": "Shared to", - "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", + "partner_list_user_photos": "Фотографије корисника {user}", + "partner_list_view_all": "Прикажи све", + "partner_page_empty_message": "Ваше фотографије још увек нису дељене ни са једним партнером.", + "partner_page_no_more_users": "Нема више корисника за додавање", + "partner_page_partner_add_failed": "Додавање партнера није успело", + "partner_page_select_partner": "Изаберите партнера", + "partner_page_shared_to_title": "Дељено са", + "partner_page_stop_sharing_content": "{partner} више нец́е моц́и да приступи вашим фотографијама.", "partner_sharing": "Партнерско дељење", "partners": "Партнери", "password": "Шифра", @@ -1319,7 +1341,7 @@ "path": "Путања", "pattern": "Шаблон", "pause": "Пауза", - "pause_memories": "Паузирајте сећања", + "pause_memories": "Паузирајте сец́ања", "paused": "Паузирано", "pending": "На чекању", "people": "Особе", @@ -1330,61 +1352,65 @@ "permanent_deletion_warning_setting_description": "Прикажи упозорење када трајно бришете датотеке", "permanently_delete": "Трајно избрисати", "permanently_delete_assets_count": "Трајно избриши {count, plural, one {датотеку} other {датотеке}}", - "permanently_delete_assets_prompt": "Да ли сте сигурни да желите да трајно избришете {count, plural, one {ову датотеку?} other {ове # датотеке?}}Ово ће их такође уклонити {count, plural, one {из њиховог} other {из њихових}} албума.", + "permanently_delete_assets_prompt": "Да ли сте сигурни да желите да трајно избришете {count, plural, one {ову датотеку?} other {ове # датотеке?}}Ово ц́е их такође уклонити {count, plural, one {из њиховог} other {из њихових}} албума.", "permanently_deleted_asset": "Трајно избрисана датотека", "permanently_deleted_assets_count": "Трајно избрисано {count, plural, one {# датотека} other {# датотеке}}", - "permission_onboarding_back": "Back", - "permission_onboarding_continue_anyway": "Continue anyway", - "permission_onboarding_get_started": "Get started", - "permission_onboarding_go_to_settings": "Go to settings", - "permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.", - "permission_onboarding_permission_granted": "Permission granted! You are all set.", - "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", - "permission_onboarding_request": "Immich requires permission to view your photos and videos.", + "permission_onboarding_back": "Назад", + "permission_onboarding_continue_anyway": "Ипак настави", + "permission_onboarding_get_started": "Започните", + "permission_onboarding_go_to_settings": "Иди на подешавања", + "permission_onboarding_permission_denied": "Дозвола одбијена. Да бисте користили Immich, доделите дозволе за фотографије и видео записе у Подешавањима.", + "permission_onboarding_permission_granted": "Дозвола одобрена! Спремни сте.", + "permission_onboarding_permission_limited": "Дозвола ограничена. Да бисте омогуц́или Immich-u да прави резервне копије и управља целом вашом колекцијом галерије, доделите дозволе за фотографије и видео записе у Подешавањима.", + "permission_onboarding_request": "Immich захтева дозволу да види ваше фотографије и видео записе.", "person": "Особа", - "person_birthdate": "Рођен(a) {date}", + "person_birthdate": "Рођен(а) {date}", "person_hidden": "{name}{hidden, select, true { (скривено)} other {}}", "photo_shared_all_users": "Изгледа да сте поделили своје фотографије са свим корисницима или да немате ниједног корисника са којим бисте делили.", - "photos": "Слике", + "photos": "Фотографије", "photos_and_videos": "Фотографије & Видео записи", "photos_count": "{count, plural, one {{count, number} фотографија} few {{count, number} фотографије} other {{count, number} фотографија}}", "photos_from_previous_years": "Фотографије из претходних година", "pick_a_location": "Одабери локацију", + "pin_code_changed_successfully": "ПИН код је успешно промењен", + "pin_code_reset_successfully": "ПИН код је успешно ресетован", + "pin_code_setup_successfully": "Успешно подешавање ПИН кода", "place": "Место", "places": "Места", - "places_count": "{count, plural, one {{count, number} Место} other {{count, number} Местa}}", + "places_count": "{count, plural, one {{count, number} Место} other {{count, number} Места}}", "play": "Покрени", - "play_memories": "Покрени сећања", + "play_memories": "Покрени сец́ања", "play_motion_photo": "Покрени покретну фотографију", "play_or_pause_video": "Покрени или паузирај видео запис", "port": "порт", - "preferences_settings_subtitle": "Manage the app's preferences", - "preferences_settings_title": "Preferences", + "preferences_settings_subtitle": "Управљајте подешавањима апликације", + "preferences_settings_title": "Подешавања", "preset": "Унапред подешено", "preview": "Преглед", "previous": "Прошло", - "previous_memory": "Prethodno сећање", - "previous_or_next_photo": "Prethodna или следећа фотографија", - "primary": "Примарна (Primary)", + "previous_memory": "Претходно сец́ање", + "previous_or_next_photo": "Претходна или следец́а фотографија", + "primary": "Примарна (Примарy)", "privacy": "Приватност", - "profile_drawer_app_logs": "Logs", - "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", - "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", - "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", - "profile_drawer_github": "GitHub", - "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.", - "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.", + "profile": "Профил", + "profile_drawer_app_logs": "Евиденција", + "profile_drawer_client_out_of_date_major": "Мобилна апликација је застарела. Молимо вас да је ажурирате на најновију главну верзију.", + "profile_drawer_client_out_of_date_minor": "Мобилна апликација је застарела. Молимо вас да је ажурирате на најновију споредну верзију.", + "profile_drawer_client_server_up_to_date": "Клијент и сервер су најновије верзије", + "profile_drawer_github": "ГитХуб", + "profile_drawer_server_out_of_date_major": "Сервер је застарео. Молимо вас да ажурирате на најновију главну верзију.", + "profile_drawer_server_out_of_date_minor": "Сервер је застарео. Молимо вас да ажурирате на најновију споредну верзију.", "profile_image_of_user": "Слика профила од корисника {user}", "profile_picture_set": "Профилна слика постављена.", "public_album": "Јавни албум", "public_share": "Јавно дељење", "purchase_account_info": "Подржавам софтвер", - "purchase_activated_subtitle": "Хвала вам што подржавате Иммицх и софтвер отвореног кода", - "purchase_activated_time": "Активирано {date, date}", + "purchase_activated_subtitle": "Хвала вам што подржавате Immich и софтвер отвореног кода", + "purchase_activated_time": "Активирано {date}", "purchase_activated_title": "Ваш кључ је успешно активиран", - "purchase_button_activate": "Активираj", + "purchase_button_activate": "Активирај", "purchase_button_buy": "Купи", - "purchase_button_buy_immich": "Купи Имич", + "purchase_button_buy_immich": "Купите Immich", "purchase_button_never_show_again": "Никада више не приказуј", "purchase_button_reminder": "Подсети ме за 30 дана", "purchase_button_remove_key": "Уклоните кључ", @@ -1394,20 +1420,20 @@ "purchase_individual_description_2": "Статус подршке", "purchase_individual_title": "Индивидуална лиценца", "purchase_input_suggestion": "Имате кључ производа? Унесите кључ испод", - "purchase_license_subtitle": "Купите Имич да бисте подржали континуирани развој услуге", + "purchase_license_subtitle": "Купите Immich да бисте подржали континуирани развој услуге", "purchase_lifetime_description": "Доживотна лиценца", "purchase_option_title": "ОПЦИЈЕ КУПОВИНЕ", - "purchase_panel_info_1": "Изградња Имич-а захтева много времена и труда, а имамо инжењере који раде на томе са пуним радним временом како бисмо је учинили што је могуће бољом. Наша мисија је да софтвер отвореног кода и етичке пословне праксе постану одржив извор прихода за програмере и да створимо екосистем који поштује приватност са стварним алтернативама експлоатативним услугама у облаку.", - "purchase_panel_info_2": "Пошто смо се обавезали да нећемо додавати платне зидове, ова куповина вам неће дати никакве додатне функције у Имич-у. Ослањамо се на кориснике попут вас да подрже Имич-ов стални развој.", + "purchase_panel_info_1": "Изградња Immich-a захтева много времена и труда, а имамо инжењере који раде на томе са пуним радним временом како бисмо је учинили што је могуц́е бољом. Наша мисија је да софтвер отвореног кода и етичке пословне праксе постану одржив извор прихода за програмере и да створимо екосистем који поштује приватност са стварним алтернативама експлоатативним услугама у облаку.", + "purchase_panel_info_2": "Пошто смо се обавезали да нец́емо додавати платне зидове, ова куповина вам нец́е дати никакве додатне функције у Immich-u. Ослањамо се на кориснике попут вас да подрже Immich-ов стални развој.", "purchase_panel_title": "Подржите пројекат", "purchase_per_server": "По серверу", "purchase_per_user": "По кориснику", "purchase_remove_product_key": "Уклоните кључ производа", "purchase_remove_product_key_prompt": "Да ли сте сигурни да желите да уклоните шифру производа?", - "purchase_remove_server_product_key": "Уклоните шифру производа сервера", - "purchase_remove_server_product_key_prompt": "Да ли сте сигурни да желите да уклоните шифру производа сервера?", + "purchase_remove_server_product_key": "Уклоните шифру производа са сервера", + "purchase_remove_server_product_key_prompt": "Да ли сте сигурни да желите да уклоните шифру производа са сервера?", "purchase_server_description_1": "За цео сервер", - "purchase_server_description_2": "Значка подршке", + "purchase_server_description_2": "Статус подршке", "purchase_server_title": "Сервер", "purchase_settings_server_activated": "Кључем производа сервера управља администратор", "rating": "Оцена звездица", @@ -1417,23 +1443,25 @@ "reaction_options": "Опције реакције", "read_changelog": "Прочитајте дневник промена", "reassign": "Поново додај", - "reassigned_assets_to_existing_person": "Поново додељено {count, plural, one {# датотека} other {# датотеке}} постојећој {name, select, null {особи} other {{name}}}", + "reassigned_assets_to_existing_person": "Поново додељено {count, plural, one {# датотека} other {# датотеке}} постојец́ој {name, select, null {особи} other {{name}}}", "reassigned_assets_to_new_person": "Поново додељено {count, plural, one {# датотека} other {# датотеке}} новој особи", - "reassing_hint": "Доделите изабрана средства постојећој особи", + "reassing_hint": "Доделите изабрана средства постојец́ој особи", "recent": "Скорашњи", "recent-albums": "Недавни албуми", "recent_searches": "Скорашње претраге", - "recently_added": "Recently added", - "recently_added_page_title": "Recently Added", + "recently_added": "Недавно додато", + "recently_added_page_title": "Недавно Додато", + "recently_taken": "Недавно снимљено", + "recently_taken_page_title": "Недавно Снимљено", "refresh": "Освежи", - "refresh_encoded_videos": "Освежите кодиране (енцодед) видео записе", + "refresh_encoded_videos": "Освежите кодиране (енcodeд) видео записе", "refresh_faces": "Освежи лица", "refresh_metadata": "Освежите метаподатке", "refresh_thumbnails": "Освежите сличице", "refreshed": "Освежено", - "refreshes_every_file": "Поново чита све постојеће и нове датотеке", - "refreshing_encoded_video": "Освежавање кодираног (енцодед) видеа", - "refreshing_faces": "Освежавањe лица", + "refreshes_every_file": "Поново чита све постојец́е и нове датотеке", + "refreshing_encoded_video": "Освежавање кодираног (енcodeд) видеа", + "refreshing_faces": "Освежавање лица", "refreshing_metadata": "Освежавање мета-података", "regenerating_thumbnails": "Обнављање сличица", "remove": "Уклони", @@ -1449,31 +1477,32 @@ "remove_photo_from_memory": "Уклоните фотографију из ове меморије", "remove_url": "Уклони URL", "remove_user": "Уклони корисника", - "removed_api_key": "Уклоњен АПИ кључ (key): {name}", + "removed_api_key": "Уклоњен АПИ кључ (кеy): {name}", "removed_from_archive": "Уклоњено из архиве", "removed_from_favorites": "Уклоњено из омиљених (фаворитес)", "removed_from_favorites_count": "{count, plural, other {Уклоњено #}} из омиљених", "removed_memory": "Уклоњена меморија", "removed_photo_from_memory": "Слика је уклоњена из меморије", - "removed_tagged_assets": "Уклоњена ознака (tag) из {count, plural, one {# датотеке} other {# датотека}}", + "removed_tagged_assets": "Уклоњена ознака из {count, plural, one {# датотеке} other {# датотека}}", "rename": "Преименуј", "repair": "Поправи", - "repair_no_results_message": "Овде ће се појавити датотеке које нису праћене и недостају", + "repair_no_results_message": "Овде ц́е се појавити датотеке које нису прац́ене и недостају", "replace_with_upload": "Замените са уплоад-ом", - "repository": "Репозиторијум (Repository)", + "repository": "Репозиторијум (Репоситорy)", "require_password": "Потребна лозинка", "require_user_to_change_password_on_first_login": "Захтевати од корисника да промени лозинку при првом пријављивању", "rescan": "Поново скенирај", "reset": "Ресетовати", "reset_password": "Ресетовати лозинку", "reset_people_visibility": "Ресетујте видљивост особа", + "reset_pin_code": "Ресетуј ПИН код", "reset_to_default": "Ресетујте на подразумеване вредности", "resolve_duplicates": "Реши дупликате", "resolved_all_duplicates": "Сви дупликати су разрешени", "restore": "Поврати", "restore_all": "Поврати све", "restore_user": "Поврати корисника", - "restored_asset": "Повраћено средство", + "restored_asset": "Поврац́ено средство", "resume": "Поново покрени", "retry_upload": "Покушајте поново да уплоадујете", "review_duplicates": "Прегледајте дупликате", @@ -1481,12 +1510,12 @@ "role_editor": "Уредник", "role_viewer": "Гледалац", "save": "Сачувај", - "save_to_gallery": "Save to gallery", - "saved_api_key": "Сачуван АПИ кључ (key)", + "save_to_gallery": "Сачувај у галерију", + "saved_api_key": "Сачуван АПИ кључ (кеy)", "saved_profile": "Сачуван профил", "saved_settings": "Сачувана подешавања", "say_something": "Реци нешто", - "scaffold_body_error_occurred": "Error occurred", + "scaffold_body_error_occurred": "Дошло је до грешке", "scan_all_libraries": "Скенирај све библиотеке", "scan_library": "Скенирај", "scan_settings": "Подешавања скенирања", @@ -1497,50 +1526,50 @@ "search_by_description": "Тражи по опису", "search_by_description_example": "Дан пешачења у Сапи", "search_by_filename": "Претражите по имену датотеке или екстензији", - "search_by_filename_example": "нпр. IMG_1234.JPG или PNG", + "search_by_filename_example": "нпр. ИМГ_1234.ЈПГ или ПНГ", "search_camera_make": "Претрага произвођача камере...", "search_camera_model": "Претражи модел камере...", "search_city": "Претражи град...", "search_country": "Тражи земљу...", - "search_filter_apply": "Apply filter", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", - "search_filter_display_option_not_in_album": "Not in album", - "search_filter_display_options": "Display Options", - "search_filter_filename": "Search by file name", - "search_filter_location": "Location", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", - "search_filter_media_type_title": "Select media type", - "search_filter_people_title": "Select people", + "search_filter_apply": "Примени филтер", + "search_filter_camera_title": "Изаберите тип камере", + "search_filter_date": "Дате", + "search_filter_date_interval": "{start} до {end}", + "search_filter_date_title": "Изаберите период", + "search_filter_display_option_not_in_album": "Нот ин албум", + "search_filter_display_options": "Опције приказа", + "search_filter_filename": "Претрага по имену датотеке", + "search_filter_location": "Локација", + "search_filter_location_title": "Изаберите локацију", + "search_filter_media_type": "Медиа Тyпе", + "search_filter_media_type_title": "Изаберите тип медија", + "search_filter_people_title": "Изаберите људе", "search_for": "Тражи", - "search_for_existing_person": "Потражите постојећу особу", - "search_no_more_result": "No more results", + "search_for_existing_person": "Потражите постојец́у особу", + "search_no_more_result": "Нема више резултата", "search_no_people": "Без особа", "search_no_people_named": "Нема особа са именом „{name}“", - "search_no_result": "No results found, try a different search term or combination", + "search_no_result": "Нису пронађени резултати, покушајте са другим термином за претрагу или комбинацијом", "search_options": "Опције претраге", - "search_page_categories": "Categories", - "search_page_motion_photos": "Motion Photos", - "search_page_no_objects": "No Objects Info Available", - "search_page_no_places": "No Places Info Available", - "search_page_screenshots": "Screenshots", - "search_page_search_photos_videos": "Search for your photos and videos", - "search_page_selfies": "Selfies", - "search_page_things": "Things", - "search_page_view_all_button": "View all", - "search_page_your_activity": "Your activity", - "search_page_your_map": "Your Map", + "search_page_categories": "Категорије", + "search_page_motion_photos": "Фотографије у покрету", + "search_page_no_objects": "Без информација", + "search_page_no_places": "Нема информација о месту", + "search_page_screenshots": "Снимци екрана", + "search_page_search_photos_videos": "Претражите своје фотографије и видео записе", + "search_page_selfies": "Селфији", + "search_page_things": "Ствари", + "search_page_view_all_button": "Прикажи све", + "search_page_your_activity": "Ваша активност", + "search_page_your_map": "Ваша мапа", "search_people": "Претражи особе", "search_places": "Претражи места", "search_rating": "Претрага по оцени...", - "search_result_page_new_search_hint": "New Search", + "search_result_page_new_search_hint": "Нова претрага", "search_settings": "Претрага подешавања", "search_state": "Тражи регион...", - "search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ", - "search_suggestion_list_smart_search_hint_2": "m:your-search-term", + "search_suggestion_list_smart_search_hint_1": "Паметна претрага је подразумевано омогуц́ена, за претрагу метаподатака користите синтаксу ", + "search_suggestion_list_smart_search_hint_2": "м:ваш-појам-за-претрагу", "search_tags": "Претражи ознаке (tags)...", "search_timezone": "Претражи временску зону...", "search_type": "Врста претраге", @@ -1559,18 +1588,19 @@ "select_keep_all": "Изаберите да задржите све", "select_library_owner": "Изаберите власника библиотеке", "select_new_face": "Изаберите ново лице", + "select_person_to_tag": "Изаберите особу за означавање", "select_photos": "Одабери фотографије", "select_trash_all": "Изаберите да све баците на отпад", - "select_user_for_sharing_page_err_album": "Failed to create album", + "select_user_for_sharing_page_err_album": "Неуспешно креирање албума", "selected": "Одабрано", "selected_count": "{count, plural, other {# изабрано}}", "send_message": "Пошаљи поруку", "send_welcome_email": "Пошаљите е-пошту добродошлице", - "server_endpoint": "Server Endpoint", - "server_info_box_app_version": "App Version", - "server_info_box_server_url": "Server URL", + "server_endpoint": "Крајња тачка сервера", + "server_info_box_app_version": "Верзија Апликације", + "server_info_box_server_url": "Сервер URL", "server_offline": "Сервер ван мреже (offline)", - "server_online": "Сервер нa мрежи (online)", + "server_online": "Сервер на мрежи (online)", "server_stats": "Статистика сервера", "server_version": "Верзија сервера", "set": "Постави", @@ -1580,91 +1610,92 @@ "set_date_of_birth": "Подесите датум рођења", "set_profile_picture": "Постави профилну слику", "set_slideshow_to_fullscreen": "Поставите пројекцију слајдова на цео екран", - "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", - "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", - "setting_image_viewer_original_title": "Load original image", - "setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.", - "setting_image_viewer_preview_title": "Load preview image", - "setting_image_viewer_title": "Images", - "setting_languages_apply": "Apply", - "setting_languages_subtitle": "Change the app's language", - "setting_languages_title": "Languages", - "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", - "setting_notifications_notify_hours": "{} hours", - "setting_notifications_notify_immediately": "immediately", - "setting_notifications_notify_minutes": "{} minutes", - "setting_notifications_notify_never": "never", - "setting_notifications_notify_seconds": "{} seconds", - "setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset", - "setting_notifications_single_progress_title": "Show background backup detail progress", - "setting_notifications_subtitle": "Adjust your notification preferences", - "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", - "setting_notifications_total_progress_title": "Show background backup total progress", - "setting_video_viewer_looping_title": "Looping", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", + "setting_image_viewer_help": "Прегледач детаља прво учитава малу сличицу, затим преглед средње величине (ако је омогуц́ен), и на крају оригинал (ако је омогуц́ен).", + "setting_image_viewer_original_subtitle": "Активирај учитавање слика у пуној резолуцији (Велика!). Деактивацијом ове ставке можеш да смањиш потрошњу интернета и заузетог простора на уређају.", + "setting_image_viewer_original_title": "Учитај оригиналну слику", + "setting_image_viewer_preview_subtitle": "Активирај учитавање слика у средњој резолуцији. Деактивирај да се директно учитава оригинал, или да се само користи минијатура.", + "setting_image_viewer_preview_title": "Прегледај слику", + "setting_image_viewer_title": "Слике", + "setting_languages_apply": "Примени", + "setting_languages_subtitle": "Промените језик апликације", + "setting_languages_title": "Језици", + "setting_notifications_notify_failures_grace_period": "Обавести о грешкама у прављењу резервних копија у позадини: {duration}", + "setting_notifications_notify_hours": "{count} сати", + "setting_notifications_notify_immediately": "одмах", + "setting_notifications_notify_minutes": "{count} минута", + "setting_notifications_notify_never": "никада", + "setting_notifications_notify_seconds": "{count} секунди", + "setting_notifications_single_progress_subtitle": "Детаљне информације о отпремању, по запису", + "setting_notifications_single_progress_title": "Прикажи детаље позадинског прављења резервних копија", + "setting_notifications_subtitle": "Измени нотификације", + "setting_notifications_total_progress_subtitle": "Укупно отпремљених ставки (завршено/укупно ставки)", + "setting_notifications_total_progress_title": "Прикажи укупан напредак прављења резервних копија у позадини", + "setting_video_viewer_looping_title": "Петљање (Лоопинг)", + "setting_video_viewer_original_video_subtitle": "Приликом стримовања видеа са сервера, репродукујте оригинал чак и када је доступно транскодирање. Може довести до баферовања. Видео снимци доступни локално се репродукују у оригиналном квалитету без обзира на ово подешавање.", + "setting_video_viewer_original_video_title": "Присилно оригинални видео", "settings": "Подешавања", - "settings_require_restart": "Please restart Immich to apply this setting", + "settings_require_restart": "Рестартујте Immich да примените ову промену", "settings_saved": "Подешавања сачувана", + "setup_pin_code": "Подесите ПИН код", "share": "Подели", - "share_add_photos": "Add photos", - "share_assets_selected": "{} selected", - "share_dialog_preparing": "Preparing...", + "share_add_photos": "Додај фотографије", + "share_assets_selected": "Изабрано је {count}", + "share_dialog_preparing": "Припремање...", "shared": "Дељено", - "shared_album_activities_input_disable": "Comment is disabled", - "shared_album_activity_remove_content": "Do you want to delete this activity?", - "shared_album_activity_remove_title": "Delete Activity", - "shared_album_section_people_action_error": "Error leaving/removing from album", - "shared_album_section_people_action_leave": "Remove user from album", - "shared_album_section_people_action_remove_user": "Remove user from album", - "shared_album_section_people_title": "PEOPLE", + "shared_album_activities_input_disable": "Коментар је oneмогуц́ен", + "shared_album_activity_remove_content": "Да ли желите да обришете ову активност?", + "shared_album_activity_remove_title": "Обриши активност", + "shared_album_section_people_action_error": "Грешка при напуштању/уклањању из албума", + "shared_album_section_people_action_leave": "Уклони корисника из албума", + "shared_album_section_people_action_remove_user": "Уклони корисника из албума", + "shared_album_section_people_title": "ПЕОПЛЕ", "shared_by": "Поделио", "shared_by_user": "Дели {user}", "shared_by_you": "Ви делите", "shared_from_partner": "Слике од {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", - "shared_link_app_bar_title": "Shared Links", - "shared_link_clipboard_copied_massage": "Copied to clipboard", - "shared_link_clipboard_text": "Link: {}\nPassword: {}", - "shared_link_create_error": "Error while creating shared link", - "shared_link_edit_description_hint": "Enter the share description", + "shared_intent_upload_button_progress_text": "Отпремљено је {current} / {total}", + "shared_link_app_bar_title": "Дељени linkови", + "shared_link_clipboard_copied_massage": "Копирано у међуспремник (цлипбоард)", + "shared_link_clipboard_text": "Линк: {link}\nЛозинка: {password}", + "shared_link_create_error": "Грешка при креирању дељеног linkа", + "shared_link_edit_description_hint": "Унесите опис дељења", "shared_link_edit_expire_after_option_day": "1 day", - "shared_link_edit_expire_after_option_days": "{} days", - "shared_link_edit_expire_after_option_hour": "1 hour", - "shared_link_edit_expire_after_option_hours": "{} hours", - "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{} minutes", - "shared_link_edit_expire_after_option_months": "{} months", - "shared_link_edit_expire_after_option_year": "{} year", - "shared_link_edit_password_hint": "Enter the share password", - "shared_link_edit_submit_button": "Update link", - "shared_link_error_server_url_fetch": "Cannot fetch the server url", - "shared_link_expires_day": "Expires in {} day", - "shared_link_expires_days": "Expires in {} days", - "shared_link_expires_hour": "Expires in {} hour", - "shared_link_expires_hours": "Expires in {} hours", - "shared_link_expires_minute": "Expires in {} minute", - "shared_link_expires_minutes": "Expires in {} minutes", - "shared_link_expires_never": "Expires ∞", - "shared_link_expires_second": "Expires in {} second", - "shared_link_expires_seconds": "Expires in {} seconds", - "shared_link_individual_shared": "Individual shared", + "shared_link_edit_expire_after_option_days": "{count} дана", + "shared_link_edit_expire_after_option_hour": "1 сат", + "shared_link_edit_expire_after_option_hours": "{count} сати", + "shared_link_edit_expire_after_option_minute": "1 минуте", + "shared_link_edit_expire_after_option_minutes": "{count} минута", + "shared_link_edit_expire_after_option_months": "{count} месеци", + "shared_link_edit_expire_after_option_year": "{count} година", + "shared_link_edit_password_hint": "Унесите лозинку за дељење", + "shared_link_edit_submit_button": "Упdate link", + "shared_link_error_server_url_fetch": "Не могу да преузмем URL сервера", + "shared_link_expires_day": "Истиче за {count} дан(а)", + "shared_link_expires_days": "Истиче за {count} дана", + "shared_link_expires_hour": "Истиче за {count} сат", + "shared_link_expires_hours": "Истиче за {count} сати(а)", + "shared_link_expires_minute": "Истиче за {count} минут", + "shared_link_expires_minutes": "Истиче за {count} минута", + "shared_link_expires_never": "Истиче ∞", + "shared_link_expires_second": "Истиче за {count} секунду", + "shared_link_expires_seconds": "Истиче за {count} секунди", + "shared_link_individual_shared": "Појединачно дељено", "shared_link_info_chip_metadata": "EXIF", - "shared_link_manage_links": "Manage Shared links", + "shared_link_manage_links": "Управљајте дељеним linkовима", "shared_link_options": "Опције дељене везе", "shared_links": "Дељене везе", - "shared_links_description": "Делите фотографије и видео записе помоћу линка", + "shared_links_description": "Делите фотографије и видео записе помоц́у linkа", "shared_photos_and_videos_count": "{assetCount, plural, other {# дељене фотографије и видео записе.}}", - "shared_with_me": "Shared with me", + "shared_with_me": "Дељено са мном", "shared_with_partner": "Дели се са {partner}", "sharing": "Дељење", "sharing_enter_password": "Унесите лозинку да бисте видели ову страницу.", - "sharing_page_album": "Shared albums", - "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", - "sharing_page_empty_list": "EMPTY LIST", + "sharing_page_album": "Дељени албуми", + "sharing_page_description": "Направи дељене албуме да делиш фотографије и видео записе са људима на твојој мрежи.", + "sharing_page_empty_list": "ПРАЗНА ЛИСТА", "sharing_sidebar_description": "Прикажите везу до Дељења на бочној траци", - "sharing_silver_appbar_create_shared_album": "New shared album", - "sharing_silver_appbar_share_partner": "Share with partner", + "sharing_silver_appbar_create_shared_album": "Направи дељени албум", + "sharing_silver_appbar_share_partner": "Подели са партнером", "shift_to_permanent_delete": "притисните ⇧ да трајно избришете датотеку", "show_album_options": "Прикажи опције албума", "show_albums": "Прикажи албуме", @@ -1693,8 +1724,8 @@ "sign_up": "Пријави се", "size": "Величина", "skip_to_content": "Пређи на садржај", - "skip_to_folders": "Прескочи на фасцикле", - "skip_to_tags": "Прескочи на ознаке (tags)", + "skip_to_folders": "Прескочи до мапа (фолдерс)", + "skip_to_tags": "Прескочи до ознака (tags)", "slideshow": "Слајдови", "slideshow_settings": "Подешавања слајдова", "sort_albums_by": "Сортирај албуме по...", @@ -1718,51 +1749,52 @@ "status": "Статус", "stop_motion_photo": "Заустави покретну фотографију", "stop_photo_sharing": "Желите да зауставите дељење фотографија?", - "stop_photo_sharing_description": "{partner} више неће моћи да приступи вашим фотографијама.", + "stop_photo_sharing_description": "{partner} више нец́е моц́и да приступи вашим фотографијама.", "stop_sharing_photos_with_user": "Престаните да делите своје фотографије са овим корисником", "storage": "Складиште (Storage space)", "storage_label": "Ознака за складиштење", + "storage_quota": "Квота складиштења", "storage_usage": "Користи се {used} од {available}", "submit": "Достави", "suggestions": "Сугестије", "sunrise_on_the_beach": "Излазак сунца на плажи", "support": "Подршка", "support_and_feedback": "Подршка и повратне информације", - "support_third_party_description": "Ваша иммицх инсталација је спакована од стране треће стране. Проблеми са којима се суочавате могу бити узроковани тим пакетом, па вас молимо да им прво поставите проблеме користећи доње везе.", + "support_third_party_description": "Ваша иммицх инсталација је спакована од стране трец́е стране. Проблеми са којима се суочавате могу бити узроковани тим пакетом, па вас молимо да им прво поставите проблеме користец́и доње везе.", "swap_merge_direction": "Замените правац спајања", "sync": "Синхронизација", - "sync_albums": "Sync albums", - "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", + "sync_albums": "Синхронизуј албуме", + "sync_albums_manual_subtitle": "Синхронизујте све отпремљене видео записе и фотографије са изабраним резервним албумима", + "sync_upload_album_setting_subtitle": "Креирајте и отпремите своје фотографије и видео записе у одабране албуме на Immich-u", "tag": "Ознака (tag)", - "tag_assets": "Означите датотеке", + "tag_assets": "Означите (tag) средства", "tag_created": "Направљена ознака (tag): {tag}", "tag_feature_description": "Прегледавање фотографија и видео снимака груписаних по логичним темама ознака", "tag_not_found_question": "Не можете да пронађете ознаку (tag)? Направите нову ознаку", "tag_people": "Означите људе", "tag_updated": "Ажурирана ознака (tag): {tag}", - "tagged_assets": "Означено (tagged) {count, plural, one {# датотека} other {# датотеке}}", + "tagged_assets": "Означено (tagгед) {count, plural, one {# датотека} other {# датотеке}}", "tags": "Ознаке (tags)", "template": "Шаблон (Темплате)", "theme": "Теме", "theme_selection": "Избор теме", "theme_selection_description": "Аутоматски поставите тему на светлу или тамну на основу системских преференција вашег претраживача", - "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", - "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", - "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", - "theme_setting_colorful_interface_title": "Colorful interface", - "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", - "theme_setting_image_viewer_quality_title": "Image viewer quality", - "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", - "theme_setting_primary_color_title": "Primary color", - "theme_setting_system_primary_color_title": "Use system color", - "theme_setting_system_theme_switch": "Automatic (Follow system setting)", - "theme_setting_theme_subtitle": "Choose the app's theme setting", - "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", - "theme_setting_three_stage_loading_title": "Enable three-stage loading", - "they_will_be_merged_together": "Они ће бити спојени заједно", - "third_party_resources": "Ресурси трећих страна", - "time_based_memories": "Сећања заснована на времену", + "theme_setting_asset_list_storage_indicator_title": "Прикажи индикатор простора на записима", + "theme_setting_asset_list_tiles_per_row_title": "Број записа по реду {count}", + "theme_setting_colorful_interface_subtitle": "Нанесите основну боју на позадинске површине.", + "theme_setting_colorful_interface_title": "Шарени интерфејс", + "theme_setting_image_viewer_quality_subtitle": "Прилагодите квалитет приказа за детаљно прегледавање слике", + "theme_setting_image_viewer_quality_title": "Квалитет прегледача слика", + "theme_setting_primary_color_subtitle": "Изаберите боју за главне радње и акценте.", + "theme_setting_primary_color_title": "Примарна боја", + "theme_setting_system_primary_color_title": "Користи системску боју", + "theme_setting_system_theme_switch": "Аутоматски (Прати опције система)", + "theme_setting_theme_subtitle": "Одабери тему система", + "theme_setting_three_stage_loading_subtitle": "Тростепено учитавање можда убрза учитавање, по цену потрошње података", + "theme_setting_three_stage_loading_title": "Активирај тростепено учитавање", + "they_will_be_merged_together": "Они ц́е бити спојени заједно", + "third_party_resources": "Ресурси трец́их страна", + "time_based_memories": "Сец́ања заснована на времену", "timeline": "Временска линија", "timezone": "Временска зона", "to_archive": "Архивирај", @@ -1770,36 +1802,38 @@ "to_favorite": "Постави као фаворит", "to_login": "Пријава", "to_parent": "Врати се назад", - "to_trash": "Смеће", - "toggle_settings": "Намести подешавања", - "toggle_theme": "Намести тамну тему", + "to_trash": "Смец́е", + "toggle_settings": "Nameсти подешавања", + "toggle_theme": "Nameсти тамну тему", "total": "Укупно", "total_usage": "Укупна употреба", "trash": "Отпад", "trash_all": "Баци све у отпад", "trash_count": "Отпад {count, number}", "trash_delete_asset": "Отпад/Избриши датотеку", - "trash_emptied": "Emptied trash", - "trash_no_results_message": "Слике и видео записи у отпаду ће се појавити овде.", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_title": "Trash ({})", - "trashed_items_will_be_permanently_deleted_after": "Датотеке у отпаду ће бити трајно избрисане након {days, plural, one {# дан} few {# дана} other {# дана}}.", + "trash_emptied": "Испразнио смец́е", + "trash_no_results_message": "Слике и видео записи у отпаду ц́е се појавити овде.", + "trash_page_delete_all": "Обриши све", + "trash_page_empty_trash_dialog_content": "Да ли желите да испразните своја премештена средства? Ови предмети ц́е бити трајно уклоњени из Immich-a", + "trash_page_info": "Ставке избачене из отпада биц́е трајно обрисане након {days} дана", + "trash_page_no_assets": "Нема елемената у отпаду", + "trash_page_restore_all": "Врати све", + "trash_page_select_assets_btn": "Изаберите средства", + "trash_page_title": "Отпад ({count})", + "trashed_items_will_be_permanently_deleted_after": "Датотеке у отпаду ц́е бити трајно избрисане након {days, plural, one {# дан} few {# дана} other {# дана}}.", "type": "Врста", + "unable_to_change_pin_code": "Није могуц́е променити ПИН код", + "unable_to_setup_pin_code": "Није могуц́е подесити ПИН код", "unarchive": "Врати из архиве", - "unarchived_count": "{count, plural, other {Nearhivirano#}}", + "unarchived_count": "{count, plural, other {Неархивирано#}}", "unfavorite": "Избаци из омиљених (унфаворите)", "unhide_person": "Откриј особу", "unknown": "Непознат", "unknown_country": "Непозната земља", "unknown_year": "Непозната Година", "unlimited": "Неограничено", - "unlink_motion_video": "Прекините везу са видео снимком", - "unlink_oauth": "Прекини везу са Oauth-om", + "unlink_motion_video": "Одвежи видео од слике", + "unlink_oauth": "Прекини везу са Оаутх-ом", "unlinked_oauth_account": "Опозвана веза OAuth налога", "unmute_memories": "Укључи успомене", "unnamed_album": "Неименовани албум", @@ -1810,14 +1844,15 @@ "unselect_all_duplicates": "Поништи избор свих дупликата", "unstack": "Разгомилај (Ун-стацк)", "unstacked_assets_count": "Несложено {count, plural, one {# датотека} other {# датотеке}}", - "untracked_files": "Непраћене Датотеке", - "untracked_files_decription": "Апликација не прати ове датотеке. one могу настати због неуспешних премештења, због прекинутих отпремања или као преостатак због грешке", - "up_next": "Следећ(е/и)", + "untracked_files": "Непрац́ене Датотеке", + "untracked_files_decription": "Апликација не прати ове датотеке. Оне могу настати због неуспешних премештења, због прекинутих отпремања или као преостатак због грешке", + "up_next": "Следец́е", + "updated_at": "Ажурирано", "updated_password": "Ажурирана лозинка", "upload": "Уплоадуј", "upload_concurrency": "Паралелно уплоадовање", - "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", - "upload_dialog_title": "Upload Asset", + "upload_dialog_info": "Да ли желите да направите резервну копију изабраних елемената на серверу?", + "upload_dialog_title": "Отпреми елемент", "upload_errors": "Отпремање је завршено са {count, plural, one {# грешком} other {# грешака}}, освежите страницу да бисте видели нове датотеке за отпремање (уплоад).", "upload_progress": "Преостало {remaining, number} – Обрађено {processed, number}/{total, number}", "upload_skipped_duplicates": "Прескочено {count, plural, one {# дупла датотека} other {# дуплих датотека}}", @@ -1825,43 +1860,45 @@ "upload_status_errors": "Грешке", "upload_status_uploaded": "Отпремљено (Уплоадед)", "upload_success": "Отпремање је успешно, освежите страницу да бисте видели нова средства за отпремање (уплоад).", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", - "url": "УРЛ", + "upload_to_immich": "Отпреми у Immich ({count})", + "uploading": "Отпремање", + "url": "URL", "usage": "Употреба", - "use_current_connection": "use current connection", + "use_current_connection": "користи тренутну везу", "use_custom_date_range": "Уместо тога користите прилагођени период", "user": "Корисник", "user_id": "ИД корисника", "user_liked": "{user} је лајковао {type, select, photo {ову фотографију} video {овај видео запис} asset {ову датотеку} other {ово}}", + "user_pin_code_settings": "ПИН код", + "user_pin_code_settings_description": "Управљајте својим ПИН кодом", "user_purchase_settings": "Куповина", "user_purchase_settings_description": "Управљајте куповином", "user_role_set": "Постави {user} као {role}", - "user_usage_detail": "Детаљи коришћења корисника", - "user_usage_stats": "Статистика коришћења налога", - "user_usage_stats_description": "Погледајте статистику коришћења налога", + "user_usage_detail": "Детаљи коришц́ења корисника", + "user_usage_stats": "Статистика коришц́ења налога", + "user_usage_stats_description": "Погледајте статистику коришц́ења налога", "username": "Корисничко име", "users": "Корисници", "utilities": "Алати", "validate": "Провери", - "validate_endpoint_error": "Please enter a valid URL", + "validate_endpoint_error": "Молимо вас да унесете важец́и URL", "variables": "Променљиве (вариаблес)", "version": "Верзија", "version_announcement_closing": "Твој пријатељ, Алекс", - "version_announcement_message": "Здраво пријатељу, постоји нова верзија апликације, молимо вас да одвојите време да посетите напомене о издању и уверите се у своје docker-compose.yml, и .env подешавање је ажурирано како би се спречиле било какве погрешне конфигурације, посебно ако користите WatchTower или било који механизам који аутоматски управља ажурирањем ваше апликације.", - "version_announcement_overlay_release_notes": "release notes", - "version_announcement_overlay_text_1": "Hi friend, there is a new release of", - "version_announcement_overlay_text_2": "please take your time to visit the ", - "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", - "version_announcement_overlay_title": "New Server Version Available 🎉", + "version_announcement_message": "Здраво! Доступна је нова верзија Immich-a. Молимо вас да одвојите мало времена да прочитате белешке о издању како бисте били сигурни да је ваше подешавање ажурирано и спречили евентуалне погрешне конфигурације, посебно ако користите WатцхТоwер или било који механизам који аутоматски ажурира вашу Immich инстанцу.", + "version_announcement_overlay_release_notes": "новине нове верзије", + "version_announcement_overlay_text_1": "Ћао, нова верзија", + "version_announcement_overlay_text_2": "молимо Вас издвојите времена да поглеdate ", + "version_announcement_overlay_text_3": " и проверите да су Ваш доцкер-цомпосе и .енв најновије верзије да би избегли грешке у раду. Поготову ако користите WатцхТоwер или било који други механизам који аутоматски инсталира нове верзије ваше серверске апликације.", + "version_announcement_overlay_title": "Нова верзија сервера је доступна 🎉", "version_history": "Историја верзија", - "version_history_item": "Инсталирано {version} on {date}", + "version_history_item": "Инсталирано {version} {date}", "video": "Видео запис", "video_hover_setting": "Пусти сличицу видеа када лебди", - "video_hover_setting_description": "Пусти сличицу видеа када миш пређе преко ставке. Чак и када је oneмогућена, репродукција се може покренути преласком миша преко икone за репродукцију.", + "video_hover_setting_description": "Пусти сличицу видеа када миш пређе преко ставке. Чак и када је oneмогуц́ена, репродукција се може покренути преласком миша преко икone за репродукцију.", "videos": "Видео записи", "videos_count": "{count, plural, one {# видео запис} few {# видео записа} other {# видео записа}}", - "view": "Гледај (view)", + "view": "Гледај (виеw)", "view_album": "Погледај албум", "view_all": "Прикажи Све", "view_all_users": "Прикажи све кориснике", @@ -1869,24 +1906,24 @@ "view_link": "Погледај везу", "view_links": "Прикажи везе", "view_name": "Погледати", - "view_next_asset": "Погледајте следећу датотеку", + "view_next_asset": "Погледајте следец́у датотеку", "view_previous_asset": "Погледај претходну датотеку", - "view_qr_code": "Погледајте QR код", + "view_qr_code": "Погледајте QР код", "view_stack": "Прикажи гомилу", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack", + "viewer_remove_from_stack": "Уклони из стека", + "viewer_stack_use_as_main_asset": "Користи као главни ресурс", + "viewer_unstack": "Ун-Стацк", "visibility_changed": "Видљивост је промењена за {count, plural, one {# особу} other {# особе}}", "waiting": "Чекам", "warning": "Упозорење", "week": "Недеља", "welcome": "Добродошли", - "welcome_to_immich": "Добродошли у Имич (Immich)", - "wifi_name": "WiFi Name", + "welcome_to_immich": "Добродошли у иммицх", + "wifi_name": "Назив Wi-Fi мреже", "year": "Година", "years_ago": "пре {years, plural, one {# године} other {# година}}", "yes": "Да", "you_dont_have_any_shared_links": "Немате ниједно дељење везе", - "your_wifi_name": "Your WiFi name", + "your_wifi_name": "Име ваше Wi-Fi мреже", "zoom_image": "Зумирај слику" } diff --git a/i18n/sr_Latn.json b/i18n/sr_Latn.json index 055a156d71..2486ed0161 100644 --- a/i18n/sr_Latn.json +++ b/i18n/sr_Latn.json @@ -1,6 +1,6 @@ { - "about": "O Aplikaciji", - "account": "Profil", + "about": "O aplikaciji", + "account": "Nalog", "account_settings": "Podešavanja za Profil", "acknowledge": "Potvrdi", "action": "Postupak", @@ -15,7 +15,7 @@ "add_a_name": "Dodaj ime", "add_a_title": "Dodaj naslov", "add_endpoint": "Dodajte krajnju tačku", - "add_exclusion_pattern": "Dodaj obrazac izuzimanja", + "add_exclusion_pattern": "Dodajte obrazac izuzimanja", "add_import_path": "Dodaj putanju za preuzimanje", "add_location": "Dodaj lokaciju", "add_more_users": "Dodaj korisnike", @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "Da li ste sigurni da želite da onemogućite sve metode prijavljivanja? Prijava će biti potpuno onemogućena.", "authentication_settings_reenable": "Da biste ponovo omogućili, koristite komandu servera.", "background_task_job": "Pozadinski zadaci", - "backup_database": "Rezervna kopija baze podataka", - "backup_database_enable_description": "Omogućite rezervne kopije baze podataka", - "backup_keep_last_amount": "Količina prethodnih rezervnih kopija za čuvanje", - "backup_settings": "Podešavanja rezervne kopije", - "backup_settings_description": "Upravljajte postavkama rezervne kopije baze podataka", + "backup_database": "Kreirajte rezervnu kopiju baze podataka", + "backup_database_enable_description": "Omogući dampove baze podataka", + "backup_keep_last_amount": "Količina prethodnih dampova koje treba zadržati", + "backup_settings": "Podešavanja dampa baze podataka", + "backup_settings_description": "Upravljajte podešavanjima dampa baze podataka. Napomena: Ovi poslovi se ne prate i nećete biti obavešteni o neuspehu.", "check_all": "Proveri sve", "cleanup": "Čišćenje", "cleared_jobs": "Očišćeni poslovi za: {job}", @@ -53,6 +53,7 @@ "confirm_email_below": "Da biste potvrdili, unesite \"{email}\" ispod", "confirm_reprocess_all_faces": "Da li ste sigurni da želite da ponovo obradite sva lica? Ovo će takođe obrisati imenovane osobe.", "confirm_user_password_reset": "Da li ste sigurni da želite da resetujete lozinku korisnika {user}?", + "confirm_user_pin_code_reset": "Da li ste sigurni da želite da resetujete PIN kod korisnika {user}?", "create_job": "Kreirajte posao", "cron_expression": "Cron izraz (expression)", "cron_expression_description": "Podesite interval skeniranja koristeći cron format. Za više informacija pogledajte npr. Crontab Guru", @@ -106,7 +107,7 @@ "library_scanning_enable_description": "Omogućite periodično skeniranje biblioteke", "library_settings": "Spoljna biblioteka", "library_settings_description": "Upravljajte podešavanjima spoljne biblioteke", - "library_tasks_description": "Skenirajte spoljne biblioteke u potrazi za novim i/ili promenjenim sredstvima", + "library_tasks_description": "Obavljaj zadatke biblioteke", "library_watching_enable_description": "Pratite spoljne biblioteke za promene datoteka", "library_watching_settings": "Nadgledanje biblioteke (EKSPERIMENTALNO)", "library_watching_settings_description": "Automatski pratite promenjene datoteke", @@ -141,7 +142,7 @@ "machine_learning_smart_search_description": "Potražite slike semantički koristeći ugrađeni CLIP", "machine_learning_smart_search_enabled": "Omogućite pametnu pretragu", "machine_learning_smart_search_enabled_description": "Ako je onemogućeno, slike neće biti kodirane za pametnu pretragu.", - "machine_learning_url_description": "URL servera za mašinsko učenje. Ako je obezbeđeno više URL-ova, svaki server će biti pokušan redom, jedan po jedan, dok jedan ne odgovori uspešno, po redosledu od prvog do poslednjeg. Serveri koji ne reaguju biće privremeno zanemareni dok se ne vrate na mrežu.", + "machine_learning_url_description": "URL servera za mašinsko učenje. Ako je navedeno više URL adresa, svaki server će biti pokušavan pojedinačno dok ne odgovori uspešno, redom od prvog do poslednjeg. Serveri koji ne odgovore biće privremeno ignorisani dok se ponovo ne povežu sa mrežom.", "manage_concurrency": "Upravljanje paralelnošću", "manage_log_settings": "Upravljajte podešavanjima evidencije", "map_dark_style": "Tamni stil", @@ -192,6 +193,7 @@ "oauth_auto_register": "Automatska registracija", "oauth_auto_register_description": "Automatski registrujte nove korisnike nakon što se prijavite pomoću OAuth-a", "oauth_button_text": "Tekst dugmeta", + "oauth_client_secret_description": "Potrebno ako OAuth provajder ne podržava PKCE (Proof Key for Code Exchange)", "oauth_enable_description": "Prijavite se pomoću OAuth-a", "oauth_mobile_redirect_uri": "URI za preusmeravanje mobilnih uređaja", "oauth_mobile_redirect_uri_override": "Zamena URI-ja mobilnog preusmeravanja", @@ -205,6 +207,8 @@ "oauth_storage_quota_claim_description": "Automatski podesite kvotu memorijskog prostora korisnika na vrednost ovog zahteva.", "oauth_storage_quota_default": "Podrazumevana kvota za skladištenje (GiB)", "oauth_storage_quota_default_description": "Kvota u GiB koja se koristi kada nema potraživanja (unesite 0 za neograničenu kvotu).", + "oauth_timeout": "Vremensko ograničenje zahteva", + "oauth_timeout_description": "Vremensko ograničenje za zahteve u milisekundama", "offline_paths": "Vanmrežne putanje", "offline_paths_description": "Ovi rezultati mogu biti posledica ručnog brisanja datoteka koje nisu deo spoljne biblioteke.", "password_enable_description": "Prijavite se pomoću e-pošte i lozinke", @@ -244,7 +248,7 @@ "storage_template_hash_verification_enabled_description": "Omogućava heš verifikaciju, ne onemogućavajte ovo osim ako niste sigurni u posledice", "storage_template_migration": "Migracija šablona za skladištenje", "storage_template_migration_description": "Primenite trenutni {template} na prethodno otpremljene elemente", - "storage_template_migration_info": "Šablon za skladištenje će pretvoriti sve ekstenzije u mala slova. Promene šablona će se primeniti samo na nove datoteke. Da biste retroaktivno primenili šablon na prethodno otpremljene datoteke, pokrenite {job}.", + "storage_template_migration_info": "Promene šablona će se primeniti samo na nove datoteke. Da biste retroaktivno primenili šablon na prethodno otpremljene datoteke, pokrenite {job}.", "storage_template_migration_job": "Posao migracije skladišta", "storage_template_more_details": "Za više detalja o ovoj funkciji pogledajte Šablon za skladište i njegove implikacije", "storage_template_onboarding_description": "Kada je omogućena, ova funkcija će automatski organizovati datoteke na osnovu šablona koji definiše korisnik. Zbog problema sa stabilnošću ova funkcija je podrazumevano isključena. Za više informacija pogledajte dokumentaciju.", @@ -303,7 +307,7 @@ "transcoding_max_b_frames": "Maksimalni B-kadri", "transcoding_max_b_frames_description": "Više vrednosti poboljšavaju efikasnost kompresije, ali usporavaju kodiranje. Možda nije kompatibilno sa hardverskim ubrzanjem na starijim uređajima. 0 onemogućava B-kadre, dok -1 automatski postavlja ovu vrednost.", "transcoding_max_bitrate": "Maksimalni bitrate", - "transcoding_max_bitrate_description": "Podešavanje maksimalnog bitrate-a može učiniti veličine datoteka predvidljivijim uz manju cenu kvaliteta. Pri 720p, tipične vrednosti su 2600 kbit/s za VP9 ili HEVC, ili 4500 kbit/s za H.264. Onemogućeno ako je postavljeno na 0.", + "transcoding_max_bitrate_description": "Podešavanje maksimalnog bitrate-a može učiniti veličine datoteka predvidljivijim uz manju cenu kvaliteta. Pri 720p, tipične vrednosti su 2600k za VP9 ili HEVC, ili 4500k za H.264. Onemogućeno ako je postavljeno na 0.", "transcoding_max_keyframe_interval": "Maksimalni interval keyframe-a", "transcoding_max_keyframe_interval_description": "Postavlja maksimalnu udaljenost kadrova između ključnih kadrova. Niže vrednosti pogoršavaju efikasnost kompresije, ali poboljšavaju vreme traženja i mogu poboljšati kvalitet scena sa brzim kretanjem. 0 automatski postavlja ovu vrednost.", "transcoding_optimal_description": "Video snimci veći od ciljne rezolucije ili nisu u prihvaćenom formatu", @@ -317,7 +321,7 @@ "transcoding_reference_frames_description": "Broj okvira (frames) za referencu prilikom kompresije datog okvira. Više vrednosti poboljšavaju efikasnost kompresije, ali usporavaju kodiranje. 0 automatski postavlja ovu vrednost.", "transcoding_required_description": "Samo video snimci koji nisu u prihvaćenom formatu", "transcoding_settings": "Podešavanja video transkodiranja", - "transcoding_settings_description": "Upravljajte koje video snimke želite da transkodujete i kako ih obraditi", + "transcoding_settings_description": "Upravljajte rezolucijom i informacijama o kodiranju video datoteka", "transcoding_target_resolution": "Ciljana rezolucija", "transcoding_target_resolution_description": "Veće rezolucije mogu da sačuvaju više detalja, ali im je potrebno više vremena za kodiranje, imaju veće veličine datoteka i mogu da smanje brzinu aplikacije.", "transcoding_temporal_aq": "Vremenski (Temporal) AQ", @@ -345,6 +349,7 @@ "user_delete_delay_settings_description": "Broj dana nakon uklanjanja za trajno brisanje korisničkog naloga i datoteka. Posao brisanja korisnika se pokreće u ponoć da bi se proverili korisnici koji su spremni za brisanje. Promene ove postavke će biti procenjene pri sledećem izvršenju.", "user_delete_immediately": "Nalog i datoteke {user} će biti stavljeni na čekanje za trajno brisanje odmah.", "user_delete_immediately_checkbox": "Stavite korisnika i datoteke u red za trenutno brisanje", + "user_details": "Detalji korisnika", "user_management": "Upravljanje korisnicima", "user_password_has_been_reset": "Lozinka korisnika je resetovana:", "user_password_reset_description": "Molimo da dostavite privremenu lozinku korisniku i obavestite ga da će morati da promeni lozinku prilikom sledećeg prijavljivanja.", @@ -366,18 +371,18 @@ "advanced": "Napredno", "advanced_settings_enable_alternate_media_filter_subtitle": "Koristite ovu opciju za filtriranje medija tokom sinhronizacije na osnovu alternativnih kriterijuma. Pokušajte ovo samo ako imate problema sa aplikacijom da otkrije sve albume.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTALNO] Koristite filter za sinhronizaciju albuma na alternativnom uređaju", - "advanced_settings_log_level_title": "Nivo evidencije (log): {}", + "advanced_settings_log_level_title": "Nivo evidencije (log): {level}", "advanced_settings_prefer_remote_subtitle": "Neki uređaji veoma sporo učitavaju sličice sa sredstava na uređaju. Aktivirajte ovo podešavanje da biste umesto toga učitali udaljene slike.", "advanced_settings_prefer_remote_title": "Preferirajte udaljene slike", "advanced_settings_proxy_headers_subtitle": "Definišite proksi zaglavlja koje Immich treba da pošalje sa svakim mrežnim zahtevom", "advanced_settings_proxy_headers_title": "Proksi Headeri (headers)", "advanced_settings_self_signed_ssl_subtitle": "Preskače verifikaciju SSL sertifikata za krajnju tačku servera. Obavezno za samopotpisane sertifikate.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", + "advanced_settings_self_signed_ssl_title": "Dozvoli samopotpisane SSL sertifikate", "advanced_settings_sync_remote_deletions_subtitle": "Automatski izbrišite ili vratite sredstvo na ovom uređaju kada se ta radnja preduzme na vebu", "advanced_settings_sync_remote_deletions_title": "Sinhronizujte udaljena brisanja [EKSPERIMENTALNO]", - "advanced_settings_tile_subtitle": "Advanced user's settings", - "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", - "advanced_settings_troubleshooting_title": "Troubleshooting", + "advanced_settings_tile_subtitle": "Napredna korisnička podešavanja", + "advanced_settings_troubleshooting_subtitle": "Omogućite dodatne funkcije za rešavanje problema", + "advanced_settings_troubleshooting_title": "Rešavanje problema", "age_months": "Starost{months, plural, one {# mesec} other {# meseci}}", "age_year_months": "Starost 1 godina, {months, plural, one {# mesec} other {# mesec(a/i)}}", "age_years": "{years, plural, other {Starost #}}", @@ -397,20 +402,20 @@ "album_remove_user_confirmation": "Da li ste sigurni da želite da uklonite {user}?", "album_share_no_users": "Izgleda da ste podelili ovaj album sa svim korisnicima ili da nemate nijednog korisnika sa kojim biste delili.", "album_thumbnail_card_item": "1 stavka", - "album_thumbnail_card_items": "{} stavki", - "album_thumbnail_card_shared": "Deljeno", - "album_thumbnail_shared_by": "Deli {}", + "album_thumbnail_card_items": "{count} stavki", + "album_thumbnail_card_shared": " Deljeno", + "album_thumbnail_shared_by": "Deli {user}", "album_updated": "Album ažuriran", "album_updated_setting_description": "Primite obaveštenje e-poštom kada deljeni album ima nova svojstva", "album_user_left": "Napustio/la {album}", "album_user_removed": "Uklonjen {user}", - "album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?", + "album_viewer_appbar_delete_confirm": "Da li ste sigurni da želite da izbrišete ovaj album sa svog naloga?", "album_viewer_appbar_share_err_delete": "Neuspešno brisanje albuma", "album_viewer_appbar_share_err_leave": "Neuspešno izlaženje iz albuma", "album_viewer_appbar_share_err_remove": "Problemi sa brisanjem zapisa iz albuma", "album_viewer_appbar_share_err_title": "Neuspešno menjanje naziva albuma", "album_viewer_appbar_share_leave": "Izađi iz albuma", - "album_viewer_appbar_share_to": "Share To", + "album_viewer_appbar_share_to": "Podeli sa", "album_viewer_page_share_add_users": "Dodaj korisnike", "album_with_link_access": "Neka svako ko ima vezu vidi fotografije i ljude u ovom albumu.", "albums": "Albumi", @@ -429,32 +434,32 @@ "api_key_description": "Ova vrednost će biti prikazana samo jednom. Obavezno kopirajte pre nego što zatvorite prozor.", "api_key_empty": "Ime vašeg API ključa ne bi trebalo da bude prazno", "api_keys": "API ključevi (keys)", - "app_bar_signout_dialog_content": "Are you sure you want to sign out?", - "app_bar_signout_dialog_ok": "Yes", - "app_bar_signout_dialog_title": "Sign out", + "app_bar_signout_dialog_content": "Da li ste sigurni da želite da se odjavite?", + "app_bar_signout_dialog_ok": "Da", + "app_bar_signout_dialog_title": "Odjavite se", "app_settings": "Podešavanja aplikacije", "appears_in": "Pojavljuje se u", "archive": "Arhiva", "archive_or_unarchive_photo": "Arhivirajte ili poništite arhiviranje fotografije", - "archive_page_no_archived_assets": "No archived assets found", - "archive_page_title": "Archive ({})", + "archive_page_no_archived_assets": "Nisu pronađena arhivirana sredstva", + "archive_page_title": "Arhiva ({count})", "archive_size": "Veličina arhive", "archive_size_description": "Podesi veličinu arhive za preuzimanje (u GiB)", - "archived": "Archived", + "archived": "Arhivirano", "archived_count": "{count, plural, other {Arhivirano #}}", "are_these_the_same_person": "Da li su ovo ista osoba?", "are_you_sure_to_do_this": "Jeste li sigurni da želite ovo da uradite?", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", + "asset_action_delete_err_read_only": "Ne mogu da obrišem element(e) samo za čitanje, preskačem", + "asset_action_share_err_offline": "Nije moguće preuzeti oflajn resurs(e), preskačem", "asset_added_to_album": "Dodato u album", "asset_adding_to_album": "Dodaje se u album…", "asset_description_updated": "Opis datoteke je ažuriran", "asset_filename_is_offline": "Datoteka {filename} je van mreže (offline)", "asset_has_unassigned_faces": "Datoteka ima nedodeljena lica", "asset_hashing": "Heširanje…", - "asset_list_group_by_sub_title": "Group by", + "asset_list_group_by_sub_title": "Grupiši po", "asset_list_layout_settings_dynamic_layout_title": "Dinamični raspored", - "asset_list_layout_settings_group_automatically": "Automatic", + "asset_list_layout_settings_group_automatically": "Automatski", "asset_list_layout_settings_group_by": "Grupiši zapise po", "asset_list_layout_settings_group_by_month_day": "Mesec + Dan", "asset_list_layout_sub_title": "Layout", @@ -462,54 +467,54 @@ "asset_list_settings_title": "Mrežni prikaz fotografija", "asset_offline": "Datoteka odsutna", "asset_offline_description": "Ova vanjska datoteka se više ne nalazi na disku. Molimo kontaktirajte svog Immich administratora za pomoć.", - "asset_restored_successfully": "Asset restored successfully", + "asset_restored_successfully": "Imovina je uspešno vraćena", "asset_skipped": "Preskočeno", "asset_skipped_in_trash": "U otpad", "asset_uploaded": "Otpremljeno (Uploaded)", "asset_uploading": "Otpremanje…", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", - "asset_viewer_settings_title": "Asset Viewer", + "asset_viewer_settings_subtitle": "Upravljajte podešavanjima pregledača galerije", + "asset_viewer_settings_title": "Pregledač imovine", "assets": "Zapisi", "assets_added_count": "Dodato {count, plural, one {# datoteka} other {# datoteka}}", "assets_added_to_album_count": "Dodato je {count, plural, one {# datoteka} other {# datoteka}} u album", "assets_added_to_name_count": "Dodato {count, plural, one {# datoteka} other {# datoteke}} u {hasName, select, true {{name}} other {novi album}}", "assets_count": "{count, plural, one {# datoteka} few {# datoteke} other {# datoteka}}", - "assets_deleted_permanently": "{} asset(s) deleted permanently", - "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_deleted_permanently": "{count} elemenata trajno obrisano", + "assets_deleted_permanently_from_server": "{count} resurs(a) trajno obrisan(a) sa Immich servera", "assets_moved_to_trash_count": "Premešteno {count, plural, one {# datoteka} few {# datoteke} other {# datoteka}} u otpad", "assets_permanently_deleted_count": "Trajno izbrisano {count, plural, one {# datoteka} few {# datoteke} other {# datoteka}}", "assets_removed_count": "Uklonjeno {count, plural, one {# datoteka} few {# datoteke} other {# datoteka}}", - "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_removed_permanently_from_device": "{count} elemenata trajno uklonjeno sa vašeg uređaja", "assets_restore_confirmation": "Da li ste sigurni da želite da vratite sve svoje datoteke koje su u otpadu? Ne možete poništiti ovu radnju! Imajte na umu da se vanmrežna sredstva ne mogu vratiti na ovaj način.", "assets_restored_count": "Vraćeno {count, plural, one {# datoteka} few {# datoteke} other {# datoteka}}", - "assets_restored_successfully": "{} asset(s) restored successfully", - "assets_trashed": "{} asset(s) trashed", + "assets_restored_successfully": "{count} elemenata uspešno vraćeno", + "assets_trashed": "{count} elemenata je prebačeno u otpad", "assets_trashed_count": "Bačeno u otpad {count, plural, one {# datoteka} few{# datoteke} other {# datoteka}}", - "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "assets_trashed_from_server": "{count} resurs(a) obrisanih sa Immich servera", "assets_were_part_of_album_count": "{count, plural, one {Datoteka je} other {Datoteke su}} već deo albuma", "authorized_devices": "Ovlašćeni uređaji", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", + "automatic_endpoint_switching_subtitle": "Povežite se lokalno preko određenog Wi-Fi-ja kada je dostupan i koristite alternativne veze na drugim mestima", + "automatic_endpoint_switching_title": "Automatska promena URL-ova", "back": "Nazad", "back_close_deselect": "Nazad, zatvorite ili opozovite izbor", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", - "backup_album_selection_page_albums_device": "Albuma na uređaju ({})", + "background_location_permission": "Dozvola za lokaciju u pozadini", + "background_location_permission_content": "Da bi se menjale mreže dok se radi u pozadini, Imih mora *uvek* imati precizan pristup lokaciji kako bi aplikacija mogla da pročita ime Wi-Fi mreže", + "backup_album_selection_page_albums_device": "Albuma na uređaju ({count})", "backup_album_selection_page_albums_tap": "Dodirni da uključiš, dodirni dvaput da isključiš", "backup_album_selection_page_assets_scatter": "Zapisi se mogu naći u više različitih albuma. Odatle albumi se mogu uključiti ili isključiti tokom procesa pravljenja pozadinskih kopija.", "backup_album_selection_page_select_albums": "Odaberi albume", "backup_album_selection_page_selection_info": "Informacije o selekciji", "backup_album_selection_page_total_assets": "Ukupno jedinstvenih ***", "backup_all": "Sve", - "backup_background_service_backup_failed_message": "Neuspešno pravljenje rezervne kopije. Pokušavam ponovo...", - "backup_background_service_connection_failed_message": "Neuspešno povezivanje sa serverom. Pokušavam ponovo...", - "backup_background_service_current_upload_notification": "Otpremanje {}", - "backup_background_service_default_notification": "Proveravanje novih zapisa", + "backup_background_service_backup_failed_message": "Pravljenje rezervne kopije elemenata nije uspelo. Pokušava se ponovo…", + "backup_background_service_connection_failed_message": "Povezivanje sa serverom nije uspelo. Pokušavam ponovo…", + "backup_background_service_current_upload_notification": "Otpremanje {filename}", + "backup_background_service_default_notification": "Proveravanje novih zapisa…", "backup_background_service_error_title": "Greška u pravljenju rezervnih kopija", - "backup_background_service_in_progress_notification": "Pravljenje rezervnih kopija zapisa", - "backup_background_service_upload_failure_notification": "Neuspešno otpremljeno: {}", + "backup_background_service_in_progress_notification": "Pravljenje rezervnih kopija zapisa…", + "backup_background_service_upload_failure_notification": "Neuspešno otpremljeno: {filename}", "backup_controller_page_albums": "Napravi rezervnu kopiju albuma", - "backup_controller_page_background_app_refresh_disabled_content": "Aktiviraj pozadinsko osvežavanje u Opcije Generalne Pozadinsko Osvežavanje kako bi napravili rezervne kopije u pozadini", + "backup_controller_page_background_app_refresh_disabled_content": "Aktiviraj pozadinsko osvežavanje u Opcije > Generalne > Pozadinsko Osvežavanje kako bi napravili rezervne kopije u pozadini.", "backup_controller_page_background_app_refresh_disabled_title": "Pozadinsko osvežavanje isključeno", "backup_controller_page_background_app_refresh_enable_button_text": "Idi u podešavanja", "backup_controller_page_background_battery_info_link": "Pokaži mi kako", @@ -518,22 +523,22 @@ "backup_controller_page_background_battery_info_title": "Optimizacija Baterije", "backup_controller_page_background_charging": "Samo tokom punjenja", "backup_controller_page_background_configure_error": "Neuspešno konfigurisanje pozadinskog servisa", - "backup_controller_page_background_delay": "Vreme između pravljejna rezervnih kopija zapisa: {}", + "backup_controller_page_background_delay": "Vreme između pravljejna rezervnih kopija zapisa: {duration}", "backup_controller_page_background_description": "Uključi pozadinski servis da automatski praviš rezervne kopije, bez da otvaraš aplikaciju", "backup_controller_page_background_is_off": "Automatsko pravljenje rezervnih kopija u pozadini je isključeno", "backup_controller_page_background_is_on": "Automatsko pravljenje rezervnih kopija u pozadini je uključeno", "backup_controller_page_background_turn_off": "Isključi pozadinski servis", "backup_controller_page_background_turn_on": "Uključi pozadinski servis", - "backup_controller_page_background_wifi": "Samo na WiFi", + "backup_controller_page_background_wifi": "Samo na Wi-Fi", "backup_controller_page_backup": "Napravi rezervnu kopiju", "backup_controller_page_backup_selected": "Odabrano: ", "backup_controller_page_backup_sub": "Završeno pravljenje rezervne kopije fotografija i videa", - "backup_controller_page_created": "Napravljeno:{}", + "backup_controller_page_created": "Napravljeno:{date}", "backup_controller_page_desc_backup": "Uključi pravljenje rezervnih kopija u prvom planu da automatski napravite rezervne kopije kada otvorite aplikaciju.", "backup_controller_page_excluded": "Isključeno: ", - "backup_controller_page_failed": "Neuspešno ({})", - "backup_controller_page_filename": "Ime fajla:{} [{}]", - "backup_controller_page_id": "ID:{}", + "backup_controller_page_failed": "Neuspešno ({count})", + "backup_controller_page_filename": "Ime fajla: {filename} [{size}]", + "backup_controller_page_id": "ID:{id}", "backup_controller_page_info": "Informacije", "backup_controller_page_none_selected": "Ništa odabrano", "backup_controller_page_remainder": "Podsetnik", @@ -542,7 +547,7 @@ "backup_controller_page_start_backup": "Pokreni pravljenje rezervne kopije", "backup_controller_page_status_off": "Automatsko pravljenje rezervnih kopija u prvom planu je isključeno", "backup_controller_page_status_on": "Automatsko pravljenje rezervnih kopija u prvom planu je uključeno", - "backup_controller_page_storage_format": "{} od {} iskorišćeno", + "backup_controller_page_storage_format": "{used} od {total} iskorišćeno", "backup_controller_page_to_backup": "Albumi koji će se otpremiti", "backup_controller_page_total_sub": "Sve jedinstvene fotografije i videi iz odabranih albuma", "backup_controller_page_turn_off": "Isključi pravljenje rezervnih kopija u prvom planu", @@ -550,12 +555,12 @@ "backup_controller_page_uploading_file_info": "Otpremanje svojstava datoteke", "backup_err_only_album": "Nemoguće brisanje jedinog albuma", "backup_info_card_assets": "zapisi", - "backup_manual_cancelled": "Cancelled", - "backup_manual_in_progress": "Upload already in progress. Try after sometime", - "backup_manual_success": "Success", + "backup_manual_cancelled": "Otkazano", + "backup_manual_in_progress": "Otpremanje je već u toku. Pokušajte kasnije", + "backup_manual_success": "Uspeh", "backup_manual_title": "Upload status", "backup_options_page_title": "Backup options", - "backup_setting_subtitle": "Manage background and foreground upload settings", + "backup_setting_subtitle": "Upravljajte podešavanjima otpremanja u pozadini i prednjem planu", "backward": "Unazad", "birthdate_saved": "Datum rođenja uspešno sačuvan", "birthdate_set_description": "Datum rođenja se koristi da bi se izračunale godine ove osobe u dobu određene fotografije.", @@ -567,35 +572,35 @@ "bulk_keep_duplicates_confirmation": "Da li ste sigurni da želite da zadržite {count, plural, one {1 dupliranu datoteku} few {# duplirane datoteke} other {# dupliranih datoteka}}? Ovo će rešiti sve duplirane grupe bez brisanja bilo čega.", "bulk_trash_duplicates_confirmation": "Da li ste sigurni da želite grupno da odbacite {count, plural, one {1 dupliranu datoteku} few {# duplirane datoteke} other {# dupliranih datoteka}}? Ovo će zadržati najveću datoteku svake grupe i odbaciti sve ostale duplikate.", "buy": "Kupite licencu Immich-a", - "cache_settings_album_thumbnails": "Sličice na stranici biblioteke", + "cache_settings_album_thumbnails": "Sličice na stranici biblioteke ({count} assets)", "cache_settings_clear_cache_button": "Obriši keš memoriju", "cache_settings_clear_cache_button_title": "Ova opcija briše keš memoriju aplikacije. Ovo će bitno uticati na performanse aplikacije dok se keš memorija ne učita ponovo.", "cache_settings_duplicated_assets_clear_button": "CLEAR", - "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", - "cache_settings_duplicated_assets_title": "Duplicated Assets ({})", - "cache_settings_image_cache_size": "Veličina keš memorije slika ({} stavki)", + "cache_settings_duplicated_assets_subtitle": "Fotografije i video snimci koje je aplikacija stavila na crnu listu", + "cache_settings_duplicated_assets_title": "Duplirani elementi ({count})", + "cache_settings_image_cache_size": "Veličina keš memorije slika ({count} assets)", "cache_settings_statistics_album": "Minijature biblioteka", - "cache_settings_statistics_assets": "{} stavki ({})", + "cache_settings_statistics_assets": "{count} stavki ({size})", "cache_settings_statistics_full": "Pune slike", "cache_settings_statistics_shared": "Minijature deljenih albuma", "cache_settings_statistics_thumbnail": "Minijature", "cache_settings_statistics_title": "Iskorišćena keš memorija", "cache_settings_subtitle": "Kontrole za keš memoriju mobilne aplikacije Immich", - "cache_settings_thumbnail_size": "Keš memorija koju zauzimaju minijature ({} stavki)", - "cache_settings_tile_subtitle": "Control the local storage behaviour", - "cache_settings_tile_title": "Local Storage", + "cache_settings_thumbnail_size": "Keš memorija koju zauzimaju minijature ({count} stavki)", + "cache_settings_tile_subtitle": "Kontrolišite ponašanje lokalnog skladištenja", + "cache_settings_tile_title": "Lokalna memorija", "cache_settings_title": "Opcije za keširanje", "camera": "Kamera", "camera_brand": "Brend kamere", "camera_model": "Model kamere", "cancel": "Odustani", "cancel_search": "Otkaži pretragu", - "canceled": "Canceled", + "canceled": "Otkazano", "cannot_merge_people": "Ne može spojiti osobe", "cannot_undo_this_action": "Ne možete poništiti ovu radnju!", "cannot_update_the_description": "Ne može ažurirati opis", "change_date": "Promeni datum", - "change_display_order": "Change display order", + "change_display_order": "Promeni redosled prikaza", "change_expiration_time": "Promeni vreme isteka", "change_location": "Promeni mesto", "change_name": "Promeni ime", @@ -603,16 +608,17 @@ "change_password": "Promeni Lozinku", "change_password_description": "Ovo je ili prvi put da se prijavljujete na sistem ili je podnet zahtev za promenu lozinke. Unesite novu lozinku ispod.", "change_password_form_confirm_password": "Ponovo unesite šifru", - "change_password_form_description": "Ćao, {name}\n\nOvo je verovatno Vaše prvo pristupanje sistemu, ili je podnešen zahtev za promenu šifre. Molimo Vas, unesite novu šifru ispod", + "change_password_form_description": "Ćao, {name}\n\nOvo je verovatno Vaše prvo pristupanje sistemu, ili je podnešen zahtev za promenu šifre. Molimo Vas, unesite novu šifru ispod.", "change_password_form_new_password": "Nova šifra", "change_password_form_password_mismatch": "Šifre se ne podudaraju", "change_password_form_reenter_new_password": "Ponovo unesite novu šifru", + "change_pin_code": "Promena PIN koda", "change_your_password": "Promeni svoju šifru", "changed_visibility_successfully": "Vidljivost je uspešno promenjena", "check_all": "Štiklirati sve", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", + "check_corrupt_asset_backup": "Proverite da li postoje oštećene rezervne kopije imovine", + "check_corrupt_asset_backup_button": "Izvršite proveru", + "check_corrupt_asset_backup_description": "Pokrenite ovu proveru samo preko Wi-Fi mreže i nakon što se napravi rezervna kopija svih podataka. Postupak može potrajati nekoliko minuta.", "check_logs": "Proverite dnevnike (logs)", "choose_matching_people_to_merge": "Izaberite odgovarajuće osobe za spajanje", "city": "Grad", @@ -624,11 +630,11 @@ "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Enter Password", "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove_msg": "Client certificate is removed", - "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", + "client_cert_import_success_msg": "Sertifikat klijenta je uvezen", + "client_cert_invalid_msg": "Nevažeća datoteka sertifikata ili pogrešna lozinka", + "client_cert_remove_msg": "Sertifikat klijenta je uklonjen", + "client_cert_subtitle": "Podržava samo PKCS12 (.p12, .pfx) format. Uvoz/uklanjanje sertifikata je dostupno samo pre prijave", + "client_cert_title": "SSL klijentski sertifikat", "clockwise": "U smeru kazaljke", "close": "Zatvori", "collapse": "Skupi", @@ -640,26 +646,27 @@ "comments_and_likes": "Komentari i lajkovi", "comments_are_disabled": "Komentari su onemogućeni", "common_create_new_album": "Kreiraj novi album", - "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.", - "completed": "Completed", + "common_server_error": "Molimo vas da proverite mrežnu vezu, uverite se da je server dostupan i da su verzije aplikacija/servera kompatibilne.", + "completed": "Završeno", "confirm": "Potvrdi", "confirm_admin_password": "Potvrdi Administrativnu Lozinku", "confirm_delete_face": "Da li ste sigurni da želite da izbrišete osobu {name} iz dela?", "confirm_delete_shared_link": "Da li ste sigurni da želite da izbrišete ovaj deljeni link?", "confirm_keep_this_delete_others": "Sve ostale datoteke u grupi će biti izbrisane osim ove datoteke. Da li ste sigurni da želite da nastavite?", + "confirm_new_pin_code": "Potvrdite novi PIN kod", "confirm_password": "Ponovo unesi šifru", "contain": "Obuhvati", "context": "Kontekst", "continue": "Nastavi", - "control_bottom_app_bar_album_info_shared": "{} stvari podeljeno", + "control_bottom_app_bar_album_info_shared": "{count} stvari podeljeno", "control_bottom_app_bar_create_new_album": "Kreiraj novi album", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", - "control_bottom_app_bar_edit_location": "Edit Location", - "control_bottom_app_bar_edit_time": "Edit Date & Time", - "control_bottom_app_bar_share_link": "Share Link", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", + "control_bottom_app_bar_delete_from_immich": "Obriši iz Immich-a", + "control_bottom_app_bar_delete_from_local": "Obriši sa uređaja", + "control_bottom_app_bar_edit_location": "Izmeni lokaciju", + "control_bottom_app_bar_edit_time": "Izmeni datum i vreme", + "control_bottom_app_bar_share_link": "Deli link", + "control_bottom_app_bar_share_to": "Podeli sa", + "control_bottom_app_bar_trash_from_immich": "Premesti u otpad", "copied_image_to_clipboard": "Kopirana slika u međuspremnik (clipboard).", "copied_to_clipboard": "Kopirano u međuspremnik (clipboard)!", "copy_error": "Greška pri kopiranju", @@ -683,25 +690,27 @@ "create_new_person": "Napravi novu osobu", "create_new_person_hint": "Dodelite izabrane datoteke novoj osobi", "create_new_user": "Napravi novog korisnika", - "create_shared_album_page_share_add_assets": "DODAJ ", + "create_shared_album_page_share_add_assets": "DODAJ SREDSTVA", "create_shared_album_page_share_select_photos": "Odaberi fotografije", "create_tag": "Kreirajte oznaku (tag)", "create_tag_description": "Napravite novu oznaku (tag). Za ugnežđene oznake, unesite punu putanju oznake uključujući kose crte.", "create_user": "Napravi korisnika", "created": "Napravljen", - "crop": "Crop", - "curated_object_page_title": "Things", + "created_at": "Kreirano", + "crop": "Obrezivanje", + "curated_object_page_title": "Stvari", "current_device": "Trenutni uređaj", - "current_server_address": "Current server address", + "current_pin_code": "Trenutni PIN kod", + "current_server_address": "Trenutna adresa servera", "custom_locale": "Prilagođena lokacija (locale)", "custom_locale_description": "Formatirajte datume i brojeve na osnovu jezika i regiona", - "daily_title_text_date": "E, MMM dd", - "daily_title_text_date_year": "E, MMM dd, yyyy", + "daily_title_text_date": "E dd MMM", + "daily_title_text_date_year": "E dd MMM yyyy", "dark": "Tamno", "date_after": "Datum posle", "date_and_time": "Datum i Vreme", "date_before": "Datum pre", - "date_format": "E, LLL d, y • h:mm a", + "date_format": "E d LLL y • H:mm", "date_of_birth_saved": "Datum rođenja uspešno sačuvan", "date_range": "Raspon datuma", "day": "Dan", @@ -716,21 +725,21 @@ "delete_album": "Obriši album", "delete_api_key_prompt": "Da li ste sigurni da želite da izbrišete ovaj API ključ (key)?", "delete_dialog_alert": "Ove stvari će permanentno biti obrisane sa Immich-a i Vašeg uređaja", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", - "delete_dialog_ok_force": "Delete Anyway", + "delete_dialog_alert_local": "Ove stavke će biti trajno uklonjene sa vašeg uređaja, ali će i dalje biti dostupne na Immich serveru", + "delete_dialog_alert_local_non_backed_up": "Neke stavke nisu rezervno kopirane na Immich-u i biće trajno uklonjene sa vašeg uređaja", + "delete_dialog_alert_remote": "Ove stavke će biti trajno izbrisane sa Immich servera", + "delete_dialog_ok_force": "Ipak obriši", "delete_dialog_title": "Obriši permanentno", "delete_duplicates_confirmation": "Da li ste sigurni da želite da trajno izbrišete ove duplikate?", "delete_face": "Izbriši osobu", "delete_key": "Izbriši ključ", "delete_library": "Obriši biblioteku", "delete_link": "Obriši vezu", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", + "delete_local_dialog_ok_backed_up_only": "Obriši samo rezervne kopije", + "delete_local_dialog_ok_force": "Ipak obriši", "delete_others": "Izbrišite druge", "delete_shared_link": "Obriši deljenu vezu", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_title": "Obriši deljeni link", "delete_tag": "Obriši oznaku (tag)", "delete_tag_confirmation_prompt": "Da li stvarno želite da izbrišete oznaku {tagName}?", "delete_user": "Obriši korisnika", @@ -738,7 +747,7 @@ "deletes_missing_assets": "Briše sredstva koja nedostaju sa diska", "description": "Opis", "description_input_hint_text": "Add description...", - "description_input_submit_error": "Error updating description, check the log for more details", + "description_input_submit_error": "Greška pri ažuriranju opisa, proverite dnevnik za više detalja", "details": "Detalji", "direction": "Smer", "disabled": "Onemogućeno", @@ -755,26 +764,26 @@ "documentation": "Dokumentacija", "done": "Urađeno", "download": "Preuzmi", - "download_canceled": "Download canceled", - "download_complete": "Download complete", - "download_enqueue": "Download enqueued", + "download_canceled": "Preuzmi otkazano", + "download_complete": "Preuzmi završeno", + "download_enqueue": "Preuzimanje je stavljeno u red", "download_error": "Download Error", - "download_failed": "Download failed", - "download_filename": "file: {}", - "download_finished": "Download finished", + "download_failed": "Preuzimanje nije uspelo", + "download_filename": "datoteka: {filename}", + "download_finished": "Preuzimanje završeno", "download_include_embedded_motion_videos": "Ugrađeni video snimci", "download_include_embedded_motion_videos_description": "Uključite video zapise ugrađene u fotografije u pokretu kao zasebnu datoteku", - "download_notfound": "Download not found", - "download_paused": "Download paused", + "download_notfound": "Preuzimanje nije pronađeno", + "download_paused": "Preuzimanje je pauzirano", "download_settings": "Preuzimanje", "download_settings_description": "Upravljajte podešavanjima vezanim za preuzimanje datoteka", - "download_started": "Download started", - "download_sucess": "Download success", - "download_sucess_android": "The media has been downloaded to DCIM/Immich", - "download_waiting_to_retry": "Waiting to retry", + "download_started": "Preuzimanje je započeto", + "download_sucess": "Preuzimanje je uspešno", + "download_sucess_android": "Mediji su preuzeti na DCIM/Immich", + "download_waiting_to_retry": "Čekanje na ponovni pokušaj", "downloading": "Preuzimanje u toku", "downloading_asset_filename": "Preuzimanje datoteke {filename}", - "downloading_media": "Downloading media", + "downloading_media": "Preuzimanje medija", "drop_files_to_upload": "Ubacite datoteke bilo gde da ih otpremite (upload-ujete)", "duplicates": "Duplikati", "duplicates_description": "Razrešite svaku grupu tako što ćete navesti duplikate, ako ih ima", @@ -791,7 +800,7 @@ "edit_key": "Izmeni ključ", "edit_link": "Uredi vezu", "edit_location": "Uredi lokaciju", - "edit_location_dialog_title": "Location", + "edit_location_dialog_title": "Lokacija", "edit_name": "Uredi ime", "edit_people": "Uredi osobe", "edit_tag": "Uredi oznaku (tag)", @@ -804,19 +813,20 @@ "editor_crop_tool_h2_aspect_ratios": "Proporcije (aspect ratios)", "editor_crop_tool_h2_rotation": "Rotacija", "email": "E-pošta", - "empty_folder": "This folder is empty", + "email_notifications": "Obaveštenja e-poštom", + "empty_folder": "Ova mapa je prazna", "empty_trash": "Ispraznite smeće", "empty_trash_confirmation": "Da li ste sigurni da želite da ispraznite smeće? Ovo će trajno ukloniti sve datoteke u smeću iz Immich-a.\nNe možete poništiti ovu radnju!", "enable": "Omogući (Enable)", "enabled": "Omogućeno (Enabled)", "end_date": "Krajnji datum", - "enqueued": "Enqueued", - "enter_wifi_name": "Enter WiFi name", + "enqueued": "Stavljeno u red", + "enter_wifi_name": "Unesite naziv Wi-Fi mreže", "error": "Greška", - "error_change_sort_album": "Failed to change album sort order", + "error_change_sort_album": "Promena redosleda sortiranja albuma nije uspela", "error_delete_face": "Greška pri brisanju osobe iz dela", "error_loading_image": "Greška pri učitavanju slike", - "error_saving_image": "Error: {}", + "error_saving_image": "Greška: {error}", "error_title": "Greška – Nešto je pošlo naopako", "errors": { "cannot_navigate_next_asset": "Nije moguće doći do sledeće datoteke", @@ -846,10 +856,12 @@ "failed_to_keep_this_delete_others": "Nije uspelo zadržavanje ovog dela i brisanje ostalih datoteka", "failed_to_load_asset": "Učitavanje datoteka nije uspelo", "failed_to_load_assets": "Nije uspelo učitavanje datoteka", + "failed_to_load_notifications": "Učitavanje obaveštenja nije uspelo", "failed_to_load_people": "Učitavanje osoba nije uspelo", "failed_to_remove_product_key": "Uklanjanje ključa proizvoda nije uspelo", "failed_to_stack_assets": "Slaganje datoteka nije uspelo", "failed_to_unstack_assets": "Rasklapanje datoteka nije uspelo", + "failed_to_update_notification_status": "Ažuriranje statusa obaveštenja nije uspelo", "import_path_already_exists": "Ova putanja uvoza već postoji.", "incorrect_email_or_password": "Neispravan e-mail ili lozinka", "paths_validation_failed": "{paths, plural, one {# putanja nije prošla} few {# putanje nisu prošle} other {# putanja nisu prošle}} proveru valjanosti", @@ -917,6 +929,7 @@ "unable_to_remove_reaction": "Nije moguće ukloniti reakciju", "unable_to_repair_items": "Nije moguće popraviti stavke", "unable_to_reset_password": "Nije moguće resetovati lozinku", + "unable_to_reset_pin_code": "Nije moguće resetovati PIN kod", "unable_to_resolve_duplicate": "Nije moguće razrešiti duplikat", "unable_to_restore_assets": "Nije moguće vratiti datoteke", "unable_to_restore_trash": "Nije moguće povratiti otpad", @@ -950,10 +963,10 @@ "exif_bottom_sheet_location": "LOKACIJA", "exif_bottom_sheet_people": "PEOPLE", "exif_bottom_sheet_person_add_person": "Add name", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", + "exif_bottom_sheet_person_age": "Starost {age}", + "exif_bottom_sheet_person_age_months": "Starost {months} meseci", + "exif_bottom_sheet_person_age_year_months": "Starost 1 godina, {months} meseci", + "exif_bottom_sheet_person_age_years": "Starost {years}", "exit_slideshow": "Izađi iz projekcije slajdova", "expand_all": "Proširi sve", "experimental_settings_new_asset_list_subtitle": "U izradi", @@ -970,16 +983,16 @@ "extension": "Ekstenzija (Extension)", "external": "Spoljašnji", "external_libraries": "Spoljašnje Biblioteke", - "external_network": "External network", - "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", + "external_network": "Spoljna mreža", + "external_network_sheet_info": "Kada nije na željenoj Wi-Fi mreži, aplikacija će se povezati sa serverom preko prve od dole navedenih URL adresa do kojih može da dođe, počevši od vrha do dna", "face_unassigned": "Neraspoređeni", - "failed": "Failed", + "failed": "Neuspešno", "failed_to_load_assets": "Datoteke nisu uspešno učitane", - "failed_to_load_folder": "Failed to load folder", + "failed_to_load_folder": "Učitavanje fascikle nije uspelo", "favorite": "Favorit", "favorite_or_unfavorite_photo": "Omiljena ili neomiljena fotografija", "favorites": "Favoriti", - "favorites_page_no_favorites": "No favorite assets found", + "favorites_page_no_favorites": "Nije pronađen nijedan omiljeni materijal", "feature_photo_updated": "Glavna fotografija je ažurirana", "features": "Funkcije (features)", "features_setting_description": "Upravljajte funkcijama aplikacije", @@ -992,34 +1005,34 @@ "filter_places": "Filtrirajte mesta", "find_them_fast": "Brzo ih pronađite po imenu pomoću pretrage", "fix_incorrect_match": "Ispravite netačno podudaranje", - "folder": "Folder", - "folder_not_found": "Folder not found", + "folder": "Fascikla", + "folder_not_found": "Fascikla nije pronađena", "folders": "Fascikle (Folders)", "folders_feature_description": "Pregledavanje prikaza fascikle za fotografije i video zapisa u sistemu datoteka", "forward": "Napred", "general": "Generalno", "get_help": "Nađi pomoć", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "get_wifiname_error": "Nije moguće dobiti ime Wi-Fi mreže. Uverite se da ste dali potrebne dozvole i da ste povezani na Wi-Fi mrežu", "getting_started": "Počinjem", "go_back": "Vrati se", "go_to_folder": "Idi u fasciklu", "go_to_search": "Idi na pretragu", - "grant_permission": "Grant permission", + "grant_permission": "Daj dozvolu", "group_albums_by": "Grupni albumi po...", "group_country": "Grupa po država", "group_no": "Bez grupisanja", "group_owner": "Grupirajte po vlasniku", "group_places_by": "Grupirajte mesta po...", "group_year": "Grupirajte po godini", - "haptic_feedback_switch": "Enable haptic feedback", - "haptic_feedback_title": "Haptic Feedback", + "haptic_feedback_switch": "Omogući haptičku povratnu informaciju", + "haptic_feedback_title": "Haptičke povratne informacije", "has_quota": "Ima kvotu", - "header_settings_add_header_tip": "Add Header", - "header_settings_field_validator_msg": "Value cannot be empty", - "header_settings_header_name_input": "Header name", - "header_settings_header_value_input": "Header value", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", + "header_settings_add_header_tip": "Dodaj zaglavlje", + "header_settings_field_validator_msg": "Vrednost ne može biti prazna", + "header_settings_header_name_input": "Naziv zaglavlja", + "header_settings_header_value_input": "Vrednost zaglavlja", + "headers_settings_tile_subtitle": "Definišite proksi zaglavlja koja aplikacija treba da šalje sa svakim mrežnim zahtevom", + "headers_settings_tile_title": "Prilagođeni proksi zaglavci", "hi_user": "Zdravo {name} ({email})", "hide_all_people": "Sakrij sve osobe", "hide_gallery": "Sakrij galeriju", @@ -1027,24 +1040,25 @@ "hide_password": "Sakrij lozinku", "hide_person": "Sakrij osobu", "hide_unnamed_people": "Sakrij neimenovane osobe", - "home_page_add_to_album_conflicts": "Dodat {added} zapis u album {album}. {failed} zapisi su već u albumu ", + "home_page_add_to_album_conflicts": "Dodat {added} zapis u album {album}. {failed} zapisi su već u albumu.", "home_page_add_to_album_err_local": "Trenutno nemoguće dodati lokalne zapise u albume, preskacu se", "home_page_add_to_album_success": "Dodate {added} stavke u album {album}.", - "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", - "home_page_archive_err_local": "Can not archive local assets yet, skipping", - "home_page_archive_err_partner": "Can not archive partner assets, skipping", + "home_page_album_err_partner": "Još uvek nije moguće dodati partnerska sredstva u album, preskačem", + "home_page_archive_err_local": "Još uvek nije moguće arhivirati lokalne resurse, preskačem", + "home_page_archive_err_partner": "Ne mogu da arhiviram partnersku imovinu, preskačem", "home_page_building_timeline": "Kreiranje hronološke linije", - "home_page_delete_err_partner": "Can not delete partner assets, skipping", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", + "home_page_delete_err_partner": "Ne mogu da obrišem partnersku imovinu, preskačem", + "home_page_delete_remote_err_local": "Lokalna sredstva u obrisavanju udaljenog izbora, preskakanje", "home_page_favorite_err_local": "Trenutno nije moguce dodati lokalne zapise u favorite, preskacu se", - "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", + "home_page_favorite_err_partner": "Još uvek nije moguće označiti partnerske resurse kao omiljene, preskačem", "home_page_first_time_notice": "Ako je ovo prvi put da koristite aplikaciju, molimo Vas da odaberete albume koje želite da sačuvate", - "home_page_share_err_local": "Can not share local assets via link, skipping", - "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", + "home_page_share_err_local": "Ne mogu da delim lokalne resurse preko linka, preskačem", + "home_page_upload_err_limit": "Možete otpremiti najviše 30 elemenata istovremeno, preskačući", "host": "Domaćin (Host)", "hour": "Sat", - "ignore_icloud_photos": "Ignore iCloud photos", - "ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server", + "id": "ID", + "ignore_icloud_photos": "Ignorišite iCloud fotografije", + "ignore_icloud_photos_description": "Fotografije koje su sačuvane na iCloud-u neće biti otpremljene na Immich server", "image": "Fotografija", "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} snimljeno {date}", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} snimljeno sa {person1} {date}", @@ -1056,10 +1070,10 @@ "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} sa {person1} i {person2} {date}", "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} snimljenou {city}, {country} sa {person1}, {person2} i {person3} {date}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} sa {person1}, {person2} i još {additionalCount, number} drugih {date}", - "image_saved_successfully": "Image saved", - "image_viewer_page_state_provider_download_started": "Download Started", + "image_saved_successfully": "Slika je sačuvana", + "image_viewer_page_state_provider_download_started": "Preuzimanje je započeto", "image_viewer_page_state_provider_download_success": "Preuzimanje Uspešno", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "Greška pri deljenju", "immich_logo": "Logo Immich-a", "immich_web_interface": "Web interfejs Immich-a", "import_from_json": "Uvezi iz JSON-a", @@ -1078,8 +1092,8 @@ "night_at_midnight": "Svaka noć u ponoć", "night_at_twoam": "Svaka noć u 2am" }, - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", + "invalid_date": "Nevažeći datum", + "invalid_date_format": "Nevažeći format datuma", "invite_people": "Pozovite ljude", "invite_to_album": "Pozovi na album", "items_count": "{count, plural, one {# datoteka} other {# datoteka}}", @@ -1100,11 +1114,11 @@ "level": "Nivo", "library": "Biblioteka", "library_options": "Opcije biblioteke", - "library_page_device_albums": "Albums on Device", + "library_page_device_albums": "Albumi na uređaju", "library_page_new_album": "Novi album", - "library_page_sort_asset_count": "Number of assets", + "library_page_sort_asset_count": "Broj sredstava", "library_page_sort_created": "Najnovije kreirano", - "library_page_sort_last_modified": "Last modified", + "library_page_sort_last_modified": "Poslednja izmena", "library_page_sort_title": "Naziv albuma", "light": "Svetlo", "like_deleted": "Lajkuj izbrisano", @@ -1116,22 +1130,22 @@ "loading": "Učitavanje", "loading_search_results_failed": "Učitavanje rezultata pretrage nije uspelo", "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", - "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", - "location_picker_choose_on_map": "Choose on map", - "location_picker_latitude_error": "Enter a valid latitude", - "location_picker_latitude_hint": "Enter your latitude here", - "location_picker_longitude_error": "Enter a valid longitude", - "location_picker_longitude_hint": "Enter your longitude here", + "local_network_sheet_info": "Aplikacija će se povezati sa serverom preko ove URL adrese kada koristi navedenu Vi-Fi mrežu", + "location_permission": "Dozvola za lokaciju", + "location_permission_content": "Da bi koristio funkciju automatskog prebacivanja, Immich-u je potrebna precizna dozvola za lokaciju kako bi mogao da pročita naziv trenutne Wi-Fi mreže", + "location_picker_choose_on_map": "Izaberite na mapi", + "location_picker_latitude_error": "Unesite važeću geografsku širinu", + "location_picker_latitude_hint": "Unesite svoju geografsku širinu ovde", + "location_picker_longitude_error": "Unesite važeću geografsku dužinu", + "location_picker_longitude_hint": "Unesite svoju geografsku dužinu ovde", "log_out": "Odjavi se", "log_out_all_devices": "Odjavite se sa svih uređaja", "logged_out_all_devices": "Odjavljeni su svi uređaji", "logged_out_device": "Odjavljen uređaj", "login": "Prijava", - "login_disabled": "Login has been disabled", - "login_form_api_exception": "API exception. Please check the server URL and try again.", - "login_form_back_button_text": "Back", + "login_disabled": "Prijava je onemogućena", + "login_form_api_exception": "Izuzetak API-ja. Molimo vas da proverite URL adresu servera i pokušate ponovo.", + "login_form_back_button_text": "Nazad", "login_form_email_hint": "vašemail@email.com", "login_form_endpoint_hint": "http://ip-vašeg-servera:port", "login_form_endpoint_url": "URL Servera", @@ -1143,14 +1157,14 @@ "login_form_failed_get_oauth_server_config": "Evidencija grešaka koristeći OAuth, proveriti serverski link (URL)", "login_form_failed_get_oauth_server_disable": "OAuth opcija nije dostupna na ovom serveru", "login_form_failed_login": "Neuspešna prijava, proveri URL servera, email i šifru", - "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", + "login_form_handshake_exception": "Došlo je do izuzetka rukostiskanja sa serverom. Omogućite podršku za samopotpisane sertifikate u podešavanjima ako koristite samopotpisani sertifikat.", "login_form_password_hint": "šifra", "login_form_save_login": "Ostani prijavljen", "login_form_server_empty": "Enter a server URL.", - "login_form_server_error": "Could not connect to server.", + "login_form_server_error": "Nije moguće povezati se sa serverom.", "login_has_been_disabled": "Prijava je onemogućena.", - "login_password_changed_error": "There was an error updating your password", - "login_password_changed_success": "Password updated successfully", + "login_password_changed_error": "Došlo je do greške prilikom ažuriranja lozinke", + "login_password_changed_success": "Lozinka je uspešno ažurirana", "logout_all_device_confirmation": "Da li ste sigurni da želite da se odjavite sa svih uređaja?", "logout_this_device_confirmation": "Da li ste sigurni da želite da se odjavite sa ovog uređaja?", "longitude": "Geografska dužina", @@ -1168,40 +1182,43 @@ "manage_your_devices": "Upravljajte svojim prijavljenim uređajima", "manage_your_oauth_connection": "Upravljajte svojom OAuth vezom", "map": "Mapa", - "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", - "map_cannot_get_user_location": "Cannot get user's location", - "map_location_dialog_yes": "Yes", - "map_location_picker_page_use_location": "Use this location", - "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", - "map_location_service_disabled_title": "Location Service disabled", + "map_assets_in_bound": "{count} fotografija", + "map_assets_in_bounds": "{count} fotografija", + "map_cannot_get_user_location": "Nije moguće dobiti lokaciju korisnika", + "map_location_dialog_yes": "Da", + "map_location_picker_page_use_location": "Koristite ovu lokaciju", + "map_location_service_disabled_content": "Usluga lokacije mora biti omogućena da bi se prikazivala sredstva sa vaše trenutne lokacije. Da li želite da je sada omogućite?", + "map_location_service_disabled_title": "Usluga lokacije je onemogućena", "map_marker_for_images": "Označivač na mapi za slike snimljene u {city}, {country}", "map_marker_with_image": "Marker na mapi sa slikom", - "map_no_assets_in_bounds": "No photos in this area", - "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", - "map_no_location_permission_title": "Location Permission denied", + "map_no_assets_in_bounds": "Nema fotografija u ovoj oblasti", + "map_no_location_permission_content": "Potrebna je dozvola za lokaciju da bi se prikazali resursi sa vaše trenutne lokacije. Da li želite da je sada dozvolite?", + "map_no_location_permission_title": "Dozvola za lokaciju je odbijena", "map_settings": "Podešavanja mape", - "map_settings_dark_mode": "Dark mode", - "map_settings_date_range_option_day": "Past 24 hours", - "map_settings_date_range_option_days": "Past {} days", - "map_settings_date_range_option_year": "Past year", - "map_settings_date_range_option_years": "Past {} years", - "map_settings_dialog_title": "Map Settings", - "map_settings_include_show_archived": "Include Archived", - "map_settings_include_show_partners": "Include Partners", - "map_settings_only_show_favorites": "Show Favorite Only", - "map_settings_theme_settings": "Map Theme", - "map_zoom_to_see_photos": "Zoom out to see photos", + "map_settings_dark_mode": "Tamni režim", + "map_settings_date_range_option_day": "Poslednja 24 sata", + "map_settings_date_range_option_days": "Prethodnih {days} dana", + "map_settings_date_range_option_year": "Prošla godina", + "map_settings_date_range_option_years": "Proteklih {years} godina", + "map_settings_dialog_title": "Podešavanja Mape", + "map_settings_include_show_archived": "Uključi arhivirano", + "map_settings_include_show_partners": "Uključi partnere", + "map_settings_only_show_favorites": "Prikaži samo omiljene", + "map_settings_theme_settings": "Tema mape", + "map_zoom_to_see_photos": "Umanjite da biste videli fotografije", + "mark_all_as_read": "Označi sve kao pročitano", + "mark_as_read": "Označi kao pročitano", + "marked_all_as_read": "Sve je označeno kao pročitano", "matches": "Podudaranja", "media_type": "Vrsta medija", "memories": "Sećanja", - "memories_all_caught_up": "All caught up", - "memories_check_back_tomorrow": "Check back tomorrow for more memories", + "memories_all_caught_up": "Sve je uhvaćeno", + "memories_check_back_tomorrow": "Vratite se sutra za još uspomena", "memories_setting_description": "Upravljajte onim što vidite u svojim sećanjima", - "memories_start_over": "Start Over", - "memories_swipe_to_close": "Swipe up to close", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", + "memories_start_over": "Počni ispočetka", + "memories_swipe_to_close": "Prevucite nagore da biste zatvorili", + "memories_year_ago": "Pre godinu dana", + "memories_years_ago": "pre {years} godina", "memory": "Memorija", "memory_lane_title": "Traka sećanja {title}", "menu": "Meni", @@ -1218,20 +1235,23 @@ "month": "Mesec", "monthly_title_text_date_format": "MMMM y", "more": "Više", + "moved_to_archive": "Premešteno {count, plural, one {# datoteka} other {# datoteke}} u arhivu", + "moved_to_library": "Premešteno {count, plural, one {# datoteka} other {# datoteke}} u biblioteku", "moved_to_trash": "Premešteno u smeće", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "multiselect_grid_edit_date_time_err_read_only": "Ne možete da izmenite datum elemenata samo za čitanje, preskačem", + "multiselect_grid_edit_gps_err_read_only": "Ne mogu da izmenim lokaciju elemenata samo za čitanje, preskačem", "mute_memories": "Priguši sećanja", "my_albums": "Moji albumi", "name": "Ime", "name_or_nickname": "Ime ili nadimak", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", + "networking_settings": "Umrežavanje", + "networking_subtitle": "Upravljajte podešavanjima krajnje tačke servera", "never": "Nikada", "new_album": "Novi Album", "new_api_key": "Novi API ključ (key)", "new_password": "Nova šifra", "new_person": "Nova osoba", + "new_pin_code": "Novi PIN kod", "new_user_created": "Novi korisnik je kreiran", "new_version_available": "DOSTUPNA NOVA VERZIJA", "newest_first": "Najnovije prvo", @@ -1243,23 +1263,25 @@ "no_albums_yet": "Izgleda da još nemate nijedan album.", "no_archived_assets_message": "Arhivirajte fotografije i video zapise da biste ih sakrili iz prikaza fotografija", "no_assets_message": "KLIKNITE DA UPLOADIRATE SVOJU PRVU FOTOGRAFIJU", - "no_assets_to_show": "No assets to show", + "no_assets_to_show": "Nema elemenata za prikaz", "no_duplicates_found": "Nije pronađen nijedan duplikat.", "no_exif_info_available": "Nema dostupnih exif informacija", "no_explore_results_message": "Uploadujte još fotografija da biste istražili svoju kolekciju.", "no_favorites_message": "Postavite favorite da biste brzo našli vaše najbolje slike i video snimke", "no_libraries_message": "Napravite spoljnu biblioteku da biste videli svoje fotografije i video zapise", "no_name": "Nema imena", + "no_notifications": "Nema obaveštenja", + "no_people_found": "Nisu pronađeni odgovarajući ljudi", "no_places": "Nema mesta", "no_results": "Nema rezultata", "no_results_description": "Pokušajte sa sinonimom ili opštijom ključnom reči", "no_shared_albums_message": "Napravite album da biste delili fotografije i video zapise sa ljudima u vašoj mreži", "not_in_any_album": "Nema ni u jednom albumu", - "not_selected": "Not selected", + "not_selected": "Nije izabrano", "note_apply_storage_label_to_previously_uploaded assets": "Napomena: Da biste primenili oznaku za skladištenje na prethodno uploadirane datoteke, pokrenite", "notes": "Napomene", - "notification_permission_dialog_content": "Da bi ukljucili notifikacije, idite u Opcije i odaberite Dozvoli", - "notification_permission_list_tile_content": "Dozvoli Notifikacije\n", + "notification_permission_dialog_content": "Da bi ukljucili notifikacije, idite u Opcije i odaberite Dozvoli.", + "notification_permission_list_tile_content": "Dajte dozvolu za omogućavanje obaveštenja.", "notification_permission_list_tile_enable_button": "Uključi Notifikacije", "notification_permission_list_tile_title": "Dozvole za notifikacije", "notification_toggle_setting_description": "Omogućite obaveštenja putem e-pošte", @@ -1272,7 +1294,7 @@ "offline_paths_description": "Ovi rezultati mogu biti posledica ručnog brisanja datoteka koje nisu deo spoljne biblioteke.", "ok": "Ok", "oldest_first": "Najstarije prvo", - "on_this_device": "On this device", + "on_this_device": "Na ovom uređaju", "onboarding": "Pristupanje (Onboarding)", "onboarding_privacy_description": "Sledeće (opcione) funkcije se oslanjaju na spoljne usluge i mogu se onemogućiti u bilo kom trenutku u podešavanjima administracije.", "onboarding_theme_description": "Izaberite temu boja za svoj nalog. Ovo možete kasnije da promenite u podešavanjima.", @@ -1297,14 +1319,14 @@ "partner_can_access": "{partner} može da pristupi", "partner_can_access_assets": "Sve vaše fotografije i video snimci osim onih u arhiviranim i izbrisanim", "partner_can_access_location": "Lokacija na kojoj su vaše fotografije snimljene", - "partner_list_user_photos": "{user}'s photos", - "partner_list_view_all": "View all", - "partner_page_empty_message": "Your photos are not yet shared with any partner.", - "partner_page_no_more_users": "No more users to add", - "partner_page_partner_add_failed": "Failed to add partner", - "partner_page_select_partner": "Select partner", - "partner_page_shared_to_title": "Shared to", - "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", + "partner_list_user_photos": "Fotografije korisnika {user}", + "partner_list_view_all": "Prikaži sve", + "partner_page_empty_message": "Vaše fotografije još uvek nisu deljene ni sa jednim partnerom.", + "partner_page_no_more_users": "Nema više korisnika za dodavanje", + "partner_page_partner_add_failed": "Dodavanje partnera nije uspelo", + "partner_page_select_partner": "Izaberite partnera", + "partner_page_shared_to_title": "Deljeno sa", + "partner_page_stop_sharing_content": "{partner} više neće moći da pristupi vašim fotografijama.", "partner_sharing": "Partnersko deljenje", "partners": "Partneri", "password": "Šifra", @@ -1333,23 +1355,26 @@ "permanently_delete_assets_prompt": "Da li ste sigurni da želite da trajno izbrišete {count, plural, one {ovu datoteku?} other {ove # datoteke?}}Ovo će ih takođe ukloniti {count, plural, one {iz njihovog} other {iz njihovih}} albuma.", "permanently_deleted_asset": "Trajno izbrisana datoteka", "permanently_deleted_assets_count": "Trajno izbrisano {count, plural, one {# datoteka} other {# datoteke}}", - "permission_onboarding_back": "Back", - "permission_onboarding_continue_anyway": "Continue anyway", - "permission_onboarding_get_started": "Get started", - "permission_onboarding_go_to_settings": "Go to settings", - "permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.", - "permission_onboarding_permission_granted": "Permission granted! You are all set.", - "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", - "permission_onboarding_request": "Immich requires permission to view your photos and videos.", + "permission_onboarding_back": "Nazad", + "permission_onboarding_continue_anyway": "Ipak nastavi", + "permission_onboarding_get_started": "Započnite", + "permission_onboarding_go_to_settings": "Idi na podešavanja", + "permission_onboarding_permission_denied": "Dozvola odbijena. Da biste koristili Immich, dodelite dozvole za fotografije i video zapise u Podešavanjima.", + "permission_onboarding_permission_granted": "Dozvola odobrena! Spremni ste.", + "permission_onboarding_permission_limited": "Dozvola ograničena. Da biste omogućili Immich-u da pravi rezervne kopije i upravlja celom vašom kolekcijom galerije, dodelite dozvole za fotografije i video zapise u Podešavanjima.", + "permission_onboarding_request": "Immich zahteva dozvolu da vidi vaše fotografije i video zapise.", "person": "Osoba", "person_birthdate": "Rođen(a) {date}", "person_hidden": "{name}{hidden, select, true { (skriveno)} other {}}", "photo_shared_all_users": "Izgleda da ste podelili svoje fotografije sa svim korisnicima ili da nemate nijednog korisnika sa kojim biste delili.", - "photos": "Slike", + "photos": "Fotografije", "photos_and_videos": "Fotografije & Video zapisi", "photos_count": "{count, plural, one {{count, number} fotografija} few {{count, number} fotografije} other {{count, number} fotografija}}", "photos_from_previous_years": "Fotografije iz prethodnih godina", "pick_a_location": "Odaberi lokaciju", + "pin_code_changed_successfully": "PIN kod je uspešno promenjen", + "pin_code_reset_successfully": "PIN kod je uspešno resetovan", + "pin_code_setup_successfully": "Uspešno podešavanje PIN koda", "place": "Mesto", "places": "Mesta", "places_count": "{count, plural, one {{count, number} Mesto} other {{count, number} Mesta}}", @@ -1358,8 +1383,8 @@ "play_motion_photo": "Pokreni pokretnu fotografiju", "play_or_pause_video": "Pokreni ili pauziraj video zapis", "port": "port", - "preferences_settings_subtitle": "Manage the app's preferences", - "preferences_settings_title": "Preferences", + "preferences_settings_subtitle": "Upravljajte podešavanjima aplikacije", + "preferences_settings_title": "Podešavanja", "preset": "Unapred podešeno", "preview": "Pregled", "previous": "Prošlo", @@ -1367,20 +1392,21 @@ "previous_or_next_photo": "Prethodna ili sledeća fotografija", "primary": "Primarna (Primary)", "privacy": "Privatnost", + "profile": "Profil", "profile_drawer_app_logs": "Evidencija", - "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", - "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", + "profile_drawer_client_out_of_date_major": "Mobilna aplikacija je zastarela. Molimo vas da je ažurirate na najnoviju glavnu verziju.", + "profile_drawer_client_out_of_date_minor": "Mobilna aplikacija je zastarela. Molimo vas da je ažurirate na najnoviju sporednu verziju.", "profile_drawer_client_server_up_to_date": "Klijent i server su najnovije verzije", "profile_drawer_github": "GitHub", - "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.", - "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.", + "profile_drawer_server_out_of_date_major": "Server je zastareo. Molimo vas da ažurirate na najnoviju glavnu verziju.", + "profile_drawer_server_out_of_date_minor": "Server je zastareo. Molimo vas da ažurirate na najnoviju sporednu verziju.", "profile_image_of_user": "Slika profila od korisnika {user}", "profile_picture_set": "Profilna slika postavljena.", "public_album": "Javni album", "public_share": "Javno deljenje", "purchase_account_info": "Podržavam softver", "purchase_activated_subtitle": "Hvala vam što podržavate Immich i softver otvorenog koda", - "purchase_activated_time": "Aktivirano {date, date}", + "purchase_activated_time": "Aktivirano {date}", "purchase_activated_title": "Vaš ključ je uspešno aktiviran", "purchase_button_activate": "Aktiviraj", "purchase_button_buy": "Kupi", @@ -1423,8 +1449,10 @@ "recent": "Skorašnji", "recent-albums": "Nedavni albumi", "recent_searches": "Skorašnje pretrage", - "recently_added": "Recently added", - "recently_added_page_title": "Recently Added", + "recently_added": "Nedavno dodato", + "recently_added_page_title": "Nedavno Dodato", + "recently_taken": "Nedavno snimljeno", + "recently_taken_page_title": "Nedavno Snimljeno", "refresh": "Osveži", "refresh_encoded_videos": "Osvežite kodirane (encoded) video zapise", "refresh_faces": "Osveži lica", @@ -1467,6 +1495,7 @@ "reset": "Resetovati", "reset_password": "Resetovati lozinku", "reset_people_visibility": "Resetujte vidljivost osoba", + "reset_pin_code": "Resetuj PIN kod", "reset_to_default": "Resetujte na podrazumevane vrednosti", "resolve_duplicates": "Reši duplikate", "resolved_all_duplicates": "Svi duplikati su razrešeni", @@ -1481,12 +1510,12 @@ "role_editor": "Urednik", "role_viewer": "Gledalac", "save": "Sačuvaj", - "save_to_gallery": "Save to gallery", + "save_to_gallery": "Sačuvaj u galeriju", "saved_api_key": "Sačuvan API ključ (key)", "saved_profile": "Sačuvan profil", "saved_settings": "Sačuvana podešavanja", "say_something": "Reci nešto", - "scaffold_body_error_occurred": "Error occurred", + "scaffold_body_error_occurred": "Došlo je do greške", "scan_all_libraries": "Skeniraj sve biblioteke", "scan_library": "Skeniraj", "scan_settings": "Podešavanja skeniranja", @@ -1502,45 +1531,45 @@ "search_camera_model": "Pretraži model kamere...", "search_city": "Pretraži grad...", "search_country": "Traži zemlju...", - "search_filter_apply": "Apply filter", - "search_filter_camera_title": "Select camera type", + "search_filter_apply": "Primeni filter", + "search_filter_camera_title": "Izaberite tip kamere", "search_filter_date": "Date", "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", + "search_filter_date_title": "Izaberite period", "search_filter_display_option_not_in_album": "Not in album", - "search_filter_display_options": "Display Options", - "search_filter_filename": "Search by file name", - "search_filter_location": "Location", - "search_filter_location_title": "Select location", + "search_filter_display_options": "Opcije prikaza", + "search_filter_filename": "Pretraga po imenu datoteke", + "search_filter_location": "Lokacija", + "search_filter_location_title": "Izaberite lokaciju", "search_filter_media_type": "Media Type", - "search_filter_media_type_title": "Select media type", - "search_filter_people_title": "Select people", + "search_filter_media_type_title": "Izaberite tip medija", + "search_filter_people_title": "Izaberite ljude", "search_for": "Traži", "search_for_existing_person": "Potražite postojeću osobu", - "search_no_more_result": "No more results", + "search_no_more_result": "Nema više rezultata", "search_no_people": "Bez osoba", "search_no_people_named": "Nema osoba sa imenom „{name}“", - "search_no_result": "No results found, try a different search term or combination", + "search_no_result": "Nisu pronađeni rezultati, pokušajte sa drugim terminom za pretragu ili kombinacijom", "search_options": "Opcije pretrage", - "search_page_categories": "Categories", - "search_page_motion_photos": "Motion Photos", + "search_page_categories": "Kategorije", + "search_page_motion_photos": "Fotografije u pokretu", "search_page_no_objects": "Bez informacija", "search_page_no_places": "Nema informacija o mestu", - "search_page_screenshots": "Screenshots", - "search_page_search_photos_videos": "Search for your photos and videos", - "search_page_selfies": "Selfies", + "search_page_screenshots": "Snimci ekrana", + "search_page_search_photos_videos": "Pretražite svoje fotografije i video zapise", + "search_page_selfies": "Selfiji", "search_page_things": "Stvari", - "search_page_view_all_button": "View all", - "search_page_your_activity": "Your activity", - "search_page_your_map": "Your Map", + "search_page_view_all_button": "Prikaži sve", + "search_page_your_activity": "Vaša aktivnost", + "search_page_your_map": "Vaša mapa", "search_people": "Pretraži osobe", "search_places": "Pretraži mesta", "search_rating": "Pretraga po oceni...", "search_result_page_new_search_hint": "Nova pretraga", "search_settings": "Pretraga podešavanja", "search_state": "Traži region...", - "search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ", - "search_suggestion_list_smart_search_hint_2": "m:your-search-term", + "search_suggestion_list_smart_search_hint_1": "Pametna pretraga je podrazumevano omogućena, za pretragu metapodataka koristite sintaksu ", + "search_suggestion_list_smart_search_hint_2": "m:vaš-pojam-za-pretragu", "search_tags": "Pretraži oznake (tags)...", "search_timezone": "Pretraži vremensku zonu...", "search_type": "Vrsta pretrage", @@ -1559,6 +1588,7 @@ "select_keep_all": "Izaberite da zadržite sve", "select_library_owner": "Izaberite vlasnika biblioteke", "select_new_face": "Izaberite novo lice", + "select_person_to_tag": "Izaberite osobu za označavanje", "select_photos": "Odaberi fotografije", "select_trash_all": "Izaberite da sve bacite na otpad", "select_user_for_sharing_page_err_album": "Neuspešno kreiranje albuma", @@ -1566,7 +1596,7 @@ "selected_count": "{count, plural, other {# izabrano}}", "send_message": "Pošalji poruku", "send_welcome_email": "Pošaljite e-poštu dobrodošlice", - "server_endpoint": "Server Endpoint", + "server_endpoint": "Krajnja tačka servera", "server_info_box_app_version": "Verzija Aplikacije", "server_info_box_server_url": "Server URL", "server_offline": "Server van mreže (offline)", @@ -1580,87 +1610,88 @@ "set_date_of_birth": "Podesite datum rođenja", "set_profile_picture": "Postavi profilnu sliku", "set_slideshow_to_fullscreen": "Postavite projekciju slajdova na ceo ekran", - "setting_image_viewer_help": "Detaljno pregledanje prvo učitava minijaturu, pa srednju, pa original. (Ako te opcije uključene)", + "setting_image_viewer_help": "Pregledač detalja prvo učitava malu sličicu, zatim pregled srednje veličine (ako je omogućen), i na kraju original (ako je omogućen).", "setting_image_viewer_original_subtitle": "Aktiviraj učitavanje slika u punoj rezoluciji (Velika!). Deaktivacijom ove stavke možeš da smanjiš potrošnju interneta i zauzetog prostora na uređaju.", "setting_image_viewer_original_title": "Učitaj originalnu sliku", "setting_image_viewer_preview_subtitle": "Aktiviraj učitavanje slika u srednjoj rezoluciji. Deaktiviraj da se direktno učitava original, ili da se samo koristi minijatura.", "setting_image_viewer_preview_title": "Pregledaj sliku", - "setting_image_viewer_title": "Images", - "setting_languages_apply": "Apply", - "setting_languages_subtitle": "Change the app's language", - "setting_languages_title": "Languages", - "setting_notifications_notify_failures_grace_period": "Neuspešne rezervne kopije: {}", - "setting_notifications_notify_hours": "{} sati", + "setting_image_viewer_title": "Slike", + "setting_languages_apply": "Primeni", + "setting_languages_subtitle": "Promenite jezik aplikacije", + "setting_languages_title": "Jezici", + "setting_notifications_notify_failures_grace_period": "Obavesti o greškama u pravljenju rezervnih kopija u pozadini: {duration}", + "setting_notifications_notify_hours": "{count} sati", "setting_notifications_notify_immediately": "odmah", - "setting_notifications_notify_minutes": "{} minuta", + "setting_notifications_notify_minutes": "{count} minuta", "setting_notifications_notify_never": "nikada", - "setting_notifications_notify_seconds": "{} sekundi", + "setting_notifications_notify_seconds": "{count} sekundi", "setting_notifications_single_progress_subtitle": "Detaljne informacije o otpremanju, po zapisu", "setting_notifications_single_progress_title": "Prikaži detalje pozadinskog pravljenja rezervnih kopija", "setting_notifications_subtitle": "Izmeni notifikacije", "setting_notifications_total_progress_subtitle": "Ukupno otpremljenih stavki (završeno/ukupno stavki)", - "setting_notifications_total_progress_title": "Prikaži ukupan napredak pozadinskog bekapovanja.\n\n", - "setting_video_viewer_looping_title": "Looping", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", + "setting_notifications_total_progress_title": "Prikaži ukupan napredak pravljenja rezervnih kopija u pozadini", + "setting_video_viewer_looping_title": "Petljanje (Looping)", + "setting_video_viewer_original_video_subtitle": "Prilikom strimovanja videa sa servera, reprodukujte original čak i kada je dostupno transkodiranje. Može dovesti do baferovanja. Video snimci dostupni lokalno se reprodukuju u originalnom kvalitetu bez obzira na ovo podešavanje.", + "setting_video_viewer_original_video_title": "Prisilno originalni video", "settings": "Podešavanja", "settings_require_restart": "Restartujte Immich da primenite ovu promenu", "settings_saved": "Podešavanja sačuvana", + "setup_pin_code": "Podesite PIN kod", "share": "Podeli", "share_add_photos": "Dodaj fotografije", - "share_assets_selected": "{} selected", + "share_assets_selected": "Izabrano je {count}", "share_dialog_preparing": "Pripremanje...", "shared": "Deljeno", - "shared_album_activities_input_disable": "Comment is disabled", - "shared_album_activity_remove_content": "Do you want to delete this activity?", - "shared_album_activity_remove_title": "Delete Activity", - "shared_album_section_people_action_error": "Error leaving/removing from album", - "shared_album_section_people_action_leave": "Remove user from album", - "shared_album_section_people_action_remove_user": "Remove user from album", + "shared_album_activities_input_disable": "Komentar je onemogućen", + "shared_album_activity_remove_content": "Da li želite da obrišete ovu aktivnost?", + "shared_album_activity_remove_title": "Obriši aktivnost", + "shared_album_section_people_action_error": "Greška pri napuštanju/uklanjanju iz albuma", + "shared_album_section_people_action_leave": "Ukloni korisnika iz albuma", + "shared_album_section_people_action_remove_user": "Ukloni korisnika iz albuma", "shared_album_section_people_title": "PEOPLE", "shared_by": "Podelio", "shared_by_user": "Deli {user}", "shared_by_you": "Vi delite", "shared_from_partner": "Slike od {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", - "shared_link_app_bar_title": "Shared Links", - "shared_link_clipboard_copied_massage": "Copied to clipboard", - "shared_link_clipboard_text": "Link: {}\nPassword: {}", - "shared_link_create_error": "Error while creating shared link", - "shared_link_edit_description_hint": "Enter the share description", + "shared_intent_upload_button_progress_text": "Otpremljeno je {current} / {total}", + "shared_link_app_bar_title": "Deljeni linkovi", + "shared_link_clipboard_copied_massage": "Kopirano u međuspremnik (clipboard)", + "shared_link_clipboard_text": "Link: {link}\nLozinka: {password}", + "shared_link_create_error": "Greška pri kreiranju deljenog linka", + "shared_link_edit_description_hint": "Unesite opis deljenja", "shared_link_edit_expire_after_option_day": "1 day", - "shared_link_edit_expire_after_option_days": "{} days", - "shared_link_edit_expire_after_option_hour": "1 hour", - "shared_link_edit_expire_after_option_hours": "{} hours", + "shared_link_edit_expire_after_option_days": "{count} dana", + "shared_link_edit_expire_after_option_hour": "1 sat", + "shared_link_edit_expire_after_option_hours": "{count} sati", "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{} minutes", - "shared_link_edit_expire_after_option_months": "{} months", - "shared_link_edit_expire_after_option_year": "{} year", - "shared_link_edit_password_hint": "Enter the share password", + "shared_link_edit_expire_after_option_minutes": "{count} minuta", + "shared_link_edit_expire_after_option_months": "{count} meseci", + "shared_link_edit_expire_after_option_year": "{count} godina", + "shared_link_edit_password_hint": "Unesite lozinku za deljenje", "shared_link_edit_submit_button": "Update link", - "shared_link_error_server_url_fetch": "Cannot fetch the server url", - "shared_link_expires_day": "Expires in {} day", - "shared_link_expires_days": "Expires in {} days", - "shared_link_expires_hour": "Expires in {} hour", - "shared_link_expires_hours": "Expires in {} hours", - "shared_link_expires_minute": "Expires in {} minute", - "shared_link_expires_minutes": "Expires in {} minutes", - "shared_link_expires_never": "Expires ∞", - "shared_link_expires_second": "Expires in {} second", - "shared_link_expires_seconds": "Expires in {} seconds", - "shared_link_individual_shared": "Individual shared", + "shared_link_error_server_url_fetch": "Ne mogu da preuzmem URL servera", + "shared_link_expires_day": "Ističe za {count} dan(a)", + "shared_link_expires_days": "Ističe za {count} dana", + "shared_link_expires_hour": "Ističe za {count} sat", + "shared_link_expires_hours": "Ističe za {count} sati(a)", + "shared_link_expires_minute": "Ističe za {count} minut", + "shared_link_expires_minutes": "Ističe za {count} minuta", + "shared_link_expires_never": "Ističe ∞", + "shared_link_expires_second": "Ističe za {count} sekundu", + "shared_link_expires_seconds": "Ističe za {count} sekundi", + "shared_link_individual_shared": "Pojedinačno deljeno", "shared_link_info_chip_metadata": "EXIF", - "shared_link_manage_links": "Manage Shared links", + "shared_link_manage_links": "Upravljajte deljenim linkovima", "shared_link_options": "Opcije deljene veze", "shared_links": "Deljene veze", "shared_links_description": "Delite fotografije i video zapise pomoću linka", "shared_photos_and_videos_count": "{assetCount, plural, other {# deljene fotografije i video zapise.}}", - "shared_with_me": "Shared with me", + "shared_with_me": "Deljeno sa mnom", "shared_with_partner": "Deli se sa {partner}", "sharing": "Deljenje", "sharing_enter_password": "Unesite lozinku da biste videli ovu stranicu.", "sharing_page_album": "Deljeni albumi", - "sharing_page_description": "Napravi deljene albume da deliš fotografije i video zapise sa ljudima na tvojoj mreži", + "sharing_page_description": "Napravi deljene albume da deliš fotografije i video zapise sa ljudima na tvojoj mreži.", "sharing_page_empty_list": "PRAZNA LISTA", "sharing_sidebar_description": "Prikažite vezu do Deljenja na bočnoj traci", "sharing_silver_appbar_create_shared_album": "Napravi deljeni album", @@ -1722,6 +1753,7 @@ "stop_sharing_photos_with_user": "Prestanite da delite svoje fotografije sa ovim korisnikom", "storage": "Skladište (Storage space)", "storage_label": "Oznaka za skladištenje", + "storage_quota": "Kvota skladištenja", "storage_usage": "Koristi se {used} od {available}", "submit": "Dostavi", "suggestions": "Sugestije", @@ -1731,9 +1763,9 @@ "support_third_party_description": "Vaša immich instalacija je spakovana od strane treće strane. Problemi sa kojima se suočavate mogu biti uzrokovani tim paketom, pa vas molimo da im prvo postavite probleme koristeći donje veze.", "swap_merge_direction": "Zamenite pravac spajanja", "sync": "Sinhronizacija", - "sync_albums": "Sync albums", - "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", + "sync_albums": "Sinhronizuj albume", + "sync_albums_manual_subtitle": "Sinhronizujte sve otpremljene video zapise i fotografije sa izabranim rezervnim albumima", + "sync_upload_album_setting_subtitle": "Kreirajte i otpremite svoje fotografije i video zapise u odabrane albume na Immich-u", "tag": "Oznaka (tag)", "tag_assets": "Označite (tag) sredstva", "tag_created": "Napravljena oznaka (tag): {tag}", @@ -1748,14 +1780,14 @@ "theme_selection": "Izbor teme", "theme_selection_description": "Automatski postavite temu na svetlu ili tamnu na osnovu sistemskih preferencija vašeg pretraživača", "theme_setting_asset_list_storage_indicator_title": "Prikaži indikator prostora na zapisima", - "theme_setting_asset_list_tiles_per_row_title": "Broj zapisa po redu ({})", - "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", - "theme_setting_colorful_interface_title": "Colorful interface", + "theme_setting_asset_list_tiles_per_row_title": "Broj zapisa po redu {count}", + "theme_setting_colorful_interface_subtitle": "Nanesite osnovnu boju na pozadinske površine.", + "theme_setting_colorful_interface_title": "Šareni interfejs", "theme_setting_image_viewer_quality_subtitle": "Prilagodite kvalitet prikaza za detaljno pregledavanje slike", "theme_setting_image_viewer_quality_title": "Kvalitet pregledača slika", - "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", - "theme_setting_primary_color_title": "Primary color", - "theme_setting_system_primary_color_title": "Use system color", + "theme_setting_primary_color_subtitle": "Izaberite boju za glavne radnje i akcente.", + "theme_setting_primary_color_title": "Primarna boja", + "theme_setting_system_primary_color_title": "Koristi sistemsku boju", "theme_setting_system_theme_switch": "Automatski (Prati opcije sistema)", "theme_setting_theme_subtitle": "Odaberi temu sistema", "theme_setting_three_stage_loading_subtitle": "Trostepeno učitavanje možda ubrza učitavanje, po cenu potrošnje podataka", @@ -1779,17 +1811,19 @@ "trash_all": "Baci sve u otpad", "trash_count": "Otpad {count, number}", "trash_delete_asset": "Otpad/Izbriši datoteku", - "trash_emptied": "Emptied trash", + "trash_emptied": "Ispraznio smeće", "trash_no_results_message": "Slike i video zapisi u otpadu će se pojaviti ovde.", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_title": "Trash ({})", + "trash_page_delete_all": "Obriši sve", + "trash_page_empty_trash_dialog_content": "Da li želite da ispraznite svoja premeštena sredstva? Ovi predmeti će biti trajno uklonjeni iz Immich-a", + "trash_page_info": "Stavke izbačene iz otpada biće trajno obrisane nakon {days} dana", + "trash_page_no_assets": "Nema elemenata u otpadu", + "trash_page_restore_all": "Vrati sve", + "trash_page_select_assets_btn": "Izaberite sredstva", + "trash_page_title": "Otpad ({count})", "trashed_items_will_be_permanently_deleted_after": "Datoteke u otpadu će biti trajno izbrisane nakon {days, plural, one {# dan} few {# dana} other {# dana}}.", "type": "Vrsta", + "unable_to_change_pin_code": "Nije moguće promeniti PIN kod", + "unable_to_setup_pin_code": "Nije moguće podesiti PIN kod", "unarchive": "Vrati iz arhive", "unarchived_count": "{count, plural, other {Nearhivirano#}}", "unfavorite": "Izbaci iz omiljenih (unfavorite)", @@ -1813,11 +1847,12 @@ "untracked_files": "Nepraćene Datoteke", "untracked_files_decription": "Aplikacija ne prati ove datoteke. One mogu nastati zbog neuspešnih premeštenja, zbog prekinutih otpremanja ili kao preostatak zbog greške", "up_next": "Sledeće", + "updated_at": "Ažurirano", "updated_password": "Ažurirana lozinka", "upload": "Uploaduj", "upload_concurrency": "Paralelno uploadovanje", - "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", - "upload_dialog_title": "Upload Asset", + "upload_dialog_info": "Da li želite da napravite rezervnu kopiju izabranih elemenata na serveru?", + "upload_dialog_title": "Otpremi element", "upload_errors": "Otpremanje je završeno sa {count, plural, one {# greškom} other {# grešaka}}, osvežite stranicu da biste videli nove datoteke za otpremanje (upload).", "upload_progress": "Preostalo {remaining, number} – Obrađeno {processed, number}/{total, number}", "upload_skipped_duplicates": "Preskočeno {count, plural, one {# dupla datoteka} other {# duplih datoteka}}", @@ -1825,15 +1860,17 @@ "upload_status_errors": "Greške", "upload_status_uploaded": "Otpremljeno (Uploaded)", "upload_success": "Otpremanje je uspešno, osvežite stranicu da biste videli nova sredstva za otpremanje (upload).", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", + "upload_to_immich": "Otpremi u Immich ({count})", + "uploading": "Otpremanje", "url": "URL", "usage": "Upotreba", - "use_current_connection": "use current connection", + "use_current_connection": "koristi trenutnu vezu", "use_custom_date_range": "Umesto toga koristite prilagođeni period", "user": "Korisnik", "user_id": "ID korisnika", "user_liked": "{user} je lajkovao {type, select, photo {ovu fotografiju} video {ovaj video zapis} asset {ovu datoteku} other {ovo}}", + "user_pin_code_settings": "PIN kod", + "user_pin_code_settings_description": "Upravljajte svojim PIN kodom", "user_purchase_settings": "Kupovina", "user_purchase_settings_description": "Upravljajte kupovinom", "user_role_set": "Postavi {user} kao {role}", @@ -1844,15 +1881,15 @@ "users": "Korisnici", "utilities": "Alati", "validate": "Proveri", - "validate_endpoint_error": "Please enter a valid URL", + "validate_endpoint_error": "Molimo vas da unesete važeći URL", "variables": "Promenljive (variables)", "version": "Verzija", "version_announcement_closing": "Tvoj prijatelj, Aleks", - "version_announcement_message": "Zdravo prijatelju, postoji nova verzija aplikacije, molimo vas da odvojite vreme da posetite napomene o izdanju i uverite se da je server ažuriran kako bi se sprečile bilo kakve pogrešne konfiguracije, posebno ako koristite WatchTower ili bilo koji mehanizam koji automatski upravlja ažuriranjem vaše aplikacije.", + "version_announcement_message": "Zdravo! Dostupna je nova verzija Immich-a. Molimo vas da odvojite malo vremena da pročitate beleške o izdanju kako biste bili sigurni da je vaše podešavanje ažurirano i sprečili eventualne pogrešne konfiguracije, posebno ako koristite WatchTower ili bilo koji mehanizam koji automatski ažurira vašu Immich instancu.", "version_announcement_overlay_release_notes": "novine nove verzije", "version_announcement_overlay_text_1": "Ćao, nova verzija", - "version_announcement_overlay_text_2": "molimo Vas izdvojite vremena da pogledate", - "version_announcement_overlay_text_3": "i proverite da su Vaš docker-compose i .env najnovije verzije da bi izbegli greške u radu. Pogotovu ako koristite WatchTower ili bilo koji drugi mehanizam koji automatski instalira nove verzije vaše serverske aplikacije.", + "version_announcement_overlay_text_2": "molimo Vas izdvojite vremena da pogledate ", + "version_announcement_overlay_text_3": " i proverite da su Vaš docker-compose i .env najnovije verzije da bi izbegli greške u radu. Pogotovu ako koristite WatchTower ili bilo koji drugi mehanizam koji automatski instalira nove verzije vaše serverske aplikacije.", "version_announcement_overlay_title": "Nova verzija servera je dostupna 🎉", "version_history": "Istorija verzija", "version_history_item": "Instalirano {version} {date}", @@ -1873,8 +1910,8 @@ "view_previous_asset": "Pogledaj prethodnu datoteku", "view_qr_code": "Pogledajte QR kod", "view_stack": "Prikaži gomilu", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", + "viewer_remove_from_stack": "Ukloni iz steka", + "viewer_stack_use_as_main_asset": "Koristi kao glavni resurs", "viewer_unstack": "Un-Stack", "visibility_changed": "Vidljivost je promenjena za {count, plural, one {# osobu} other {# osobe}}", "waiting": "Čekam", @@ -1882,11 +1919,11 @@ "week": "Nedelja", "welcome": "Dobrodošli", "welcome_to_immich": "Dobrodošli u immich", - "wifi_name": "WiFi Name", + "wifi_name": "Naziv Wi-Fi mreže", "year": "Godina", "years_ago": "pre {years, plural, one {# godine} other {# godina}}", "yes": "Da", "you_dont_have_any_shared_links": "Nemate nijedno deljenje veze", - "your_wifi_name": "Your WiFi name", + "your_wifi_name": "Ime vaše Wi-Fi mreže", "zoom_image": "Zumiraj sliku" } diff --git a/i18n/sv.json b/i18n/sv.json index 175e47a51f..b7e723d8be 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -1376,7 +1376,7 @@ "public_share": "Offentlig delning", "purchase_account_info": "Supporter", "purchase_activated_subtitle": "Tack för att du stödjer Immich och open source-mjukvara", - "purchase_activated_time": "Aktiverad {date, date}", + "purchase_activated_time": "Aktiverad {date}", "purchase_activated_title": "Aktiveringan av din nyckel lyckades", "purchase_button_activate": "Aktivera", "purchase_button_buy": "Köp", diff --git a/i18n/ta.json b/i18n/ta.json index dd5cce6330..ab90eaadd5 100644 --- a/i18n/ta.json +++ b/i18n/ta.json @@ -984,7 +984,7 @@ "public_share": "பொது பங்கு", "purchase_account_info": "ஆதரவாளர்", "purchase_activated_subtitle": "இம்மிச் மற்றும் திறந்த மூல மென்பொருளை ஆதரித்ததற்கு நன்றி", - "purchase_activated_time": "{தேதி, தேதி} இல் செயல்படுத்தப்பட்டது", + "purchase_activated_time": "{date} இல் செயல்படுத்தப்பட்டது", "purchase_activated_title": "உங்கள் திறவுகோல் வெற்றிகரமாக செயல்படுத்தப்பட்டுள்ளது", "purchase_button_activate": "செயல்படுத்து", "purchase_button_buy": "வாங்க", diff --git a/i18n/te.json b/i18n/te.json index 0dcb2f6df1..ac980cbf93 100644 --- a/i18n/te.json +++ b/i18n/te.json @@ -1006,7 +1006,7 @@ "public_share": "పబ్లిక్ షేర్", "purchase_account_info": "మద్దతుదారు", "purchase_activated_subtitle": "Immich మరియు ఓపెన్ సోర్స్ సాఫ్ట్‌వేర్‌లకు మద్దతు ఇచ్చినందుకు ధన్యవాదాలు", - "purchase_activated_time": "{date, date}న యాక్టివేట్ చేయబడింది", + "purchase_activated_time": "{date}న యాక్టివేట్ చేయబడింది", "purchase_activated_title": "మీ కీ విజయవంతంగా యాక్టివేట్ చేయబడింది", "purchase_button_activate": "యాక్టివేట్ చేయండి", "purchase_button_buy": "కొను", diff --git a/i18n/th.json b/i18n/th.json index a77bb94116..39203d8fc9 100644 --- a/i18n/th.json +++ b/i18n/th.json @@ -598,6 +598,7 @@ "change_password_form_new_password": "รหัสผ่านใหม่", "change_password_form_password_mismatch": "รหัสผ่านไม่ตรงกัน", "change_password_form_reenter_new_password": "กรอกรหัสผ่านใหม่", + "change_pin_code": "เปลี่ยนรหัสประจำตัว (PIN)", "change_your_password": "เปลี่ยนรหัสผ่านของคุณ", "changed_visibility_successfully": "เปลี่ยนการมองเห็นเรียบร้อยแล้ว", "check_all": "เลือกทั้งหมด", @@ -638,6 +639,7 @@ "confirm_delete_face": "คุณแน่ใจว่าต้องการลบใบหน้า{name}ออกหรือไม่?", "confirm_delete_shared_link": "คุณต้องการที่จะลบลิงก์ที่แชร์ใช่หรือไม่ ?", "confirm_keep_this_delete_others": "จะลบทั้งหมดในรายการ และยกเว้นสื่อนี้หรือไม่ คุณแน่ใจใช่ไหมที่ต้องการดำเนินการต่อ?", + "confirm_new_pin_code": "ยืนยันรหัสประจำตัว (PIN)", "confirm_password": "ยืนยันรหัสผ่าน", "contain": "มีอยู่", "context": "บริบท", @@ -683,6 +685,7 @@ "crop": "Crop", "curated_object_page_title": "สิ่งของ", "current_device": "อุปกรณ์ปัจจุบัน", + "current_pin_code": "รหัสประจำตัว (PIN) ปัจจุบัน", "current_server_address": "Current server address", "custom_locale": "ปรับภาษาท้องถิ่นเอง", "custom_locale_description": "ใช้รูปแบบวันที่และตัวเลขจากภาษาและขอบเขต", @@ -1218,6 +1221,7 @@ "new_api_key": "สร้าง API คีย์ใหม่", "new_password": "รหัสผ่านใหม่", "new_person": "คนใหม่", + "new_pin_code": "รหัสประจำตัว (PIN) ใหม่", "new_user_created": "สร้างผู้ใช้ใหม่แล้ว", "new_version_available": "มีเวอร์ชันใหม่ให้ใช้งาน", "newest_first": "ใหม่สุดก่อน", @@ -1334,6 +1338,9 @@ "photos_count": "{count, plural, one {{count, number} รูป} other {{count, number} รูป}}", "photos_from_previous_years": "ภาพถ่ายจากปีก่อน", "pick_a_location": "เลือกตําแหน่ง", + "pin_code_changed_successfully": "เปลี่ยนรหัสประจำตัว (PIN) สำเร็จ", + "pin_code_reset_successfully": "ตั้งรหัสประจำตัว (PIN) ใหม่สำเร็จ", + "pin_code_setup_successfully": "ตั้งรหัสประจำตัว (PIN) สำเร็จ", "place": "สถานที่", "places": "สถานที่", "play": "เล่น", @@ -1363,7 +1370,7 @@ "public_share": "แชร์แบบสาธารณะ", "purchase_account_info": "ผู้สนับสนุน", "purchase_activated_subtitle": "ขอบคุณสำหรับการสนับสนุน Immich และซอฟต์แวร์เสรี (Open source software)", - "purchase_activated_time": "เปิดใช้งานวันที่ {date, date}", + "purchase_activated_time": "เปิดใช้งานวันที่ {date}", "purchase_activated_title": "รหัสของคุณถูกเปิดใช้งานเรียบร้อยแล้ว", "purchase_button_activate": "เปิดใช้งาน", "purchase_button_buy": "ซื้อ", @@ -1449,6 +1456,7 @@ "reset": "รีเซ็ต", "reset_password": "ตั้งค่ารหัสผ่านใหม่", "reset_people_visibility": "ปรับการมองเห็นใหม่", + "reset_pin_code": "ตั้งรหัสประจำตัว (PIN) ใหม่", "reset_to_default": "กลับไปค่าเริ่มต้น", "resolve_duplicates": "แก้ไขข้อมูลซ้ำซ้อน", "resolved_all_duplicates": "แก้ไขข้อมูลซ้ำซ้อนทั้งหมด", @@ -1588,6 +1596,7 @@ "settings": "ตั้งค่า", "settings_require_restart": "กรุณารีสตาร์ท Immmich เพื่อใช้การตั้งค่า", "settings_saved": "บันทึกการตั้งค่าแล้ว", + "setup_pin_code": "ตั้งรหัสประจำตัว (PIN)", "share": "แชร์", "share_add_photos": "เพิ่มรูปภาพ", "share_assets_selected": "{} ถูกเลือก", @@ -1766,6 +1775,8 @@ "trash_page_title": "ขยะ ({})", "trashed_items_will_be_permanently_deleted_after": "รายการที่ถูกลบจะถูกลบทิ้งภายใน {days, plural, one {# วัน} other {# วัน}}.", "type": "ประเภท", + "unable_to_change_pin_code": "ไม่สามารถเปลี่ยนรหัสประจำตัว (PIN)", + "unable_to_setup_pin_code": "ไม่สามารถตั้งรหัสประจำตัว (PIN)", "unarchive": "นำออกจากที่เก็บถาวร", "unfavorite": "นำออกจากรายการโปรด", "unhide_person": "ยกเลิกซ่อนบุคคล", @@ -1797,6 +1808,8 @@ "use_custom_date_range": "ใช้การปรับแต่งช่วงเวลา", "user": "ผู้ใช้", "user_id": "ไอดีผู้ใช้", + "user_pin_code_settings": "รหัสประจำตัว (PIN)", + "user_pin_code_settings_description": "จัดการรหัสประจำตัว (PIN)", "user_purchase_settings": "ซื้อ", "user_purchase_settings_description": "จัดการการซื้อ", "user_role_set": "ตั้ง {role} ให้กับ {user}", diff --git a/i18n/tr.json b/i18n/tr.json index 12795bc9bd..5923b27743 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -53,6 +53,7 @@ "confirm_email_below": "Onaylamak için aşağıya {email} yazın", "confirm_reprocess_all_faces": "Tüm yüzleri tekrardan işlemek istediğinize emin misiniz? Bu işlem isimlendirilmiş insanları da silecek.", "confirm_user_password_reset": "{user} adlı kullanıcının şifresini sıfırlamak istediğinize emin misiniz?", + "confirm_user_pin_code_reset": "{user} adlı kullanıcının PIN kodunu sıfırlamak istediğinize emin misiniz?", "create_job": "Görev oluştur", "cron_expression": "Cron İfadesi", "cron_expression_description": "Cron formatını kullanarak tarama aralığını belirle. Daha fazla bilgi için örneğin Crontab Guru’ya bakın", @@ -598,6 +599,7 @@ "change_password_form_new_password": "Yeni Parola", "change_password_form_password_mismatch": "Parolalar eşleşmiyor", "change_password_form_reenter_new_password": "Tekrar Yeni Parola", + "change_pin_code": "PIN kodunu değiştirin", "change_your_password": "Şifreni değiştir", "changed_visibility_successfully": "Görünürlük başarıyla değiştirildi", "check_all": "Tümünü Seç", @@ -638,6 +640,7 @@ "confirm_delete_face": "Varlıktan {name} yüzünü silmek istediğinizden emin misiniz?", "confirm_delete_shared_link": "Bu paylaşılan bağlantıyı silmek istediğinizden emin misiniz?", "confirm_keep_this_delete_others": "Yığındaki diğer tüm öğeler bu varlık haricinde silinecektir. Devam etmek istediğinizden emin misiniz?", + "confirm_new_pin_code": "Yeni PIN kodunu onaylayın", "confirm_password": "Şifreyi onayla", "contain": "İçermek", "context": "Bağlam", @@ -683,6 +686,7 @@ "crop": "Kes", "curated_object_page_title": "Nesneler", "current_device": "Mevcut cihaz", + "current_pin_code": "Mevcut PIN kodu", "current_server_address": "Mevcut sunucu adresi", "custom_locale": "Özel Yerel Ayar", "custom_locale_description": "Tarihleri ve sayıları dile ve bölgeye göre biçimlendirin", @@ -1221,6 +1225,7 @@ "new_api_key": "Yeni API Anahtarı", "new_password": "Yeni şifre", "new_person": "Yeni kişi", + "new_pin_code": "Yeni PIN kodu", "new_user_created": "Yeni kullanıcı oluşturuldu", "new_version_available": "YENİ VERSİYON MEVCUT", "newest_first": "Önce en yeniler", @@ -1337,6 +1342,9 @@ "photos_count": "{count, plural, one {{count, number} fotoğraf} other {{count, number} fotoğraf}}", "photos_from_previous_years": "Önceki yıllardan fotoğraflar", "pick_a_location": "Bir konum seçin", + "pin_code_changed_successfully": "PIN kodu başarıyla değiştirildi", + "pin_code_reset_successfully": "PIN kodu başarıyla sıfırlandı", + "pin_code_setup_successfully": "PIN kodu başarıyla ayarlandı", "place": "Konum", "places": "Konumlar", "places_count": "{count, plural, one {{count, number} yer} other {{count, number} yer}}", @@ -1367,7 +1375,7 @@ "public_share": "Genel paylaşım", "purchase_account_info": "Destekçi", "purchase_activated_subtitle": "Immich ve açık kaynak yazılıma destek olduğunuz için teşekkür ederiz", - "purchase_activated_time": "{date, date} tarihinde etkinleştirildi", + "purchase_activated_time": "{date} tarihinde etkinleştirildi", "purchase_activated_title": "Anahtarınız başarıyla etkinleştirildi", "purchase_button_activate": "Aktifleştir", "purchase_button_buy": "Satın al", @@ -1452,6 +1460,7 @@ "reset": "Sıfırla", "reset_password": "Şifreyi sıfırla", "reset_people_visibility": "Kişilerin görünürlüğünü sıfırla", + "reset_pin_code": "PIN kodunu sıfırlayın", "reset_to_default": "Varsayılana sıfırla", "resolve_duplicates": "Çiftleri çöz", "resolved_all_duplicates": "Tüm çiftler çözüldü", @@ -1590,6 +1599,7 @@ "settings": "Ayarlar", "settings_require_restart": "Bu ayarı uygulamak için lütfen Immich'i yeniden başlatın", "settings_saved": "Ayarlar kaydedildi", + "setup_pin_code": "PIN kodunu ayarlayın", "share": "Paylaş", "share_add_photos": "Fotoğraf ekle", "share_assets_selected": "{} seçili", @@ -1774,6 +1784,8 @@ "trash_page_title": "Çöp Kutusu ({})", "trashed_items_will_be_permanently_deleted_after": "Silinen öğeler {days, plural, one {# gün} other {# gün}} sonra kalıcı olarak silinecek.", "type": "Tür", + "unable_to_change_pin_code": "PIN kodu değiştirilemedi", + "unable_to_setup_pin_code": "PIN kodu ayarlanamadı", "unarchive": "Arşivden çıkar", "unarchived_count": "{count, plural, other {# arşivden çıkarıldı}}", "unfavorite": "Favorilerden kaldır", @@ -1818,6 +1830,8 @@ "user": "Kullanıcı", "user_id": "Kullanıcı ID", "user_liked": "{type, select, photo {Bu fotoğraf} video {Bu video} asset {Bu dosya} other {Bu}} {user} tarafından beğenildi", + "user_pin_code_settings": "PIN Kodu", + "user_pin_code_settings_description": "PIN kodunuzu yönetin", "user_purchase_settings": "Satın Alma", "user_purchase_settings_description": "Satın alma işlemlerini yönet", "user_role_set": "{user}, {role} olarak ayarlandı", diff --git a/i18n/uk.json b/i18n/uk.json index a1ce9461f5..9775d040d3 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -53,6 +53,7 @@ "confirm_email_below": "Для підтвердження введіть \"{email}\" нижче", "confirm_reprocess_all_faces": "Ви впевнені, що хочете повторно визначити всі обличчя? Це також призведе до видалення імен з усіх облич.", "confirm_user_password_reset": "Ви впевнені, що хочете скинути пароль користувача {user}?", + "confirm_user_pin_code_reset": "Ви впевнені, що хочете скинути PIN-код {user}?", "create_job": "Створити завдання", "cron_expression": "Cron вираз", "cron_expression_description": "Встановіть інтервал сканування, використовуючи формат cron. Для отримання додаткової інформації зверніться до напр. Crontab Guru", @@ -192,6 +193,7 @@ "oauth_auto_register": "Автоматична реєстрація", "oauth_auto_register_description": "Автоматично реєструвати нових користувачів після входу через OAuth", "oauth_button_text": "Текст кнопки", + "oauth_client_secret_description": "Потрібно, якщо постачальник OAuth не підтримує PKCE (Proof Key for Code Exchange)", "oauth_enable_description": "Увійти за допомогою OAuth", "oauth_mobile_redirect_uri": "URI мобільного перенаправлення", "oauth_mobile_redirect_uri_override": "Перевизначення URI мобільного перенаправлення", @@ -205,6 +207,8 @@ "oauth_storage_quota_claim_description": "Автоматично встановити квоту сховища користувача на значення цієї вимоги.", "oauth_storage_quota_default": "Квота за замовчуванням (GiB)", "oauth_storage_quota_default_description": "Квота в GiB, що використовується, коли налаштування не надано (введіть 0 для необмеженої квоти).", + "oauth_timeout": "Тайм-аут для запитів", + "oauth_timeout_description": "Максимальний час очікування відповіді в мілісекундах", "offline_paths": "Недоступні Шляхи", "offline_paths_description": "Ці результати можуть бути пов'язані з ручним видаленням файлів, які не входять до зовнішньої бібліотеки.", "password_enable_description": "Увійти за електронною поштою та паролем", @@ -366,7 +370,7 @@ "advanced": "Розширені", "advanced_settings_enable_alternate_media_filter_subtitle": "Використовуйте цей варіант для фільтрації медіафайлів під час синхронізації за альтернативними критеріями. Спробуйте це, якщо у вас виникають проблеми з тим, що додаток не виявляє всі альбоми.", "advanced_settings_enable_alternate_media_filter_title": "[ЕКСПЕРИМЕНТАЛЬНИЙ] Використовуйте альтернативний фільтр синхронізації альбомів пристрою", - "advanced_settings_log_level_title": "Рівень логування: {}", + "advanced_settings_log_level_title": "Рівень логування: {level}", "advanced_settings_prefer_remote_subtitle": "Деякі пристрої вельми повільно завантажують мініатюри із елементів на пристрої. Активуйте для завантаження віддалених мініатюр натомість.", "advanced_settings_prefer_remote_title": "Перевага віддаленим зображенням", "advanced_settings_proxy_headers_subtitle": "Визначте заголовки проксі-сервера, які Immich має надсилати з кожним мережевим запитом", @@ -374,7 +378,7 @@ "advanced_settings_self_signed_ssl_subtitle": "Пропускає перевірку SSL-сертифіката сервера. Потрібне для самопідписаних сертифікатів.", "advanced_settings_self_signed_ssl_title": "Дозволити самопідписані SSL-сертифікати", "advanced_settings_sync_remote_deletions_subtitle": "Автоматично видаляти або відновлювати ресурс на цьому пристрої, коли ця дія виконується в веб-інтерфейсі", - "advanced_settings_sync_remote_deletions_title": "Синхронізація видалених видалень [ЕКСПЕРИМЕНТАЛЬНО]", + "advanced_settings_sync_remote_deletions_title": "Синхронізація віддалених видалень [ЕКСПЕРИМЕНТАЛЬНО]", "advanced_settings_tile_subtitle": "Розширені користувацькі налаштування", "advanced_settings_troubleshooting_subtitle": "Увімкніть додаткові функції для усунення несправностей", "advanced_settings_troubleshooting_title": "Усунення несправностей", @@ -397,9 +401,9 @@ "album_remove_user_confirmation": "Ви впевнені, що хочете видалити {user}?", "album_share_no_users": "Схоже, ви поділилися цим альбомом з усіма користувачами або у вас немає жодного користувача, з яким можна було б поділитися.", "album_thumbnail_card_item": "1 елемент", - "album_thumbnail_card_items": "{} елементів", + "album_thumbnail_card_items": "{count} елементів", "album_thumbnail_card_shared": " · Спільний", - "album_thumbnail_shared_by": "Поділився {}", + "album_thumbnail_shared_by": "Поділився {user}", "album_updated": "Альбом оновлено", "album_updated_setting_description": "Отримуйте сповіщення на електронну пошту, коли у спільному альбомі з'являються нові ресурси", "album_user_left": "Ви покинули {album}", @@ -437,7 +441,7 @@ "archive": "Архівувати", "archive_or_unarchive_photo": "Архівувати або розархівувати фото", "archive_page_no_archived_assets": "Немає архівних елементів", - "archive_page_title": "Архів ({})", + "archive_page_title": "Архів ({count})", "archive_size": "Розмір архіву", "archive_size_description": "Налаштувати розмір архіву для завантаження (у GiB)", "archived": "Архів", @@ -474,18 +478,18 @@ "assets_added_to_album_count": "Додано {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}} до альбому", "assets_added_to_name_count": "Додано {count, plural, one {# елемент} other {# елементів}} до {hasName, select, true {{name}} other {нового альбому}}", "assets_count": "{count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", - "assets_deleted_permanently": "{} елемент(и) остаточно видалено", - "assets_deleted_permanently_from_server": "{} елемент(и) видалено назавжди з сервера Immich", + "assets_deleted_permanently": "{count} елемент(и) остаточно видалено", + "assets_deleted_permanently_from_server": "{count} елемент(и) видалено назавжди з сервера Immich", "assets_moved_to_trash_count": "Переміщено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}} у смітник", "assets_permanently_deleted_count": "Остаточно видалено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", "assets_removed_count": "Вилучено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", - "assets_removed_permanently_from_device": "{} елемент(и) видалені назавжди з вашого пристрою", + "assets_removed_permanently_from_device": "{count} елемент(и) видалені назавжди з вашого пристрою", "assets_restore_confirmation": "Ви впевнені, що хочете відновити всі свої активи з смітника? Цю дію не можна скасувати! Зверніть увагу, що будь-які офлайн-активи не можуть бути відновлені таким чином.", "assets_restored_count": "Відновлено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", - "assets_restored_successfully": "{} елемент(и) успішно відновлено", - "assets_trashed": "{} елемент(и) поміщено до кошика", + "assets_restored_successfully": "{count} елемент(и) успішно відновлено", + "assets_trashed": "{count} елемент(и) поміщено до кошика", "assets_trashed_count": "Поміщено в смітник {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", - "assets_trashed_from_server": "{} елемент(и) поміщено до кошика на сервері Immich", + "assets_trashed_from_server": "{count} елемент(и) поміщено до кошика на сервері Immich", "assets_were_part_of_album_count": "{count, plural, one {Ресурс був} few {Ресурси були} other {Ресурси були}} вже частиною альбому", "authorized_devices": "Авторизовані пристрої", "automatic_endpoint_switching_subtitle": "Підключатися локально через зазначену Wi-Fi мережу, коли це можливо, і використовувати альтернативні з'єднання в інших випадках", @@ -494,7 +498,7 @@ "back_close_deselect": "Повернутися, закрити або скасувати вибір", "background_location_permission": "Дозвіл до місцезнаходження у фоні", "background_location_permission_content": "Щоб перемикати мережі у фоновому режимі, Immich має *завжди* мати доступ до точної геолокації, щоб зчитувати назву Wi-Fi мережі", - "backup_album_selection_page_albums_device": "Альбоми на пристрої ({})", + "backup_album_selection_page_albums_device": "Альбоми на пристрої ({count})", "backup_album_selection_page_albums_tap": "Торкніться, щоб включити, двічі, щоб виключити", "backup_album_selection_page_assets_scatter": "Елементи можуть належати до кількох альбомів водночас. Таким чином, альбоми можуть бути включені або вилучені під час резервного копіювання.", "backup_album_selection_page_select_albums": "Оберіть альбоми", @@ -503,11 +507,11 @@ "backup_all": "Усі", "backup_background_service_backup_failed_message": "Не вдалося зробити резервну копію елементів. Повторюю…", "backup_background_service_connection_failed_message": "Не вдалося зв'язатися із сервером. Повторюю…", - "backup_background_service_current_upload_notification": "Завантажується {}", + "backup_background_service_current_upload_notification": "Завантажується {filename}", "backup_background_service_default_notification": "Перевіряю наявність нових елементів…", "backup_background_service_error_title": "Помилка резервного копіювання", "backup_background_service_in_progress_notification": "Резервне копіювання ваших елементів…", - "backup_background_service_upload_failure_notification": "Не вдалося завантажити {}", + "backup_background_service_upload_failure_notification": "Не вдалося завантажити {filename}", "backup_controller_page_albums": "Резервне копіювання альбомів", "backup_controller_page_background_app_refresh_disabled_content": "Для фонового резервного копіювання увімкніть фонове оновлення в меню \"Налаштування > Загальні > Фонове оновлення програми\".", "backup_controller_page_background_app_refresh_disabled_title": "Фонове оновлення програми вимкнене", @@ -518,22 +522,22 @@ "backup_controller_page_background_battery_info_title": "Оптимізація батареї", "backup_controller_page_background_charging": "Лише під час заряджання", "backup_controller_page_background_configure_error": "Не вдалося налаштувати фоновий сервіс", - "backup_controller_page_background_delay": "Затримка резервного копіювання нових елементів: {}", + "backup_controller_page_background_delay": "Затримка резервного копіювання нових елементів: {duration}", "backup_controller_page_background_description": "Увімкніть фонову службу, щоб автоматично створювати резервні копії будь-яких нових елементів без необхідності відкривати програму", "backup_controller_page_background_is_off": "Автоматичне фонове резервне копіювання вимкнено", "backup_controller_page_background_is_on": "Автоматичне фонове резервне копіювання ввімкнено", "backup_controller_page_background_turn_off": "Вимкнути фоновий сервіс", "backup_controller_page_background_turn_on": "Увімкнути фоновий сервіс", - "backup_controller_page_background_wifi": "Лише на WiFi", + "backup_controller_page_background_wifi": "Лише на Wi-Fi", "backup_controller_page_backup": "Резервне копіювання", "backup_controller_page_backup_selected": "Обрано: ", "backup_controller_page_backup_sub": "Резервні копії знімків та відео", - "backup_controller_page_created": "Створено: {}", + "backup_controller_page_created": "Створено: {date}", "backup_controller_page_desc_backup": "Увімкніть резервне копіювання на передньому плані, щоб автоматично завантажувати нові елементи на сервер під час відкриття програми.", "backup_controller_page_excluded": "Вилучено: ", - "backup_controller_page_failed": "Невдалі ({})", - "backup_controller_page_filename": "Назва файлу: {} [{}]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_failed": "Невдалі ({count})", + "backup_controller_page_filename": "Назва файлу: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "Інформація про резервну копію", "backup_controller_page_none_selected": "Нічого не обрано", "backup_controller_page_remainder": "Залишок", @@ -542,7 +546,7 @@ "backup_controller_page_start_backup": "Почати резервне копіювання", "backup_controller_page_status_off": "Автоматичне резервне копіювання в активному режимі вимкнено", "backup_controller_page_status_on": "Автоматичне резервне копіювання в активному режимі ввімкнено", - "backup_controller_page_storage_format": "Використано: {} з {}", + "backup_controller_page_storage_format": "Використано: {used} з {total}", "backup_controller_page_to_backup": "Альбоми до резервного копіювання", "backup_controller_page_total_sub": "Усі унікальні знімки та відео з вибраних альбомів", "backup_controller_page_turn_off": "Вимкнути резервне копіювання в активному режимі", @@ -567,21 +571,21 @@ "bulk_keep_duplicates_confirmation": "Ви впевнені, що хочете залишити {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}}? Це дозволить вирішити всі групи дублікатів без видалення чого-небудь.", "bulk_trash_duplicates_confirmation": "Ви впевнені, що хочете викинути в смітник {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}} масово? Це залишить найбільший ресурс у кожній групі і викине в смітник всі інші дублікати.", "buy": "Придбайте Immich", - "cache_settings_album_thumbnails": "Мініатюри сторінок бібліотеки ({} елементи)", + "cache_settings_album_thumbnails": "Мініатюри сторінок бібліотеки ({count} елементи)", "cache_settings_clear_cache_button": "Очистити кеш", "cache_settings_clear_cache_button_title": "Очищає кеш програми. Це суттєво знизить продуктивність програми, доки кеш не буде перебудовано.", "cache_settings_duplicated_assets_clear_button": "ОЧИСТИТИ", "cache_settings_duplicated_assets_subtitle": "Фото та відео, занесені додатком у чорний список", - "cache_settings_duplicated_assets_title": "Дубльовані елементи ({})", - "cache_settings_image_cache_size": "Розмір кешованих зображень ({} елементи)", + "cache_settings_duplicated_assets_title": "Дубльовані елементи ({count})", + "cache_settings_image_cache_size": "Розмір кешованих зображень ({count} елементи)", "cache_settings_statistics_album": "Бібліотечні мініатюри", - "cache_settings_statistics_assets": "{} елементи ({})", + "cache_settings_statistics_assets": "{count} елементи ({size})", "cache_settings_statistics_full": "Повнорзомірні зображення", "cache_settings_statistics_shared": "Мініатюри спільних альбомів", "cache_settings_statistics_thumbnail": "Мініатюри", "cache_settings_statistics_title": "Використання кешу", "cache_settings_subtitle": "Контролює кешування у мобільному застосунку", - "cache_settings_thumbnail_size": "Розмір кешованих мініатюр ({} елементи)", + "cache_settings_thumbnail_size": "Розмір кешованих мініатюр ({count} елементи)", "cache_settings_tile_subtitle": "Керування поведінкою локального сховища", "cache_settings_tile_title": "Локальне сховище", "cache_settings_title": "Налаштування кешування", @@ -607,6 +611,7 @@ "change_password_form_new_password": "Новий пароль", "change_password_form_password_mismatch": "Паролі не співпадають", "change_password_form_reenter_new_password": "Повторіть новий пароль", + "change_pin_code": "Змінити PIN-код", "change_your_password": "Змініть свій пароль", "changed_visibility_successfully": "Видимість успішно змінено", "check_all": "Позначити всі", @@ -647,11 +652,12 @@ "confirm_delete_face": "Ви впевнені, що хочете видалити обличчя {name} з активу?", "confirm_delete_shared_link": "Ви впевнені, що хочете видалити це спільне посилання?", "confirm_keep_this_delete_others": "Усі інші ресурси в стеку буде видалено, окрім цього ресурсу. Ви впевнені, що хочете продовжити?", + "confirm_new_pin_code": "Підтвердьте новий PIN-код", "confirm_password": "Підтвердити пароль", "contain": "Містити", "context": "Контекст", "continue": "Продовжуйте", - "control_bottom_app_bar_album_info_shared": "{} елементи · Спільні", + "control_bottom_app_bar_album_info_shared": "{count} елементи · Спільні", "control_bottom_app_bar_create_new_album": "Створити новий альбом", "control_bottom_app_bar_delete_from_immich": "Видалити з Immich", "control_bottom_app_bar_delete_from_local": "Видалити з пристрою", @@ -692,6 +698,7 @@ "crop": "Кадрувати", "curated_object_page_title": "Речі", "current_device": "Поточний пристрій", + "current_pin_code": "Поточний PIN-код", "current_server_address": "Поточна адреса сервера", "custom_locale": "Користувацький регіон", "custom_locale_description": "Форматувати дати та числа з урахуванням мови та регіону", @@ -760,7 +767,7 @@ "download_enqueue": "Завантаження поставлено в чергу", "download_error": "Помилка завантаження", "download_failed": "Завантаження не вдалося", - "download_filename": "файл: {}", + "download_filename": "файл: {filename}", "download_finished": "Завантаження закінчено", "download_include_embedded_motion_videos": "Вбудовані відео", "download_include_embedded_motion_videos_description": "Включати відео, вбудовані в рухомі фотографії, як окремий файл", @@ -811,12 +818,12 @@ "enabled": "Увімкнено", "end_date": "Дата завершення", "enqueued": "У черзі", - "enter_wifi_name": "Введіть назву WiFi", + "enter_wifi_name": "Введіть назву Wi-Fi", "error": "Помилка", "error_change_sort_album": "Не вдалося змінити порядок сортування альбому", "error_delete_face": "Помилка при видаленні обличчя з активу", "error_loading_image": "Помилка завантаження зображення", - "error_saving_image": "Помилка: {}", + "error_saving_image": "Помилка: {error}", "error_title": "Помилка: щось пішло не так", "errors": { "cannot_navigate_next_asset": "Не вдається перейти до наступного ресурсу", @@ -846,10 +853,12 @@ "failed_to_keep_this_delete_others": "Не вдалося зберегти цей ресурс і видалити інші ресурси", "failed_to_load_asset": "Не вдалося завантажити ресурс", "failed_to_load_assets": "Не вдалося завантажити ресурси", + "failed_to_load_notifications": "Не вдалося завантажити сповіщення", "failed_to_load_people": "Не вдалося завантажити людей", "failed_to_remove_product_key": "Не вдалося видалити ключ продукту", "failed_to_stack_assets": "Не вдалося згорнути ресурси", "failed_to_unstack_assets": "Не вдалося розгорнути ресурси", + "failed_to_update_notification_status": "Не вдалося оновити статус сповіщення", "import_path_already_exists": "Цей шлях імпорту вже існує.", "incorrect_email_or_password": "Неправильна адреса електронної пошти або пароль", "paths_validation_failed": "{paths, plural, one {# шлях} few {# шляхи} many {# шляхів} other {# шляху}} не пройшло перевірку", @@ -917,6 +926,7 @@ "unable_to_remove_reaction": "Не вдалося видалити реакцію", "unable_to_repair_items": "Не вдалося відновити елементи", "unable_to_reset_password": "Не вдається скинути пароль", + "unable_to_reset_pin_code": "Неможливо скинути PIN-код", "unable_to_resolve_duplicate": "Не вдається вирішити дублікат", "unable_to_restore_assets": "Неможливо відновити активи", "unable_to_restore_trash": "Не вдалося відновити вміст", @@ -950,10 +960,10 @@ "exif_bottom_sheet_location": "МІСЦЕ", "exif_bottom_sheet_people": "ЛЮДИ", "exif_bottom_sheet_person_add_person": "Додати ім'я", - "exif_bottom_sheet_person_age": "Вік {}", - "exif_bottom_sheet_person_age_months": "Вік {} місяців", - "exif_bottom_sheet_person_age_year_months": "Вік 1 рік, {} місяців", - "exif_bottom_sheet_person_age_years": "Вік {}", + "exif_bottom_sheet_person_age": "Вік {age}", + "exif_bottom_sheet_person_age_months": "Вік {months} місяців", + "exif_bottom_sheet_person_age_year_months": "Вік 1 рік, {months} місяців", + "exif_bottom_sheet_person_age_years": "Вік {years}", "exit_slideshow": "Вийти зі слайд-шоу", "expand_all": "Розгорнути все", "experimental_settings_new_asset_list_subtitle": "В розробці", @@ -971,7 +981,7 @@ "external": "Зовнішні", "external_libraries": "Зовнішні бібліотеки", "external_network": "Зовнішня мережа", - "external_network_sheet_info": "Коли ви не підключені до переважної мережі WiFi, додаток підключатиметься до сервера через першу з наведених нижче URL-адрес, яку він зможе досягти, починаючи зверху вниз", + "external_network_sheet_info": "Коли ви не підключені до переважної мережі Wi-Fi, додаток підключатиметься до сервера через першу з наведених нижче URL-адрес, яку він зможе досягти, починаючи зверху вниз", "face_unassigned": "Не призначено", "failed": "Не вдалося", "failed_to_load_assets": "Не вдалося завантажити ресурси", @@ -1118,7 +1128,7 @@ "local_network": "Локальна мережа", "local_network_sheet_info": "Додаток підключатиметься до сервера через цей URL, коли використовується вказана Wi-Fi мережа", "location_permission": "Дозвіл до місцезнаходження", - "location_permission_content": "Щоб перемикати мережі у фоновому режимі, Immich має *завжди* мати доступ до точної геолокації, щоб зчитувати назву Wi-Fi мережі", + "location_permission_content": "Щоб перемикати мережі у фоновому режимі, Immich має завжди мати доступ до точної геолокації, щоб зчитувати назву Wi-Fi мережі", "location_picker_choose_on_map": "Обрати на мапі", "location_picker_latitude_error": "Вкажіть дійсну широту", "location_picker_latitude_hint": "Вкажіть широту", @@ -1168,8 +1178,8 @@ "manage_your_devices": "Керуйте пристроями, які увійшли в систему", "manage_your_oauth_connection": "Налаштування підключеного OAuth", "map": "Мапа", - "map_assets_in_bound": "{} фото", - "map_assets_in_bounds": "{} фото", + "map_assets_in_bound": "{count} фото", + "map_assets_in_bounds": "{count} фото", "map_cannot_get_user_location": "Не можу отримати місцезнаходження", "map_location_dialog_yes": "Так", "map_location_picker_page_use_location": "Це місцезнаходження", @@ -1183,15 +1193,18 @@ "map_settings": "Налаштування мапи", "map_settings_dark_mode": "Темний режим", "map_settings_date_range_option_day": "Минулі 24 години", - "map_settings_date_range_option_days": "Минулих {} днів", + "map_settings_date_range_option_days": "Минулих {days} днів", "map_settings_date_range_option_year": "Минулий рік", - "map_settings_date_range_option_years": "Минулі {} роки", + "map_settings_date_range_option_years": "Минулі {years} роки", "map_settings_dialog_title": "Налаштування мапи", "map_settings_include_show_archived": "Відображати архів", "map_settings_include_show_partners": "Відображати знімки партнера", "map_settings_only_show_favorites": "Лише улюбені", "map_settings_theme_settings": "Тема карти", "map_zoom_to_see_photos": "Зменшіть, аби переглянути знімки", + "mark_all_as_read": "Позначити всі як прочитані", + "mark_as_read": "Позначити як прочитане", + "marked_all_as_read": "Позначено всі як прочитані", "matches": "Збіги", "media_type": "Тип медіа", "memories": "Спогади", @@ -1201,7 +1214,7 @@ "memories_start_over": "Почати заново", "memories_swipe_to_close": "Змахніть вгору, щоб закрити", "memories_year_ago": "Рік тому", - "memories_years_ago": "{} років тому", + "memories_years_ago": "{years} років тому", "memory": "Пам'ять", "memory_lane_title": "Алея Спогадів {title}", "menu": "Меню", @@ -1218,6 +1231,8 @@ "month": "Місяць", "monthly_title_text_date_format": "MMMM y", "more": "Більше", + "moved_to_archive": "Переміщено {count, plural, one {# актив} other {# активів}} в архів", + "moved_to_library": "Переміщено {count, plural, one {# актив} other {# активів}} в бібліотеку", "moved_to_trash": "Перенесено до смітника", "multiselect_grid_edit_date_time_err_read_only": "Неможливо редагувати дату елементів лише для читання, пропущено", "multiselect_grid_edit_gps_err_read_only": "Неможливо редагувати місцезнаходження елементів лише для читання, пропущено", @@ -1232,6 +1247,7 @@ "new_api_key": "Новий ключ API", "new_password": "Новий пароль", "new_person": "Нова людина", + "new_pin_code": "Новий PIN-код", "new_user_created": "Створено нового користувача", "new_version_available": "ДОСТУПНА НОВА ВЕРСІЯ", "newest_first": "Спочатку нові", @@ -1250,6 +1266,8 @@ "no_favorites_message": "Додавайте улюблені файли, щоб швидко знаходити ваші найкращі зображення та відео", "no_libraries_message": "Створіть зовнішню бібліотеку для перегляду фотографій і відео", "no_name": "Без імені", + "no_notifications": "Немає сповіщень", + "no_people_found": "Людей, що відповідають запиту, не знайдено", "no_places": "Місць немає", "no_results": "Немає результатів", "no_results_description": "Спробуйте використовувати синонім або більш загальне ключове слово", @@ -1304,7 +1322,7 @@ "partner_page_partner_add_failed": "Не вдалося додати партнера", "partner_page_select_partner": "Обрати партнера", "partner_page_shared_to_title": "Спільне із", - "partner_page_stop_sharing_content": "{} втратить доступ до ваших знімків.", + "partner_page_stop_sharing_content": "{partner} втратить доступ до ваших знімків.", "partner_sharing": "Спільне використання", "partners": "Партнери", "password": "Пароль", @@ -1350,6 +1368,9 @@ "photos_count": "{count, plural, one {{count, number} Фотографія} few {{count, number} Фотографії} many {{count, number} Фотографій} other {{count, number} Фотографій}}", "photos_from_previous_years": "Фотографії минулих років у цей день", "pick_a_location": "Виберіть місце розташування", + "pin_code_changed_successfully": "PIN-код успішно змінено", + "pin_code_reset_successfully": "PIN-код успішно скинуто", + "pin_code_setup_successfully": "PIN-код успішно налаштовано", "place": "Місце", "places": "Місця", "places_count": "{count, plural, one {{count, number} Місце} other {{count, number} Місця}}", @@ -1380,7 +1401,7 @@ "public_share": "Публічний доступ", "purchase_account_info": "Підтримка", "purchase_activated_subtitle": "Дякуємо за підтримку Immich та програмного забезпечення з відкритим кодом", - "purchase_activated_time": "Активовано {date, date}", + "purchase_activated_time": "Активовано {date}", "purchase_activated_title": "Ваш ключ було успішно активовано", "purchase_button_activate": "Активувати", "purchase_button_buy": "Купити", @@ -1425,6 +1446,8 @@ "recent_searches": "Нещодавні пошукові запити", "recently_added": "Нещодавно додані", "recently_added_page_title": "Нещодавні", + "recently_taken": "Недавно зроблено", + "recently_taken_page_title": "Недавно зроблені", "refresh": "Оновити", "refresh_encoded_videos": "Оновити закодовані відео", "refresh_faces": "Оновити обличчя", @@ -1467,6 +1490,7 @@ "reset": "Скидання", "reset_password": "Скинути пароль", "reset_people_visibility": "Відновити видимість людей", + "reset_pin_code": "Скинути PIN-код", "reset_to_default": "Скидання до налаштувань за замовчуванням", "resolve_duplicates": "Усунути дублікати", "resolved_all_duplicates": "Усі дублікати усунуто", @@ -1559,6 +1583,7 @@ "select_keep_all": "Залишити все обране", "select_library_owner": "Вибрати власника бібліотеки", "select_new_face": "Обрати нове обличчя", + "select_person_to_tag": "Виберіть людину для позначення", "select_photos": "Вибрати Знімки", "select_trash_all": "Видалити все вибране", "select_user_for_sharing_page_err_album": "Не вдалося створити альбом", @@ -1589,12 +1614,12 @@ "setting_languages_apply": "Застосувати", "setting_languages_subtitle": "Змінити мову додатку", "setting_languages_title": "Мова", - "setting_notifications_notify_failures_grace_period": "Повідомити про помилки фонового резервного копіювання: {}", - "setting_notifications_notify_hours": "{} годин", + "setting_notifications_notify_failures_grace_period": "Повідомити про помилки фонового резервного копіювання: {duration}", + "setting_notifications_notify_hours": "{count} годин", "setting_notifications_notify_immediately": "негайно", - "setting_notifications_notify_minutes": "{} хвилин", + "setting_notifications_notify_minutes": "{count} хвилин", "setting_notifications_notify_never": "ніколи", - "setting_notifications_notify_seconds": "{} секунд", + "setting_notifications_notify_seconds": "{count} секунд", "setting_notifications_single_progress_subtitle": "Детальна інформація про хід завантаження для кожного елементу", "setting_notifications_single_progress_title": "Показати хід фонового резервного копіювання", "setting_notifications_subtitle": "Налаштування параметрів сповіщень", @@ -1606,9 +1631,10 @@ "settings": "Налаштування", "settings_require_restart": "Перезавантажте програму для застосування цього налаштування", "settings_saved": "Налаштування збережені", + "setup_pin_code": "Налаштувати PIN-код", "share": "Поділитися", "share_add_photos": "Додати знімки", - "share_assets_selected": "{} обрано", + "share_assets_selected": "{count} обрано", "share_dialog_preparing": "Підготовка...", "shared": "Спільні", "shared_album_activities_input_disable": "Коментування вимкнено", @@ -1622,32 +1648,32 @@ "shared_by_user": "Спільний доступ з {user}", "shared_by_you": "Ви поділились", "shared_from_partner": "Фото від {partner}", - "shared_intent_upload_button_progress_text": "{} / {} Завантажено", + "shared_intent_upload_button_progress_text": "{current} / {total} Завантажено", "shared_link_app_bar_title": "Спільні посилання", "shared_link_clipboard_copied_massage": "Скопійовано в буфер обміну", - "shared_link_clipboard_text": "Посилання: {}\nПароль: {}", + "shared_link_clipboard_text": "Посилання: {link}\nПароль: {password}", "shared_link_create_error": "Помилка під час створення спільного посилання", "shared_link_edit_description_hint": "Введіть опис для спільного доступу", "shared_link_edit_expire_after_option_day": "1 день", - "shared_link_edit_expire_after_option_days": "{} днів", + "shared_link_edit_expire_after_option_days": "{count} днів", "shared_link_edit_expire_after_option_hour": "1 годину", - "shared_link_edit_expire_after_option_hours": "{} годин", + "shared_link_edit_expire_after_option_hours": "{count} годин", "shared_link_edit_expire_after_option_minute": "1 хвилину", - "shared_link_edit_expire_after_option_minutes": "{} хвилин", - "shared_link_edit_expire_after_option_months": "{} місяців", - "shared_link_edit_expire_after_option_year": "{} років", + "shared_link_edit_expire_after_option_minutes": "{count} хвилин", + "shared_link_edit_expire_after_option_months": "{count} місяців", + "shared_link_edit_expire_after_option_year": "{count} років", "shared_link_edit_password_hint": "Введіть пароль для спільного доступу", "shared_link_edit_submit_button": "Оновити посилання", "shared_link_error_server_url_fetch": "Неможливо запитати URL із сервера", - "shared_link_expires_day": "Закінчується через {} день", - "shared_link_expires_days": "Закінчується через {} днів", - "shared_link_expires_hour": "Закінчується через {} годину", - "shared_link_expires_hours": "Закінчується через {} годин", - "shared_link_expires_minute": "Закінчується через {} хвилину", - "shared_link_expires_minutes": "Закінчується через {} хвилин", + "shared_link_expires_day": "Закінчується через {count} день", + "shared_link_expires_days": "Закінчується через {count} днів", + "shared_link_expires_hour": "Закінчується через {count} годину", + "shared_link_expires_hours": "Закінчується через {count} годин", + "shared_link_expires_minute": "Закінчується через {count} хвилину", + "shared_link_expires_minutes": "Закінчується через {count} хвилин", "shared_link_expires_never": "Закінчується ∞", - "shared_link_expires_second": "Закінчується через {} секунду", - "shared_link_expires_seconds": "Закінчується через {} секунд", + "shared_link_expires_second": "Закінчується через {count} секунду", + "shared_link_expires_seconds": "Закінчується через {count} секунд", "shared_link_individual_shared": "Індивідуальний спільний доступ", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "Керування спільними посиланнями", @@ -1748,7 +1774,7 @@ "theme_selection": "Вибір теми", "theme_selection_description": "Автоматично встановлювати тему на світлу або темну залежно від системних налаштувань вашого браузера", "theme_setting_asset_list_storage_indicator_title": "Показувати піктограму сховища на плитках елементів", - "theme_setting_asset_list_tiles_per_row_title": "Кількість елементів у рядку ({})", + "theme_setting_asset_list_tiles_per_row_title": "Кількість елементів у рядку ({count})", "theme_setting_colorful_interface_subtitle": "Застосувати основний колір на поверхню фону.", "theme_setting_colorful_interface_title": "Барвистий інтерфейс", "theme_setting_image_viewer_quality_subtitle": "Налаштування якості перегляду повноекранних зображень", @@ -1783,13 +1809,15 @@ "trash_no_results_message": "Тут з'являтимуться видалені фото та відео.", "trash_page_delete_all": "Видалити усі", "trash_page_empty_trash_dialog_content": "Ви хочете очистити кошик? Ці елементи будуть остаточно видалені з Immich", - "trash_page_info": "Поміщені у кошик елементи буде остаточно видалено через {} днів", + "trash_page_info": "Поміщені у кошик елементи буде остаточно видалено через {days} днів", "trash_page_no_assets": "Віддалені елементи відсутні", "trash_page_restore_all": "Відновити усі", "trash_page_select_assets_btn": "Вибрані елементи", - "trash_page_title": "Кошик ({})", + "trash_page_title": "Кошик ({count})", "trashed_items_will_be_permanently_deleted_after": "Видалені елементи будуть остаточно видалені через {days, plural, one {# день} few {# дні} many {# днів} other {# днів}}.", "type": "Тип", + "unable_to_change_pin_code": "Неможливо змінити PIN-код", + "unable_to_setup_pin_code": "Неможливо налаштувати PIN-код", "unarchive": "Розархівувати", "unarchived_count": "{count, plural, other {Повернуто з архіву #}}", "unfavorite": "Видалити з улюблених", @@ -1825,7 +1853,7 @@ "upload_status_errors": "Помилки", "upload_status_uploaded": "Завантажено", "upload_success": "Завантаження успішне. Оновіть сторінку, щоб побачити нові завантажені ресурси.", - "upload_to_immich": "Завантажити в Immich ({})", + "upload_to_immich": "Завантажити в Immich ({count})", "uploading": "Завантаження", "url": "URL", "usage": "Використання", @@ -1834,6 +1862,8 @@ "user": "Користувач", "user_id": "ID Користувача", "user_liked": "{user} вподобав {type, select, photo {це фото} video {це відео} asset {цей ресурс} other {це}}", + "user_pin_code_settings": "PIN-код", + "user_pin_code_settings_description": "Керуйте своїм PIN-кодом", "user_purchase_settings": "Придбати", "user_purchase_settings_description": "Керувати вашою покупкою", "user_role_set": "Призначити {user} на роль {role}", diff --git a/i18n/ur.json b/i18n/ur.json index c439165e2d..d34cfa759d 100644 --- a/i18n/ur.json +++ b/i18n/ur.json @@ -11,13 +11,13 @@ "activity_changed": "سرگرمی {enabled, select, true {فعال ہے} other {غیر فعال ہے}}", "add": "شامل کریں", "add_a_description": "تفصیل شامل کریں", - "add_a_location": "مقام شامل کریں", - "add_a_name": "نام شامل کریں", - "add_a_title": "عنوان شامل کریں", - "add_endpoint": "اختتامی نقطہ شامل کریں", + "add_a_location": "کا اندراج کریں", + "add_a_name": "نام کا اندراج کریں", + "add_a_title": "عنوان کا اندراج کریں", + "add_endpoint": "اینڈ پوائنٹ درج کریں", "add_exclusion_pattern": "خارج کرنے کا نمونہ شامل کریں", "add_import_path": "درآمد کا راستہ شامل کریں", - "add_location": "مقام شامل کریں", + "add_location": "جگہ درج کریں", "add_more_users": "مزید صارفین شامل کریں", "add_partner": "ساتھی شامل کریں", "add_path": "راستہ شامل کریں", @@ -32,6 +32,8 @@ "added_to_favorites": "پسندیدہ میں شامل کیا گیا", "added_to_favorites_count": "پسندیدہ میں {count, number} شامل کیے گئے", "admin": { + "add_exclusion_pattern_description": "اخراج کے نمونے شامل کریں۔ *، **، اور ? کا استعمال کرتے ہوئے Globbing کا استعمال کیا جا سکتا ہے۔ \"RAW\" نامی کسی بھی ڈائریکٹری میں تمام فائلوں کو نظر انداز کرنے کے لیے، \"**/Raw/**\" استعمال کریں۔ \".tif\" سے ختم ہونے والی تمام فائلوں کو نظر انداز کرنے کے لیے، \"**/*.tif\" استعمال کریں۔ کسی مطلق راستے کو نظر انداز کرنے کے لیے، \"/path/to/ignore/**\" کا استعمال کریں۔", + "asset_offline_description": "لائبریری کا یہ بیرونی اثاثہ اب ڈسک پر نہیں ملا اور اسے کوڑے دان میں ڈال دیا گیا ہے۔ اگر فائل لائبریری کے اندر منتقل کی گئی تھی، تو نئے متعلقہ اثاثے کے لیے اپنی ٹائم لائن چیک کریں۔ اس اثاثے کو بحال کرنے کے لیے، براہ کرم یقینی بنائیں کہ نیچے دیے گئے فائل کے راستے تک Immich کو رسائی حاصل ہے اور لائبریری کو اسکین کریں۔", "authentication_settings_disable_all": "کیا آپ واقعی لاگ ان کے تمام طریقوں کو غیر فعال کرنا چاہتے ہیں؟ لاگ ان مکمل طور پر غیر فعال ہو جائے گا۔", "check_all": "سب چیک کریں", "cleanup": "صاف کرو", @@ -41,5 +43,29 @@ "image_quality": "معیار", "image_settings": "تصویر کی ترتیبات" }, + "change_pin_code": "پن کوڈ تبدیل کریں", + "confirm_new_pin_code": "نئے پن کوڈ کی تصدیق کریں", + "current_pin_code": "موجودہ پن کوڈ", + "new_pin_code": "نیا پن کوڈ", + "pin_code_changed_successfully": "پن کوڈ کو کامیابی سے تبدیل کر دیا گیا", + "pin_code_reset_successfully": "پن کوڈ کامیابی کے ساتھ ری سیٹ ہو گیا", + "pin_code_setup_successfully": "پن کوڈ کامیابی کے ساتھ سیٹ اپ ہو گیا", + "reset_pin_code": "پن کوڈ دوبارہ ترتیب دیں", + "setup_pin_code": "ایک نیا پن کوڈ ترتیب دیں", + "sunrise_on_the_beach": "ساحل سمندر پر طلوع آفتاب", + "unable_to_change_pin_code": "پن کوڈ تبدیل کرنے سے قاصر", + "unable_to_setup_pin_code": "پن کوڈ ترتیب کرنے سے قاصر", + "user_pin_code_settings": "پن کوڈ", + "user_pin_code_settings_description": "اپنے پن کوڈ کا نظم کریں", + "user_purchase_settings": "خریداری", + "user_purchase_settings_description": "اپنی خریداری کا انتظام کریں", + "version_announcement_closing": "آپ کا دوست، ایلکس", + "video": "ویڈیو", + "videos": "ویڈیوز", + "view": "دیکھیں", + "view_all": "سب دیکھیں", + "waiting": "انتظار", + "week": "ہفتہ", + "year": "سال", "zoom_image": "زوم تصویر" } diff --git a/i18n/vi.json b/i18n/vi.json index 7a0bc83a96..22d485d142 100644 --- a/i18n/vi.json +++ b/i18n/vi.json @@ -1350,7 +1350,7 @@ "public_share": "Chia sẻ công khai", "purchase_account_info": "Người hỗ trợ", "purchase_activated_subtitle": "Cảm ơn bạn đã hỗ trợ Immich và phần mềm mã nguồn mở", - "purchase_activated_time": "Đã kích hoạt vào {date, date}", + "purchase_activated_time": "Đã kích hoạt vào {date}", "purchase_activated_title": "Khóa của bạn đã được kích hoạt thành công", "purchase_button_activate": "Kích hoạt", "purchase_button_buy": "Mua", diff --git a/i18n/zh_Hant.json b/i18n/zh_Hant.json index 0dc86f00c0..8551385330 100644 --- a/i18n/zh_Hant.json +++ b/i18n/zh_Hant.json @@ -14,7 +14,7 @@ "add_a_location": "新增地點", "add_a_name": "加入姓名", "add_a_title": "新增標題", - "add_endpoint": "Add endpoint", + "add_endpoint": "新增端點", "add_exclusion_pattern": "加入篩選條件", "add_import_path": "新增匯入路徑", "add_location": "新增地點", @@ -39,11 +39,11 @@ "authentication_settings_disable_all": "確定要停用所有登入方式嗎?這樣會完全無法登入。", "authentication_settings_reenable": "如需重新啟用,請使用 伺服器指令 。", "background_task_job": "背景執行", - "backup_database": "備份資料庫", + "backup_database": "建立數據庫備份", "backup_database_enable_description": "啟用資料庫備份", "backup_keep_last_amount": "保留先前備份的數量", - "backup_settings": "備份設定", - "backup_settings_description": "管理資料庫備份設定", + "backup_settings": "資料庫備份設定", + "backup_settings_description": "管理資料庫備份設定。 注意: 這項作業不會被監控,且你將無法於失敗時收到通知。", "check_all": "全選", "cleanup": "清理", "cleared_jobs": "已刪除「{job}」任務", @@ -53,24 +53,25 @@ "confirm_email_below": "請在底下輸入 {email} 來確認", "confirm_reprocess_all_faces": "確定要重新處理所有臉孔嗎?這會清除已命名的人物。", "confirm_user_password_reset": "您確定要重設 {user} 的密碼嗎?", + "confirm_user_pin_code_reset": "確定要重置 {user} 的 PIN 碼嗎?", "create_job": "建立作業", "cron_expression": "Cron 運算式", "cron_expression_description": "以 Cron 格式設定掃描時段。詳細資訊請參閱 Crontab Guru", "cron_expression_presets": "現成的 Cron 運算式", "disable_login": "停用登入", - "duplicate_detection_job_description": "對檔案執行機器學習來偵測相似圖片。(此功能仰賴智慧搜尋)", + "duplicate_detection_job_description": "依靠智慧搜尋。執行機器學習對項目來偵測相似圖片。", "exclusion_pattern_description": "排除規則讓您在掃描資料庫時忽略特定文件和文件夾。用於當您有不想導入的文件(例如 RAW 文件)或文件夾。", "external_library_created_at": "外部相簿(於 {date} 建立)", "external_library_management": "外部相簿管理", "face_detection": "臉孔偵測", - "face_detection_description": "使用機器學習偵測檔案中的臉孔(影片只會偵測縮圖中的臉孔)。選擇「重新整理」會重新處理所有檔案。選擇「重設」會清除目前所有的臉孔資料。選擇「遺失的」會把尚未處理的檔案排入處理佇列。臉孔偵測完成後,會把偵測到的臉孔排入臉部辨識佇列,將其分組到現有的或新的人物中。", + "face_detection_description": "使用機器學習偵測項目中的臉孔(影片只會偵測縮圖中的臉孔)。選擇「重新處理」會重新處理所有尚未處理以及已經處理的項目。選擇「重設」會清除目前所有的臉孔資料。選擇「排入未處理」會把尚未處理的項目排入處理序列中。臉孔偵測完成後,會把偵測到的臉孔排入臉部辨識序列中,將其分組到現有的或新的人物中。", "facial_recognition_job_description": "將偵測到的臉孔依照人物分組。此步驟會在臉孔偵測完成後執行。選擇「重設」會重新分組所有臉孔。選擇「遺失的」會把尚未指定人物的臉孔排入佇列。", "failed_job_command": "{job} 任務的 {command} 指令執行失敗", - "force_delete_user_warning": "警告:這將立即刪除使用者及其資料。操作後無法反悔且刪除的檔案無法恢復。", + "force_delete_user_warning": "警告:這將立即刪除使用者及所有項目。無法還原刪除的檔案。", "forcing_refresh_library_files": "強制重新整理所有圖庫檔案", "image_format": "格式", "image_format_description": "WebP 能產生相對於 JPEG 更小的檔案,但編碼速度較慢。", - "image_fullsize_description": "剝離圖片詮釋資料/元數據後的全尺寸圖片,在圖片被放大的情況下使用", + "image_fullsize_description": "剝離圖片詳細資料/元數據後的全尺寸圖片,在圖片被放大的情況下使用", "image_fullsize_enabled": "開啟全尺寸圖片生成", "image_fullsize_enabled_description": "為非網路友好圖片格式的圖片生成全尺寸圖像。在開啟 “偏好嵌入的預覽” 的選項後,嵌入預覽會在沒有轉換格式下的狀況被使用。這項選項不影響JPEG等網路友好圖片格式。", "image_fullsize_quality_description": "從1-100的全尺寸圖片品質。越高的數字代表著產出的品質越高,檔案更大。", @@ -106,7 +107,7 @@ "library_scanning_enable_description": "啟用圖庫定期掃描", "library_settings": "外部圖庫", "library_settings_description": "管理外部圖庫設定", - "library_tasks_description": "掃描外部資料庫以尋找新增或更改的資源", + "library_tasks_description": "掃描外部資源以尋找新增或更改的項目", "library_watching_enable_description": "監控外部圖庫的檔案變化", "library_watching_settings": "圖庫監控(實驗中)", "library_watching_settings_description": "自動監控檔案的變化", @@ -117,7 +118,7 @@ "machine_learning_clip_model_description": "這裡有份 CLIP 模型名單。註:更換模型後須對所有圖片重新執行「智慧搜尋」作業。", "machine_learning_duplicate_detection": "重複項目偵測", "machine_learning_duplicate_detection_enabled": "啟用重複項目偵測", - "machine_learning_duplicate_detection_enabled_description": "即使停用,完全一樣的素材仍會被忽略。", + "machine_learning_duplicate_detection_enabled_description": "關閉該功能會忽略有重複的項目。", "machine_learning_duplicate_detection_setting_description": "用 CLIP 向量比對潛在重複", "machine_learning_enabled": "啟用機器學習", "machine_learning_enabled_description": "若停用,則無視下方的設定,所有機器學習的功能都將停用。", @@ -166,11 +167,11 @@ "metadata_settings": "詳細資料設定", "metadata_settings_description": "管理詮釋資料設定", "migration_job": "遷移", - "migration_job_description": "將照片和人臉的縮圖遷移到最新的文件夾結構", + "migration_job_description": "將項目和臉孔的縮圖移到新的延伸資料夾", "no_paths_added": "未添加路徑", "no_pattern_added": "未添加pattern", - "note_apply_storage_label_previous_assets": "註:要將儲存標籤用於先前上傳的檔案,請執行", - "note_cannot_be_changed_later": "註:之後就無法更改嘍!", + "note_apply_storage_label_previous_assets": "*註:執行套用儲存標籤前先上傳項目", + "note_cannot_be_changed_later": "*註:之後無法修改!", "notification_email_from_address": "寄件地址", "notification_email_from_address_description": "寄件者電子郵件地址(例:Immich Photo Server )", "notification_email_host_description": "電子郵件伺服器主機(例:smtp.immich.app)", @@ -192,6 +193,7 @@ "oauth_auto_register": "自動註冊", "oauth_auto_register_description": "使用 OAuth 登錄後自動註冊新用戶", "oauth_button_text": "按鈕文字", + "oauth_client_secret_description": "如果 OAuth 提供者不支援 PKCE(授權碼驗證碼交換機制),則此為必填項目", "oauth_enable_description": "用 OAuth 登入", "oauth_mobile_redirect_uri": "移動端重定向 URI", "oauth_mobile_redirect_uri_override": "移動端重定向 URI 覆蓋", @@ -205,6 +207,8 @@ "oauth_storage_quota_claim_description": "自動將使用者的儲存配額定為此宣告之值。", "oauth_storage_quota_default": "預設儲存配額(GiB)", "oauth_storage_quota_default_description": "未宣告時所使用的配額(單位:GiB)(輸入 0 表示不限制配額)。", + "oauth_timeout": "請求逾時", + "oauth_timeout_description": "請求的逾時時間(毫秒)", "offline_paths": "失效路徑", "offline_paths_description": "這些可能是手動刪除非外部圖庫的檔案時所遺留的。", "password_enable_description": "用電子郵件和密碼登入", @@ -236,15 +240,15 @@ "sidecar_job": "邊車模式詮釋資料", "sidecar_job_description": "從檔案系統搜索或同步邊車模式詮釋資料", "slideshow_duration_description": "每張圖片放映的秒數", - "smart_search_job_description": "對檔案執行機器學習,以利智慧搜尋", + "smart_search_job_description": "執行機器學習有助於智慧搜尋", "storage_template_date_time_description": "檔案的創建時戳會用於判斷時間資訊", "storage_template_date_time_sample": "時間樣式 {date}", "storage_template_enable_description": "啟用存儲模板引擎", "storage_template_hash_verification_enabled": "散列函数驗證已啟用", "storage_template_hash_verification_enabled_description": "啟用散列函数驗證,除非您很清楚地知道這個選項的作用,否則請勿停用此功能", "storage_template_migration": "存儲模板遷移", - "storage_template_migration_description": "將當前的 {template} 應用於先前上傳的檔案", - "storage_template_migration_info": "檔案儲存模板將把所有檔案副檔名改爲小寫。模板更改僅適用於新檔案。若要追溯應用模板至先前上傳的檔案,請運行 {job}。", + "storage_template_migration_description": "套用前 {template} 先上傳項目", + "storage_template_migration_info": "透用儲存範例將將把所有檔案副檔名改爲小寫。模板更新僅適用於新項目。若要套用過去範例請先上傳項目,請執行 {job}。", "storage_template_migration_job": "存儲模板遷移任務", "storage_template_more_details": "欲了解更多有關此功能的詳細信息,請參閱 存儲模板 及其 影響", "storage_template_onboarding_description": "啟用此功能後,將根據用戶自定義的模板自動組織文件。由於穩定性問題,此功能已默認關閉。欲了解更多信息,請參閱 文檔。", @@ -272,7 +276,7 @@ "thumbnail_generation_job": "產生縮圖", "thumbnail_generation_job_description": "為每個檔案產生大、小及模糊縮圖,也為每位人物產生縮圖", "transcoding_acceleration_api": "加速 API", - "transcoding_acceleration_api_description": "該 API 將用您的設備加速轉碼。設置是“盡力而為”:如果失敗,它將退回到軟體轉碼。VP9 轉碼是否可行取決於您的硬體。", + "transcoding_acceleration_api_description": "API 將用於硬體加速。設定優先使用:失敗會使用軟體轉碼。是否支援 VP9 編碼格式依照您的硬體支援而定。", "transcoding_acceleration_nvenc": "NVENC(需要 NVIDIA GPU)", "transcoding_acceleration_qsv": "快速同步(需要第七代或高於第七代的 Intel CPU)", "transcoding_acceleration_rkmpp": "RKMPP(僅適用於 Rockchip SoC)", @@ -334,16 +338,16 @@ "transcoding_video_codec_description": "VP9 具有高效能且相容於網頁,但轉碼時間較長。HEVC 的效能相近,但網頁相容性較低。H.264 具有廣泛的相容性且轉碼速度快,但產生的檔案較大。AV1 是目前效率最好的編解碼器,但較舊設備不支援。", "trash_enabled_description": "啟用垃圾桶功能", "trash_number_of_days": "日數", - "trash_number_of_days_description": "永久刪除之前,將檔案保留在垃圾桶中的日數", + "trash_number_of_days_description": "永久刪除前項目將保留在垃圾桶中數天", "trash_settings": "垃圾桶", "trash_settings_description": "管理垃圾桶設定", "untracked_files": "未被追蹤的檔案", - "untracked_files_description": "這些檔案不會被追蹤。它們可能是移動失誤、上傳中斷或遇到漏洞而遺留的產物", + "untracked_files_description": "這些檔案不會被追蹤。它們可能是移動失敗、上傳失敗、漏洞而造成的。", "user_cleanup_job": "清理使用者", - "user_delete_delay": "{user} 的帳號和項目將於 {delay, plural, other {# 天}}後永久刪除。", + "user_delete_delay": "{user} 的帳號和項目會在 {delay, plural, other {# 天}} 後永久刪除。", "user_delete_delay_settings": "延後刪除", - "user_delete_delay_settings_description": "移除後,永久刪除使用者帳號和檔案的天數。使用者刪除作業會在午夜檢查是否有可以刪除的使用者。變更這項設定後,會在下次執行時檢查。", - "user_delete_immediately": "{user} 的帳號和項目將立即永久刪除。", + "user_delete_delay_settings_description": "天數後將永久刪除帳號與項目。刪除任務會在 00:00 後檢查可以刪除的使用者。變更設定後會在下次執行檢查。", + "user_delete_immediately": "{user} 的帳號和項目將 立即 永久刪除。", "user_delete_immediately_checkbox": "將使用者和項目立即刪除", "user_management": "使用者管理", "user_password_has_been_reset": "使用者密碼已重設:", @@ -364,13 +368,17 @@ "admin_password": "管理者密碼", "administration": "管理", "advanced": "進階", - "advanced_settings_log_level_title": "日誌等級: {}", - "advanced_settings_prefer_remote_subtitle": "在某些裝置上,從本地的項目載入縮圖的速度非常慢。\n啓用此選項以載入遙距項目。", + "advanced_settings_enable_alternate_media_filter_subtitle": "使用此選項可在同步時依照替代條件篩選媒體。僅當應用程式在偵測所有相簿時出現問題時才建議使用。", + "advanced_settings_enable_alternate_media_filter_title": "[實驗]使用其他的裝置相簿同步篩選器", + "advanced_settings_log_level_title": "日誌等級:{level}", + "advanced_settings_prefer_remote_subtitle": "特定裝置載入縮圖的速度非常緩慢。開啟載入遠端項目的功能。", "advanced_settings_prefer_remote_title": "優先遙距項目", "advanced_settings_proxy_headers_subtitle": "定義代理標頭,套用於Immich的每次網絡請求", "advanced_settings_proxy_headers_title": "代理標頭", "advanced_settings_self_signed_ssl_subtitle": "略過伺服器端點的 SSL 證書驗證(該選項適用於使用自簽名證書的伺服器)。", "advanced_settings_self_signed_ssl_title": "允許自簽名 SSL 證書", + "advanced_settings_sync_remote_deletions_subtitle": "在網頁上執行刪除或還原操作時,自動在此裝置上刪除或還原檔案", + "advanced_settings_sync_remote_deletions_title": "同步遠端刪除[實驗]", "advanced_settings_tile_subtitle": "進階用戶設定", "advanced_settings_troubleshooting_subtitle": "啓用用於故障排除的額外功能", "advanced_settings_troubleshooting_title": "故障排除", @@ -393,143 +401,143 @@ "album_remove_user_confirmation": "確定要移除 {user} 嗎?", "album_share_no_users": "看來您與所有使用者共享了這本相簿,或沒有其他使用者可供分享。", "album_thumbnail_card_item": "1 項", - "album_thumbnail_card_items": "{} 項", + "album_thumbnail_card_items": "{count} 項", "album_thumbnail_card_shared": " · 已共享", - "album_thumbnail_shared_by": "由 {} 共享", + "album_thumbnail_shared_by": "由 {user} 共享", "album_updated": "更新相簿時", - "album_updated_setting_description": "當共享相簿有新檔案時,用電子郵件通知我", - "album_user_left": "已離開 {album}", - "album_user_removed": "已移除 {user}", - "album_viewer_appbar_delete_confirm": "確定要從賬戶中刪除此相簿嗎?", - "album_viewer_appbar_share_err_delete": "刪除相簿失敗", - "album_viewer_appbar_share_err_leave": "退出共享失敗", - "album_viewer_appbar_share_err_remove": "從相簿中移除時出現錯誤", - "album_viewer_appbar_share_err_title": "修改相簿標題失敗", - "album_viewer_appbar_share_leave": "退出共享", - "album_viewer_appbar_share_to": "共享給", - "album_viewer_page_share_add_users": "新增用戶", + "album_updated_setting_description": "當共享相簿有新項目時用電子郵件通知我", + "album_user_left": "離開 {album}", + "album_user_removed": "移除 {user}", + "album_viewer_appbar_delete_confirm": "確定要從帳號中刪除此相簿嗎?", + "album_viewer_appbar_share_err_delete": "無法刪除相簿", + "album_viewer_appbar_share_err_leave": "無法離開相簿", + "album_viewer_appbar_share_err_remove": "從相簿中移除項目時出現錯誤", + "album_viewer_appbar_share_err_title": "無法編輯相簿標題", + "album_viewer_appbar_share_leave": "離開相簿", + "album_viewer_appbar_share_to": "分享給", + "album_viewer_page_share_add_users": "邀請其他人", "album_with_link_access": "知道連結的使用者都可以查看這本相簿中的相片和使用者。", "albums": "相簿", "albums_count": "{count, plural, one {{count, number} 本相簿} other {{count, number} 本相簿}}", "all": "全部", "all_albums": "所有相簿", - "all_people": "所有人", + "all_people": "所有人物", "all_videos": "所有影片", "allow_dark_mode": "允許深色模式", "allow_edits": "允許編輯", - "allow_public_user_to_download": "開放給使用者下載", - "allow_public_user_to_upload": "開放讓使用者上傳", - "alt_text_qr_code": "QR 碼圖片", + "allow_public_user_to_download": "開放使用者下載", + "allow_public_user_to_upload": "開放使用者上傳", + "alt_text_qr_code": "QR code 圖片", "anti_clockwise": "逆時針", "api_key": "API 金鑰", - "api_key_description": "此值僅顯示一次。請確保在關閉窗口之前複製它。", - "api_key_empty": "您的 API 金鑰名稱不能為空", + "api_key_description": "此金鑰僅顯示一次。請在關閉前複製它。", + "api_key_empty": "您的 API 金鑰名稱不能為空值", "api_keys": "API 金鑰", - "app_bar_signout_dialog_content": "您確定要退出嗎?", + "app_bar_signout_dialog_content": "您確定要登出?", "app_bar_signout_dialog_ok": "是", - "app_bar_signout_dialog_title": "退出登入", + "app_bar_signout_dialog_title": "登出", "app_settings": "應用程式設定", - "appears_in": "出現在", + "appears_in": "地點", "archive": "封存", "archive_or_unarchive_photo": "封存或取消封存照片", - "archive_page_no_archived_assets": "未找到歸檔項目", - "archive_page_title": "封存 ({})", - "archive_size": "封存量", - "archive_size_description": "設定要下載的封存量(單位:GiB)", - "archived": "已存檔", + "archive_page_no_archived_assets": "未找到封存項目", + "archive_page_title": "封存 ({count})", + "archive_size": "封存檔案大小", + "archive_size_description": "設定要下載的封存檔案大小 (單位: GB)", + "archived": "已封存", "archived_count": "{count, plural, other {已封存 # 個項目}}", - "are_these_the_same_person": "這也是同一個人嗎?", - "are_you_sure_to_do_this": "您確定要這麼做嗎?", - "asset_action_delete_err_read_only": "無法刪除唯讀項目,略過", - "asset_action_share_err_offline": "無法獲取離線項目,略過", - "asset_added_to_album": "已加入相簿", - "asset_adding_to_album": "加入相簿中…", - "asset_description_updated": "檔案描述已更新", - "asset_filename_is_offline": "檔案 {filename} 離線了", - "asset_has_unassigned_faces": "檔案中有未指定的臉孔", + "are_these_the_same_person": "同一位人物?", + "are_you_sure_to_do_this": "您確定嗎?", + "asset_action_delete_err_read_only": "略過無法刪除唯讀項目", + "asset_action_share_err_offline": "略過無法取得的離線項目", + "asset_added_to_album": "已建立相簿", + "asset_adding_to_album": "新增到相簿…", + "asset_description_updated": "項目說明已更新", + "asset_filename_is_offline": "項目 {filename} 已離線", + "asset_has_unassigned_faces": "項目有未新增臉孔", "asset_hashing": "計算雜湊值…", - "asset_list_group_by_sub_title": "分組方式", - "asset_list_layout_settings_dynamic_layout_title": "動態佈局", + "asset_list_group_by_sub_title": "分類方式", + "asset_list_layout_settings_dynamic_layout_title": "動態排版", "asset_list_layout_settings_group_automatically": "自動", - "asset_list_layout_settings_group_by": "項目分組方式", - "asset_list_layout_settings_group_by_month_day": "月和日", - "asset_list_layout_sub_title": "佈局", - "asset_list_settings_subtitle": "照片網格佈局設定", - "asset_list_settings_title": "照片網格", - "asset_offline": "檔案離線", - "asset_offline_description": "磁碟中找不到此外部檔案。請向您的 Immich 管理員尋求協助。", - "asset_restored_successfully": "已成功恢復所有項目", - "asset_skipped": "已略過", - "asset_skipped_in_trash": "已丟掉", + "asset_list_layout_settings_group_by": "項目分類方式", + "asset_list_layout_settings_group_by_month_day": "月份和日期", + "asset_list_layout_sub_title": "排版", + "asset_list_settings_subtitle": "照片排版設定", + "asset_list_settings_title": "照片排列", + "asset_offline": "項目離線", + "asset_offline_description": "磁碟中找不到此項目。請向您的 Immich 管理員尋求協助。", + "asset_restored_successfully": "已復原所有項目", + "asset_skipped": "跳過", + "asset_skipped_in_trash": "移至垃圾桶", "asset_uploaded": "已上傳", "asset_uploading": "上傳中…", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", - "asset_viewer_settings_title": "資源查看器", - "assets": "檔案", + "asset_viewer_settings_subtitle": "管理相簿瀏覽設定", + "asset_viewer_settings_title": "項目瀏覽", + "assets": "項目", "assets_added_count": "已新增 {count, plural, one {# 個項目} other {# 個項目}}", - "assets_added_to_album_count": "已將 {count, plural, other {# 個檔案}}加入相簿", - "assets_added_to_name_count": "已將 {count, plural, other {# 個檔案}}加入{hasName, select, true {{name}} other {新相簿}}", - "assets_count": "{count, plural, one {# 個檔案} other {# 個檔案}}", - "assets_deleted_permanently": "{} 個項目已被永久刪除", - "assets_deleted_permanently_from_server": "已從伺服器中永久移除 {} 個項目", - "assets_moved_to_trash_count": "已將 {count, plural, other {# 個檔案}}丟進垃圾桶", - "assets_permanently_deleted_count": "已永久刪除 {count, plural, one {# 個檔案} other {# 個檔案}}", - "assets_removed_count": "已移除 {count, plural, one {# 個檔案} other {# 個檔案}}", - "assets_removed_permanently_from_device": "已從裝置中永久移除 {} 個項目", - "assets_restore_confirmation": "確定要還原所有丟掉的檔案嗎?此步驟無法取消喔!註:這無法還原任何離線檔案。", - "assets_restored_count": "已還原 {count, plural, other {# 個檔案}}", - "assets_restored_successfully": "已成功恢復 {} 個項目", - "assets_trashed": "{} 個回收桶項目", - "assets_trashed_count": "已丟掉 {count, plural, other {# 個檔案}}", - "assets_trashed_from_server": "{} 個項目已放入回收桶", - "assets_were_part_of_album_count": "{count, plural, one {檔案已} other {檔案已}} 是相簿的一部分", + "assets_added_to_album_count": "已將 {count, plural, other {# 個項目}}加入相簿", + "assets_added_to_name_count": "已將 {count, plural, other {# 個項目}}加入{hasName, select, true {{name}} other {新相簿}}", + "assets_count": "{count, plural, one {# 個項目} other {# 個項目}}", + "assets_deleted_permanently": "{count} 個項目已被永久刪除", + "assets_deleted_permanently_from_server": "已從伺服器中永久移除 {count} 個項目", + "assets_moved_to_trash_count": "已將 {count, plural, other {# 個項目}}丟進垃圾桶", + "assets_permanently_deleted_count": "永久刪除 {count, plural, one {# 個項目} other {# 個項目}}", + "assets_removed_count": "移除 {count, plural, one {# 個項目} other {# 個項目}}", + "assets_removed_permanently_from_device": "從裝置中永久移除 {count} 個項目", + "assets_restore_confirmation": "確定要還原所有捨棄項目嗎?此步驟無法還原!(*註:這無法還原任何離線項目)", + "assets_restored_count": "已還原 {count, plural, other {# 個項目}}", + "assets_restored_successfully": "成功復原 {count} 個項目", + "assets_trashed": "捨棄 {count} 個項目", + "assets_trashed_count": "捨棄 {count, plural, other {# 個項目}}", + "assets_trashed_from_server": "{count} 個項目移至垃圾桶", + "assets_were_part_of_album_count": "{count, plural, one {項目已} other {項目已}} 已在相簿", "authorized_devices": "授權裝置", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", + "automatic_endpoint_switching_subtitle": "優先使用 Wi-Fi 連線,其他狀況使用其他連線方式", + "automatic_endpoint_switching_title": "自動切換連結", "back": "返回", "back_close_deselect": "返回、關閉及取消選取", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", - "backup_album_selection_page_albums_device": "裝置上的相簿( {} )", - "backup_album_selection_page_albums_tap": "單擊選中,雙擊取消", - "backup_album_selection_page_assets_scatter": "項目會分散在多個相簿中。因此,可以在備份過程中包含或排除相簿。", + "background_location_permission": "背景定位權限", + "background_location_permission_content": "開啟背景執行時自動切換網路,請充許 Immich 一律充許使用精確位置權限,以確認 Wi-Fi 網路名稱", + "backup_album_selection_page_albums_device": "裝置上的相簿({count})", + "backup_album_selection_page_albums_tap": "點擊選取,連續點擊兩次取消", + "backup_album_selection_page_assets_scatter": "項目會分散在不同相簿。因此,可以設定要備份的相簿。", "backup_album_selection_page_select_albums": "選擇相簿", "backup_album_selection_page_selection_info": "選擇資訊", - "backup_album_selection_page_total_assets": "總計", + "backup_album_selection_page_total_assets": "總計項目", "backup_all": "全部", - "backup_background_service_backup_failed_message": "備份失敗,正在重試…", - "backup_background_service_connection_failed_message": "連接伺服器失敗,正在重試…", - "backup_background_service_current_upload_notification": "正在上傳 {}", + "backup_background_service_backup_failed_message": "備份失敗,重新備份中…", + "backup_background_service_connection_failed_message": "無法連線伺服器,重新連線中…", + "backup_background_service_current_upload_notification": "正在上傳 {filename}", "backup_background_service_default_notification": "正在檢查新項目…", - "backup_background_service_error_title": "備份失敗", + "backup_background_service_error_title": "備份錯誤", "backup_background_service_in_progress_notification": "正在備份…", - "backup_background_service_upload_failure_notification": "上傳失敗 {}", + "backup_background_service_upload_failure_notification": "無法上傳 {filename}", "backup_controller_page_albums": "備份相簿", - "backup_controller_page_background_app_refresh_disabled_content": "要使用背景備份功能,請在「設定」>「備份」>「背景套用更新」中啓用背本程式更新。", - "backup_controller_page_background_app_refresh_disabled_title": "背景套用更新已禁用", + "backup_controller_page_background_app_refresh_disabled_content": "開啟應用程式背景自動重新整理,請在「設定>備份>背景重新整理」開啟背景重新整理。", + "backup_controller_page_background_app_refresh_disabled_title": "關閉應用程式背景重新整理", "backup_controller_page_background_app_refresh_enable_button_text": "前往設定", "backup_controller_page_background_battery_info_link": "怎麼做", "backup_controller_page_background_battery_info_message": "為了獲得最佳的背景備份體驗,請禁用會任何限制 Immich 背景活動的電池優化。\n\n由於這是裝置相關的,因此請查找裝置製造商提供的資訊進行操作。", "backup_controller_page_background_battery_info_ok": "我知道了", "backup_controller_page_background_battery_info_title": "電池最佳化", "backup_controller_page_background_charging": "僅在充電時", - "backup_controller_page_background_configure_error": "設定背景服務失敗", - "backup_controller_page_background_delay": "延遲 {} 後備份", + "backup_controller_page_background_configure_error": "設定背景失敗", + "backup_controller_page_background_delay": "延遲 {duration} 後備份", "backup_controller_page_background_description": "打開背景服務以自動備份任何新項目,且無需打開套用", "backup_controller_page_background_is_off": "背景自動備份已關閉", "backup_controller_page_background_is_on": "背景自動備份已開啓", "backup_controller_page_background_turn_off": "關閉背景服務", "backup_controller_page_background_turn_on": "開啓背景服務", - "backup_controller_page_background_wifi": "僅使用 WiFi", + "backup_controller_page_background_wifi": "僅使用 Wi-Fi", "backup_controller_page_backup": "備份", - "backup_controller_page_backup_selected": "已選中:", + "backup_controller_page_backup_selected": "已選中: ", "backup_controller_page_backup_sub": "已備份的照片和短片", - "backup_controller_page_created": "新增時間: {}", + "backup_controller_page_created": "新增時間: {date}", "backup_controller_page_desc_backup": "打開前台備份,以本程式運行時自動備份新項目。", - "backup_controller_page_excluded": "已排除:", - "backup_controller_page_failed": "失敗( {} )", - "backup_controller_page_filename": "文件名稱: {} [ {} ]", - "backup_controller_page_id": "ID: {}", + "backup_controller_page_excluded": "已排除: ", + "backup_controller_page_failed": "失敗({count})", + "backup_controller_page_filename": "文件名稱: {filename} [{size}]", + "backup_controller_page_id": "ID: {id}", "backup_controller_page_info": "備份資訊", "backup_controller_page_none_selected": "未選擇", "backup_controller_page_remainder": "剩餘", @@ -538,7 +546,7 @@ "backup_controller_page_start_backup": "開始備份", "backup_controller_page_status_off": "前台自動備份已關閉", "backup_controller_page_status_on": "前台自動備份已開啓", - "backup_controller_page_storage_format": "{} / {} 已使用", + "backup_controller_page_storage_format": "{used} / {total} 已使用", "backup_controller_page_to_backup": "要備份的相簿", "backup_controller_page_total_sub": "選中相簿中所有不重複的短片和圖片", "backup_controller_page_turn_off": "關閉前台備份", @@ -551,7 +559,7 @@ "backup_manual_success": "成功", "backup_manual_title": "上傳狀態", "backup_options_page_title": "備份選項", - "backup_setting_subtitle": "Manage background and foreground upload settings", + "backup_setting_subtitle": "管理後台與前台上傳設定", "backward": "倒轉", "birthdate_saved": "出生日期儲存成功", "birthdate_set_description": "出生日期會用來計算此人拍照時的歲數。", @@ -563,21 +571,21 @@ "bulk_keep_duplicates_confirmation": "您確定要保留 {count, plural, one {# 個重複檔案} other {# 個重複檔案}} 嗎?這將解決所有重複組而不刪除任何內容。", "bulk_trash_duplicates_confirmation": "確定要一次丟掉 {count, plural, other {# 個重複的檔案}}嗎?這樣每組重複的檔案中,最大的會留下來,其它的會被丟進垃圾桶。", "buy": "購置 Immich", - "cache_settings_album_thumbnails": "圖庫縮圖( {} 項)", + "cache_settings_album_thumbnails": "圖庫縮圖({count} 項)", "cache_settings_clear_cache_button": "清除緩存", "cache_settings_clear_cache_button_title": "清除套用緩存。在重新生成緩存之前,將顯著影響套用的性能。", "cache_settings_duplicated_assets_clear_button": "清除", "cache_settings_duplicated_assets_subtitle": "已加入黑名單的照片和短片", - "cache_settings_duplicated_assets_title": "重複項目( {} )", - "cache_settings_image_cache_size": "圖片緩存大小( {} 項)", + "cache_settings_duplicated_assets_title": "重複項目({count})", + "cache_settings_image_cache_size": "圖片快取大小({count} 項)", "cache_settings_statistics_album": "圖庫縮圖", - "cache_settings_statistics_assets": "{} 項( {} )", + "cache_settings_statistics_assets": "{count} 項 ({size})", "cache_settings_statistics_full": "完整圖片", "cache_settings_statistics_shared": "共享相簿縮圖", "cache_settings_statistics_thumbnail": "縮圖", "cache_settings_statistics_title": "緩存使用情況", "cache_settings_subtitle": "控制 Immich app 的緩存行為", - "cache_settings_thumbnail_size": "縮圖緩存大小( {} 項)", + "cache_settings_thumbnail_size": "縮圖快取大小({count} 項)", "cache_settings_tile_subtitle": "設定本地存儲行為", "cache_settings_tile_title": "本地存儲", "cache_settings_title": "緩存設定", @@ -586,12 +594,12 @@ "camera_model": "相機型號", "cancel": "取消", "cancel_search": "取消搜尋", - "canceled": "Canceled", + "canceled": "已取消", "cannot_merge_people": "無法合併人物", "cannot_undo_this_action": "此步驟無法取消喔!", "cannot_update_the_description": "無法更新描述", "change_date": "更改日期", - "change_display_order": "Change display order", + "change_display_order": "更換顯示順序", "change_expiration_time": "更改失效期限", "change_location": "更改位置", "change_name": "改名", @@ -599,16 +607,17 @@ "change_password": "更改密碼", "change_password_description": "這是您第一次登入系統,或您被要求更改密碼。請在下面輸入新密碼。", "change_password_form_confirm_password": "確認密碼", - "change_password_form_description": "您好 {name} :\n\n這是您首次登入系統,或被管理員要求更改密碼。\n請在下方輸入新密碼。", + "change_password_form_description": "您好 {name} :\n\n這是您首次登入系統,或被管理員要求更改密碼。請在下方輸入新密碼。", "change_password_form_new_password": "新密碼", "change_password_form_password_mismatch": "密碼不一致", "change_password_form_reenter_new_password": "再次輸入新密碼", + "change_pin_code": "更改PIN碼", "change_your_password": "更改您的密碼", "changed_visibility_successfully": "已成功更改可見性", "check_all": "全選", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", + "check_corrupt_asset_backup": "檢查損毀的備份項目", + "check_corrupt_asset_backup_button": "執行檢查", + "check_corrupt_asset_backup_description": "僅在已備份所有項目且連接 Wi-Fi 時執行此檢查。此程序可能需要幾分鐘。", "check_logs": "檢查日誌", "choose_matching_people_to_merge": "選擇要合併的匹配人物", "city": "城市", @@ -637,23 +646,24 @@ "comments_are_disabled": "留言已停用", "common_create_new_album": "新增相簿", "common_server_error": "請檢查您的網絡連接,確保伺服器可連接,且本程式與伺服器版本兼容。", - "completed": "Completed", + "completed": "已完成", "confirm": "確認", "confirm_admin_password": "確認管理者密碼", "confirm_delete_face": "您確定要從項目中刪除 {name} 的臉孔嗎?", "confirm_delete_shared_link": "確定刪除連結嗎?", "confirm_keep_this_delete_others": "所有的其他堆疊項目將被刪除。確定繼續嗎?", + "confirm_new_pin_code": "確認新 PIN 碼", "confirm_password": "確認密碼", "contain": "包含", "context": "情境", "continue": "繼續", - "control_bottom_app_bar_album_info_shared": "{} 項 · 已共享", + "control_bottom_app_bar_album_info_shared": "{count} 項 · 已共享", "control_bottom_app_bar_create_new_album": "新增相簿", "control_bottom_app_bar_delete_from_immich": "從Immich伺服器中刪除", "control_bottom_app_bar_delete_from_local": "從移動裝置中刪除", "control_bottom_app_bar_edit_location": "編輯位置資訊", "control_bottom_app_bar_edit_time": "編輯日期和時間", - "control_bottom_app_bar_share_link": "Share Link", + "control_bottom_app_bar_share_link": "分享連結", "control_bottom_app_bar_share_to": "發送給", "control_bottom_app_bar_trash_from_immich": "放入回收桶", "copied_image_to_clipboard": "圖片已複製到剪貼簿。", @@ -688,7 +698,8 @@ "crop": "裁剪", "curated_object_page_title": "事物", "current_device": "此裝置", - "current_server_address": "Current server address", + "current_pin_code": "當前 PIN 碼", + "current_server_address": "目前的伺服器位址", "custom_locale": "自訂區域", "custom_locale_description": "依語言和區域設定日期和數字格式", "daily_title_text_date": "E, MMM dd", @@ -739,7 +750,7 @@ "direction": "方向", "disabled": "停用", "disallow_edits": "不允許編輯", - "discord": "Discord", + "discord": "Discord 社群", "discover": "探索", "dismiss_all_errors": "忽略所有錯誤", "dismiss_error": "忽略錯誤", @@ -756,7 +767,7 @@ "download_enqueue": "已加入下載隊列", "download_error": "下載出錯", "download_failed": "下載失敗", - "download_filename": "文件: {}", + "download_filename": "文件: {filename}", "download_finished": "下載完成", "download_include_embedded_motion_videos": "嵌入影片", "download_include_embedded_motion_videos_description": "把嵌入動態照片的影片作為單獨的檔案包含在內", @@ -800,19 +811,20 @@ "editor_crop_tool_h2_aspect_ratios": "長寬比", "editor_crop_tool_h2_rotation": "旋轉", "email": "電子郵件", - "empty_folder": "This folder is empty", + "email_notifications": "Email 通知", + "empty_folder": "此資料夾為空", "empty_trash": "清空垃圾桶", "empty_trash_confirmation": "確定要清空垃圾桶嗎?這會永久刪除 Immich 垃圾桶中所有的檔案。\n此步驟無法取消喔!", "enable": "啟用", "enabled": "己啟用", "end_date": "結束日期", - "enqueued": "Enqueued", - "enter_wifi_name": "Enter WiFi name", + "enqueued": "排入佇列中", + "enter_wifi_name": "輸入 Wi-Fi 名稱", "error": "錯誤", - "error_change_sort_album": "Failed to change album sort order", + "error_change_sort_album": "無法改變相簿排序", "error_delete_face": "從項目中刪除臉孔時發生錯誤", "error_loading_image": "載入圖片時出錯", - "error_saving_image": "錯誤: {}", + "error_saving_image": "錯誤: {error}", "error_title": "錯誤 - 出問題了", "errors": { "cannot_navigate_next_asset": "無法瀏覽下一個檔案", @@ -834,7 +846,7 @@ "error_removing_assets_from_album": "從相簿中移除檔案時出錯了,請到控制臺瞭解詳細資訊", "error_selecting_all_assets": "選擇所有檔案時出錯", "exclusion_pattern_already_exists": "此排除模式已存在。", - "failed_job_command": "命令 {command} 執行失敗,作業:{job}", + "failed_job_command": "執行 {command} 命令任務錯誤: {job}", "failed_to_create_album": "相簿建立失敗", "failed_to_create_shared_link": "建立共享連結失敗", "failed_to_edit_shared_link": "編輯共享連結失敗", @@ -842,10 +854,12 @@ "failed_to_keep_this_delete_others": "無法保留此項目並刪除其他項目", "failed_to_load_asset": "檔案載入失敗", "failed_to_load_assets": "檔案載入失敗", + "failed_to_load_notifications": "無法載入通知", "failed_to_load_people": "無法載入人物", "failed_to_remove_product_key": "無法移除產品密鑰", "failed_to_stack_assets": "無法堆疊檔案", "failed_to_unstack_assets": "無法解除堆疊檔案", + "failed_to_update_notification_status": "無法更新通知狀態", "import_path_already_exists": "此匯入路徑已存在。", "incorrect_email_or_password": "電子郵件或密碼有誤", "paths_validation_failed": "{paths, plural, one {# 個路徑} other {# 個路徑}} 驗證失敗", @@ -857,7 +871,7 @@ "unable_to_add_comment": "無法新增留言", "unable_to_add_exclusion_pattern": "無法添加排除模式", "unable_to_add_import_path": "無法添加匯入路徑", - "unable_to_add_partners": "無法添加夥伴", + "unable_to_add_partners": "無法添加親朋好友", "unable_to_add_remove_archive": "無法{archived, select, true {從封存中移除檔案} other {將檔案加入封存}}", "unable_to_add_remove_favorites": "無法將檔案{favorite, select, true {加入收藏} other {從收藏中移除}}", "unable_to_archive_unarchive": "無法{archived, select, true {封存} other {取消封存}}", @@ -909,10 +923,11 @@ "unable_to_remove_assets_from_shared_link": "刪除共享連結中檔案失敗", "unable_to_remove_deleted_assets": "無法移除離線檔案", "unable_to_remove_library": "無法移除資料庫", - "unable_to_remove_partner": "無法移除夥伴", + "unable_to_remove_partner": "無法移除親朋好友", "unable_to_remove_reaction": "無法移除反應", "unable_to_repair_items": "無法糾正項目", "unable_to_reset_password": "無法重設密碼", + "unable_to_reset_pin_code": "無法重置 PIN 碼", "unable_to_resolve_duplicate": "無法解決重複項", "unable_to_restore_assets": "無法還原檔案", "unable_to_restore_trash": "無法還原垃圾桶中的項目", @@ -946,10 +961,10 @@ "exif_bottom_sheet_location": "位置", "exif_bottom_sheet_people": "人物", "exif_bottom_sheet_person_add_person": "新增姓名", - "exif_bottom_sheet_person_age": "Age {}", - "exif_bottom_sheet_person_age_months": "Age {} months", - "exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months", - "exif_bottom_sheet_person_age_years": "Age {}", + "exif_bottom_sheet_person_age": "年齡 {age}", + "exif_bottom_sheet_person_age_months": "年齡 {months} 月", + "exif_bottom_sheet_person_age_year_months": "1 歲 {months} 個月", + "exif_bottom_sheet_person_age_years": "{years} 歲", "exit_slideshow": "退出幻燈片", "expand_all": "展開全部", "experimental_settings_new_asset_list_subtitle": "正在處理", @@ -966,12 +981,12 @@ "extension": "副檔名", "external": "外部", "external_libraries": "外部圖庫", - "external_network": "External network", - "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", + "external_network": "外部網路", + "external_network_sheet_info": "若無法使用偏好的 Wi-Fi,將依列表從上到下選擇可連線的伺服器網址", "face_unassigned": "未指定", - "failed": "Failed", + "failed": "失敗", "failed_to_load_assets": "無法加載檔案", - "failed_to_load_folder": "Failed to load folder", + "failed_to_load_folder": "無法載入資料夾", "favorite": "收藏", "favorite_or_unfavorite_photo": "收藏或取消收藏照片", "favorites": "收藏", @@ -985,21 +1000,22 @@ "filetype": "檔案類型", "filter": "篩選", "filter_people": "篩選人物", + "filter_places": "篩選地點", "find_them_fast": "搜尋名稱,快速找人", "fix_incorrect_match": "修復不相符的", - "folder": "Folder", - "folder_not_found": "Folder not found", + "folder": "資料夾", + "folder_not_found": "未找到資料夾", "folders": "資料夾", "folders_feature_description": "以資料夾瀏覽檔案系統中的照片和影片", "forward": "順序", "general": "一般", "get_help": "線上求助", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", + "get_wifiname_error": "無法取得 Wi-Fi 名稱。請確認您已授予必要的權限,並已連接至 Wi-Fi 網路", "getting_started": "開始使用", "go_back": "返回", "go_to_folder": "轉至資料夾", "go_to_search": "前往搜尋", - "grant_permission": "Grant permission", + "grant_permission": "獲得權限", "group_albums_by": "分類群組的方式...", "group_country": "按國家分組", "group_no": "無分組", @@ -1022,22 +1038,23 @@ "hide_password": "隱藏密碼", "hide_person": "隱藏人物", "hide_unnamed_people": "隱藏未命名人物", - "home_page_add_to_album_conflicts": "已在相簿 {album} 中新增 {added} 項。\n其中 {failed} 項在相簿中已存在。", + "home_page_add_to_album_conflicts": "已在相簿 {album} 中新增 {added} 項。其中 {failed} 項在相簿中已存在。", "home_page_add_to_album_err_local": "暫不能將本地項目新增到相簿中,略過", - "home_page_add_to_album_success": "已在相簿 {album} 中新增 {added} 項。", - "home_page_album_err_partner": "暫無法將同伴的項目新增到相簿,略過", - "home_page_archive_err_local": "暫無法歸檔本地項目,略過", - "home_page_archive_err_partner": "無法存檔同伴的項目,略過", + "home_page_add_to_album_success": "已在相簿 {album} 中新增 {added} 項。", + "home_page_album_err_partner": "暫無法將親朋好友的項目新增到相簿,略過", + "home_page_archive_err_local": "暫無法封存本地項目,略過", + "home_page_archive_err_partner": "無法封存親朋好友的項目,略過", "home_page_building_timeline": "正在生成時間線", - "home_page_delete_err_partner": "無法刪除同伴的項目,略過", + "home_page_delete_err_partner": "無法刪除親朋好友的項目,略過", "home_page_delete_remote_err_local": "遙距項目刪除模式,略過本地項目", "home_page_favorite_err_local": "暫不能收藏本地項目,略過", - "home_page_favorite_err_partner": "暫無法收藏同伴的項目,略過", - "home_page_first_time_notice": "如果這是您第一次使用本程式,請確保選擇一個要備份的本地相簿,以便可以在時間線中預覽該相簿中的照片和短片。", + "home_page_favorite_err_partner": "暫無法收藏親朋好友的項目,略過", + "home_page_first_time_notice": "如果這是您第一次使用本程式,請確保選擇一個要備份的本地相簿,以便可以在時間線中預覽該相簿中的照片和短片", "home_page_share_err_local": "暫無法通過鏈接共享本地項目,略過", "home_page_upload_err_limit": "一次最多只能上傳 30 個項目,略過", "host": "主機", "hour": "時", + "id": "ID", "ignore_icloud_photos": "忽略iCloud照片", "ignore_icloud_photos_description": "存儲在iCloud中的照片不會上傳至Immich伺服器", "image": "圖片", @@ -1063,7 +1080,7 @@ "in_archive": "已封存", "include_archived": "包含已封存", "include_shared_albums": "包含共享相簿", - "include_shared_partner_assets": "包括共享夥伴檔案", + "include_shared_partner_assets": "包括共享親朋好友檔案", "individual_share": "個別分享", "individual_shares": "個別分享", "info": "資訊", @@ -1111,9 +1128,9 @@ "loading": "載入中", "loading_search_results_failed": "載入搜尋結果失敗", "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", - "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", + "local_network_sheet_info": "當使用指定的 Wi-Fi 網路時,應用程式將透過此連結連線至伺服器", + "location_permission": "定位權限", + "location_permission_content": "為了使用自動切換功能,Immich 需要精確的定位權限,以便讀取目前所連接的 Wi-Fi 網路名稱", "location_picker_choose_on_map": "在地圖上選擇", "location_picker_latitude_error": "輸入有效的緯度值", "location_picker_latitude_hint": "請在此處輸入您的緯度值", @@ -1141,7 +1158,7 @@ "login_form_handshake_exception": "與伺服器通信時出現握手異常。如果您使用的是自簽名證書,請在設定中啓用自簽名證書支持。", "login_form_password_hint": "密碼", "login_form_save_login": "保持登入", - "login_form_server_empty": "輸入伺服器地址", + "login_form_server_empty": "輸入伺服器連結。", "login_form_server_error": "無法連接到伺服器。", "login_has_been_disabled": "已停用登入功能。", "login_password_changed_error": "密碼更新失敗", @@ -1156,15 +1173,15 @@ "main_menu": "主頁面", "make": "製造商", "manage_shared_links": "管理共享連結", - "manage_sharing_with_partners": "管理與夥伴的分享", + "manage_sharing_with_partners": "管理與親朋好友的分享", "manage_the_app_settings": "管理應用程式設定", "manage_your_account": "管理您的帳號", "manage_your_api_keys": "管理您的 API 金鑰", "manage_your_devices": "管理已登入的裝置", "manage_your_oauth_connection": "管理您的 OAuth 連接", "map": "地圖", - "map_assets_in_bound": "{} 張照片", - "map_assets_in_bounds": "{} 張照片", + "map_assets_in_bound": "{count} 張照片", + "map_assets_in_bounds": "{count} 張照片", "map_cannot_get_user_location": "無法獲取用戶位置", "map_location_dialog_yes": "確定", "map_location_picker_page_use_location": "使用此位置", @@ -1178,15 +1195,18 @@ "map_settings": "地圖設定", "map_settings_dark_mode": "深色模式", "map_settings_date_range_option_day": "過去24小時", - "map_settings_date_range_option_days": "{} 天前", + "map_settings_date_range_option_days": "{days} 天前", "map_settings_date_range_option_year": "1年前", - "map_settings_date_range_option_years": "{} 年前", + "map_settings_date_range_option_years": "{years} 年前", "map_settings_dialog_title": "地圖設定", - "map_settings_include_show_archived": "包括已歸檔項目", - "map_settings_include_show_partners": "包含夥伴", + "map_settings_include_show_archived": "包括已封存項目", + "map_settings_include_show_partners": "包含親朋好友", "map_settings_only_show_favorites": "僅顯示收藏的項目", "map_settings_theme_settings": "地圖主題", "map_zoom_to_see_photos": "縮小以查看項目", + "mark_all_as_read": "全部標記為已讀", + "mark_as_read": "標記為已讀", + "marked_all_as_read": "已全部標記為已讀", "matches": "相符", "media_type": "媒體類型", "memories": "回憶", @@ -1196,7 +1216,7 @@ "memories_start_over": "再看一次", "memories_swipe_to_close": "上滑關閉", "memories_year_ago": "1年前", - "memories_years_ago": "{} 年前", + "memories_years_ago": "{years} 年前", "memory": "回憶", "memory_lane_title": "回憶長廊{title}", "menu": "選單", @@ -1213,6 +1233,8 @@ "month": "月", "monthly_title_text_date_format": "MMMM y", "more": "更多", + "moved_to_archive": "已封存 {count, plural, one {# 個項目} other {# 個項目}}", + "moved_to_library": "已移動 {count, plural, one {# 個項目} other {# 個項目}} 至相簿", "moved_to_trash": "已丟進垃圾桶", "multiselect_grid_edit_date_time_err_read_only": "無法編輯唯讀項目的日期,略過", "multiselect_grid_edit_gps_err_read_only": "無法編輯唯讀項目的位置資訊,略過", @@ -1220,13 +1242,14 @@ "my_albums": "我的相簿", "name": "名稱", "name_or_nickname": "名稱或暱稱", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", + "networking_settings": "網路", + "networking_subtitle": "管理伺服器端點設定", "never": "永不失效", "new_album": "新相簿", "new_api_key": "新的 API 金鑰", "new_password": "新密碼", "new_person": "新的人物", + "new_pin_code": "新 PIN 碼", "new_user_created": "已建立新使用者", "new_version_available": "新版本已發布", "newest_first": "最新優先", @@ -1245,13 +1268,15 @@ "no_favorites_message": "加入收藏,加速尋找影像", "no_libraries_message": "建立外部圖庫來查看您的照片和影片", "no_name": "無名", + "no_notifications": "沒有通知", + "no_people_found": "找不到符合的人物", "no_places": "沒有地點", "no_results": "沒有結果", "no_results_description": "試試同義詞或更通用的關鍵字吧", "no_shared_albums_message": "建立相簿分享照片和影片", "not_in_any_album": "不在任何相簿中", - "not_selected": "Not selected", - "note_apply_storage_label_to_previously_uploaded assets": "註:要將儲存標籤用於先前上傳的檔案,請執行", + "not_selected": "未選擇", + "note_apply_storage_label_to_previously_uploaded assets": "*註:執行套用儲存標籤前先上傳項目", "notes": "提示", "notification_permission_dialog_content": "要啓用通知,請前往「設定」,並選擇「允許」。", "notification_permission_list_tile_content": "授予通知權限。", @@ -1275,6 +1300,7 @@ "onboarding_welcome_user": "歡迎,{user}", "online": "在線", "only_favorites": "僅顯示己收藏", + "open": "開啟", "open_in_map_view": "開啟地圖檢視", "open_in_openstreetmap": "用 OpenStreetMap 開啟", "open_the_search_filters": "開啟搜尋篩選器", @@ -1287,20 +1313,20 @@ "other_variables": "其他變數", "owned": "我的", "owner": "所有者", - "partner": "同伴", + "partner": "親朋好友", "partner_can_access": "{partner} 可以存取", "partner_can_access_assets": "除了已封存和已刪除之外,您所有的照片和影片", "partner_can_access_location": "您照片拍攝的位置", "partner_list_user_photos": "{user} 的照片", "partner_list_view_all": "展示全部", - "partner_page_empty_message": "您的照片尚未與任何同伴共享。", + "partner_page_empty_message": "您的照片尚未與任何親朋好友共享。", "partner_page_no_more_users": "無需新增更多用戶", - "partner_page_partner_add_failed": "新增同伴失敗", - "partner_page_select_partner": "選擇同伴", + "partner_page_partner_add_failed": "新增親朋好友失敗", + "partner_page_select_partner": "選擇親朋好友", "partner_page_shared_to_title": "共享給", - "partner_page_stop_sharing_content": "{} 將無法再存取您的照片。", - "partner_sharing": "夥伴分享", - "partners": "夥伴", + "partner_page_stop_sharing_content": "{partner} 將無法再存取您的照片。", + "partner_sharing": "親朋好友分享", + "partners": "親朋好友", "password": "密碼", "password_does_not_match": "密碼不相符", "password_required": "需要密碼", @@ -1344,6 +1370,9 @@ "photos_count": "{count, plural, other {{count, number} 張照片}}", "photos_from_previous_years": "往年的照片", "pick_a_location": "選擇位置", + "pin_code_changed_successfully": "變更 PIN 碼成功", + "pin_code_reset_successfully": "重置 PIN 碼成功", + "pin_code_setup_successfully": "設定 PIN 碼成功", "place": "地點", "places": "地點", "places_count": "{count, plural, one {{count, number} 個地點} other {{count, number} 個地點}}", @@ -1352,7 +1381,7 @@ "play_motion_photo": "播放動態照片", "play_or_pause_video": "播放或暫停影片", "port": "埠口", - "preferences_settings_subtitle": "Manage the app's preferences", + "preferences_settings_subtitle": "管理應用程式偏好設定", "preferences_settings_title": "偏好設定", "preset": "預設", "preview": "預覽", @@ -1374,7 +1403,7 @@ "public_share": "公開分享", "purchase_account_info": "擁護者", "purchase_activated_subtitle": "感謝您對 Immich 及開源軟體的支援", - "purchase_activated_time": "於 {date, date} 啟用", + "purchase_activated_time": "於 {date} 啟用", "purchase_activated_title": "金鑰成功啟用了", "purchase_button_activate": "啟用", "purchase_button_buy": "購置", @@ -1419,6 +1448,8 @@ "recent_searches": "最近搜尋項目", "recently_added": "近期新增", "recently_added_page_title": "最近新增", + "recently_taken": "最近拍攝", + "recently_taken_page_title": "最近拍攝", "refresh": "重新整理", "refresh_encoded_videos": "重新整理已編碼的影片", "refresh_faces": "重整面部資料", @@ -1461,6 +1492,7 @@ "reset": "重設", "reset_password": "重設密碼", "reset_people_visibility": "重設人物可見性", + "reset_pin_code": "重置 PIN 碼", "reset_to_default": "重設回預設", "resolve_duplicates": "解決重複項", "resolved_all_duplicates": "已解決所有重複項目", @@ -1503,7 +1535,7 @@ "search_filter_date_title": "選擇日期範圍", "search_filter_display_option_not_in_album": "不在相簿中", "search_filter_display_options": "顯示選項", - "search_filter_filename": "Search by file name", + "search_filter_filename": "依檔案名稱搜尋", "search_filter_location": "位置", "search_filter_location_title": "選擇位置", "search_filter_media_type": "媒體類型", @@ -1511,17 +1543,17 @@ "search_filter_people_title": "選擇人物", "search_for": "搜尋", "search_for_existing_person": "搜尋現有的人物", - "search_no_more_result": "No more results", + "search_no_more_result": "無更多結果", "search_no_people": "沒有人找到", "search_no_people_named": "沒有名為「{name}」的人物", - "search_no_result": "No results found, try a different search term or combination", + "search_no_result": "找不到結果,請嘗試其他搜尋字詞或組合", "search_options": "搜尋選項", "search_page_categories": "類別", - "search_page_motion_photos": "動態照片\n", + "search_page_motion_photos": "動態照片", "search_page_no_objects": "找不到物件資訊", "search_page_no_places": "找不到地點資訊", "search_page_screenshots": "屏幕截圖", - "search_page_search_photos_videos": "Search for your photos and videos", + "search_page_search_photos_videos": "搜尋您的照片和影片", "search_page_selfies": "自拍", "search_page_things": "事物", "search_page_view_all_button": "查看全部", @@ -1533,7 +1565,7 @@ "search_result_page_new_search_hint": "搜尋新的", "search_settings": "搜尋設定", "search_state": "搜尋地區…", - "search_suggestion_list_smart_search_hint_1": "默認情況下啓用智能搜尋,要搜尋中繼數據,請使用相關語法", + "search_suggestion_list_smart_search_hint_1": "預設情況下啟用智慧搜尋,要搜尋中繼數據,請使用相關語法 ", "search_suggestion_list_smart_search_hint_2": "m:您的搜尋關鍵詞", "search_tags": "搜尋標籤...", "search_timezone": "搜尋時區…", @@ -1553,6 +1585,7 @@ "select_keep_all": "全部保留", "select_library_owner": "選擇圖庫擁有者", "select_new_face": "選擇新臉孔", + "select_person_to_tag": "選擇要標記的人物", "select_photos": "選照片", "select_trash_all": "全部刪除", "select_user_for_sharing_page_err_album": "新增相簿失敗", @@ -1560,7 +1593,7 @@ "selected_count": "{count, plural, other {選了 # 項}}", "send_message": "傳訊息", "send_welcome_email": "傳送歡迎電子郵件", - "server_endpoint": "Server Endpoint", + "server_endpoint": "伺服器端點", "server_info_box_app_version": "App 版本", "server_info_box_server_url": "伺服器地址", "server_offline": "伺服器已離線", @@ -1581,28 +1614,29 @@ "setting_image_viewer_preview_title": "載入預覽圖", "setting_image_viewer_title": "圖片", "setting_languages_apply": "套用", - "setting_languages_subtitle": "Change the app's language", + "setting_languages_subtitle": "變更應用程式語言", "setting_languages_title": "語言", - "setting_notifications_notify_failures_grace_period": "背景備份失敗通知: {}", - "setting_notifications_notify_hours": "{} 小時", + "setting_notifications_notify_failures_grace_period": "背景備份失敗通知:{duration}", + "setting_notifications_notify_hours": "{count} 小時", "setting_notifications_notify_immediately": "立即", - "setting_notifications_notify_minutes": "{} 分鐘", + "setting_notifications_notify_minutes": "{count} 分鐘", "setting_notifications_notify_never": "從不", - "setting_notifications_notify_seconds": "{} 秒", + "setting_notifications_notify_seconds": "{count} 秒", "setting_notifications_single_progress_subtitle": "每項的詳細上傳進度資訊", "setting_notifications_single_progress_title": "顯示背景備份詳細進度", "setting_notifications_subtitle": "調整通知選項", "setting_notifications_total_progress_subtitle": "總體上傳進度(已完成/總計)", "setting_notifications_total_progress_title": "顯示背景備份總進度", "setting_video_viewer_looping_title": "循環播放", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", + "setting_video_viewer_original_video_subtitle": "從伺服器串流影片時,會優先播放原始畫質,即使已有轉檔版本可用。這可能會導致播放時出現緩衝情況。若影片已儲存在本機,則一律以原始畫質播放,與此設定無關。", + "setting_video_viewer_original_video_title": "一律播放原始影片", "settings": "設定", "settings_require_restart": "請重啓 Immich 以使設定生效", "settings_saved": "設定已儲存", + "setup_pin_code": "設定 PIN 碼", "share": "分享", "share_add_photos": "新增項目", - "share_assets_selected": "{} 已選擇", + "share_assets_selected": "{count} 已選擇", "share_dialog_preparing": "正在準備...", "shared": "共享", "shared_album_activities_input_disable": "已禁用評論", @@ -1616,32 +1650,32 @@ "shared_by_user": "由 {user} 分享", "shared_by_you": "由你分享", "shared_from_partner": "來自 {partner} 的照片", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", + "shared_intent_upload_button_progress_text": "{current} / {total} 已上傳", "shared_link_app_bar_title": "共享鏈接", "shared_link_clipboard_copied_massage": "複製到剪貼板", - "shared_link_clipboard_text": "鏈接: {} \n密碼: {}", + "shared_link_clipboard_text": "連結: {link}\n密碼: {password}", "shared_link_create_error": "新增共享鏈接出錯", "shared_link_edit_description_hint": "編輯共享描述", "shared_link_edit_expire_after_option_day": "1天", - "shared_link_edit_expire_after_option_days": "{} 天", + "shared_link_edit_expire_after_option_days": "{count} 天", "shared_link_edit_expire_after_option_hour": "1小時", - "shared_link_edit_expire_after_option_hours": "{} 小時", + "shared_link_edit_expire_after_option_hours": "{count} 小時", "shared_link_edit_expire_after_option_minute": "1分鐘", - "shared_link_edit_expire_after_option_minutes": "{} 分鐘", - "shared_link_edit_expire_after_option_months": "{} 個月", - "shared_link_edit_expire_after_option_year": "{} 年", + "shared_link_edit_expire_after_option_minutes": "{count} 分鐘", + "shared_link_edit_expire_after_option_months": "{count} 個月", + "shared_link_edit_expire_after_option_year": "{count} 年", "shared_link_edit_password_hint": "輸入共享密碼", "shared_link_edit_submit_button": "更新鏈接", "shared_link_error_server_url_fetch": "無法獲取伺服器地址", - "shared_link_expires_day": "{} 天後過期", - "shared_link_expires_days": "{} 天後過期", - "shared_link_expires_hour": "{} 小時後過期", - "shared_link_expires_hours": "{} 小時後過期", - "shared_link_expires_minute": "{} 分鐘後過期", - "shared_link_expires_minutes": "將在 {} 分鐘後過期", + "shared_link_expires_day": "{count} 天後過期", + "shared_link_expires_days": "{count} 天後過期", + "shared_link_expires_hour": "{count} 小時後過期", + "shared_link_expires_hours": "{count} 小時後過期", + "shared_link_expires_minute": "{count} 分鐘後過期", + "shared_link_expires_minutes": "將在 {count} 分鐘後過期", "shared_link_expires_never": "永不過期", - "shared_link_expires_second": "{} 秒後過期", - "shared_link_expires_seconds": "將在 {} 秒後過期", + "shared_link_expires_second": "將在 {count} 秒後過期", + "shared_link_expires_seconds": "將在 {count} 秒後過期", "shared_link_individual_shared": "個人共享", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "管理共享鏈接", @@ -1658,7 +1692,7 @@ "sharing_page_empty_list": "空白清單", "sharing_sidebar_description": "在側邊欄顯示共享連結", "sharing_silver_appbar_create_shared_album": "新增共享相簿", - "sharing_silver_appbar_share_partner": "共享給同伴", + "sharing_silver_appbar_share_partner": "共享給親朋好友", "shift_to_permanent_delete": "按 ⇧ 永久刪除檔案", "show_album_options": "顯示相簿選項", "show_albums": "顯示相簿", @@ -1716,6 +1750,7 @@ "stop_sharing_photos_with_user": "停止與此用戶共享你的照片", "storage": "儲存空間", "storage_label": "儲存標籤", + "storage_quota": "儲存空間", "storage_usage": "用了 {used} / 共 {available}", "submit": "提交", "suggestions": "建議", @@ -1728,7 +1763,7 @@ "sync_albums": "同步相簿", "sync_albums_manual_subtitle": "將所有上傳的短片和照片同步到選定的備份相簿", "sync_upload_album_setting_subtitle": "新增照片和短片並上傳到 Immich 上的選定相簿中", - "tag": "標記", + "tag": "標籤", "tag_assets": "標記檔案", "tag_created": "已建立標記:{tag}", "tag_feature_description": "以邏輯標記要旨分組瀏覽照片和影片", @@ -1742,12 +1777,12 @@ "theme_selection": "主題選項", "theme_selection_description": "依瀏覽器系統偏好自動設定深、淺色主題", "theme_setting_asset_list_storage_indicator_title": "在項目標題上顯示使用之儲存空間", - "theme_setting_asset_list_tiles_per_row_title": "每行展示 {} 項", - "theme_setting_colorful_interface_subtitle": "套用主色調到背景", + "theme_setting_asset_list_tiles_per_row_title": "每行展示 {count} 項", + "theme_setting_colorful_interface_subtitle": "套用主色調到背景。", "theme_setting_colorful_interface_title": "彩色界面", "theme_setting_image_viewer_quality_subtitle": "調整查看大圖時的圖片質量", "theme_setting_image_viewer_quality_title": "圖片質量", - "theme_setting_primary_color_subtitle": "選擇顏色作為主色調", + "theme_setting_primary_color_subtitle": "選擇顏色作為主色調。", "theme_setting_primary_color_title": "主色調", "theme_setting_system_primary_color_title": "使用系統顏色", "theme_setting_system_theme_switch": "自動(跟隨系統設定)", @@ -1773,17 +1808,19 @@ "trash_all": "全部丟掉", "trash_count": "丟掉 {count, number} 個檔案", "trash_delete_asset": "將檔案丟進垃圾桶 / 刪除", - "trash_emptied": "已清空回收桶\n", + "trash_emptied": "已清空回收桶", "trash_no_results_message": "垃圾桶中的照片和影片將顯示在這裡。", "trash_page_delete_all": "刪除全部", "trash_page_empty_trash_dialog_content": "是否清空回收桶?這些項目將被從Immich中永久刪除", - "trash_page_info": "回收桶中項目將在 {} 天後永久刪除", + "trash_page_info": "回收桶中項目將在 {days} 天後永久刪除", "trash_page_no_assets": "暫無已刪除項目", "trash_page_restore_all": "恢復全部", "trash_page_select_assets_btn": "選擇項目", - "trash_page_title": "回收桶 ( {} )", + "trash_page_title": "垃圾桶 ({count})", "trashed_items_will_be_permanently_deleted_after": "垃圾桶中的項目會在 {days, plural, other {# 天}}後永久刪除。", "type": "類型", + "unable_to_change_pin_code": "無法變更 PIN 碼", + "unable_to_setup_pin_code": "無法設定 PIN 碼", "unarchive": "取消封存", "unarchived_count": "{count, plural, other {已取消封存 # 個項目}}", "unfavorite": "取消收藏", @@ -1819,15 +1856,18 @@ "upload_status_errors": "錯誤", "upload_status_uploaded": "已上傳", "upload_success": "上傳成功,要查看新上傳的檔案請重新整理頁面。", - "upload_to_immich": "Upload to Immich ({})", - "uploading": "Uploading", + "upload_to_immich": "上傳至 Immich ({count})", + "uploading": "上傳中", "url": "網址", "usage": "用量", - "use_current_connection": "use current connection", + "use_current_connection": "使用目前的連線", "use_custom_date_range": "改用自訂日期範圍", "user": "使用者", + "user_has_been_deleted": "此用戶以被刪除", "user_id": "使用者 ID", "user_liked": "{user} 喜歡了 {type, select, photo {這張照片} video {這段影片} asset {這個檔案} other {它}}", + "user_pin_code_settings": "PIN 碼", + "user_pin_code_settings_description": "管理你的 PIN 碼", "user_purchase_settings": "購置", "user_purchase_settings_description": "管理你的購買", "user_role_set": "設 {user} 為{role}", @@ -1838,15 +1878,15 @@ "users": "使用者", "utilities": "工具", "validate": "驗證", - "validate_endpoint_error": "Please enter a valid URL", + "validate_endpoint_error": "請輸入有效的連結", "variables": "變數", "version": "版本", "version_announcement_closing": "敬祝順心,Alex", "version_announcement_message": "嗨~新版本的 Immich 推出了。為防止配置出錯,請花點時間閱讀發行說明,並確保設定是最新的,特別是使用 WatchTower 等自動更新工具時。", "version_announcement_overlay_release_notes": "發行說明", "version_announcement_overlay_text_1": "好消息,有新版本的", - "version_announcement_overlay_text_2": "請花點時間訪問", - "version_announcement_overlay_text_3": "並檢查您的 docker-compose 和 .env 是否為最新且正確的設定,特別是您在使用 WatchTower 或者其他自動更新的程式時,您需要更加細緻的檢查。", + "version_announcement_overlay_text_2": "請花點時間訪問 ", + "version_announcement_overlay_text_3": " 並檢查您的 docker-compose 和 .env 是否為最新且正確的設定,特別是您在使用 WatchTower 或者其他自動更新的程式時,您需要更加仔細地檢查。", "version_announcement_overlay_title": "服務端有新版本啦 🎉", "version_history": "版本紀錄", "version_history_item": "{date} 安裝了 {version}", @@ -1876,11 +1916,11 @@ "week": "週", "welcome": "歡迎", "welcome_to_immich": "歡迎使用 Immich", - "wifi_name": "WiFi Name", + "wifi_name": "Wi-Fi 名稱", "year": "年", "years_ago": "{years, plural, other {# 年}}前", "yes": "是", "you_dont_have_any_shared_links": "您沒有任何共享連結", - "your_wifi_name": "Your WiFi name", + "your_wifi_name": "您的 Wi-Fi 名稱", "zoom_image": "縮放圖片" } diff --git a/i18n/zh_SIMPLIFIED.json b/i18n/zh_SIMPLIFIED.json index c379efcd68..a0c6559652 100644 --- a/i18n/zh_SIMPLIFIED.json +++ b/i18n/zh_SIMPLIFIED.json @@ -53,6 +53,7 @@ "confirm_email_below": "请输入“{email}”以进行确认", "confirm_reprocess_all_faces": "确定要对全部照片重新进行面部识别吗?这将同时清除所有已命名人物。", "confirm_user_password_reset": "确定要重置用户“{user}”的密码吗?", + "confirm_user_pin_code_reset": "确定要重置{user}的PIN码吗?", "create_job": "创建任务", "cron_expression": "Cron 表达式", "cron_expression_description": "使用 Cron 格式设置扫描间隔。更多详细信息请参阅 Crontab Guru", @@ -348,6 +349,7 @@ "user_delete_delay_settings_description": "永久删除账户及其所有项目之前所保留的天数。用户删除作业会在午夜检查是否有用户可以删除。对该设置的更改将在下次执行时生效。", "user_delete_immediately": "{user}的账户及项目将立即永久删除。", "user_delete_immediately_checkbox": "立即删除检索到的用户及项目", + "user_details": "用户详情", "user_management": "用户管理", "user_password_has_been_reset": "该用户的密码被重置:", "user_password_reset_description": "请向用户提供临时密码,并告知他们下次登录时需要更改密码。", @@ -369,7 +371,7 @@ "advanced": "高级", "advanced_settings_enable_alternate_media_filter_subtitle": "使用此选项可在同步过程中根据备用条件筛选项目。仅当您在应用程序检测所有相册均遇到问题时才尝试此功能。", "advanced_settings_enable_alternate_media_filter_title": "[实验] 使用备用的设备相册同步筛选条件", - "advanced_settings_log_level_title": "日志等级: {}", + "advanced_settings_log_level_title": "日志等级: {level}", "advanced_settings_prefer_remote_subtitle": "在某些设备上,从本地的项目加载缩略图的速度非常慢。启用此选项以加载远程项目。", "advanced_settings_prefer_remote_title": "优先远程项目", "advanced_settings_proxy_headers_subtitle": "定义代理标头,应用于 Immich 的每次网络请求", @@ -400,9 +402,9 @@ "album_remove_user_confirmation": "确定要移除“{user}”吗?", "album_share_no_users": "看起来您已与所有用户共享了此相册,或者您根本没有任何用户可共享。", "album_thumbnail_card_item": "1 项", - "album_thumbnail_card_items": "{} 项", + "album_thumbnail_card_items": "{count} 项", "album_thumbnail_card_shared": " · 已共享", - "album_thumbnail_shared_by": "由 {} 共享", + "album_thumbnail_shared_by": "由 {user} 共享", "album_updated": "相册有更新", "album_updated_setting_description": "当共享相册有新项目时接收邮件通知", "album_user_left": "离开“{album}”", @@ -440,7 +442,7 @@ "archive": "归档", "archive_or_unarchive_photo": "归档或取消归档照片", "archive_page_no_archived_assets": "未找到归档项目", - "archive_page_title": "归档({})", + "archive_page_title": "归档({count})", "archive_size": "归档大小", "archive_size_description": "配置下载归档大小(GB)", "archived": "已存档", @@ -477,18 +479,18 @@ "assets_added_to_album_count": "已添加{count, plural, one {#个项目} other {#个项目}}到相册", "assets_added_to_name_count": "已添加{count, plural, one {#个项目} other {#个项目}}到{hasName, select, true {{name}} other {新相册}}", "assets_count": "{count, plural, one {#个项目} other {#个项目}}", - "assets_deleted_permanently": "{} 个项目已被永久删除", - "assets_deleted_permanently_from_server": "已永久移除 {} 个项目", + "assets_deleted_permanently": "{count} 个项目已被永久删除", + "assets_deleted_permanently_from_server": "已永久移除 {count} 个项目", "assets_moved_to_trash_count": "已移动{count, plural, one {#个项目} other {#个项目}}到回收站", "assets_permanently_deleted_count": "已永久删除{count, plural, one {#个项目} other {#个项目}}", "assets_removed_count": "已移除{count, plural, one {#个项目} other {#个项目}}", - "assets_removed_permanently_from_device": "已从设备中永久移除 {} 个项目", + "assets_removed_permanently_from_device": "已从设备中永久移除 {count} 个项目", "assets_restore_confirmation": "确定要恢复回收站中的所有项目吗?该操作无法撤消!请注意,脱机项目无法通过这种方式恢复。", "assets_restored_count": "已恢复{count, plural, one {#个项目} other {#个项目}}", - "assets_restored_successfully": "已成功恢复{}个项目", - "assets_trashed": "{} 个项目放入回收站", + "assets_restored_successfully": "已成功恢复{count}个项目", + "assets_trashed": "{count} 个项目放入回收站", "assets_trashed_count": "{count, plural, one {#个项目} other {#个项目}}已放入回收站", - "assets_trashed_from_server": "{} 个项目已放入回收站", + "assets_trashed_from_server": "{count} 个项目已放入回收站", "assets_were_part_of_album_count": "{count, plural, one {项目} other {项目}}已经在相册中", "authorized_devices": "已授权设备", "automatic_endpoint_switching_subtitle": "当连接到指定的 Wi-Fi 时使用本地连接,在其它环境下使用替代连接", @@ -497,7 +499,7 @@ "back_close_deselect": "返回、关闭或反选", "background_location_permission": "后台定位权限", "background_location_permission_content": "为了在后台运行时切换网络,Immich 必须*始终*拥有精确的位置访问权限,这样应用程序才能读取 Wi-Fi 网络的名称", - "backup_album_selection_page_albums_device": "设备上的相册({})", + "backup_album_selection_page_albums_device": "设备上的相册({count})", "backup_album_selection_page_albums_tap": "单击选中,双击取消", "backup_album_selection_page_assets_scatter": "项目会分散在多个相册中。因此,可以在备份过程中包含或排除相册。", "backup_album_selection_page_select_albums": "选择相册", @@ -506,11 +508,11 @@ "backup_all": "全部", "backup_background_service_backup_failed_message": "备份失败,正在重试…", "backup_background_service_connection_failed_message": "连接服务器失败,正在重试…", - "backup_background_service_current_upload_notification": "正在上传 {}", + "backup_background_service_current_upload_notification": "正在上传 {filename}", "backup_background_service_default_notification": "正在检查新项目…", "backup_background_service_error_title": "备份失败", "backup_background_service_in_progress_notification": "正在备份…", - "backup_background_service_upload_failure_notification": "上传失败 {}", + "backup_background_service_upload_failure_notification": "{filename}上传失败", "backup_controller_page_albums": "备份相册", "backup_controller_page_background_app_refresh_disabled_content": "要使用后台备份功能,请在“设置”>“常规”>“后台应用刷新”中启用后台应用程序刷新。", "backup_controller_page_background_app_refresh_disabled_title": "后台应用刷新已禁用", @@ -521,7 +523,7 @@ "backup_controller_page_background_battery_info_title": "电池优化", "backup_controller_page_background_charging": "仅充电时", "backup_controller_page_background_configure_error": "配置后台服务失败", - "backup_controller_page_background_delay": "延迟备份的新项目:{}", + "backup_controller_page_background_delay": "延迟备份的新项目:{duration}", "backup_controller_page_background_description": "打开后台服务以自动备份任何新项目,且无需打开应用", "backup_controller_page_background_is_off": "后台自动备份已关闭", "backup_controller_page_background_is_on": "后台自动备份已开启", @@ -531,12 +533,12 @@ "backup_controller_page_backup": "备份", "backup_controller_page_backup_selected": "已选中: ", "backup_controller_page_backup_sub": "已备份的照片和视频", - "backup_controller_page_created": "创建时间:{}", + "backup_controller_page_created": "创建时间:{date}", "backup_controller_page_desc_backup": "打开前台备份,以在程序运行时自动备份新项目。", "backup_controller_page_excluded": "已排除: ", - "backup_controller_page_failed": "失败({})", - "backup_controller_page_filename": "文件名称:{} [{}]", - "backup_controller_page_id": "ID:{}", + "backup_controller_page_failed": "失败({count})", + "backup_controller_page_filename": "文件名称:{filename} [{size}]", + "backup_controller_page_id": "ID:{id}", "backup_controller_page_info": "备份信息", "backup_controller_page_none_selected": "未选择", "backup_controller_page_remainder": "剩余", @@ -545,7 +547,7 @@ "backup_controller_page_start_backup": "开始备份", "backup_controller_page_status_off": "前台自动备份已关闭", "backup_controller_page_status_on": "前台自动备份已开启", - "backup_controller_page_storage_format": "{}/{} 已使用", + "backup_controller_page_storage_format": "{used}/{total} 已使用", "backup_controller_page_to_backup": "要备份的相册", "backup_controller_page_total_sub": "选中相册中所有不重复的视频和图像", "backup_controller_page_turn_off": "关闭前台备份", @@ -570,21 +572,21 @@ "bulk_keep_duplicates_confirmation": "您确定要保留{count, plural, one {#个重复项目} other {#个重复项目}}吗?这将清空所有重复记录,但不会删除任何内容。", "bulk_trash_duplicates_confirmation": "您确定要批量删除{count, plural, one {#个重复项目} other {#个重复项目}}吗?这将保留每组中最大的项目并删除所有其它重复项目。", "buy": "购买 Immich", - "cache_settings_album_thumbnails": "图库页面缩略图({} 项)", + "cache_settings_album_thumbnails": "图库页面缩略图({count} 项)", "cache_settings_clear_cache_button": "清除缓存", "cache_settings_clear_cache_button_title": "清除应用缓存。在重新生成缓存之前,将显著影响应用的性能。", "cache_settings_duplicated_assets_clear_button": "清除", "cache_settings_duplicated_assets_subtitle": "已加入黑名单的照片和视频", - "cache_settings_duplicated_assets_title": "重复项目({})", - "cache_settings_image_cache_size": "图像缓存大小({} 项)", + "cache_settings_duplicated_assets_title": "重复项目({count})", + "cache_settings_image_cache_size": "图像缓存大小({count} 项)", "cache_settings_statistics_album": "图库缩略图", - "cache_settings_statistics_assets": "{} 项({})", + "cache_settings_statistics_assets": "{count} 项({size})", "cache_settings_statistics_full": "完整图像", "cache_settings_statistics_shared": "共享相册缩略图", "cache_settings_statistics_thumbnail": "缩略图", "cache_settings_statistics_title": "缓存使用情况", "cache_settings_subtitle": "控制 Immich app 的缓存行为", - "cache_settings_thumbnail_size": "缩略图缓存大小({} 项)", + "cache_settings_thumbnail_size": "缩略图缓存大小({count} 项)", "cache_settings_tile_subtitle": "设置本地存储行为", "cache_settings_tile_title": "本地存储", "cache_settings_title": "缓存设置", @@ -610,6 +612,7 @@ "change_password_form_new_password": "新密码", "change_password_form_password_mismatch": "密码不匹配", "change_password_form_reenter_new_password": "再次输入新密码", + "change_pin_code": "修改PIN码", "change_your_password": "修改您的密码", "changed_visibility_successfully": "更改可见性成功", "check_all": "检查所有", @@ -650,11 +653,12 @@ "confirm_delete_face": "您确定要从资产中删除 {name} 的脸吗?", "confirm_delete_shared_link": "确定要删除此共享链接吗?", "confirm_keep_this_delete_others": "除此项目外,堆叠中的所有其它项目都将被删除。确定要继续吗?", + "confirm_new_pin_code": "确认新的PIN码", "confirm_password": "确认密码", "contain": "包含", "context": "以文搜图", "continue": "继续", - "control_bottom_app_bar_album_info_shared": "已共享 {} 项", + "control_bottom_app_bar_album_info_shared": "已共享 {count} 项", "control_bottom_app_bar_create_new_album": "新建相册", "control_bottom_app_bar_delete_from_immich": "从 Immich 服务器中删除", "control_bottom_app_bar_delete_from_local": "从移动设备中删除", @@ -692,9 +696,11 @@ "create_tag_description": "创建一个新标签。对于嵌套标签,请输入标签的完整路径,包括正斜杠(/)。", "create_user": "创建用户", "created": "已创建", + "created_at": "已创建", "crop": "裁剪", "curated_object_page_title": "事物", "current_device": "当前设备", + "current_pin_code": "当前PIN码", "current_server_address": "当前服务器地址", "custom_locale": "自定义地区", "custom_locale_description": "日期和数字显示格式跟随语言和地区", @@ -763,7 +769,7 @@ "download_enqueue": "已加入下载队列", "download_error": "下载出错", "download_failed": "下载失败", - "download_filename": "文件:{}", + "download_filename": "文件:{filename}", "download_finished": "下载完成", "download_include_embedded_motion_videos": "内嵌视频", "download_include_embedded_motion_videos_description": "将实况照片中的内嵌视频作为单独文件纳入", @@ -807,6 +813,7 @@ "editor_crop_tool_h2_aspect_ratios": "长宽比", "editor_crop_tool_h2_rotation": "旋转", "email": "邮箱", + "email_notifications": "邮件通知", "empty_folder": "文件夹为空", "empty_trash": "清空回收站", "empty_trash_confirmation": "确定要清空回收站?这将永久删除回收站中的所有项目。\n注意:该操作无法撤消!", @@ -819,7 +826,7 @@ "error_change_sort_album": "更改相册排序失败", "error_delete_face": "删除人脸失败", "error_loading_image": "加载图片时出错", - "error_saving_image": "错误:{}", + "error_saving_image": "错误:{error}", "error_title": "错误 - 好像出了问题", "errors": { "cannot_navigate_next_asset": "无法导航到下一个项目", @@ -922,6 +929,7 @@ "unable_to_remove_reaction": "无法移除回应", "unable_to_repair_items": "无法修复项目", "unable_to_reset_password": "无法重置密码", + "unable_to_reset_pin_code": "无法重置PIN码", "unable_to_resolve_duplicate": "无法解决重复项", "unable_to_restore_assets": "无法恢复项目", "unable_to_restore_trash": "无法恢复回收站", @@ -955,10 +963,10 @@ "exif_bottom_sheet_location": "位置", "exif_bottom_sheet_people": "人物", "exif_bottom_sheet_person_add_person": "添加姓名", - "exif_bottom_sheet_person_age": "{} 岁", - "exif_bottom_sheet_person_age_months": "{} 月龄", - "exif_bottom_sheet_person_age_year_months": "1岁 {} 个月", - "exif_bottom_sheet_person_age_years": "{} 岁", + "exif_bottom_sheet_person_age": "{age} 岁", + "exif_bottom_sheet_person_age_months": "{months} 月龄", + "exif_bottom_sheet_person_age_year_months": "1岁 {months} 个月", + "exif_bottom_sheet_person_age_years": "{years} 岁", "exit_slideshow": "退出幻灯片放映", "expand_all": "全部展开", "experimental_settings_new_asset_list_subtitle": "正在处理", @@ -1043,11 +1051,12 @@ "home_page_delete_remote_err_local": "远程项目删除模式,跳过本地项目", "home_page_favorite_err_local": "暂不能收藏本地项目,跳过", "home_page_favorite_err_partner": "暂无法收藏同伴的项目,跳过", - "home_page_first_time_notice": "如果这是您第一次使用该应用程序,请确保选择一个要备份的本地相册,以便可以在时间线中预览该相册中的照片和视频。", + "home_page_first_time_notice": "如果这是您第一次使用该应用程序,请确保选择一个要备份的本地相册,以便可以在时间线中预览该相册中的照片和视频", "home_page_share_err_local": "暂无法通过链接共享本地项目,跳过", "home_page_upload_err_limit": "一次最多只能上传 30 个项目,跳过", "host": "服务器", "hour": "时", + "id": "ID", "ignore_icloud_photos": "忽略 iCloud 照片", "ignore_icloud_photos_description": "存储在 iCloud 中的照片不会上传至 Immich 服务器", "image": "图片", @@ -1151,7 +1160,7 @@ "login_form_handshake_exception": "与服务器通信时出现握手异常。如果您使用的是自签名证书,请在设置中启用自签名证书支持。", "login_form_password_hint": "密码", "login_form_save_login": "保持登录", - "login_form_server_empty": "输入服务器地址", + "login_form_server_empty": "输入服务器地址。", "login_form_server_error": "无法连接到服务器。", "login_has_been_disabled": "登录已禁用。", "login_password_changed_error": "更新密码时出错", @@ -1173,8 +1182,8 @@ "manage_your_devices": "管理已登录设备", "manage_your_oauth_connection": "管理您的 OAuth 绑定", "map": "地图", - "map_assets_in_bound": "{} 张照片", - "map_assets_in_bounds": "{} 张照片", + "map_assets_in_bound": "{count} 张照片", + "map_assets_in_bounds": "{count} 张照片", "map_cannot_get_user_location": "无法获取用户位置", "map_location_dialog_yes": "是", "map_location_picker_page_use_location": "使用此位置", @@ -1188,9 +1197,9 @@ "map_settings": "地图设置", "map_settings_dark_mode": "深色模式", "map_settings_date_range_option_day": "过去24小时", - "map_settings_date_range_option_days": "{} 天前", + "map_settings_date_range_option_days": "{days} 天前", "map_settings_date_range_option_year": "1年前", - "map_settings_date_range_option_years": "{} 年前", + "map_settings_date_range_option_years": "{years} 年前", "map_settings_dialog_title": "地图设置", "map_settings_include_show_archived": "包括已归档项目", "map_settings_include_show_partners": "包含伙伴", @@ -1209,7 +1218,7 @@ "memories_start_over": "再看一次", "memories_swipe_to_close": "上划关闭", "memories_year_ago": "1年前", - "memories_years_ago": "{} 年前", + "memories_years_ago": "{years, plural, other {#年}} 前", "memory": "回忆", "memory_lane_title": "记忆线{title}", "menu": "菜单", @@ -1242,6 +1251,7 @@ "new_api_key": "新增 API Key", "new_password": "新密码", "new_person": "新人物", + "new_pin_code": "新的PIN码", "new_user_created": "已创建新用户", "new_version_available": "有新版本发布啦", "newest_first": "最新优先", @@ -1261,6 +1271,7 @@ "no_libraries_message": "创建外部图库来查看您的照片和视频", "no_name": "未命名", "no_notifications": "没有通知", + "no_people_found": "未找到匹配的人物", "no_places": "无位置", "no_results": "无结果", "no_results_description": "尝试使用同义词或更通用的关键词", @@ -1315,7 +1326,7 @@ "partner_page_partner_add_failed": "添加同伴失败", "partner_page_select_partner": "选择同伴", "partner_page_shared_to_title": "共享给", - "partner_page_stop_sharing_content": "{} 将无法再访问您的照片。", + "partner_page_stop_sharing_content": "{partner} 将无法再访问您的照片。", "partner_sharing": "同伴共享", "partners": "同伴", "password": "密码", @@ -1361,6 +1372,9 @@ "photos_count": "{count, plural, one {{count, number}张照片} other {{count, number}张照片}}", "photos_from_previous_years": "过往的今昔瞬间", "pick_a_location": "选择位置", + "pin_code_changed_successfully": "修改PIN码成功", + "pin_code_reset_successfully": "重置PIN码成功", + "pin_code_setup_successfully": "设置PIN码成功", "place": "地点", "places": "地点", "places_count": "{count, plural, one {{count, number} 个地点} other {{count, number} 个地点}}", @@ -1378,6 +1392,7 @@ "previous_or_next_photo": "上一张或下一张照片", "primary": "首要", "privacy": "隐私", + "profile": "详情", "profile_drawer_app_logs": "日志", "profile_drawer_client_out_of_date_major": "客户端有大版本升级,请尽快升级至最新版。", "profile_drawer_client_out_of_date_minor": "客户端有小版本升级,请尽快升级至最新版。", @@ -1391,7 +1406,7 @@ "public_share": "公开共享", "purchase_account_info": "支持者", "purchase_activated_subtitle": "感谢您对 Immich 和开源软件的支持", - "purchase_activated_time": "激活于{date, date}", + "purchase_activated_time": "激活于{date}", "purchase_activated_title": "您的密钥已成功激活", "purchase_button_activate": "激活", "purchase_button_buy": "购买", @@ -1480,6 +1495,7 @@ "reset": "重置", "reset_password": "重置密码", "reset_people_visibility": "重置人物识别", + "reset_pin_code": "重置PIN码", "reset_to_default": "恢复默认值", "resolve_duplicates": "处理重复项", "resolved_all_duplicates": "处理所有重复项", @@ -1572,6 +1588,7 @@ "select_keep_all": "全部保留", "select_library_owner": "选择图库所有者", "select_new_face": "选择新面孔", + "select_person_to_tag": "选择要标记的人物", "select_photos": "选择照片", "select_trash_all": "全部删除", "select_user_for_sharing_page_err_album": "创建相册失败", @@ -1602,12 +1619,12 @@ "setting_languages_apply": "应用", "setting_languages_subtitle": "更改应用语言", "setting_languages_title": "语言", - "setting_notifications_notify_failures_grace_period": "后台备份失败通知:{}", - "setting_notifications_notify_hours": "{} 小时", + "setting_notifications_notify_failures_grace_period": "后台备份失败通知:{duration}", + "setting_notifications_notify_hours": "{count} 小时", "setting_notifications_notify_immediately": "立即", - "setting_notifications_notify_minutes": "{} 分钟", + "setting_notifications_notify_minutes": "{count} 分钟", "setting_notifications_notify_never": "从不", - "setting_notifications_notify_seconds": "{} 秒", + "setting_notifications_notify_seconds": "{count} 秒", "setting_notifications_single_progress_subtitle": "每项的详细上传进度信息", "setting_notifications_single_progress_title": "显示后台备份详细进度", "setting_notifications_subtitle": "调整通知首选项", @@ -1619,9 +1636,10 @@ "settings": "设置", "settings_require_restart": "请重启 Immich 以使设置生效", "settings_saved": "设置已保存", + "setup_pin_code": "设置PIN码", "share": "共享", "share_add_photos": "添加项目", - "share_assets_selected": "{} 已选择", + "share_assets_selected": "{count} 已选择", "share_dialog_preparing": "正在准备...", "shared": "共享", "shared_album_activities_input_disable": "评论已禁用", @@ -1635,32 +1653,32 @@ "shared_by_user": "由“{user}”共享", "shared_by_you": "您的共享", "shared_from_partner": "来自“{partner}”的照片", - "shared_intent_upload_button_progress_text": "{} / {} 已上传", + "shared_intent_upload_button_progress_text": "{current} / {total} 已上传", "shared_link_app_bar_title": "共享链接", "shared_link_clipboard_copied_massage": "复制到剪贴板", - "shared_link_clipboard_text": "链接:{}\n密码:{}", + "shared_link_clipboard_text": "链接:{link}\n密码:{password}", "shared_link_create_error": "创建共享链接出错", "shared_link_edit_description_hint": "编辑共享描述", "shared_link_edit_expire_after_option_day": "1天", - "shared_link_edit_expire_after_option_days": "{} 天", + "shared_link_edit_expire_after_option_days": "{count} 天", "shared_link_edit_expire_after_option_hour": "1小时", - "shared_link_edit_expire_after_option_hours": "{} 小时", + "shared_link_edit_expire_after_option_hours": "{count} 小时", "shared_link_edit_expire_after_option_minute": "1分钟", - "shared_link_edit_expire_after_option_minutes": "{} 分钟", - "shared_link_edit_expire_after_option_months": "{} 月龄", - "shared_link_edit_expire_after_option_year": "{} 年", + "shared_link_edit_expire_after_option_minutes": "{count} 分钟", + "shared_link_edit_expire_after_option_months": "{count} 月龄", + "shared_link_edit_expire_after_option_year": "{count} 年", "shared_link_edit_password_hint": "输入共享密码", "shared_link_edit_submit_button": "更新链接", "shared_link_error_server_url_fetch": "无法获取服务器地址", - "shared_link_expires_day": "{} 天后过期", - "shared_link_expires_days": "{} 天后过期", - "shared_link_expires_hour": "{} 小时后过期", - "shared_link_expires_hours": "{} 小时后过期", - "shared_link_expires_minute": "{} 分钟后过期", - "shared_link_expires_minutes": "{} 分钟后过期", + "shared_link_expires_day": "{count} 天后过期", + "shared_link_expires_days": "{count} 天后过期", + "shared_link_expires_hour": "{count} 小时后过期", + "shared_link_expires_hours": "{count} 小时后过期", + "shared_link_expires_minute": "{count} 分钟后过期", + "shared_link_expires_minutes": "{count} 分钟后过期", "shared_link_expires_never": "过期时间 ∞", - "shared_link_expires_second": "{} 秒后过期", - "shared_link_expires_seconds": "{} 秒后过期", + "shared_link_expires_second": "{count} 秒后过期", + "shared_link_expires_seconds": "{count} 秒后过期", "shared_link_individual_shared": "个人共享", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "管理共享链接", @@ -1735,6 +1753,7 @@ "stop_sharing_photos_with_user": "停止与此用户共享照片", "storage": "存储空间", "storage_label": "存储标签", + "storage_quota": "存储配额", "storage_usage": "已用:{used}/{available}", "submit": "提交", "suggestions": "建议", @@ -1761,12 +1780,12 @@ "theme_selection": "主题选项", "theme_selection_description": "跟随浏览器自动设置主题颜色", "theme_setting_asset_list_storage_indicator_title": "在项目标题上显示存储占用", - "theme_setting_asset_list_tiles_per_row_title": "每行展示 {} 项", - "theme_setting_colorful_interface_subtitle": "应用主色调到背景", + "theme_setting_asset_list_tiles_per_row_title": "每行展示 {count} 项", + "theme_setting_colorful_interface_subtitle": "应用主色调到背景。", "theme_setting_colorful_interface_title": "彩色界面", "theme_setting_image_viewer_quality_subtitle": "调整查看大图时的图像质量", "theme_setting_image_viewer_quality_title": "图像质量", - "theme_setting_primary_color_subtitle": "选择颜色作为主色调", + "theme_setting_primary_color_subtitle": "选择颜色作为主色调。", "theme_setting_primary_color_title": "主色调", "theme_setting_system_primary_color_title": "使用系统颜色", "theme_setting_system_theme_switch": "自动(跟随系统设置)", @@ -1796,13 +1815,15 @@ "trash_no_results_message": "删除的照片和视频将在此处展示。", "trash_page_delete_all": "删除全部", "trash_page_empty_trash_dialog_content": "是否清空回收站?这些项目将被从 Immich 中永久删除", - "trash_page_info": "回收站中项目将在 {} 天后永久删除", + "trash_page_info": "回收站中项目将在 {days} 天后永久删除", "trash_page_no_assets": "暂无已删除项目", "trash_page_restore_all": "恢复全部", "trash_page_select_assets_btn": "选择项目", - "trash_page_title": "回收站 ({})", + "trash_page_title": "回收站 ({count})", "trashed_items_will_be_permanently_deleted_after": "回收站中的项目将在{days, plural, one {#天} other {#天}}后被永久删除。", "type": "类型", + "unable_to_change_pin_code": "无法修改PIN码", + "unable_to_setup_pin_code": "无法设置PIN码", "unarchive": "取消归档", "unarchived_count": "{count, plural, other {取消归档 # 项}}", "unfavorite": "取消收藏", @@ -1826,6 +1847,7 @@ "untracked_files": "未跟踪的文件", "untracked_files_decription": "应用程序不会跟踪这些文件。它们可能是由于移动失败、上传中断或因错误而遗留", "up_next": "下一个", + "updated_at": "已更新", "updated_password": "更新密码", "upload": "上传", "upload_concurrency": "上传并发", @@ -1838,15 +1860,18 @@ "upload_status_errors": "错误", "upload_status_uploaded": "已上传", "upload_success": "上传成功,刷新页面查看新上传的项目。", - "upload_to_immich": "上传至 Immich({})", + "upload_to_immich": "上传至 Immich({count})", "uploading": "正在上传", "url": "URL", "usage": "用量", "use_current_connection": "使用当前连接", "use_custom_date_range": "自定义日期范围", "user": "用户", + "user_has_been_deleted": "此用户已被删除。", "user_id": "用户 ID", "user_liked": "“{user}”点赞了{type, select, photo {该照片} video {该视频} asset {该项目} other {它}}", + "user_pin_code_settings": "PIN码", + "user_pin_code_settings_description": "管理你的PIN码", "user_purchase_settings": "购买", "user_purchase_settings_description": "管理购买订单", "user_role_set": "设置“{user}”为“{role}”", From 56156b97e760a5fb2335b1e0caf7f27270ca7c1d Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Sun, 18 May 2025 15:51:33 +0200 Subject: [PATCH 247/356] chore: upgrade to tailwind v4 (#18353) --- web/package-lock.json | 1302 ++++++++++------- web/package.json | 7 +- web/postcss.config.cjs | 3 +- web/src/app.css | 200 +-- web/src/app.html | 2 +- .../album-page/album-card-group.svelte | 6 +- .../components/album-page/album-viewer.svelte | 2 +- .../components/album-page/albums-table.svelte | 2 +- .../asset-viewer/asset-viewer-nav-bar.svelte | 2 +- .../assets/thumbnail/thumbnail.svelte | 6 +- .../lib/components/elements/dropdown.svelte | 4 +- .../lib/components/elements/group-tab.svelte | 2 +- .../components/elements/radio-button.svelte | 2 +- .../components/faces-page/people-card.svelte | 2 +- .../components/forms/edit-album-form.svelte | 2 +- .../components/layouts/AuthPageLayout.svelte | 6 +- .../layouts/user-page-layout.svelte | 8 +- .../memory-page/memory-viewer.svelte | 2 +- .../components/photos-page/memory-lane.svelte | 8 +- .../places-page/places-card-group.svelte | 6 +- .../components/places-page/places-list.svelte | 2 +- .../shared-components/combobox.svelte | 4 +- .../context-menu/context-menu.svelte | 2 +- .../shared-components/control-app-bar.svelte | 2 +- .../full-screen-modal.svelte | 6 +- .../gallery-viewer/gallery-viewer.svelte | 2 +- .../navigation-bar/account-info-panel.svelte | 2 +- .../navigation-bar/navigation-bar.svelte | 7 +- .../navigation-bar/notification-panel.svelte | 2 +- .../shared-components/password-field.svelte | 2 +- .../scrubber/scrubber.svelte | 10 +- .../search-bar/search-text-section.svelte | 6 +- .../settings/setting-select.svelte | 2 +- .../shared-components/tree/tree.svelte | 2 +- web/src/lib/components/sidebar/sidebar.svelte | 2 +- .../duplicates-compare-control.svelte | 2 +- web/src/lib/modals/AlbumShareModal.svelte | 4 +- web/src/lib/modals/SearchFilterModal.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 6 +- .../[[assetId=id]]/+page.svelte | 2 +- web/src/routes/(user)/people/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- web/src/routes/+layout.svelte | 1 - web/tailwind.config.js | 7 - web/vite.config.js | 2 + 48 files changed, 930 insertions(+), 733 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 12d65473c9..cc29dd6856 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.21.1", + "@immich/ui": "^0.22.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", @@ -35,6 +35,7 @@ "svelte-i18n": "^4.0.1", "svelte-maplibre": "^1.0.0", "svelte-persisted-store": "^0.12.0", + "tabbable": "^6.2.0", "thumbhash": "^0.1.1" }, "devDependencies": { @@ -46,6 +47,8 @@ "@sveltejs/enhanced-img": "^0.5.0", "@sveltejs/kit": "^2.15.2", "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tailwindcss/postcss": "^4.1.7", + "@tailwindcss/vite": "^4.1.7", "@testing-library/jest-dom": "^6.4.2", "@testing-library/svelte": "^5.2.6", "@testing-library/user-event": "^14.5.2", @@ -72,7 +75,7 @@ "rollup-plugin-visualizer": "^5.14.0", "svelte": "^5.25.3", "svelte-check": "^4.1.5", - "tailwindcss": "^3.4.17", + "tailwindcss": "^4.1.7", "tslib": "^2.6.2", "typescript": "^5.7.3", "typescript-eslint": "^8.28.0", @@ -103,6 +106,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -794,21 +798,21 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", - "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", + "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", - "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz", + "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.6.0", + "@floating-ui/core": "^1.7.0", "@floating-ui/utils": "^0.2.9" } }, @@ -1337,9 +1341,9 @@ "link": true }, "node_modules/@immich/ui": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.21.1.tgz", - "integrity": "sha512-ofDbLMYgM3Bnrv1nCbyPV5Gw9PdWvyhTAJPtojw4C3r2m7CbRW1kJDHt5M79n6xAVgjMOFyre1lOE5cwSSvRQA==", + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.22.0.tgz", + "integrity": "sha512-bBx9hPy7/VECZPcEiBGty6Lu9jmD4vJf6VL2ud+LHLQcpZebv4FVFZzzVFf7ctBwooYJWTEfWZTPNgAo0rbQtQ==", "license": "GNU Affero General Public License version 3", "dependencies": { "@mdi/js": "^7.4.47", @@ -1364,6 +1368,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -1381,6 +1386,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1393,6 +1399,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1405,12 +1412,14 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -1428,6 +1437,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -1443,6 +1453,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -1456,6 +1467,29 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -1700,6 +1734,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -1713,6 +1748,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -1722,6 +1758,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -1782,6 +1819,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -2222,6 +2260,374 @@ "tslib": "^2.8.0" } }, + "node_modules/@tailwindcss/node": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.7.tgz", + "integrity": "sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.7" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.7.tgz", + "integrity": "sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.7", + "@tailwindcss/oxide-darwin-arm64": "4.1.7", + "@tailwindcss/oxide-darwin-x64": "4.1.7", + "@tailwindcss/oxide-freebsd-x64": "4.1.7", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.7", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.7", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.7", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.7", + "@tailwindcss/oxide-linux-x64-musl": "4.1.7", + "@tailwindcss/oxide-wasm32-wasi": "4.1.7", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.7", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.7" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.7.tgz", + "integrity": "sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.7.tgz", + "integrity": "sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.7.tgz", + "integrity": "sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.7.tgz", + "integrity": "sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.7.tgz", + "integrity": "sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.7.tgz", + "integrity": "sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.7.tgz", + "integrity": "sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.7.tgz", + "integrity": "sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.7.tgz", + "integrity": "sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.7.tgz", + "integrity": "sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.9", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.7.tgz", + "integrity": "sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.7.tgz", + "integrity": "sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.7.tgz", + "integrity": "sha512-88g3qmNZn7jDgrrcp3ZXEQfp9CVox7xjP1HN2TFKI03CltPVd/c61ydn5qJJL8FYunn0OqBaW5HNUga0kmPVvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.7", + "@tailwindcss/oxide": "4.1.7", + "postcss": "^8.4.41", + "tailwindcss": "4.1.7" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.7.tgz", + "integrity": "sha512-tYa2fO3zDe41I7WqijyVbRd8oWT0aEID1Eokz5hMT6wShLIHj3yvwj9XbfuloHP9glZ6H+aG2AN/+ZrxJ1Y5RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.7", + "@tailwindcss/oxide": "4.1.7", + "tailwindcss": "4.1.7" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -3025,37 +3431,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -3078,12 +3453,6 @@ "node": ">=10" } }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "license": "MIT" - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -3169,24 +3538,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true, "license": "MIT" }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bits-ui": { - "version": "1.3.19", - "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.3.19.tgz", - "integrity": "sha512-2blb6dkgedHUsDXqCjvmtUi4Advgd9MhaJDT8r7bEWDzHI8HGsOoYsLeh8CxpEWWEYPrlGN+7k+kpxRhIDdFrQ==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.4.8.tgz", + "integrity": "sha512-j34GsdSsJ+ZBl9h/70VkufvrlEgTKQSZvm80eM5VvuhLJWvpfEpn9+k0FVmtDQl9NSPgEVtI9imYhm8nW9Nj/w==", "license": "MIT", "dependencies": { "@floating-ui/core": "^1.6.4", @@ -3244,6 +3602,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3375,15 +3734,6 @@ "node": ">=6" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001713", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz", @@ -3647,15 +3997,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3745,6 +4086,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3773,6 +4115,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -3979,9 +4322,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "devOptional": true, "license": "Apache-2.0", "engines": { @@ -3995,24 +4338,12 @@ "dev": true, "license": "MIT" }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "license": "Apache-2.0" - }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "license": "MIT" - }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -4079,6 +4410,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, "license": "MIT" }, "node_modules/ee-first": { @@ -4171,6 +4503,20 @@ "node": ">=10.0.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -4890,6 +5236,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -4906,6 +5253,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -4932,6 +5280,7 @@ "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -4975,6 +5324,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -5056,6 +5406,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -5072,6 +5423,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -5167,6 +5519,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5181,6 +5534,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5307,6 +5661,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -5391,6 +5746,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -5476,6 +5838,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "devOptional": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5752,18 +6115,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-builtin-module": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-4.0.0.tgz", @@ -5780,21 +6131,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -5815,6 +6151,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5833,6 +6170,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -5845,6 +6183,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -5902,6 +6241,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/isobject": { @@ -5988,6 +6328,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -6000,12 +6341,13 @@ } }, "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, "license": "MIT", "bin": { - "jiti": "bin/jiti.js" + "jiti": "lib/jiti-cli.mjs" } }, "node_modules/js-tokens": { @@ -6182,6 +6524,245 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -6192,12 +6773,6 @@ "node": ">=10" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -6251,6 +6826,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, "license": "ISC" }, "node_modules/lru-queue": { @@ -6510,6 +7086,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -6519,6 +7096,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -6532,6 +7110,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -6612,6 +7191,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "devOptional": true, "license": "ISC", "engines": { "node": ">=8" @@ -6688,17 +7268,6 @@ "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", "license": "MIT" }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nan": { "version": "2.22.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", @@ -6710,6 +7279,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -6837,15 +7407,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", @@ -6881,20 +7442,12 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -7012,6 +7565,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -7091,21 +7645,17 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -7162,6 +7712,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -7177,24 +7728,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/pkce-challenge": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", @@ -7237,6 +7770,7 @@ "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -7261,42 +7795,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, "node_modules/postcss-load-config": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", @@ -7337,44 +7835,6 @@ "node": ">= 6" } }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-safe-parser": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", @@ -7447,6 +7907,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, "license": "MIT" }, "node_modules/potpack": { @@ -7638,6 +8099,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -7694,15 +8156,6 @@ "dev": true, "license": "MIT" }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, "node_modules/read-package-up": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", @@ -7872,26 +8325,6 @@ "license": "MIT", "optional": true }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -7915,6 +8348,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -8119,6 +8553,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -8365,6 +8800,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -8377,6 +8813,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8605,6 +9042,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8710,6 +9148,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -8737,6 +9176,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -8783,81 +9223,6 @@ "inline-style-parser": "0.2.4" } }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/supercluster": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", @@ -8888,18 +9253,6 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/svelte": { "version": "5.28.2", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.28.2.tgz", @@ -9152,160 +9505,19 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.6", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz", + "integrity": "sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==", + "license": "MIT" }, - "node_modules/tailwindcss/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/tailwindcss/node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/tailwindcss/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tailwindcss/node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tailwindcss/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" + "node": ">=6" } }, "node_modules/tar": { @@ -9398,27 +9610,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/three": { "version": "0.175.0", "resolved": "https://registry.npmjs.org/three/-/three-0.175.0.tgz", @@ -9526,6 +9717,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -9596,12 +9788,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "license": "Apache-2.0" - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -9824,6 +10010,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "devOptional": true, "license": "MIT" }, "node_modules/validate-npm-package-license": { @@ -10557,6 +10744,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -10636,6 +10824,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -10720,7 +10909,10 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "dev": true, "license": "ISC", + "optional": true, + "peer": true, "bin": { "yaml": "bin.mjs" }, diff --git a/web/package.json b/web/package.json index 94f48a7d97..d352afe45b 100644 --- a/web/package.json +++ b/web/package.json @@ -28,7 +28,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.21.1", + "@immich/ui": "^0.22.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", @@ -52,6 +52,7 @@ "svelte-i18n": "^4.0.1", "svelte-maplibre": "^1.0.0", "svelte-persisted-store": "^0.12.0", + "tabbable": "^6.2.0", "thumbhash": "^0.1.1" }, "devDependencies": { @@ -63,6 +64,8 @@ "@sveltejs/enhanced-img": "^0.5.0", "@sveltejs/kit": "^2.15.2", "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tailwindcss/postcss": "^4.1.7", + "@tailwindcss/vite": "^4.1.7", "@testing-library/jest-dom": "^6.4.2", "@testing-library/svelte": "^5.2.6", "@testing-library/user-event": "^14.5.2", @@ -89,7 +92,7 @@ "rollup-plugin-visualizer": "^5.14.0", "svelte": "^5.25.3", "svelte-check": "^4.1.5", - "tailwindcss": "^3.4.17", + "tailwindcss": "^4.1.7", "tslib": "^2.6.2", "typescript": "^5.7.3", "typescript-eslint": "^8.28.0", diff --git a/web/postcss.config.cjs b/web/postcss.config.cjs index 12a703d900..e5640725a9 100644 --- a/web/postcss.config.cjs +++ b/web/postcss.config.cjs @@ -1,6 +1,5 @@ module.exports = { plugins: { - tailwindcss: {}, - autoprefixer: {}, + '@tailwindcss/postcss': {}, }, }; diff --git a/web/src/app.css b/web/src/app.css index 1693aacab8..6160af1b8e 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1,6 +1,32 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss'; +@import '@immich/ui/theme/default.css'; + +@config '../tailwind.config.js'; + +@utility immich-form-input { + @apply rounded-xl bg-slate-200 px-3 py-3 text-sm focus:border-immich-primary disabled:cursor-not-allowed disabled:bg-gray-400 disabled:text-gray-100 dark:bg-gray-600 dark:text-immich-dark-fg dark:disabled:bg-gray-800 dark:disabled:text-gray-200; +} + +@utility immich-form-label { + @apply font-medium text-gray-500 dark:text-gray-300; +} + +@utility immich-scrollbar { + /* width */ + scrollbar-width: thin; +} + +@utility scrollbar-hidden { + /* Hidden scrollbar */ + /* width */ + scrollbar-width: none; +} + +@utility scrollbar-stable { + scrollbar-gutter: stable both-edges; +} + +@custom-variant dark (&:where(.dark, .dark *)); @layer base { :root { @@ -21,107 +47,97 @@ --immich-dark-success: 56 142 60; --immich-dark-warning: 245 124 0; } -} -@font-face { - font-family: 'Overpass'; - src: url('$lib/assets/fonts/overpass/Overpass.ttf') format('truetype-variations'); - font-weight: 1 999; - font-style: normal; - ascent-override: 106.25%; - size-adjust: 106.25%; -} + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: rgb(var(--immich-ui-default-border)); + } -@font-face { - font-family: 'Overpass Mono'; - src: url('$lib/assets/fonts/overpass/OverpassMono.ttf') format('truetype-variations'); - font-weight: 1 999; - font-style: monospace; - ascent-override: 106.25%; - size-adjust: 106.25%; -} - -:root { - font-family: 'Overpass', sans-serif; - /* Used by layouts to ensure proper spacing between navbar and content */ - --navbar-height: calc(theme(spacing.18) + 4px); - --navbar-height-md: calc(theme(spacing.18) + 4px - 14px); -} - -:root.dark { - color-scheme: dark; -} - -:root:not(.dark) { - color-scheme: light; -} - -html { - height: 100%; - width: 100%; -} - -html::-webkit-scrollbar { - width: 8px; -} - -/* Track */ -html::-webkit-scrollbar-track { - background: #f1f1f1; - border-radius: 16px; -} - -/* Handle */ -html::-webkit-scrollbar-thumb { - background: rgba(85, 86, 87, 0.408); - border-radius: 16px; -} - -/* Handle on hover */ -html::-webkit-scrollbar-thumb:hover { - background: #4250afad; - border-radius: 16px; -} - -body { - margin: 0; - color: #3a3a3a; -} - -input:focus-visible { - outline-offset: 0px !important; - outline: none !important; -} - -.text-white-shadow { - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); -} - -.icon-white-drop-shadow { - filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.8)); + button:not(:disabled), + [role='button']:not(:disabled) { + cursor: pointer; + } } @layer utilities { - .immich-form-input { - @apply rounded-xl bg-slate-200 px-3 py-3 text-sm focus:border-immich-primary disabled:cursor-not-allowed disabled:bg-gray-400 disabled:text-gray-100 dark:bg-gray-600 dark:text-immich-dark-fg dark:disabled:bg-gray-800 dark:disabled:text-gray-200; + @font-face { + font-family: 'Overpass'; + src: url('$lib/assets/fonts/overpass/Overpass.ttf') format('truetype-variations'); + font-weight: 1 999; + font-style: normal; + ascent-override: 106.25%; + size-adjust: 106.25%; } - .immich-form-label { - @apply font-medium text-gray-500 dark:text-gray-300; + @font-face { + font-family: 'Overpass Mono'; + src: url('$lib/assets/fonts/overpass/OverpassMono.ttf') format('truetype-variations'); + font-weight: 1 999; + font-style: monospace; + ascent-override: 106.25%; + size-adjust: 106.25%; } - /* width */ - .immich-scrollbar { - scrollbar-width: thin; + :root { + font-family: 'Overpass', sans-serif; + /* Used by layouts to ensure proper spacing between navbar and content */ + --navbar-height: calc(4.5rem + 4px); + --navbar-height-md: calc(4.5rem + 4px - 14px); } - /* Hidden scrollbar */ - /* width */ - .scrollbar-hidden { - scrollbar-width: none; + :root.dark { + color-scheme: dark; } - .scrollbar-stable { - scrollbar-gutter: stable both-edges; + :root:not(.dark) { + color-scheme: light; + } + + html { + height: 100%; + width: 100%; + } + + html::-webkit-scrollbar { + width: 8px; + } + + /* Track */ + html::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 16px; + } + + /* Handle */ + html::-webkit-scrollbar-thumb { + background: rgba(85, 86, 87, 0.408); + border-radius: 16px; + } + + /* Handle on hover */ + html::-webkit-scrollbar-thumb:hover { + background: #4250afad; + border-radius: 16px; + } + + body { + margin: 0; + color: #3a3a3a; + } + + input:focus-visible { + outline-offset: 0px !important; + outline: none !important; + } + + .text-white-shadow { + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); + } + + .icon-white-drop-shadow { + filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.8)); } } diff --git a/web/src/app.html b/web/src/app.html index 832b3265ef..776764850f 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -102,7 +102,7 @@ diff --git a/web/src/lib/components/album-page/album-card-group.svelte b/web/src/lib/components/album-page/album-card-group.svelte index 16455ec5bf..592baf9513 100644 --- a/web/src/lib/components/album-page/album-card-group.svelte +++ b/web/src/lib/components/album-page/album-card-group.svelte @@ -51,11 +51,7 @@ class="w-full text-start mt-2 pt-2 pe-2 pb-2 rounded-md transition-colors cursor-pointer dark:text-immich-dark-fg hover:text-immich-primary dark:hover:text-immich-dark-primary hover:bg-subtle dark:hover:bg-immich-dark-gray" aria-expanded={!isCollapsed} > - + {group.name} ({$t('albums_count', { values: { count: albums.length } })}) diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index 62216a750c..887c3a81e4 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -58,7 +58,7 @@ }} /> -
    +
    diff --git a/web/src/lib/components/album-page/albums-table.svelte b/web/src/lib/components/album-page/albums-table.svelte index 9f51f9a19a..ed509251df 100644 --- a/web/src/lib/components/album-page/albums-table.svelte +++ b/web/src/lib/components/album-page/albums-table.svelte @@ -56,7 +56,7 @@ {albumGroup.name} diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 9a52067feb..70600e6208 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -108,7 +108,7 @@
    {#if showCloseButton} diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index 84a021ca0b..c3df91623b 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -216,7 +216,7 @@ slow: ??ms -->
    @@ -279,7 +279,7 @@
    diff --git a/web/src/lib/components/elements/dropdown.svelte b/web/src/lib/components/elements/dropdown.svelte index ab3c446c31..36e76ae716 100644 --- a/web/src/lib/components/elements/dropdown.svelte +++ b/web/src/lib/components/elements/dropdown.svelte @@ -108,7 +108,7 @@ {#if showMenu}
    @@ -117,7 +117,7 @@ {@const buttonStyle = renderedOption.disabled ? '' : 'transition-all hover:bg-gray-300 dark:hover:bg-gray-800'} diff --git a/web/src/lib/components/places-page/places-list.svelte b/web/src/lib/components/places-page/places-list.svelte index 27eea3c5a8..e1fd858e3c 100644 --- a/web/src/lib/components/places-page/places-list.svelte +++ b/web/src/lib/components/places-page/places-list.svelte @@ -112,7 +112,7 @@ {/each} {/if} {:else} -
    +

    {$t('no_places')}

    diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index ef7c9d8cb6..b6d32f20b0 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -277,7 +277,7 @@ class:!rounded-b-none={isOpen && dropdownDirection === 'bottom'} class:!rounded-t-none={isOpen && dropdownDirection === 'top'} class:cursor-pointer={!isActive} - class="immich-form-input text-sm w-full !pe-12 transition-all" + class="immich-form-input text-sm w-full pe-12! transition-all" id={inputId} onfocus={activate} oninput={onInput} @@ -341,7 +341,7 @@ role="listbox" id={listboxId} transition:fly={{ duration: 250 }} - class="fixed z-[1] text-start text-sm w-full overflow-y-auto bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-900" + class="fixed z-1 text-start text-sm w-full overflow-y-auto bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-900" class:rounded-b-xl={dropdownDirection === 'bottom'} class:rounded-t-xl={dropdownDirection === 'top'} class:shadow={dropdownDirection === 'bottom'} diff --git a/web/src/lib/components/shared-components/context-menu/context-menu.svelte b/web/src/lib/components/shared-components/context-menu/context-menu.svelte index ad2a33dde2..e3e7c45c89 100644 --- a/web/src/lib/components/shared-components/context-menu/context-menu.svelte +++ b/web/src/lib/components/shared-components/context-menu/context-menu.svelte @@ -73,7 +73,7 @@ bind:this={menuElement} class="{isVisible ? 'max-h-dvh' - : 'max-h-0'} flex flex-col transition-all duration-[250ms] ease-in-out outline-none overflow-auto" + : 'max-h-0'} flex flex-col transition-all duration-250 ease-in-out outline-none overflow-auto" role="menu" tabindex="-1" > diff --git a/web/src/lib/components/shared-components/control-app-bar.svelte b/web/src/lib/components/shared-components/control-app-bar.svelte index 2cf4396ed9..0476ba6bfd 100644 --- a/web/src/lib/components/shared-components/control-app-bar.svelte +++ b/web/src/lib/components/shared-components/control-app-bar.svelte @@ -66,7 +66,7 @@ let buttonClass = $derived(forceDark ? 'hover:text-immich-dark-gray' : undefined); -
    +
    {/if}
    + {#if current}
    diff --git a/web/src/lib/components/photos-page/asset-select-control-bar.svelte b/web/src/lib/components/photos-page/asset-select-control-bar.svelte index 17b06c0e7e..b21693bc51 100644 --- a/web/src/lib/components/photos-page/asset-select-control-bar.svelte +++ b/web/src/lib/components/photos-page/asset-select-control-bar.svelte @@ -24,9 +24,10 @@ clearSelect: () => void; ownerId?: string | undefined; children?: Snippet; + forceDark?: boolean; } - let { assets, clearSelect, ownerId = undefined, children }: Props = $props(); + let { assets, clearSelect, ownerId = undefined, children, forceDark }: Props = $props(); setContext({ getAssets: () => assets, @@ -35,9 +36,11 @@ }); - + {#snippet leading()} -
    +

    {assets.length}

    diff --git a/web/src/lib/components/shared-components/album-selection/album-selection-modal.svelte b/web/src/lib/components/shared-components/album-selection/album-selection-modal.svelte index 3bc0161236..ab763546af 100644 --- a/web/src/lib/components/shared-components/album-selection/album-selection-modal.svelte +++ b/web/src/lib/components/shared-components/album-selection/album-selection-modal.svelte @@ -5,9 +5,9 @@ AlbumModalRowType, isSelectableRowType, } from '$lib/components/shared-components/album-selection/album-selection-utils'; - import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import { albumViewSettings } from '$lib/stores/preferences.store'; import { type AlbumResponseDto, getAllAlbums } from '@immich/sdk'; + import { Modal, ModalBody } from '@immich/ui'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; import AlbumListItem from '../../asset-viewer/album-list-item.svelte'; @@ -80,49 +80,51 @@ const handleAlbumClick = (album: AlbumResponseDto) => () => onAlbumClick(album); - -
    - {#if loading} - - {#each { length: 3 } as _} -
    -
    -
    - -
    - - + + +
    + {#if loading} + + {#each { length: 3 } as _} +
    +
    +
    + +
    + + +
    -
    - {/each} - {:else} - -
    - - {#each albumModalRows as row} - {#if row.type === AlbumModalRowType.NEW_ALBUM} - - {:else if row.type === AlbumModalRowType.SECTION} -

    {row.text}

    - {:else if row.type === AlbumModalRowType.MESSAGE} -

    {row.text}

    - {:else if row.type === AlbumModalRowType.ALBUM_ITEM && row.album} - - {/if} {/each} -
    - {/if} -
    - + {:else} + +
    + + {#each albumModalRows as row} + {#if row.type === AlbumModalRowType.NEW_ALBUM} + + {:else if row.type === AlbumModalRowType.SECTION} +

    {row.text}

    + {:else if row.type === AlbumModalRowType.MESSAGE} +

    {row.text}

    + {:else if row.type === AlbumModalRowType.ALBUM_ITEM && row.album} + + {/if} + {/each} +
    + {/if} +
    + + diff --git a/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte b/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte index 593baafc7c..9e5d5d2391 100644 --- a/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte +++ b/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte @@ -38,6 +38,10 @@ buttonClass?: string | undefined; hideContent?: boolean; children?: Snippet; + offset?: { + x: number; + y: number; + }; } & HTMLAttributes; let { @@ -51,6 +55,7 @@ buttonClass = undefined, hideContent = false, children, + offset, ...restProps }: Props = $props(); @@ -186,13 +191,14 @@ ]} > {@render children?.()} diff --git a/web/src/lib/components/shared-components/control-app-bar.svelte b/web/src/lib/components/shared-components/control-app-bar.svelte index 0476ba6bfd..6830dfefa0 100644 --- a/web/src/lib/components/shared-components/control-app-bar.svelte +++ b/web/src/lib/components/shared-components/control-app-bar.svelte @@ -66,7 +66,7 @@ let buttonClass = $derived(forceDark ? 'hover:text-immich-dark-gray' : undefined); -
    +
    {immichUser.email} + {immichUser.email} +