Merge remote-tracking branch 'origin/main' into keynav_timeline

This commit is contained in:
Min Idzelis 2025-05-20 21:29:04 +00:00
commit 94d2025718
110 changed files with 2115 additions and 5435 deletions

View File

@ -77,12 +77,22 @@ services:
- 5432:5432
healthcheck:
test: >-
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1;
Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align
--command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')";
echo "checksum failure count is $$Chksum";
[ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: >-
postgres -c shared_preload_libraries=vectors.so -c 'search_path="$$user", public, vectors' -c logging_collector=on -c max_wal_size=2GB -c shared_buffers=512MB -c wal_compression=on
postgres
-c shared_preload_libraries=vectors.so
-c 'search_path="$$user", public, vectors'
-c logging_collector=on
-c max_wal_size=2GB
-c shared_buffers=512MB
-c wal_compression=on
restart: always
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics

View File

@ -67,12 +67,22 @@ services:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
healthcheck:
test: >-
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1;
Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align
--command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')";
echo "checksum failure count is $$Chksum";
[ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: >-
postgres -c shared_preload_libraries=vectors.so -c 'search_path="$$user", public, vectors' -c logging_collector=on -c max_wal_size=2GB -c shared_buffers=512MB -c wal_compression=on
postgres
-c shared_preload_libraries=vectors.so
-c 'search_path="$$user", public, vectors'
-c logging_collector=on
-c max_wal_size=2GB
-c shared_buffers=512MB
-c wal_compression=on
restart: always
volumes:

View File

@ -75,11 +75,12 @@ npm run dev
To see local changes to `@immich/ui` in Immich, do the following:
1. Install `@immich/ui` as a sibling to `immich/`, for example `/home/user/immich` and `/home/user/ui`
1. Build the `@immich/ui` project via `npm run build`
1. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`)
1. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`)
1. Start up the stack via `make dev`
1. After making changes in `@immich/ui`, rebuild it (`npm run build`)
2. Build the `@immich/ui` project via `npm run build`
3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`)
4. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`)
5. Uncomment the import statement in `web/src/app.css` file `@import '/usr/ui/dist/theme/default.css';` and comment out `@import '@immich/ui/theme/default.css';`
6. Start up the stack via `make dev`
7. After making changes in `@immich/ui`, rebuild it (`npm run build`)
### Mobile app

View File

@ -1,4 +1,4 @@
import { AssetMediaResponseDto, AssetVisibility, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
import { AssetMediaResponseDto, AssetVisibility, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { DateTime } from 'luxon';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
@ -52,7 +52,7 @@ describe('/timeline', () => {
describe('GET /timeline/buckets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/timeline/buckets').query({ size: TimeBucketSize.Month });
const { status, body } = await request(app).get('/timeline/buckets');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
@ -60,8 +60,7 @@ describe('/timeline', () => {
it('should get time buckets by month', async () => {
const { status, body } = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ size: TimeBucketSize.Month });
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(
@ -78,33 +77,17 @@ describe('/timeline', () => {
assetIds: userAssets.map(({ id }) => id),
});
const { status, body } = await request(app)
.get('/timeline/buckets')
.query({ key: sharedLink.key, size: TimeBucketSize.Month });
const { status, body } = await request(app).get('/timeline/buckets').query({ key: sharedLink.key });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should get time buckets by day', async () => {
const { status, body } = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ size: TimeBucketSize.Day });
expect(status).toBe(200);
expect(body).toEqual([
{ count: 2, timeBucket: '1970-02-11T00:00:00.000Z' },
{ count: 1, timeBucket: '1970-02-10T00:00:00.000Z' },
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
]);
});
it('should return error if time bucket is requested with partners asset and archived', async () => {
const req1 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ size: TimeBucketSize.Month, withPartners: true, visibility: AssetVisibility.Archive });
.query({ withPartners: true, visibility: AssetVisibility.Archive });
expect(req1.status).toBe(400);
expect(req1.body).toEqual(errorDto.badRequest());
@ -112,7 +95,7 @@ describe('/timeline', () => {
const req2 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${user.accessToken}`)
.query({ size: TimeBucketSize.Month, withPartners: true, visibility: undefined });
.query({ withPartners: true, visibility: undefined });
expect(req2.status).toBe(400);
expect(req2.body).toEqual(errorDto.badRequest());
@ -122,7 +105,7 @@ describe('/timeline', () => {
const req1 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true });
.query({ withPartners: true, isFavorite: true });
expect(req1.status).toBe(400);
expect(req1.body).toEqual(errorDto.badRequest());
@ -130,7 +113,7 @@ describe('/timeline', () => {
const req2 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false });
.query({ withPartners: true, isFavorite: false });
expect(req2.status).toBe(400);
expect(req2.body).toEqual(errorDto.badRequest());
@ -140,7 +123,7 @@ describe('/timeline', () => {
const req = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${user.accessToken}`)
.query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true });
.query({ withPartners: true, isTrashed: true });
expect(req.status).toBe(400);
expect(req.body).toEqual(errorDto.badRequest());
@ -150,7 +133,6 @@ describe('/timeline', () => {
describe('GET /timeline/bucket', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/timeline/bucket').query({
size: TimeBucketSize.Month,
timeBucket: '1900-01-01',
});
@ -161,11 +143,27 @@ describe('/timeline', () => {
it('should handle 5 digit years', async () => {
const { status, body } = await request(app)
.get('/timeline/bucket')
.query({ size: TimeBucketSize.Month, timeBucket: '012345-01-01' })
.query({ timeBucket: '012345-01-01' })
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([]);
expect(body).toEqual({
city: [],
country: [],
duration: [],
id: [],
visibility: [],
isFavorite: [],
isImage: [],
isTrashed: [],
livePhotoVideoId: [],
localDateTime: [],
ownerId: [],
projectionType: [],
ratio: [],
status: [],
thumbhash: [],
});
});
// TODO enable date string validation while still accepting 5 digit years
@ -173,7 +171,7 @@ describe('/timeline', () => {
// const { status, body } = await request(app)
// .get('/timeline/bucket')
// .set('Authorization', `Bearer ${user.accessToken}`)
// .query({ size: TimeBucketSize.Month, timeBucket: 'foo' });
// .query({ timeBucket: 'foo' });
// expect(status).toBe(400);
// expect(body).toEqual(errorDto.badRequest);
@ -183,10 +181,26 @@ describe('/timeline', () => {
const { status, body } = await request(app)
.get('/timeline/bucket')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10' });
.query({ timeBucket: '1970-02-10' });
expect(status).toBe(200);
expect(body).toEqual([]);
expect(body).toEqual({
city: [],
country: [],
duration: [],
id: [],
visibility: [],
isFavorite: [],
isImage: [],
isTrashed: [],
livePhotoVideoId: [],
localDateTime: [],
ownerId: [],
projectionType: [],
ratio: [],
status: [],
thumbhash: [],
});
});
});
});

View File

@ -76,7 +76,6 @@
"library_watching_settings_description": "Dəyişdirilən faylları avtomatik olaraq yoxla",
"logging_enable_description": "Jurnalı aktivləşdir",
"logging_level_description": "Aktiv edildikdə hansı jurnal səviyyəsi istifadə olunur.",
"logging_settings": "",
"machine_learning_clip_model": "CLIP modeli",
"machine_learning_clip_model_description": "<link>Burada</link>qeyd olunan CLIP modelinin adı. Modeli dəyişdirdikdən sonra bütün şəkillər üçün 'Ağıllı Axtarış' funksiyasını yenidən işə salmalısınız.",
"machine_learning_duplicate_detection": "Dublikat Aşkarlama",

View File

@ -3,8 +3,6 @@
"account": "Akaont",
"account_settings": "Seting blo Akaont",
"acknowledge": "Akcept",
"action": "",
"actions": "",
"active": "Stap Mekem",
"activity": "Wanem hemi Mekem",
"activity_changed": "WAnem hemi Mekem hemi",
@ -16,845 +14,5 @@
"add_exclusion_pattern": "Putem wan paten wae hemi karem aot",
"add_import_path": "Putem wan pat blo import",
"add_location": "Putem wan place blo hem",
"add_more_users": "Putem mor man",
"add_partner": "",
"add_path": "",
"add_photos": "",
"add_to": "",
"add_to_album": "",
"add_to_shared_album": "",
"admin": {
"add_exclusion_pattern_description": "",
"authentication_settings": "",
"authentication_settings_description": "",
"background_task_job": "",
"check_all": "",
"config_set_by_file": "",
"confirm_delete_library": "",
"confirm_delete_library_assets": "",
"confirm_email_below": "",
"confirm_reprocess_all_faces": "",
"confirm_user_password_reset": "",
"disable_login": "",
"duplicate_detection_job_description": "",
"exclusion_pattern_description": "",
"external_library_created_at": "",
"external_library_management": "",
"face_detection": "",
"face_detection_description": "",
"facial_recognition_job_description": "",
"force_delete_user_warning": "",
"forcing_refresh_library_files": "",
"image_format_description": "",
"image_prefer_embedded_preview": "",
"image_prefer_embedded_preview_setting_description": "",
"image_prefer_wide_gamut": "",
"image_prefer_wide_gamut_setting_description": "",
"image_quality": "",
"image_settings": "",
"image_settings_description": "",
"job_concurrency": "",
"job_not_concurrency_safe": "",
"job_settings": "",
"job_settings_description": "",
"job_status": "",
"jobs_delayed": "",
"jobs_failed": "",
"library_created": "",
"library_deleted": "",
"library_import_path_description": "",
"library_scanning": "",
"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": "",
"machine_learning_clip_model": "",
"machine_learning_duplicate_detection": "",
"machine_learning_duplicate_detection_enabled_description": "",
"machine_learning_duplicate_detection_setting_description": "",
"machine_learning_enabled_description": "",
"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_description": "",
"machine_learning_max_detection_distance": "",
"machine_learning_max_detection_distance_description": "",
"machine_learning_max_recognition_distance": "",
"machine_learning_max_recognition_distance_description": "",
"machine_learning_min_detection_score": "",
"machine_learning_min_detection_score_description": "",
"machine_learning_min_recognized_faces": "",
"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_description": "",
"machine_learning_url_description": "",
"manage_concurrency": "",
"manage_log_settings": "",
"map_dark_style": "",
"map_enable_description": "",
"map_light_style": "",
"map_reverse_geocoding": "",
"map_reverse_geocoding_enable_description": "",
"map_reverse_geocoding_settings": "",
"map_settings": "",
"map_settings_description": "",
"map_style_description": "",
"metadata_extraction_job": "",
"metadata_extraction_job_description": "",
"migration_job": "",
"migration_job_description": "",
"no_paths_added": "",
"no_pattern_added": "",
"note_apply_storage_label_previous_assets": "",
"note_cannot_be_changed_later": "",
"notification_email_from_address": "",
"notification_email_from_address_description": "",
"notification_email_host_description": "",
"notification_email_ignore_certificate_errors": "",
"notification_email_ignore_certificate_errors_description": "",
"notification_email_password_description": "",
"notification_email_port_description": "",
"notification_email_sent_test_email_button": "",
"notification_email_setting_description": "",
"notification_email_test_email_failed": "",
"notification_email_test_email_sent": "",
"notification_email_username_description": "",
"notification_enable_email_notifications": "",
"notification_settings": "",
"notification_settings_description": "",
"oauth_auto_launch": "",
"oauth_auto_launch_description": "",
"oauth_auto_register": "",
"oauth_auto_register_description": "",
"oauth_button_text": "",
"oauth_enable_description": "",
"oauth_mobile_redirect_uri": "",
"oauth_mobile_redirect_uri_override": "",
"oauth_mobile_redirect_uri_override_description": "",
"oauth_settings": "",
"oauth_settings_description": "",
"oauth_storage_label_claim": "",
"oauth_storage_label_claim_description": "",
"oauth_storage_quota_claim": "",
"oauth_storage_quota_claim_description": "",
"oauth_storage_quota_default": "",
"oauth_storage_quota_default_description": "",
"offline_paths": "",
"offline_paths_description": "",
"password_enable_description": "",
"password_settings": "",
"password_settings_description": "",
"paths_validated_successfully": "",
"quota_size_gib": "",
"refreshing_all_libraries": "",
"repair_all": "",
"repair_matched_items": "",
"repaired_items": "",
"require_password_change_on_login": "",
"reset_settings_to_default": "",
"reset_settings_to_recent_saved": "",
"send_welcome_email": "",
"server_external_domain_settings": "",
"server_external_domain_settings_description": "",
"server_settings": "",
"server_settings_description": "",
"server_welcome_message": "",
"server_welcome_message_description": "",
"sidecar_job": "",
"sidecar_job_description": "",
"slideshow_duration_description": "",
"smart_search_job_description": "",
"storage_template_enable_description": "",
"storage_template_hash_verification_enabled": "",
"storage_template_hash_verification_enabled_description": "",
"storage_template_migration": "",
"storage_template_migration_job": "",
"storage_template_settings": "",
"storage_template_settings_description": "",
"system_settings": "",
"theme_custom_css_settings": "",
"theme_custom_css_settings_description": "",
"theme_settings": "",
"theme_settings_description": "",
"these_files_matched_by_checksum": "",
"thumbnail_generation_job": "",
"thumbnail_generation_job_description": "",
"transcoding_acceleration_api": "",
"transcoding_acceleration_api_description": "",
"transcoding_acceleration_nvenc": "",
"transcoding_acceleration_qsv": "",
"transcoding_acceleration_rkmpp": "",
"transcoding_acceleration_vaapi": "",
"transcoding_accepted_audio_codecs": "",
"transcoding_accepted_audio_codecs_description": "",
"transcoding_accepted_video_codecs": "",
"transcoding_accepted_video_codecs_description": "",
"transcoding_advanced_options_description": "",
"transcoding_audio_codec": "",
"transcoding_audio_codec_description": "",
"transcoding_bitrate_description": "",
"transcoding_constant_quality_mode": "",
"transcoding_constant_quality_mode_description": "",
"transcoding_constant_rate_factor": "",
"transcoding_constant_rate_factor_description": "",
"transcoding_disabled_description": "",
"transcoding_hardware_acceleration": "",
"transcoding_hardware_acceleration_description": "",
"transcoding_hardware_decoding": "",
"transcoding_hardware_decoding_setting_description": "",
"transcoding_hevc_codec": "",
"transcoding_max_b_frames": "",
"transcoding_max_b_frames_description": "",
"transcoding_max_bitrate": "",
"transcoding_max_bitrate_description": "",
"transcoding_max_keyframe_interval": "",
"transcoding_max_keyframe_interval_description": "",
"transcoding_optimal_description": "",
"transcoding_preferred_hardware_device": "",
"transcoding_preferred_hardware_device_description": "",
"transcoding_preset_preset": "",
"transcoding_preset_preset_description": "",
"transcoding_reference_frames": "",
"transcoding_reference_frames_description": "",
"transcoding_required_description": "",
"transcoding_settings": "",
"transcoding_settings_description": "",
"transcoding_target_resolution": "",
"transcoding_target_resolution_description": "",
"transcoding_temporal_aq": "",
"transcoding_temporal_aq_description": "",
"transcoding_threads": "",
"transcoding_threads_description": "",
"transcoding_tone_mapping": "",
"transcoding_tone_mapping_description": "",
"transcoding_transcode_policy": "",
"transcoding_transcode_policy_description": "",
"transcoding_two_pass_encoding": "",
"transcoding_two_pass_encoding_setting_description": "",
"transcoding_video_codec": "",
"transcoding_video_codec_description": "",
"trash_enabled_description": "",
"trash_number_of_days": "",
"trash_number_of_days_description": "",
"trash_settings": "",
"trash_settings_description": "",
"untracked_files": "",
"untracked_files_description": "",
"user_delete_delay_settings": "",
"user_delete_delay_settings_description": "",
"user_management": "",
"user_password_has_been_reset": "",
"user_password_reset_description": "",
"user_settings": "",
"user_settings_description": "",
"user_successfully_removed": "",
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job": "",
"video_conversion_job_description": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"albums_count": "",
"all": "",
"all_people": "",
"allow_dark_mode": "",
"allow_edits": "",
"api_key": "",
"api_keys": "",
"app_settings": "",
"appears_in": "",
"archive": "",
"archive_or_unarchive_photo": "",
"asset_offline": "",
"assets": "",
"authorized_devices": "",
"back": "",
"backward": "",
"blurred_background": "",
"camera": "",
"camera_brand": "",
"camera_model": "",
"cancel": "",
"cancel_search": "",
"cannot_merge_people": "",
"cannot_update_the_description": "",
"change_date": "",
"change_expiration_time": "",
"change_location": "",
"change_name": "",
"change_name_successfully": "",
"change_password": "",
"change_your_password": "",
"changed_visibility_successfully": "",
"check_all": "",
"check_logs": "",
"choose_matching_people_to_merge": "",
"city": "",
"clear": "",
"clear_all": "",
"clear_message": "",
"clear_value": "",
"close": "",
"collapse_all": "",
"color_theme": "",
"comment_options": "",
"comments_are_disabled": "",
"confirm": "",
"confirm_admin_password": "",
"confirm_delete_shared_link": "",
"confirm_password": "",
"contain": "",
"context": "",
"continue": "",
"copied_image_to_clipboard": "",
"copied_to_clipboard": "",
"copy_error": "",
"copy_file_path": "",
"copy_image": "",
"copy_link": "",
"copy_link_to_clipboard": "",
"copy_password": "",
"copy_to_clipboard": "",
"country": "",
"cover": "",
"covers": "",
"create": "",
"create_album": "",
"create_library": "",
"create_link": "",
"create_link_to_share": "",
"create_new_person": "",
"create_new_user": "",
"create_user": "",
"created": "",
"current_device": "",
"custom_locale": "",
"custom_locale_description": "",
"dark": "",
"date_after": "",
"date_and_time": "",
"date_before": "",
"date_range": "",
"day": "",
"default_locale": "",
"default_locale_description": "",
"delete": "",
"delete_album": "",
"delete_api_key_prompt": "",
"delete_key": "",
"delete_library": "",
"delete_link": "",
"delete_shared_link": "",
"delete_user": "",
"deleted_shared_link": "",
"description": "",
"details": "",
"direction": "",
"disabled": "",
"disallow_edits": "",
"discover": "",
"dismiss_all_errors": "",
"dismiss_error": "",
"display_options": "",
"display_order": "",
"display_original_photos": "",
"display_original_photos_setting_description": "",
"done": "",
"download": "",
"downloading": "",
"duration": "",
"edit_album": "",
"edit_avatar": "",
"edit_date": "",
"edit_date_and_time": "",
"edit_exclusion_pattern": "",
"edit_faces": "",
"edit_import_path": "",
"edit_import_paths": "",
"edit_key": "",
"edit_link": "",
"edit_location": "",
"edit_name": "",
"edit_people": "",
"edit_title": "",
"edit_user": "",
"edited": "",
"editor": "",
"email": "",
"empty_trash": "",
"enable": "",
"enabled": "",
"end_date": "",
"error": "",
"error_loading_image": "",
"errors": {
"cleared_jobs": "",
"exclusion_pattern_already_exists": "",
"failed_job_command": "",
"import_path_already_exists": "",
"paths_validation_failed": "",
"quota_higher_than_disk_size": "",
"repair_unable_to_check_items": "",
"unable_to_add_album_users": "",
"unable_to_add_comment": "",
"unable_to_add_exclusion_pattern": "",
"unable_to_add_import_path": "",
"unable_to_add_partners": "",
"unable_to_change_album_user_role": "",
"unable_to_change_date": "",
"unable_to_change_location": "",
"unable_to_change_password": "",
"unable_to_copy_to_clipboard": "",
"unable_to_create_api_key": "",
"unable_to_create_library": "",
"unable_to_create_user": "",
"unable_to_delete_album": "",
"unable_to_delete_asset": "",
"unable_to_delete_exclusion_pattern": "",
"unable_to_delete_import_path": "",
"unable_to_delete_shared_link": "",
"unable_to_delete_user": "",
"unable_to_edit_exclusion_pattern": "",
"unable_to_edit_import_path": "",
"unable_to_empty_trash": "",
"unable_to_enter_fullscreen": "",
"unable_to_exit_fullscreen": "",
"unable_to_hide_person": "",
"unable_to_link_oauth_account": "",
"unable_to_load_album": "",
"unable_to_load_asset_activity": "",
"unable_to_load_items": "",
"unable_to_load_liked_status": "",
"unable_to_play_video": "",
"unable_to_refresh_user": "",
"unable_to_remove_album_users": "",
"unable_to_remove_api_key": "",
"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_resolve_duplicate": "",
"unable_to_restore_assets": "",
"unable_to_restore_trash": "",
"unable_to_restore_user": "",
"unable_to_save_album": "",
"unable_to_save_api_key": "",
"unable_to_save_name": "",
"unable_to_save_profile": "",
"unable_to_save_settings": "",
"unable_to_scan_libraries": "",
"unable_to_scan_library": "",
"unable_to_set_profile_picture": "",
"unable_to_submit_job": "",
"unable_to_trash_asset": "",
"unable_to_unlink_account": "",
"unable_to_update_library": "",
"unable_to_update_location": "",
"unable_to_update_settings": "",
"unable_to_update_timeline_display_status": "",
"unable_to_update_user": ""
},
"exit_slideshow": "",
"expand_all": "",
"expire_after": "",
"expired": "",
"explore": "",
"export": "",
"export_as_json": "",
"extension": "",
"external": "",
"external_libraries": "",
"favorite": "",
"favorite_or_unfavorite_photo": "",
"favorites": "",
"feature_photo_updated": "",
"file_name": "",
"file_name_or_extension": "",
"filename": "",
"filetype": "",
"filter_people": "",
"find_them_fast": "",
"fix_incorrect_match": "",
"forward": "",
"general": "",
"get_help": "",
"getting_started": "",
"go_back": "",
"go_to_search": "",
"group_albums_by": "",
"has_quota": "",
"hide_gallery": "",
"hide_password": "",
"hide_person": "",
"host": "",
"hour": "",
"image": "",
"immich_logo": "",
"import_from_json": "",
"import_path": "",
"in_archive": "",
"include_archived": "",
"include_shared_albums": "",
"include_shared_partner_assets": "",
"individual_share": "",
"info": "",
"interval": {
"day_at_onepm": "",
"hours": "",
"night_at_midnight": "",
"night_at_twoam": ""
},
"invite_people": "",
"invite_to_album": "",
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
"level": "",
"library": "",
"library_options": "",
"light": "",
"link_options": "",
"link_to_oauth": "",
"linked_oauth_account": "",
"list": "",
"loading": "",
"loading_search_results_failed": "",
"log_out": "",
"log_out_all_devices": "",
"login_has_been_disabled": "",
"look": "",
"loop_videos": "",
"loop_videos_description": "",
"make": "",
"manage_shared_links": "",
"manage_sharing_with_partners": "",
"manage_the_app_settings": "",
"manage_your_account": "",
"manage_your_api_keys": "",
"manage_your_devices": "",
"manage_your_oauth_connection": "",
"map": "",
"map_marker_with_image": "",
"map_settings": "",
"matches": "",
"media_type": "",
"memories": "",
"memories_setting_description": "",
"menu": "",
"merge": "",
"merge_people": "",
"merge_people_successfully": "",
"minimize": "",
"minute": "",
"missing": "",
"model": "",
"month": "",
"more": "",
"moved_to_trash": "",
"my_albums": "",
"name": "",
"name_or_nickname": "",
"never": "",
"new_api_key": "",
"new_password": "",
"new_person": "",
"new_user_created": "",
"newest_first": "",
"next": "",
"next_memory": "",
"no": "",
"no_albums_message": "",
"no_archived_assets_message": "",
"no_assets_message": "",
"no_duplicates_found": "",
"no_exif_info_available": "",
"no_explore_results_message": "",
"no_favorites_message": "",
"no_libraries_message": "",
"no_name": "",
"no_places": "",
"no_results": "",
"no_shared_albums_message": "",
"not_in_any_album": "",
"note_apply_storage_label_to_previously_uploaded assets": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"offline_paths": "",
"offline_paths_description": "",
"ok": "",
"oldest_first": "",
"online": "",
"only_favorites": "",
"open_the_search_filters": "",
"options": "",
"organize_your_library": "",
"other": "",
"other_devices": "",
"other_variables": "",
"owned": "",
"owner": "",
"partner_can_access": "",
"partner_can_access_assets": "",
"partner_can_access_location": "",
"partner_sharing": "",
"partners": "",
"password": "",
"password_does_not_match": "",
"password_required": "",
"password_reset_success": "",
"past_durations": {
"days": "",
"hours": "",
"years": ""
},
"path": "",
"pattern": "",
"pause": "",
"pause_memories": "",
"paused": "",
"pending": "",
"people": "",
"people_sidebar_description": "",
"permanent_deletion_warning": "",
"permanent_deletion_warning_setting_description": "",
"permanently_delete": "",
"permanently_deleted_asset": "",
"photos": "",
"photos_count": "",
"photos_from_previous_years": "",
"pick_a_location": "",
"place": "",
"places": "",
"play": "",
"play_memories": "",
"play_motion_photo": "",
"play_or_pause_video": "",
"port": "",
"preset": "",
"preview": "",
"previous": "",
"previous_memory": "",
"previous_or_next_photo": "",
"primary": "",
"profile_picture_set": "",
"public_share": "",
"reaction_options": "",
"read_changelog": "",
"recent": "",
"recent_searches": "",
"refresh": "",
"refreshed": "",
"refreshes_every_file": "",
"remove": "",
"remove_deleted_assets": "",
"remove_from_album": "",
"remove_from_favorites": "",
"remove_from_shared_link": "",
"removed_api_key": "",
"rename": "",
"repair": "",
"repair_no_results_message": "",
"replace_with_upload": "",
"require_password": "",
"require_user_to_change_password_on_first_login": "",
"reset": "",
"reset_password": "",
"reset_people_visibility": "",
"restore": "",
"restore_all": "",
"restore_user": "",
"resume": "",
"retry_upload": "",
"review_duplicates": "",
"role": "",
"save": "",
"saved_api_key": "",
"saved_profile": "",
"saved_settings": "",
"say_something": "",
"scan_all_libraries": "",
"scan_settings": "",
"search": "",
"search_albums": "",
"search_by_context": "",
"search_camera_make": "",
"search_camera_model": "",
"search_city": "",
"search_country": "",
"search_for_existing_person": "",
"search_people": "",
"search_places": "",
"search_state": "",
"search_timezone": "",
"search_type": "",
"search_your_photos": "",
"searching_locales": "",
"second": "",
"select_album_cover": "",
"select_all": "",
"select_avatar_color": "",
"select_face": "",
"select_featured_photo": "",
"select_keep_all": "",
"select_library_owner": "",
"select_new_face": "",
"select_photos": "",
"select_trash_all": "",
"selected": "",
"send_message": "",
"send_welcome_email": "",
"server_stats": "",
"set": "",
"set_as_album_cover": "",
"set_as_profile_picture": "",
"set_date_of_birth": "",
"set_profile_picture": "",
"set_slideshow_to_fullscreen": "",
"settings": "",
"settings_saved": "",
"share": "",
"shared": "",
"shared_by": "",
"shared_by_you": "",
"shared_from_partner": "",
"shared_links": "",
"shared_with_partner": "",
"sharing": "",
"sharing_sidebar_description": "",
"show_album_options": "",
"show_and_hide_people": "",
"show_file_location": "",
"show_gallery": "",
"show_hidden_people": "",
"show_in_timeline": "",
"show_in_timeline_setting_description": "",
"show_keyboard_shortcuts": "",
"show_metadata": "",
"show_or_hide_info": "",
"show_password": "",
"show_person_options": "",
"show_progress_bar": "",
"show_search_options": "",
"shuffle": "",
"sign_out": "",
"sign_up": "",
"size": "",
"skip_to_content": "",
"slideshow": "",
"slideshow_settings": "",
"sort_albums_by": "",
"stack": "",
"stack_selected_photos": "",
"stacktrace": "",
"start": "",
"start_date": "",
"state": "",
"status": "",
"stop_motion_photo": "",
"stop_photo_sharing": "",
"stop_photo_sharing_description": "",
"stop_sharing_photos_with_user": "",
"storage": "",
"storage_label": "",
"storage_usage": "",
"submit": "",
"suggestions": "",
"sunrise_on_the_beach": "",
"swap_merge_direction": "",
"sync": "",
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"to_archive": "",
"to_favorite": "",
"toggle_settings": "",
"toggle_theme": "",
"total_usage": "",
"trash": "",
"trash_all": "",
"trash_no_results_message": "",
"trashed_items_will_be_permanently_deleted_after": "",
"type": "",
"unarchive": "",
"unfavorite": "",
"unhide_person": "",
"unknown": "",
"unknown_year": "",
"unlimited": "",
"unlink_oauth": "",
"unlinked_oauth_account": "",
"unselect_all": "",
"unstack": "",
"untracked_files": "",
"untracked_files_decription": "",
"up_next": "",
"updated_password": "",
"upload": "",
"upload_concurrency": "",
"url": "",
"usage": "",
"user": "",
"user_id": "",
"user_usage_detail": "",
"username": "",
"users": "",
"utilities": "",
"validate": "",
"variables": "",
"version": "",
"video": "",
"video_hover_setting": "",
"video_hover_setting_description": "",
"videos": "",
"videos_count": "",
"view_all": "",
"view_all_users": "",
"view_links": "",
"view_next_asset": "",
"view_previous_asset": "",
"waiting": "",
"week": "",
"welcome_to_immich": "",
"year": "",
"yes": "",
"you_dont_have_any_shared_links": "",
"zoom_image": ""
"add_more_users": "Putem mor man"
}

View File

@ -65,8 +65,6 @@
"job_settings": "تنظیمات کار",
"job_settings_description": "مدیریت همزمانی کار",
"job_status": "وضعیت کار",
"jobs_delayed": "",
"jobs_failed": "",
"library_created": "کتابخانه ایجاد شده: {library}",
"library_deleted": "کتابخانه حذف شد",
"library_import_path_description": "یک پوشه برای وارد کردن مشخص کنید. این پوشه، به همراه زیرپوشه‌ها، برای یافتن تصاویر و ویدیوها اسکن خواهد شد.",
@ -128,7 +126,6 @@
"metadata_extraction_job": "استخراج فرا داده",
"metadata_extraction_job_description": "استخراج اطلاعات ابرداده، مانند موقعیت جغرافیایی و کیفیت از هر فایل",
"migration_job": "مهاجرت",
"migration_job_description": "",
"no_paths_added": "هیچ مسیری اضافه نشده",
"no_pattern_added": "هیچ الگوی اضافه نشده",
"note_apply_storage_label_previous_assets": "توجه: برای اعمال برچسب ذخیره سازی به دارایی هایی که قبلاً بارگذاری شده اند، دستور زیر را اجرا کنید",
@ -178,8 +175,6 @@
"registration": "ثبت نام مدیر",
"registration_description": "از آنجایی که شما اولین کاربر در سیستم هستید، به عنوان مدیر تعیین شده‌اید و مسئولیت انجام وظایف مدیریتی بر عهده شما خواهد بود و کاربران اضافی توسط شما ایجاد خواهند شد.",
"repair_all": "بازسازی همه",
"repair_matched_items": "",
"repaired_items": "",
"require_password_change_on_login": "الزام کاربر به تغییر گذرواژه در اولین ورود",
"reset_settings_to_default": "بازنشانی تنظیمات به حالت پیش‌فرض",
"reset_settings_to_recent_saved": "بازنشانی تنظیمات به آخرین تنظیمات ذخیره شده",
@ -196,7 +191,6 @@
"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": "انتقال الگوی ذخیره سازی",
@ -242,7 +236,6 @@
"transcoding_hardware_acceleration": "شتاب دهنده سخت افزاری",
"transcoding_hardware_acceleration_description": "آزمایشی؛ بسیار سریع‌تر است، اما در همان بیت‌ریت کیفیت کمتری خواهد داشت",
"transcoding_hardware_decoding": "رمزگشایی سخت افزاری",
"transcoding_hardware_decoding_setting_description": "",
"transcoding_hevc_codec": "کدک HEVC",
"transcoding_max_b_frames": "بیشترین B-frames",
"transcoding_max_b_frames_description": "مقادیر بالاتر کارایی فشرده سازی را بهبود می‌بخشند، اما کدگذاری را کند می‌کنند. ممکن است با شتاب دهی سخت‌افزاری در دستگاه‌های قدیمی سازگار نباشد. مقدار( 0 ) B-frames را غیرفعال می‌کند، در حالی که مقدار ( 1 ) این مقدار را به صورت خودکار تنظیم می‌کند.",
@ -266,7 +259,6 @@
"transcoding_temporal_aq_description": "این مورد فقط برای NVENC اعمال می شود. افزایش کیفیت در صحنه های با جزئیات بالا و حرکت کم. ممکن است با دستگاه های قدیمی تر سازگار نباشد.",
"transcoding_threads": "رشته ها ( موضوعات )",
"transcoding_threads_description": "مقادیر بالاتر منجر به رمزگذاری سریع تر می شود، اما فضای کمتری برای پردازش سایر وظایف سرور در حین فعالیت باقی می گذارد. این مقدار نباید بیشتر از تعداد هسته های CPU باشد. اگر روی 0 تنظیم شود، بیشترین استفاده را خواهد داشت.",
"transcoding_tone_mapping": "",
"transcoding_tone_mapping_description": "تلاش برای حفظ ظاهر ویدیوهای HDR هنگام تبدیل به SDR. هر الگوریتم تعادل های متفاوتی را برای رنگ، جزئیات و روشنایی ایجاد می کند. Hable جزئیات را حفظ می کند، Mobius رنگ را حفظ می کند و Reinhard روشنایی را حفظ می کند.",
"transcoding_transcode_policy": "سیاست رمزگذاری",
"transcoding_transcode_policy_description": "سیاست برای زمانی که ویدیویی باید مجددا تبدیل (رمزگذاری) شود. ویدیوهای HDR همیشه تبدیل (رمزگذاری) مجدد خواهند شد (مگر رمزگذاری مجدد غیرفعال باشد).",
@ -306,15 +298,12 @@
"administration": "مدیریت",
"advanced": "پیشرفته",
"album_added": "آلبوم اضافه شد",
"album_added_notification_setting_description": "",
"album_cover_updated": "جلد آلبوم به‌روزرسانی شد",
"album_info_updated": "اطلاعات آلبوم به‌روزرسانی شد",
"album_name": "نام آلبوم",
"album_options": "گزینه‌های آلبوم",
"album_updated": "آلبوم به‌روزرسانی شد",
"album_updated_setting_description": "",
"albums": "آلبوم‌ها",
"albums_count": "",
"all": "همه",
"all_people": "همه افراد",
"allow_dark_mode": "اجازه دادن به حالت تاریک",
@ -324,18 +313,13 @@
"app_settings": "تنظیمات برنامه",
"appears_in": "ظاهر می‌شود در",
"archive": "بایگانی",
"archive_or_unarchive_photo": "",
"archive_size": "اندازه بایگانی",
"archive_size_description": "",
"asset_offline": "محتوا آفلاین",
"assets": "محتواها",
"authorized_devices": "دستگاه‌های مجاز",
"back": "بازگشت",
"backward": "عقب",
"blurred_background": "پس‌زمینه محو",
"bulk_delete_duplicates_confirmation": "",
"bulk_keep_duplicates_confirmation": "",
"bulk_trash_duplicates_confirmation": "",
"camera": "دوربین",
"camera_brand": "برند دوربین",
"camera_model": "مدل دوربین",
@ -350,10 +334,8 @@
"change_name_successfully": "نام با موفقیت تغییر یافت",
"change_password": "تغییر رمز عبور",
"change_your_password": "رمز عبور خود را تغییر دهید",
"changed_visibility_successfully": "",
"check_all": "انتخاب همه",
"check_logs": "بررسی لاگ‌ها",
"choose_matching_people_to_merge": "",
"city": "شهر",
"clear": "پاک کردن",
"clear_all": "پاک کردن همه",
@ -366,7 +348,6 @@
"comments_are_disabled": "نظرات غیرفعال هستند",
"confirm": "تأیید",
"confirm_admin_password": "تأیید رمز عبور مدیر",
"confirm_delete_shared_link": "",
"confirm_password": "تأیید رمز عبور",
"contain": "شامل",
"context": "زمینه",
@ -393,8 +374,6 @@
"create_user": "ایجاد کاربر",
"created": "ایجاد شد",
"current_device": "دستگاه فعلی",
"custom_locale": "",
"custom_locale_description": "",
"dark": "تاریک",
"date_after": "تاریخ پس از",
"date_and_time": "تاریخ و زمان",
@ -402,12 +381,8 @@
"date_range": "بازه زمانی",
"day": "روز",
"deduplicate_all": "حذف تکراری‌ها به صورت کامل",
"default_locale": "",
"default_locale_description": "",
"delete": "حذف",
"delete_album": "حذف آلبوم",
"delete_api_key_prompt": "",
"delete_duplicates_confirmation": "",
"delete_key": "حذف کلید",
"delete_library": "حذف کتابخانه",
"delete_link": "حذف لینک",
@ -425,14 +400,12 @@
"display_options": "گزینه‌های نمایش",
"display_order": "ترتیب نمایش",
"display_original_photos": "نمایش عکس‌های اصلی",
"display_original_photos_setting_description": "",
"done": "انجام شد",
"download": "دانلود",
"download_settings": "تنظیمات دانلود",
"download_settings_description": "مدیریت تنظیمات مرتبط با دانلود محتوا",
"downloading": "در حال دانلود",
"duplicates": "تکراری‌ها",
"duplicates_description": "",
"duration": "مدت زمان",
"edit_album": "ویرایش آلبوم",
"edit_avatar": "ویرایش آواتار",
@ -440,8 +413,6 @@
"edit_date_and_time": "ویرایش تاریخ و زمان",
"edit_exclusion_pattern": "ویرایش الگوی استثناء",
"edit_faces": "ویرایش چهره‌ها",
"edit_import_path": "",
"edit_import_paths": "",
"edit_key": "ویرایش کلید",
"edit_link": "ویرایش لینک",
"edit_location": "ویرایش مکان",
@ -456,73 +427,6 @@
"end_date": "تاریخ پایان",
"error": "خطا",
"error_loading_image": "خطا در بارگذاری تصویر",
"errors": {
"exclusion_pattern_already_exists": "",
"import_path_already_exists": "",
"paths_validation_failed": "",
"quota_higher_than_disk_size": "",
"repair_unable_to_check_items": "",
"unable_to_add_album_users": "",
"unable_to_add_comment": "",
"unable_to_add_exclusion_pattern": "",
"unable_to_add_import_path": "",
"unable_to_add_partners": "",
"unable_to_change_album_user_role": "",
"unable_to_change_date": "",
"unable_to_change_location": "",
"unable_to_change_password": "",
"unable_to_copy_to_clipboard": "",
"unable_to_create_api_key": "",
"unable_to_create_library": "",
"unable_to_create_user": "",
"unable_to_delete_album": "",
"unable_to_delete_asset": "",
"unable_to_delete_exclusion_pattern": "",
"unable_to_delete_import_path": "",
"unable_to_delete_shared_link": "",
"unable_to_delete_user": "",
"unable_to_edit_exclusion_pattern": "",
"unable_to_edit_import_path": "",
"unable_to_empty_trash": "",
"unable_to_enter_fullscreen": "",
"unable_to_exit_fullscreen": "",
"unable_to_hide_person": "",
"unable_to_link_oauth_account": "",
"unable_to_load_album": "",
"unable_to_load_asset_activity": "",
"unable_to_load_items": "",
"unable_to_load_liked_status": "",
"unable_to_play_video": "",
"unable_to_refresh_user": "",
"unable_to_remove_album_users": "",
"unable_to_remove_api_key": "",
"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_resolve_duplicate": "",
"unable_to_restore_assets": "",
"unable_to_restore_trash": "",
"unable_to_restore_user": "",
"unable_to_save_album": "",
"unable_to_save_api_key": "",
"unable_to_save_name": "",
"unable_to_save_profile": "",
"unable_to_save_settings": "",
"unable_to_scan_libraries": "",
"unable_to_scan_library": "",
"unable_to_set_profile_picture": "",
"unable_to_submit_job": "",
"unable_to_trash_asset": "",
"unable_to_unlink_account": "",
"unable_to_update_library": "",
"unable_to_update_location": "",
"unable_to_update_settings": "",
"unable_to_update_timeline_display_status": "",
"unable_to_update_user": ""
},
"exit_slideshow": "خروج از نمایش اسلاید",
"expand_all": "باز کردن همه",
"expire_after": "منقضی شدن بعد از",
@ -534,15 +438,12 @@
"external": "خارجی",
"external_libraries": "کتابخانه‌های خارجی",
"favorite": "علاقه‌مندی",
"favorite_or_unfavorite_photo": "",
"favorites": "علاقه‌مندی‌ها",
"feature_photo_updated": "",
"file_name": "نام فایل",
"file_name_or_extension": "نام فایل یا پسوند",
"filename": "نام فایل",
"filetype": "نوع فایل",
"filter_people": "فیلتر افراد",
"find_them_fast": "",
"fix_incorrect_match": "رفع تطابق نادرست",
"forward": "جلو",
"general": "عمومی",
@ -562,19 +463,11 @@
"immich_web_interface": "رابط وب Immich",
"import_from_json": "وارد کردن از JSON",
"import_path": "مسیر وارد کردن",
"in_albums": "",
"in_archive": "در بایگانی",
"include_archived": "شامل بایگانی شده‌ها",
"include_shared_albums": "شامل آلبوم‌های اشتراکی",
"include_shared_partner_assets": "",
"individual_share": "اشتراک فردی",
"info": "اطلاعات",
"interval": {
"day_at_onepm": "",
"hours": "",
"night_at_midnight": "",
"night_at_twoam": ""
},
"invite_people": "دعوت افراد",
"invite_to_album": "دعوت به آلبوم",
"jobs": "وظایف",
@ -601,28 +494,22 @@
"login_has_been_disabled": "ورود غیرفعال شده است.",
"look": "نگاه کردن",
"loop_videos": "پخش مداوم ویدئوها",
"loop_videos_description": "",
"make": "ساختن",
"manage_shared_links": "مدیریت لینک‌های اشتراکی",
"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_marker_with_image": "",
"map_settings": "تنظیمات نقشه",
"matches": "تطابق‌ها",
"media_type": "نوع رسانه",
"memories": "خاطرات",
"memories_setting_description": "",
"memory": "خاطره",
"menu": "منو",
"merge": "ادغام",
"merge_people": "ادغام افراد",
"merge_people_limit": "",
"merge_people_prompt": "",
"merge_people_successfully": "ادغام افراد با موفقیت انجام شد",
"minimize": "کوچک کردن",
"minute": "دقیقه",
@ -643,20 +530,12 @@
"next": "بعدی",
"next_memory": "خاطره بعدی",
"no": "خیر",
"no_albums_message": "",
"no_archived_assets_message": "",
"no_assets_message": "",
"no_duplicates_found": "هیچ تکراری یافت نشد.",
"no_exif_info_available": "اطلاعات EXIF موجود نیست",
"no_explore_results_message": "",
"no_favorites_message": "",
"no_libraries_message": "",
"no_name": "بدون نام",
"no_places": "مکانی یافت نشد",
"no_results": "نتیجه‌ای یافت نشد",
"no_shared_albums_message": "",
"not_in_any_album": "در هیچ آلبومی نیست",
"note_apply_storage_label_to_previously_uploaded assets": "",
"notes": "یادداشت‌ها",
"notification_toggle_setting_description": "اعلان‌های ایمیلی را فعال کنید",
"notifications": "اعلان‌ها",
@ -664,7 +543,6 @@
"oauth": "OAuth",
"offline": "آفلاین",
"offline_paths": "مسیرهای آفلاین",
"offline_paths_description": "",
"ok": "تأیید",
"oldest_first": "قدیمی‌ترین ابتدا",
"online": "آنلاین",
@ -679,7 +557,6 @@
"owner": "مالک",
"partner": "شریک",
"partner_can_access": "{partner} می‌تواند دسترسی داشته باشد",
"partner_can_access_assets": "",
"partner_can_access_location": "مکان‌هایی که عکس‌های شما گرفته شده‌اند",
"partner_sharing": "اشتراک‌گذاری با شریک",
"partners": "شرکا",
@ -687,11 +564,6 @@
"password_does_not_match": "رمز عبور مطابقت ندارد",
"password_required": "رمز عبور مورد نیاز است",
"password_reset_success": "بازنشانی رمز عبور موفقیت‌آمیز بود",
"past_durations": {
"days": "",
"hours": "",
"years": ""
},
"path": "مسیر",
"pattern": "الگو",
"pause": "توقف",
@ -699,14 +571,12 @@
"paused": "متوقف شده",
"pending": "در انتظار",
"people": "افراد",
"people_sidebar_description": "",
"permanent_deletion_warning": "هشدار حذف دائمی",
"permanent_deletion_warning_setting_description": "نمایش هشدار هنگام حذف دائمی محتواها",
"permanently_delete": "حذف دائمی",
"permanently_deleted_asset": "محتوای حذف شده دائمی",
"person": "فرد",
"photos": "عکس‌ها",
"photos_count": "",
"photos_from_previous_years": "عکس‌های سال‌های گذشته",
"pick_a_location": "یک مکان انتخاب کنید",
"place": "مکان",
@ -730,38 +600,27 @@
"recent_searches": "جستجوهای اخیر",
"refresh": "تازه سازی",
"refreshed": "تازه سازی شد",
"refreshes_every_file": "",
"remove": "حذف",
"remove_deleted_assets": "حذف محتواهای حذف‌شده",
"remove_from_album": "حذف از آلبوم",
"remove_from_favorites": "حذف از علاقه‌مندی‌ها",
"remove_from_shared_link": "",
"removed_api_key": "",
"rename": "تغییر نام",
"repair": "تعمیر",
"repair_no_results_message": "",
"replace_with_upload": "جایگزینی با آپلود",
"require_password": "",
"require_user_to_change_password_on_first_login": "",
"reset": "بازنشانی",
"reset_password": "بازنشانی رمز عبور",
"reset_people_visibility": "",
"resolved_all_duplicates": "",
"restore": "بازیابی",
"restore_all": "بازیابی همه",
"restore_user": "بازیابی کاربر",
"resume": "ادامه",
"retry_upload": "",
"review_duplicates": "بررسی تکراری‌ها",
"role": "نقش",
"save": "ذخیره",
"saved_api_key": "",
"saved_profile": "پروفایل ذخیره شد",
"saved_settings": "تنظیمات ذخیره شد",
"say_something": "چیزی بگویید",
"scan_all_libraries": "اسکن همه کتابخانه‌ها",
"scan_settings": "تنظیمات اسکن",
"scanning_for_album": "",
"search": "جستجو",
"search_albums": "جستجوی آلبوم‌ها",
"search_by_context": "جستجو براساس زمینه",
@ -775,8 +634,6 @@
"search_state": "جستجوی ایالت...",
"search_timezone": "جستجوی منطقه زمانی...",
"search_type": "نوع جستجو",
"search_your_photos": "",
"searching_locales": "",
"second": "ثانیه",
"select_album_cover": "انتخاب جلد آلبوم",
"select_all": "انتخاب همه",
@ -787,41 +644,28 @@
"select_library_owner": "انتخاب مالک کتابخانه",
"select_new_face": "انتخاب چهره جدید",
"select_photos": "انتخاب عکس‌ها",
"select_trash_all": "",
"selected": "انتخاب شده",
"send_message": "ارسال پیام",
"send_welcome_email": "ارسال ایمیل خوش‌آمدگویی",
"server_stats": "آمار سرور",
"set": "تنظیم",
"set_as_album_cover": "",
"set_as_profile_picture": "",
"set_date_of_birth": "تنظیم تاریخ تولد",
"set_profile_picture": "تنظیم تصویر پروفایل",
"set_slideshow_to_fullscreen": "",
"settings": "تنظیمات",
"settings_saved": "تنظیمات ذخیره شد",
"share": "اشتراک‌گذاری",
"shared": "مشترک",
"shared_by": "مشترک توسط",
"shared_by_you": "",
"shared_from_partner": "عکس‌ها از {partner}",
"shared_links": "لینک‌های اشتراکی",
"shared_photos_and_videos_count": "",
"shared_with_partner": "مشترک با {partner}",
"sharing": "اشتراک‌گذاری",
"sharing_sidebar_description": "",
"show_album_options": "نمایش گزینه‌های آلبوم",
"show_and_hide_people": "",
"show_file_location": "نمایش مسیر فایل",
"show_gallery": "نمایش گالری",
"show_hidden_people": "نمایش افراد پنهان",
"show_in_timeline": "",
"show_in_timeline_setting_description": "",
"show_keyboard_shortcuts": "",
"show_metadata": "نمایش اطلاعات متا",
"show_or_hide_info": "",
"show_password": "نمایش رمز عبور",
"show_person_options": "",
"show_progress_bar": "نمایش نوار پیشرفت",
"show_search_options": "نمایش گزینه‌های جستجو",
"shuffle": "تصادفی",
@ -831,60 +675,39 @@
"skip_to_content": "رفتن به محتوا",
"slideshow": "نمایش اسلاید",
"slideshow_settings": "تنظیمات نمایش اسلاید",
"sort_albums_by": "",
"stack": "پشته",
"stack_selected_photos": "",
"stacktrace": "",
"start": "شروع",
"start_date": "تاریخ شروع",
"state": "ایالت",
"status": "وضعیت",
"stop_motion_photo": "توقف عکس متحرک",
"stop_photo_sharing": "",
"stop_photo_sharing_description": "",
"stop_sharing_photos_with_user": "",
"storage": "فضای ذخیره‌سازی",
"storage_label": "برچسب فضای ذخیره‌سازی",
"storage_usage": "",
"submit": "ارسال",
"suggestions": "پیشنهادات",
"sunrise_on_the_beach": "",
"swap_merge_direction": "تغییر جهت ادغام",
"sync": "همگام‌سازی",
"template": "الگو",
"theme": "تم",
"theme_selection": "انتخاب تم",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "منطقه زمانی",
"to_archive": "بایگانی",
"to_favorite": "به علاقه‌مندی‌ها",
"to_trash": "",
"toggle_settings": "تغییر تنظیمات",
"toggle_theme": "تغییر تم تاریک",
"total_usage": "استفاده کلی",
"trash": "سطل زباله",
"trash_all": "",
"trash_count": "",
"trash_no_results_message": "",
"trashed_items_will_be_permanently_deleted_after": "",
"type": "نوع",
"unarchive": "",
"unfavorite": "حذف از علاقه‌مندی‌ها",
"unhide_person": "آشکار کردن فرد",
"unknown": "ناشناخته",
"unknown_year": "سال نامشخص",
"unlimited": "نامحدود",
"unlink_oauth": "لغو اتصال OAuth",
"unlinked_oauth_account": "",
"unnamed_album": "آلبوم بدون نام",
"unnamed_share": "اشتراک بدون نام",
"unselect_all": "لغو انتخاب همه",
"unstack": "",
"untracked_files": "",
"untracked_files_decription": "",
"up_next": "مورد بعدی",
"updated_password": "",
"upload": "آپلود",
"upload_concurrency": "تعداد آپلود همزمان",
"url": "آدرس",
@ -898,12 +721,8 @@
"validate": "اعتبارسنجی",
"variables": "متغیرها",
"version": "نسخه",
"version_announcement_message": "",
"video": "ویدیو",
"video_hover_setting": "",
"video_hover_setting_description": "",
"videos": "ویدیوها",
"videos_count": "",
"view": "مشاهده",
"view_all": "مشاهده همه",
"view_all_users": "مشاهده همه کاربران",
@ -913,9 +732,7 @@
"waiting": "در انتظار",
"week": "هفته",
"welcome": "خوش آمدید",
"welcome_to_immich": "",
"year": "سال",
"yes": "بله",
"you_dont_have_any_shared_links": "",
"zoom_image": "بزرگنمایی تصویر"
}

View File

@ -41,7 +41,6 @@
},
"album_user_left": "Umalis sa {album}",
"all_albums": "Lahat ng albums",
"anti_clockwise": "",
"api_key_description": "Isang beses lamang na ipapakita itong value. Siguraduhin na ikopya itong value bago iclose ang window na ito.",
"are_these_the_same_person": "Itong tao na ito ay parehas?",
"asset_adding_to_album": "Dinadagdag sa album...",

View File

@ -689,7 +689,6 @@
"edit_title": "शीर्षक संपादित करें",
"edit_user": "यूजर को संपादित करो",
"edited": "संपादित",
"editor": "",
"email": "ईमेल",
"empty_folder": "This folder is empty",
"empty_trash": "कूड़ेदान खाली करें",
@ -922,7 +921,6 @@
"info": "जानकारी",
"interval": {
"day_at_onepm": "हर दिन दोपहर 1 बजे",
"hours": "",
"night_at_midnight": "हर रात आधी रात को",
"night_at_twoam": "हर रात 2 बजे"
},
@ -1142,11 +1140,6 @@
"password_does_not_match": "पासवर्ड मैच नहीं कर रहा है",
"password_required": "पासवर्ड आवश्यक",
"password_reset_success": "पासवर्ड रीसेट सफल",
"past_durations": {
"days": "",
"hours": "",
"years": ""
},
"path": "पथ",
"pattern": "नमूना",
"pause": "विराम",

View File

@ -1,784 +1,52 @@
{
"about": "Մասին",
"account": "",
"account_settings": "",
"acknowledge": "",
"action": "Գործողություն",
"actions": "",
"active": "",
"activity": "",
"add": "Ավելացնել",
"add_a_description": "",
"add_a_location": "Ավելացնել տեղ",
"add_a_name": "Ավելացնել անուն",
"add_a_title": "",
"add_exclusion_pattern": "",
"add_import_path": "",
"add_location": "Ավելացնել տեղ",
"add_more_users": "",
"add_partner": "",
"add_path": "",
"add_photos": "Ավելացնել նկարներ",
"add_to": "",
"add_to_album": "",
"add_to_shared_album": "",
"admin": {
"add_exclusion_pattern_description": "",
"authentication_settings": "",
"authentication_settings_description": "",
"background_task_job": "",
"check_all": "",
"config_set_by_file": "",
"confirm_delete_library": "",
"confirm_delete_library_assets": "",
"confirm_email_below": "",
"confirm_reprocess_all_faces": "",
"confirm_user_password_reset": "",
"disable_login": "",
"duplicate_detection_job_description": "",
"exclusion_pattern_description": "",
"external_library_created_at": "",
"external_library_management": "",
"face_detection": "",
"face_detection_description": "",
"facial_recognition_job_description": "",
"force_delete_user_warning": "",
"forcing_refresh_library_files": "",
"image_format_description": "",
"image_prefer_embedded_preview": "",
"image_prefer_embedded_preview_setting_description": "",
"image_prefer_wide_gamut": "",
"image_prefer_wide_gamut_setting_description": "",
"image_quality": "",
"image_settings": "",
"image_settings_description": "",
"job_concurrency": "",
"job_not_concurrency_safe": "",
"job_settings": "",
"job_settings_description": "",
"job_status": "",
"jobs_delayed": "",
"jobs_failed": "",
"library_created": "",
"library_deleted": "",
"library_import_path_description": "",
"library_scanning": "",
"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": "",
"machine_learning_clip_model": "",
"machine_learning_duplicate_detection": "",
"machine_learning_duplicate_detection_enabled_description": "",
"machine_learning_duplicate_detection_setting_description": "",
"machine_learning_enabled_description": "",
"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_description": "",
"machine_learning_max_detection_distance": "",
"machine_learning_max_detection_distance_description": "",
"machine_learning_max_recognition_distance": "",
"machine_learning_max_recognition_distance_description": "",
"machine_learning_min_detection_score": "",
"machine_learning_min_detection_score_description": "",
"machine_learning_min_recognized_faces": "",
"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_description": "",
"machine_learning_url_description": "",
"manage_concurrency": "",
"manage_log_settings": "",
"map_dark_style": "",
"map_enable_description": "",
"map_light_style": "",
"map_reverse_geocoding": "",
"map_reverse_geocoding_enable_description": "",
"map_reverse_geocoding_settings": "",
"map_settings": "",
"map_settings_description": "",
"map_style_description": "",
"metadata_extraction_job": "",
"metadata_extraction_job_description": "",
"migration_job": "",
"migration_job_description": "",
"no_paths_added": "",
"no_pattern_added": "",
"note_apply_storage_label_previous_assets": "",
"note_cannot_be_changed_later": "",
"notification_email_from_address": "",
"notification_email_from_address_description": "",
"notification_email_host_description": "",
"notification_email_ignore_certificate_errors": "",
"notification_email_ignore_certificate_errors_description": "",
"notification_email_password_description": "",
"notification_email_port_description": "",
"notification_email_sent_test_email_button": "",
"notification_email_setting_description": "",
"notification_email_test_email_failed": "",
"notification_email_test_email_sent": "",
"notification_email_username_description": "",
"notification_enable_email_notifications": "",
"notification_settings": "",
"notification_settings_description": "",
"oauth_auto_launch": "",
"oauth_auto_launch_description": "",
"oauth_auto_register": "",
"oauth_auto_register_description": "",
"oauth_button_text": "",
"oauth_enable_description": "",
"oauth_mobile_redirect_uri": "",
"oauth_mobile_redirect_uri_override": "",
"oauth_mobile_redirect_uri_override_description": "",
"oauth_settings": "",
"oauth_settings_description": "",
"oauth_storage_label_claim": "",
"oauth_storage_label_claim_description": "",
"oauth_storage_quota_claim": "",
"oauth_storage_quota_claim_description": "",
"oauth_storage_quota_default": "",
"oauth_storage_quota_default_description": "",
"offline_paths": "",
"offline_paths_description": "",
"password_enable_description": "",
"password_settings": "",
"password_settings_description": "",
"paths_validated_successfully": "",
"quota_size_gib": "",
"refreshing_all_libraries": "",
"repair_all": "",
"repair_matched_items": "",
"repaired_items": "",
"require_password_change_on_login": "",
"reset_settings_to_default": "",
"reset_settings_to_recent_saved": "",
"send_welcome_email": "",
"server_external_domain_settings": "",
"server_external_domain_settings_description": "",
"server_settings": "",
"server_settings_description": "",
"server_welcome_message": "",
"server_welcome_message_description": "",
"sidecar_job": "",
"sidecar_job_description": "",
"slideshow_duration_description": "",
"smart_search_job_description": "",
"storage_template_enable_description": "",
"storage_template_hash_verification_enabled": "",
"storage_template_hash_verification_enabled_description": "",
"storage_template_migration": "",
"storage_template_migration_job": "",
"storage_template_settings": "",
"storage_template_settings_description": "",
"system_settings": "",
"theme_custom_css_settings": "",
"theme_custom_css_settings_description": "",
"theme_settings": "",
"theme_settings_description": "",
"these_files_matched_by_checksum": "",
"thumbnail_generation_job": "",
"thumbnail_generation_job_description": "",
"transcoding_acceleration_api": "",
"transcoding_acceleration_api_description": "",
"transcoding_acceleration_nvenc": "",
"transcoding_acceleration_qsv": "",
"transcoding_acceleration_rkmpp": "",
"transcoding_acceleration_vaapi": "",
"transcoding_accepted_audio_codecs": "",
"transcoding_accepted_audio_codecs_description": "",
"transcoding_accepted_video_codecs": "",
"transcoding_accepted_video_codecs_description": "",
"transcoding_advanced_options_description": "",
"transcoding_audio_codec": "",
"transcoding_audio_codec_description": "",
"transcoding_bitrate_description": "",
"transcoding_constant_quality_mode": "",
"transcoding_constant_quality_mode_description": "",
"transcoding_constant_rate_factor": "",
"transcoding_constant_rate_factor_description": "",
"transcoding_disabled_description": "",
"transcoding_hardware_acceleration": "",
"transcoding_hardware_acceleration_description": "",
"transcoding_hardware_decoding": "",
"transcoding_hardware_decoding_setting_description": "",
"transcoding_hevc_codec": "",
"transcoding_max_b_frames": "",
"transcoding_max_b_frames_description": "",
"transcoding_max_bitrate": "",
"transcoding_max_bitrate_description": "",
"transcoding_max_keyframe_interval": "",
"transcoding_max_keyframe_interval_description": "",
"transcoding_optimal_description": "",
"transcoding_preferred_hardware_device": "",
"transcoding_preferred_hardware_device_description": "",
"transcoding_preset_preset": "",
"transcoding_preset_preset_description": "",
"transcoding_reference_frames": "",
"transcoding_reference_frames_description": "",
"transcoding_required_description": "",
"transcoding_settings": "",
"transcoding_settings_description": "",
"transcoding_target_resolution": "",
"transcoding_target_resolution_description": "",
"transcoding_temporal_aq": "",
"transcoding_temporal_aq_description": "",
"transcoding_threads": "",
"transcoding_threads_description": "",
"transcoding_tone_mapping": "",
"transcoding_tone_mapping_description": "",
"transcoding_transcode_policy": "",
"transcoding_transcode_policy_description": "",
"transcoding_two_pass_encoding": "",
"transcoding_two_pass_encoding_setting_description": "",
"transcoding_video_codec": "",
"transcoding_video_codec_description": "",
"trash_enabled_description": "",
"trash_number_of_days": "",
"trash_number_of_days_description": "",
"trash_settings": "",
"trash_settings_description": "",
"untracked_files": "",
"untracked_files_description": "",
"user_delete_delay_settings": "",
"user_delete_delay_settings_description": "",
"user_management": "",
"user_password_has_been_reset": "",
"user_password_reset_description": "",
"user_settings": "",
"user_settings_description": "",
"user_successfully_removed": "",
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job": "",
"video_conversion_job_description": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"albums_count": "",
"all": "",
"all_people": "",
"allow_dark_mode": "",
"allow_edits": "",
"api_key": "",
"api_keys": "",
"app_settings": "",
"appears_in": "",
"archive": "",
"archive_or_unarchive_photo": "",
"asset_offline": "",
"assets": "",
"authorized_devices": "",
"back": "Հետ",
"backup_all": "Բոլոր",
"backup_controller_page_background_battery_info_link": "Ցույց տուր ինչպես",
"backup_controller_page_background_battery_info_ok": "Լավ",
"backward": "",
"blurred_background": "",
"camera": "",
"camera_brand": "",
"camera_model": "",
"cancel": "",
"cancel_search": "",
"cannot_merge_people": "",
"cannot_update_the_description": "",
"change_date": "",
"change_expiration_time": "",
"change_location": "Փոխել տեղը",
"change_name": "Փոխել անուն",
"change_name_successfully": "",
"change_password": "",
"change_your_password": "",
"changed_visibility_successfully": "",
"check_all": "",
"check_logs": "",
"choose_matching_people_to_merge": "",
"city": "Քաղաք",
"clear": "",
"clear_all": "",
"clear_message": "",
"clear_value": "",
"client_cert_dialog_msg_confirm": "Լավ",
"close": "",
"collapse_all": "",
"color": "Գույն",
"color_theme": "",
"comment_options": "",
"comments_are_disabled": "",
"confirm": "",
"confirm_admin_password": "",
"confirm_delete_shared_link": "",
"confirm_password": "",
"contain": "",
"context": "",
"continue": "",
"control_bottom_app_bar_edit_location": "Փոխել Տեղը",
"copied_image_to_clipboard": "",
"copied_to_clipboard": "",
"copy_error": "",
"copy_file_path": "",
"copy_image": "",
"copy_link": "",
"copy_link_to_clipboard": "",
"copy_password": "",
"copy_to_clipboard": "",
"country": "Երկիր",
"cover": "",
"covers": "",
"create": "",
"create_album": "",
"create_library": "",
"create_link": "",
"create_link_to_share": "",
"create_new": "ՍՏԵՂԾԵԼ ՆՈՐ",
"create_new_person": "Ստեղծել նոր անձ",
"create_new_user": "",
"create_shared_album_page_share_select_photos": "Ընտրե Նկարներ",
"create_user": "",
"created": "",
"curated_object_page_title": "Բաներ",
"current_device": "",
"custom_locale": "",
"custom_locale_description": "",
"dark": "Մութ",
"date_after": "",
"date_and_time": "",
"date_before": "",
"date_range": "",
"day": "Օր",
"default_locale": "",
"default_locale_description": "",
"delete": "Ջնջել",
"delete_album": "",
"delete_api_key_prompt": "",
"delete_key": "",
"delete_library": "",
"delete_link": "",
"delete_shared_link": "",
"delete_user": "",
"deleted_shared_link": "",
"description": "",
"details": "",
"direction": "",
"disabled": "",
"disallow_edits": "",
"discover": "",
"dismiss_all_errors": "",
"dismiss_error": "",
"display_options": "",
"display_order": "",
"display_original_photos": "",
"display_original_photos_setting_description": "",
"done": "",
"download": "",
"downloading": "",
"duplicates": "",
"duration": "",
"edit_album": "",
"edit_avatar": "",
"edit_date": "",
"edit_date_and_time": "",
"edit_exclusion_pattern": "",
"edit_faces": "",
"edit_import_path": "",
"edit_import_paths": "",
"edit_key": "",
"edit_link": "",
"edit_location": "Փոխել տեղը",
"edit_name": "",
"edit_people": "",
"edit_title": "",
"edit_user": "",
"edited": "",
"editor": "",
"email": "",
"empty_trash": "",
"enable": "",
"enabled": "",
"end_date": "",
"error": "",
"error_loading_image": "",
"errors": {
"cleared_jobs": "",
"exclusion_pattern_already_exists": "",
"failed_job_command": "",
"import_path_already_exists": "",
"paths_validation_failed": "",
"quota_higher_than_disk_size": "",
"repair_unable_to_check_items": "",
"unable_to_add_album_users": "",
"unable_to_add_comment": "",
"unable_to_add_exclusion_pattern": "",
"unable_to_add_import_path": "",
"unable_to_add_partners": "",
"unable_to_change_album_user_role": "",
"unable_to_change_date": "",
"unable_to_change_location": "",
"unable_to_change_password": "",
"unable_to_copy_to_clipboard": "",
"unable_to_create_api_key": "",
"unable_to_create_library": "",
"unable_to_create_user": "",
"unable_to_delete_album": "",
"unable_to_delete_asset": "",
"unable_to_delete_exclusion_pattern": "",
"unable_to_delete_import_path": "",
"unable_to_delete_shared_link": "",
"unable_to_delete_user": "",
"unable_to_edit_exclusion_pattern": "",
"unable_to_edit_import_path": "",
"unable_to_empty_trash": "",
"unable_to_enter_fullscreen": "",
"unable_to_exit_fullscreen": "",
"unable_to_hide_person": "",
"unable_to_link_oauth_account": "",
"unable_to_load_album": "",
"unable_to_load_asset_activity": "",
"unable_to_load_items": "",
"unable_to_load_liked_status": "",
"unable_to_play_video": "",
"unable_to_refresh_user": "",
"unable_to_remove_album_users": "",
"unable_to_remove_api_key": "",
"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_resolve_duplicate": "",
"unable_to_restore_assets": "",
"unable_to_restore_trash": "",
"unable_to_restore_user": "",
"unable_to_save_album": "",
"unable_to_save_api_key": "",
"unable_to_save_name": "",
"unable_to_save_profile": "",
"unable_to_save_settings": "",
"unable_to_scan_libraries": "",
"unable_to_scan_library": "",
"unable_to_set_profile_picture": "",
"unable_to_submit_job": "",
"unable_to_trash_asset": "",
"unable_to_unlink_account": "",
"unable_to_update_library": "",
"unable_to_update_location": "",
"unable_to_update_settings": "",
"unable_to_update_timeline_display_status": "",
"unable_to_update_user": ""
},
"exif_bottom_sheet_person_add_person": "Ավելացնել անուն",
"exif_bottom_sheet_person_age": "Տարիք {}",
"exif_bottom_sheet_person_age_years": "Տարիք {}",
"exit_slideshow": "",
"expand_all": "",
"expire_after": "",
"expired": "",
"explore": "",
"export": "",
"export_as_json": "",
"extension": "",
"external": "",
"external_libraries": "",
"favorite": "",
"favorite_or_unfavorite_photo": "",
"favorites": "",
"feature_photo_updated": "",
"file_name": "",
"file_name_or_extension": "",
"filename": "",
"filetype": "",
"filter_people": "",
"find_them_fast": "",
"fix_incorrect_match": "",
"forward": "",
"general": "",
"get_help": "",
"getting_started": "",
"go_back": "",
"go_to_search": "",
"group_albums_by": "",
"has_quota": "",
"hi_user": "Բարեւ {name} ({email})",
"hide_gallery": "",
"hide_password": "",
"hide_person": "",
"host": "",
"hour": "",
"image": "",
"immich_logo": "",
"immich_web_interface": "",
"import_from_json": "",
"import_path": "",
"in_archive": "",
"include_archived": "",
"include_shared_albums": "",
"include_shared_partner_assets": "",
"individual_share": "",
"info": "",
"interval": {
"day_at_onepm": "",
"hours": "",
"night_at_midnight": "",
"night_at_twoam": ""
},
"invite_people": "",
"invite_to_album": "",
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
"level": "",
"library": "",
"library_options": "",
"light": "",
"link_options": "",
"link_to_oauth": "",
"linked_oauth_account": "",
"list": "",
"loading": "",
"loading_search_results_failed": "",
"log_out": "",
"log_out_all_devices": "",
"login_has_been_disabled": "",
"look": "",
"loop_videos": "",
"loop_videos_description": "",
"make": "",
"manage_shared_links": "",
"manage_sharing_with_partners": "",
"manage_the_app_settings": "",
"manage_your_account": "",
"manage_your_api_keys": "",
"manage_your_devices": "",
"manage_your_oauth_connection": "",
"map": "",
"map_assets_in_bound": "{} նկար",
"map_assets_in_bounds": "{} նկարներ",
"map_marker_with_image": "",
"map_settings": "",
"matches": "",
"media_type": "",
"memories": "",
"memories_setting_description": "",
"menu": "",
"merge": "",
"merge_people": "",
"merge_people_successfully": "",
"minimize": "",
"minute": "",
"missing": "",
"model": "",
"month": "",
"more": "",
"moved_to_trash": "",
"my_albums": "",
"name": "",
"name_or_nickname": "",
"never": "",
"new_api_key": "",
"new_password": "",
"new_person": "",
"new_user_created": "",
"newest_first": "",
"next": "",
"next_memory": "",
"no": "",
"no_albums_message": "",
"no_archived_assets_message": "",
"no_assets_message": "",
"no_duplicates_found": "",
"no_exif_info_available": "",
"no_explore_results_message": "",
"no_favorites_message": "",
"no_libraries_message": "",
"no_name": "",
"no_places": "",
"no_results": "",
"no_shared_albums_message": "",
"not_in_any_album": "",
"note_apply_storage_label_to_previously_uploaded assets": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"offline_paths": "",
"offline_paths_description": "",
"ok": "",
"oldest_first": "",
"online": "",
"only_favorites": "",
"open_the_search_filters": "",
"options": "",
"organize_your_library": "",
"other": "",
"other_devices": "",
"other_variables": "",
"owned": "",
"owner": "",
"partner_can_access": "",
"partner_can_access_assets": "",
"partner_can_access_location": "",
"partner_list_user_photos": "{}-ին նկարները",
"partner_sharing": "",
"partners": "",
"password": "",
"password_does_not_match": "",
"password_required": "",
"password_reset_success": "",
"past_durations": {
"days": "",
"hours": "",
"years": ""
},
"path": "",
"pattern": "",
"pause": "",
"pause_memories": "",
"paused": "",
"pending": "",
"people": "",
"people_sidebar_description": "",
"permanent_deletion_warning": "",
"permanent_deletion_warning_setting_description": "",
"permanently_delete": "",
"permanently_deleted_asset": "",
"photos": "Նկարներ",
"photos_count": "",
"photos_from_previous_years": "",
"pick_a_location": "",
"place": "",
"places": "",
"play": "",
"play_memories": "",
"play_motion_photo": "",
"play_or_pause_video": "",
"port": "",
"preset": "",
"preview": "",
"previous": "",
"previous_memory": "",
"previous_or_next_photo": "",
"primary": "",
"profile_picture_set": "",
"public_share": "",
"reaction_options": "",
"read_changelog": "",
"recent": "",
"recent_searches": "",
"refresh": "",
"refreshed": "",
"refreshes_every_file": "",
"remove": "",
"remove_deleted_assets": "",
"remove_from_album": "",
"remove_from_favorites": "",
"remove_from_shared_link": "",
"removed_api_key": "",
"rename": "",
"repair": "",
"repair_no_results_message": "",
"replace_with_upload": "",
"require_password": "",
"require_user_to_change_password_on_first_login": "",
"reset": "",
"reset_password": "",
"reset_people_visibility": "",
"restore": "",
"restore_all": "",
"restore_user": "",
"resume": "",
"retry_upload": "",
"review_duplicates": "",
"role": "",
"save": "Պահե",
"saved_api_key": "",
"saved_profile": "",
"saved_settings": "",
"say_something": "",
"scan_all_libraries": "",
"scan_library": "Նայե",
"scan_settings": "",
"search": "Փնտրե",
"search_albums": "",
"search_by_context": "",
"search_camera_make": "",
"search_camera_model": "",
"search_city": "Որոնե քաղաք…",
"search_country": "",
"search_filter_date": "Ամսաթիվ",
"search_filter_date_interval": "{start} մինչեւ {end}",
"search_filter_location": "Տեղ",
"search_filter_location_title": "Ընտրե տեղ",
"search_for_existing_person": "",
"search_no_people": "Ոչ մի անձ",
"search_page_motion_photos": "Շարժվող Նկարներ",
"search_people": "",
"search_places": "",
"search_state": "",
"search_timezone": "",
"search_type": "",
"search_your_photos": "",
"searching_locales": "",
"second": "",
"select_album_cover": "",
"select_all": "",
"select_avatar_color": "",
"select_face": "",
"select_featured_photo": "",
"select_keep_all": "",
"select_library_owner": "",
"select_new_face": "",
"select_photos": "Ընտրե նկարներ",
"select_trash_all": "",
"selected": "",
"send_message": "",
"send_welcome_email": "",
"server_stats": "",
"set": "",
"set_as_album_cover": "",
"set_as_profile_picture": "",
"set_date_of_birth": "",
"set_profile_picture": "",
"set_slideshow_to_fullscreen": "",
"setting_notifications_notify_never": "երբեք",
"setting_notifications_notify_seconds": "{} վայրկյան",
"settings": "",
"settings_saved": "",
"share": "",
"share_add_photos": "Ավելացնել նկարներ",
"shared": "",
"shared_by": "",
"shared_by_you": "",
"shared_from_partner": "",
"shared_link_edit_expire_after_option_day": "1 օր",
"shared_link_edit_expire_after_option_days": "{} օր",
"shared_link_edit_expire_after_option_hour": "1 ժամ",
@ -787,118 +55,20 @@
"shared_link_edit_expire_after_option_minutes": "{} րոպե",
"shared_link_edit_expire_after_option_months": "{} ամիս",
"shared_link_edit_expire_after_option_year": "{} տարի",
"shared_links": "",
"shared_photos_and_videos_count": "",
"shared_with_partner": "",
"sharing": "",
"sharing_sidebar_description": "",
"show_album_options": "",
"show_and_hide_people": "",
"show_file_location": "",
"show_gallery": "",
"show_hidden_people": "",
"show_in_timeline": "",
"show_in_timeline_setting_description": "",
"show_keyboard_shortcuts": "",
"show_metadata": "",
"show_or_hide_info": "",
"show_password": "",
"show_person_options": "",
"show_progress_bar": "",
"show_search_options": "",
"shuffle": "",
"sign_out": "",
"sign_up": "",
"size": "",
"skip_to_content": "",
"slideshow": "",
"slideshow_settings": "",
"sort_albums_by": "",
"sort_oldest": "Ամենահին նկարը",
"sort_recent": "Ամենանոր նկարը",
"stack": "",
"stack_selected_photos": "",
"stacktrace": "",
"start": "",
"start_date": "",
"state": "",
"status": "",
"stop_motion_photo": "",
"stop_photo_sharing": "",
"stop_photo_sharing_description": "",
"stop_sharing_photos_with_user": "",
"storage": "",
"storage_label": "",
"storage_usage": "",
"submit": "",
"suggestions": "",
"sunrise_on_the_beach": "",
"swap_merge_direction": "",
"sync": "",
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "Ժամային գոտի",
"to_archive": "",
"to_favorite": "",
"to_trash": "Աղբ",
"toggle_settings": "",
"toggle_theme": "",
"total_usage": "",
"trash": "Աղբ",
"trash_all": "",
"trash_no_results_message": "",
"trash_page_title": "Աղբ ({})",
"trashed_items_will_be_permanently_deleted_after": "",
"type": "Տեսակ",
"unarchive": "",
"unfavorite": "",
"unhide_person": "",
"unknown": "Անհայտ",
"unknown_country": "Անհայտ Երկիր",
"unknown_year": "Անհայտ Տարի",
"unlimited": "",
"unlink_oauth": "",
"unlinked_oauth_account": "",
"unselect_all": "",
"unstack": "",
"untracked_files": "",
"untracked_files_decription": "",
"up_next": "",
"updated_password": "",
"upload": "",
"upload_concurrency": "",
"upload_status_errors": "Սխալներ",
"url": "",
"usage": "",
"user": "",
"user_id": "",
"user_usage_detail": "",
"username": "",
"users": "",
"utilities": "",
"validate": "",
"variables": "",
"version": "",
"version_announcement_closing": "Քո ընկերը՝ Ալեքսը",
"video": "",
"video_hover_setting": "",
"video_hover_setting_description": "",
"videos": "",
"videos_count": "",
"view_all": "",
"view_all_users": "",
"view_links": "",
"view_next_asset": "",
"view_previous_asset": "",
"waiting": "",
"week": "Շաբաթ",
"welcome": "Բարի գալուստ",
"welcome_to_immich": "",
"year": "Տարի",
"yes": "Այո",
"you_dont_have_any_shared_links": "",
"zoom_image": ""
"yes": "Այո"
}

View File

@ -14,7 +14,6 @@
"add_a_location": "დაამატე ადგილი",
"add_a_name": "დაამატე სახელი",
"add_a_title": "დაასათაურე",
"add_endpoint": "",
"add_exclusion_pattern": "დაამატე გამონაკლისი ნიმუში",
"add_import_path": "დაამატე საიმპორტო მისამართი",
"add_location": "დაამატე ადგილი",

View File

@ -2,868 +2,5 @@
"about": "دەربارە",
"account": "هەژمار",
"account_settings": "ڕێکخستنی هەژمار",
"acknowledge": "دانپێدانان",
"action": "",
"actions": "",
"active": "",
"activity": "",
"add": "",
"add_a_description": "",
"add_a_location": "",
"add_a_name": "",
"add_a_title": "",
"add_exclusion_pattern": "",
"add_import_path": "",
"add_location": "",
"add_more_users": "",
"add_partner": "",
"add_path": "",
"add_photos": "",
"add_to": "",
"add_to_album": "",
"add_to_shared_album": "",
"admin": {
"add_exclusion_pattern_description": "",
"authentication_settings": "",
"authentication_settings_description": "",
"background_task_job": "",
"check_all": "",
"config_set_by_file": "",
"confirm_delete_library": "",
"confirm_delete_library_assets": "",
"confirm_email_below": "",
"confirm_reprocess_all_faces": "",
"confirm_user_password_reset": "",
"disable_login": "",
"duplicate_detection_job_description": "",
"exclusion_pattern_description": "",
"external_library_created_at": "",
"external_library_management": "",
"face_detection": "",
"face_detection_description": "",
"facial_recognition_job_description": "",
"force_delete_user_warning": "",
"forcing_refresh_library_files": "",
"image_format_description": "",
"image_prefer_embedded_preview": "",
"image_prefer_embedded_preview_setting_description": "",
"image_prefer_wide_gamut": "",
"image_prefer_wide_gamut_setting_description": "",
"image_quality": "",
"image_settings": "",
"image_settings_description": "",
"job_concurrency": "",
"job_not_concurrency_safe": "",
"job_settings": "",
"job_settings_description": "",
"job_status": "",
"jobs_delayed": "",
"jobs_failed": "",
"library_created": "",
"library_deleted": "",
"library_import_path_description": "",
"library_scanning": "",
"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": "",
"machine_learning_clip_model": "",
"machine_learning_duplicate_detection": "",
"machine_learning_duplicate_detection_enabled": "",
"machine_learning_duplicate_detection_enabled_description": "",
"machine_learning_duplicate_detection_setting_description": "",
"machine_learning_enabled": "",
"machine_learning_enabled_description": "",
"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": "",
"machine_learning_max_detection_distance": "",
"machine_learning_max_detection_distance_description": "",
"machine_learning_max_recognition_distance": "",
"machine_learning_max_recognition_distance_description": "",
"machine_learning_min_detection_score": "",
"machine_learning_min_detection_score_description": "",
"machine_learning_min_recognized_faces": "",
"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": "",
"machine_learning_url_description": "",
"manage_concurrency": "",
"manage_log_settings": "",
"map_dark_style": "",
"map_enable_description": "",
"map_light_style": "",
"map_reverse_geocoding": "",
"map_reverse_geocoding_enable_description": "",
"map_reverse_geocoding_settings": "",
"map_settings": "",
"map_settings_description": "",
"map_style_description": "",
"metadata_extraction_job": "",
"metadata_extraction_job_description": "",
"migration_job": "",
"migration_job_description": "",
"no_paths_added": "",
"no_pattern_added": "",
"note_apply_storage_label_previous_assets": "",
"note_cannot_be_changed_later": "",
"notification_email_from_address": "",
"notification_email_from_address_description": "",
"notification_email_host_description": "",
"notification_email_ignore_certificate_errors": "",
"notification_email_ignore_certificate_errors_description": "",
"notification_email_password_description": "",
"notification_email_port_description": "",
"notification_email_sent_test_email_button": "",
"notification_email_setting_description": "",
"notification_email_test_email_failed": "",
"notification_email_test_email_sent": "",
"notification_email_username_description": "",
"notification_enable_email_notifications": "",
"notification_settings": "",
"notification_settings_description": "",
"oauth_auto_launch": "",
"oauth_auto_launch_description": "",
"oauth_auto_register": "",
"oauth_auto_register_description": "",
"oauth_button_text": "",
"oauth_enable_description": "",
"oauth_mobile_redirect_uri": "",
"oauth_mobile_redirect_uri_override": "",
"oauth_mobile_redirect_uri_override_description": "",
"oauth_settings": "",
"oauth_settings_description": "",
"oauth_storage_label_claim": "",
"oauth_storage_label_claim_description": "",
"oauth_storage_quota_claim": "",
"oauth_storage_quota_claim_description": "",
"oauth_storage_quota_default": "",
"oauth_storage_quota_default_description": "",
"offline_paths": "",
"offline_paths_description": "",
"password_enable_description": "",
"password_settings": "",
"password_settings_description": "",
"paths_validated_successfully": "",
"quota_size_gib": "",
"refreshing_all_libraries": "",
"repair_all": "",
"repair_matched_items": "",
"repaired_items": "",
"require_password_change_on_login": "",
"reset_settings_to_default": "",
"reset_settings_to_recent_saved": "",
"send_welcome_email": "",
"server_external_domain_settings": "",
"server_external_domain_settings_description": "",
"server_settings": "",
"server_settings_description": "",
"server_welcome_message": "",
"server_welcome_message_description": "",
"sidecar_job": "",
"sidecar_job_description": "",
"slideshow_duration_description": "",
"smart_search_job_description": "",
"storage_template_enable_description": "",
"storage_template_hash_verification_enabled": "",
"storage_template_hash_verification_enabled_description": "",
"storage_template_migration": "",
"storage_template_migration_job": "",
"storage_template_settings": "",
"storage_template_settings_description": "",
"system_settings": "",
"theme_custom_css_settings": "",
"theme_custom_css_settings_description": "",
"theme_settings": "",
"theme_settings_description": "",
"these_files_matched_by_checksum": "",
"thumbnail_generation_job": "",
"thumbnail_generation_job_description": "",
"transcoding_acceleration_api": "",
"transcoding_acceleration_api_description": "",
"transcoding_acceleration_nvenc": "",
"transcoding_acceleration_qsv": "",
"transcoding_acceleration_rkmpp": "",
"transcoding_acceleration_vaapi": "",
"transcoding_accepted_audio_codecs": "",
"transcoding_accepted_audio_codecs_description": "",
"transcoding_accepted_video_codecs": "",
"transcoding_accepted_video_codecs_description": "",
"transcoding_advanced_options_description": "",
"transcoding_audio_codec": "",
"transcoding_audio_codec_description": "",
"transcoding_bitrate_description": "",
"transcoding_constant_quality_mode": "",
"transcoding_constant_quality_mode_description": "",
"transcoding_constant_rate_factor": "",
"transcoding_constant_rate_factor_description": "",
"transcoding_disabled_description": "",
"transcoding_hardware_acceleration": "",
"transcoding_hardware_acceleration_description": "",
"transcoding_hardware_decoding": "",
"transcoding_hardware_decoding_setting_description": "",
"transcoding_hevc_codec": "",
"transcoding_max_b_frames": "",
"transcoding_max_b_frames_description": "",
"transcoding_max_bitrate": "",
"transcoding_max_bitrate_description": "",
"transcoding_max_keyframe_interval": "",
"transcoding_max_keyframe_interval_description": "",
"transcoding_optimal_description": "",
"transcoding_preferred_hardware_device": "",
"transcoding_preferred_hardware_device_description": "",
"transcoding_preset_preset": "",
"transcoding_preset_preset_description": "",
"transcoding_reference_frames": "",
"transcoding_reference_frames_description": "",
"transcoding_required_description": "",
"transcoding_settings": "",
"transcoding_settings_description": "",
"transcoding_target_resolution": "",
"transcoding_target_resolution_description": "",
"transcoding_temporal_aq": "",
"transcoding_temporal_aq_description": "",
"transcoding_threads": "",
"transcoding_threads_description": "",
"transcoding_tone_mapping": "",
"transcoding_tone_mapping_description": "",
"transcoding_transcode_policy": "",
"transcoding_transcode_policy_description": "",
"transcoding_two_pass_encoding": "",
"transcoding_two_pass_encoding_setting_description": "",
"transcoding_video_codec": "",
"transcoding_video_codec_description": "",
"trash_enabled_description": "",
"trash_number_of_days": "",
"trash_number_of_days_description": "",
"trash_settings": "",
"trash_settings_description": "",
"untracked_files": "",
"untracked_files_description": "",
"user_delete_delay_settings": "",
"user_delete_delay_settings_description": "",
"user_management": "",
"user_password_has_been_reset": "",
"user_password_reset_description": "",
"user_settings": "",
"user_settings_description": "",
"user_successfully_removed": "",
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
"video_conversion_job": "",
"video_conversion_job_description": ""
},
"admin_email": "",
"admin_password": "",
"administration": "",
"advanced": "",
"album_added": "",
"album_added_notification_setting_description": "",
"album_cover_updated": "",
"album_info_updated": "",
"album_name": "",
"album_options": "",
"album_updated": "",
"album_updated_setting_description": "",
"albums": "",
"albums_count": "",
"all": "",
"all_people": "",
"allow_dark_mode": "",
"allow_edits": "",
"api_key": "",
"api_keys": "",
"app_settings": "",
"appears_in": "",
"archive": "",
"archive_or_unarchive_photo": "",
"archive_size": "",
"archive_size_description": "",
"asset_offline": "",
"assets": "",
"authorized_devices": "",
"back": "",
"backward": "",
"blurred_background": "",
"camera": "",
"camera_brand": "",
"camera_model": "",
"cancel": "",
"cancel_search": "",
"cannot_merge_people": "",
"cannot_update_the_description": "",
"change_date": "",
"change_expiration_time": "",
"change_location": "",
"change_name": "",
"change_name_successfully": "",
"change_password": "",
"change_your_password": "",
"changed_visibility_successfully": "",
"check_all": "",
"check_logs": "",
"choose_matching_people_to_merge": "",
"city": "",
"clear": "",
"clear_all": "",
"clear_message": "",
"clear_value": "",
"close": "",
"collapse_all": "",
"color_theme": "",
"comment_options": "",
"comments_are_disabled": "",
"confirm": "",
"confirm_admin_password": "",
"confirm_delete_shared_link": "",
"confirm_password": "",
"contain": "",
"context": "",
"continue": "",
"copied_image_to_clipboard": "",
"copied_to_clipboard": "",
"copy_error": "",
"copy_file_path": "",
"copy_image": "",
"copy_link": "",
"copy_link_to_clipboard": "",
"copy_password": "",
"copy_to_clipboard": "",
"country": "",
"cover": "",
"covers": "",
"create": "",
"create_album": "",
"create_library": "",
"create_link": "",
"create_link_to_share": "",
"create_new_person": "",
"create_new_user": "",
"create_user": "",
"created": "",
"current_device": "",
"custom_locale": "",
"custom_locale_description": "",
"dark": "",
"date_after": "",
"date_and_time": "",
"date_before": "",
"date_range": "",
"day": "",
"default_locale": "",
"default_locale_description": "",
"delete": "",
"delete_album": "",
"delete_api_key_prompt": "",
"delete_key": "",
"delete_library": "",
"delete_link": "",
"delete_shared_link": "",
"delete_user": "",
"deleted_shared_link": "",
"description": "",
"details": "",
"direction": "",
"disabled": "",
"disallow_edits": "",
"discover": "",
"dismiss_all_errors": "",
"dismiss_error": "",
"display_options": "",
"display_order": "",
"display_original_photos": "",
"display_original_photos_setting_description": "",
"done": "",
"download": "",
"download_settings": "",
"download_settings_description": "",
"downloading": "",
"duplicates": "",
"duration": "",
"edit_album": "",
"edit_avatar": "",
"edit_date": "",
"edit_date_and_time": "",
"edit_exclusion_pattern": "",
"edit_faces": "",
"edit_import_path": "",
"edit_import_paths": "",
"edit_key": "",
"edit_link": "",
"edit_location": "",
"edit_name": "",
"edit_people": "",
"edit_title": "",
"edit_user": "",
"edited": "",
"editor": "",
"email": "",
"empty_trash": "",
"end_date": "",
"error": "",
"error_loading_image": "",
"errors": {
"cleared_jobs": "",
"exclusion_pattern_already_exists": "",
"failed_job_command": "",
"import_path_already_exists": "",
"paths_validation_failed": "",
"quota_higher_than_disk_size": "",
"repair_unable_to_check_items": "",
"unable_to_add_album_users": "",
"unable_to_add_comment": "",
"unable_to_add_exclusion_pattern": "",
"unable_to_add_import_path": "",
"unable_to_add_partners": "",
"unable_to_change_album_user_role": "",
"unable_to_change_date": "",
"unable_to_change_location": "",
"unable_to_change_password": "",
"unable_to_copy_to_clipboard": "",
"unable_to_create_api_key": "",
"unable_to_create_library": "",
"unable_to_create_user": "",
"unable_to_delete_album": "",
"unable_to_delete_asset": "",
"unable_to_delete_exclusion_pattern": "",
"unable_to_delete_import_path": "",
"unable_to_delete_shared_link": "",
"unable_to_delete_user": "",
"unable_to_edit_exclusion_pattern": "",
"unable_to_edit_import_path": "",
"unable_to_empty_trash": "",
"unable_to_enter_fullscreen": "",
"unable_to_exit_fullscreen": "",
"unable_to_hide_person": "",
"unable_to_link_oauth_account": "",
"unable_to_load_album": "",
"unable_to_load_asset_activity": "",
"unable_to_load_items": "",
"unable_to_load_liked_status": "",
"unable_to_play_video": "",
"unable_to_refresh_user": "",
"unable_to_remove_album_users": "",
"unable_to_remove_api_key": "",
"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_resolve_duplicate": "",
"unable_to_restore_assets": "",
"unable_to_restore_trash": "",
"unable_to_restore_user": "",
"unable_to_save_album": "",
"unable_to_save_api_key": "",
"unable_to_save_name": "",
"unable_to_save_profile": "",
"unable_to_save_settings": "",
"unable_to_scan_libraries": "",
"unable_to_scan_library": "",
"unable_to_set_profile_picture": "",
"unable_to_submit_job": "",
"unable_to_trash_asset": "",
"unable_to_unlink_account": "",
"unable_to_update_library": "",
"unable_to_update_location": "",
"unable_to_update_settings": "",
"unable_to_update_timeline_display_status": "",
"unable_to_update_user": ""
},
"exit_slideshow": "",
"expand_all": "",
"expire_after": "",
"expired": "",
"explore": "",
"export": "",
"export_as_json": "",
"extension": "",
"external": "",
"external_libraries": "",
"favorite": "",
"favorite_or_unfavorite_photo": "",
"favorites": "",
"feature_photo_updated": "",
"file_name": "",
"file_name_or_extension": "",
"filename": "",
"filetype": "",
"filter_people": "",
"find_them_fast": "",
"fix_incorrect_match": "",
"forward": "",
"general": "",
"get_help": "",
"getting_started": "",
"go_back": "",
"go_to_search": "",
"group_albums_by": "",
"has_quota": "",
"hide_gallery": "",
"hide_password": "",
"hide_person": "",
"host": "",
"hour": "",
"image": "",
"immich_logo": "",
"immich_web_interface": "",
"import_from_json": "",
"import_path": "",
"in_archive": "",
"include_archived": "",
"include_shared_albums": "",
"include_shared_partner_assets": "",
"individual_share": "",
"info": "",
"interval": {
"day_at_onepm": "",
"hours": "",
"night_at_midnight": "",
"night_at_twoam": ""
},
"invite_people": "",
"invite_to_album": "",
"jobs": "",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"let_others_respond": "",
"level": "",
"library": "",
"library_options": "",
"light": "",
"link_options": "",
"link_to_oauth": "",
"linked_oauth_account": "",
"list": "",
"loading": "",
"loading_search_results_failed": "",
"log_out": "",
"log_out_all_devices": "",
"login_has_been_disabled": "",
"look": "",
"loop_videos": "",
"loop_videos_description": "",
"make": "",
"manage_shared_links": "",
"manage_sharing_with_partners": "",
"manage_the_app_settings": "",
"manage_your_account": "",
"manage_your_api_keys": "",
"manage_your_devices": "",
"manage_your_oauth_connection": "",
"map": "",
"map_marker_with_image": "",
"map_settings": "",
"matches": "",
"media_type": "",
"memories": "",
"memories_setting_description": "",
"menu": "",
"merge": "",
"merge_people": "",
"merge_people_successfully": "",
"minimize": "",
"minute": "",
"missing": "",
"model": "",
"month": "",
"more": "",
"moved_to_trash": "",
"my_albums": "",
"name": "",
"name_or_nickname": "",
"never": "",
"new_api_key": "",
"new_password": "",
"new_person": "",
"new_user_created": "",
"newest_first": "",
"next": "",
"next_memory": "",
"no": "",
"no_albums_message": "",
"no_archived_assets_message": "",
"no_assets_message": "",
"no_duplicates_found": "",
"no_exif_info_available": "",
"no_explore_results_message": "",
"no_favorites_message": "",
"no_libraries_message": "",
"no_name": "",
"no_places": "",
"no_results": "",
"no_shared_albums_message": "",
"not_in_any_album": "",
"note_apply_storage_label_to_previously_uploaded assets": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"offline_paths": "",
"offline_paths_description": "",
"ok": "",
"oldest_first": "",
"online": "",
"only_favorites": "",
"open_the_search_filters": "",
"options": "",
"organize_your_library": "",
"other": "",
"other_devices": "",
"other_variables": "",
"owned": "",
"owner": "",
"partner_can_access": "",
"partner_can_access_assets": "",
"partner_can_access_location": "",
"partner_sharing": "",
"partners": "",
"password": "",
"password_does_not_match": "",
"password_required": "",
"password_reset_success": "",
"past_durations": {
"days": "",
"hours": "",
"years": ""
},
"path": "",
"pattern": "",
"pause": "",
"pause_memories": "",
"paused": "",
"pending": "",
"people": "",
"people_sidebar_description": "",
"permanent_deletion_warning": "",
"permanent_deletion_warning_setting_description": "",
"permanently_delete": "",
"permanently_deleted_asset": "",
"photos": "",
"photos_count": "",
"photos_from_previous_years": "",
"pick_a_location": "",
"place": "",
"places": "",
"play": "",
"play_memories": "",
"play_motion_photo": "",
"play_or_pause_video": "",
"port": "",
"preset": "",
"preview": "",
"previous": "",
"previous_memory": "",
"previous_or_next_photo": "",
"primary": "",
"profile_picture_set": "",
"public_share": "",
"reaction_options": "",
"read_changelog": "",
"recent": "",
"recent_searches": "",
"refresh": "",
"refreshed": "",
"refreshes_every_file": "",
"remove": "",
"remove_deleted_assets": "",
"remove_from_album": "",
"remove_from_favorites": "",
"remove_from_shared_link": "",
"removed_api_key": "",
"rename": "",
"repair": "",
"repair_no_results_message": "",
"replace_with_upload": "",
"require_password": "",
"require_user_to_change_password_on_first_login": "",
"reset": "",
"reset_password": "",
"reset_people_visibility": "",
"restore": "",
"restore_all": "",
"restore_user": "",
"resume": "",
"retry_upload": "",
"review_duplicates": "",
"role": "",
"save": "",
"saved_api_key": "",
"saved_profile": "",
"saved_settings": "",
"say_something": "",
"scan_all_libraries": "",
"scan_settings": "",
"search": "",
"search_albums": "",
"search_by_context": "",
"search_camera_make": "",
"search_camera_model": "",
"search_city": "",
"search_country": "",
"search_for_existing_person": "",
"search_people": "",
"search_places": "",
"search_state": "",
"search_timezone": "",
"search_type": "",
"search_your_photos": "",
"searching_locales": "",
"second": "",
"select_album_cover": "",
"select_all": "",
"select_avatar_color": "",
"select_face": "",
"select_featured_photo": "",
"select_keep_all": "",
"select_library_owner": "",
"select_new_face": "",
"select_photos": "",
"select_trash_all": "",
"selected": "",
"send_message": "",
"send_welcome_email": "",
"server_stats": "",
"set": "",
"set_as_album_cover": "",
"set_as_profile_picture": "",
"set_date_of_birth": "",
"set_profile_picture": "",
"set_slideshow_to_fullscreen": "",
"settings": "",
"settings_saved": "",
"share": "",
"shared": "",
"shared_by": "",
"shared_by_you": "",
"shared_from_partner": "",
"shared_links": "",
"shared_photos_and_videos_count": "",
"shared_with_partner": "",
"sharing": "",
"sharing_sidebar_description": "",
"show_album_options": "",
"show_and_hide_people": "",
"show_file_location": "",
"show_gallery": "",
"show_hidden_people": "",
"show_in_timeline": "",
"show_in_timeline_setting_description": "",
"show_keyboard_shortcuts": "",
"show_metadata": "",
"show_or_hide_info": "",
"show_password": "",
"show_person_options": "",
"show_progress_bar": "",
"show_search_options": "",
"shuffle": "",
"sign_out": "",
"sign_up": "",
"size": "",
"skip_to_content": "",
"slideshow": "",
"slideshow_settings": "",
"sort_albums_by": "",
"stack": "",
"stack_selected_photos": "",
"stacktrace": "",
"start": "",
"start_date": "",
"state": "",
"status": "",
"stop_motion_photo": "",
"stop_photo_sharing": "",
"stop_photo_sharing_description": "",
"stop_sharing_photos_with_user": "",
"storage": "",
"storage_label": "",
"storage_usage": "",
"submit": "",
"suggestions": "",
"sunrise_on_the_beach": "",
"swap_merge_direction": "",
"sync": "",
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"to_archive": "",
"to_favorite": "",
"toggle_settings": "",
"toggle_theme": "",
"total_usage": "",
"trash": "",
"trash_all": "",
"trash_no_results_message": "",
"trashed_items_will_be_permanently_deleted_after": "",
"type": "",
"unarchive": "",
"unfavorite": "",
"unhide_person": "",
"unknown": "",
"unknown_year": "",
"unlimited": "",
"unlink_oauth": "",
"unlinked_oauth_account": "",
"unselect_all": "",
"unstack": "",
"untracked_files": "",
"untracked_files_decription": "",
"up_next": "",
"updated_password": "",
"upload": "",
"upload_concurrency": "",
"url": "",
"usage": "",
"user": "",
"user_id": "",
"user_usage_detail": "",
"username": "",
"users": "",
"utilities": "",
"validate": "",
"variables": "",
"version": "",
"video": "",
"video_hover_setting": "",
"video_hover_setting_description": "",
"videos": "",
"videos_count": "",
"view_all": "",
"view_all_users": "",
"view_links": "",
"view_next_asset": "",
"view_previous_asset": "",
"waiting": "",
"week": "",
"welcome": "",
"welcome_to_immich": "",
"year": "",
"yes": "",
"you_dont_have_any_shared_links": "",
"zoom_image": ""
"acknowledge": "دانپێدانان"
}

View File

@ -71,9 +71,7 @@
"image_format": "Formatas",
"image_format_description": "WebP sukuria mažesnius failus nei JPEG, bet lėčiau juos apdoroja.",
"image_prefer_embedded_preview": "Pageidautinai rodyti įterptą peržiūrą",
"image_prefer_embedded_preview_setting_description": "",
"image_prefer_wide_gamut": "Teikti pirmenybę plačiai gamai",
"image_prefer_wide_gamut_setting_description": "",
"image_preview_description": "Vidutinio dydžio vaizdas su išvalytais metaduomenimis, naudojamas kai žiūrimas vienas objektas arba mašininiam mokymuisi",
"image_preview_quality_description": "Peržiūros kokybė nuo 1-100. Aukštesnės reikšmės yra geriau, bet sukuriami didesni failai gali sumažinti programos reagavimo laiką. Mažos vertės nustatymas gali paveikti mašininio mokymo kokybę.",
"image_preview_title": "Peržiūros nustatymai",
@ -109,22 +107,18 @@
"machine_learning_clip_model": "CLIP modelis",
"machine_learning_duplicate_detection": "Dublikatų aptikimas",
"machine_learning_duplicate_detection_enabled": "Įjungti dublikatų aptikimą",
"machine_learning_duplicate_detection_enabled_description": "",
"machine_learning_duplicate_detection_setting_description": "Naudoti CLIP įterpimus, norint rasti galimus duplikatus",
"machine_learning_enabled": "Įgalinti mašininį mokymąsi",
"machine_learning_enabled_description": "Jei išjungta, visos „ML“ funkcijos bus išjungtos, nepaisant toliau pateiktų nustatymų.",
"machine_learning_facial_recognition": "Veidų atpažinimas",
"machine_learning_facial_recognition_description": "Aptikti, atpažinti ir sugrupuoti veidus nuotraukose",
"machine_learning_facial_recognition_model": "Veidų atpažinimo modelis",
"machine_learning_facial_recognition_model_description": "",
"machine_learning_facial_recognition_setting": "Įgalinti veidų atpažinimą",
"machine_learning_facial_recognition_setting_description": "Išjungus, vaizdai nebus užšifruoti veidų atpažinimui ir nebus naudojami Žmonių sekcijoje Naršymo puslapyje.",
"machine_learning_max_detection_distance": "Maksimalus aptikimo atstumas",
"machine_learning_max_detection_distance_description": "Didžiausias atstumas tarp dviejų vaizdų, kad jie būtų laikomi dublikatais, svyruoja nuo 0,001 iki 0,1. Didesnės vertės aptiks daugiau dublikatų, tačiau gali būti klaidingai teigiami.",
"machine_learning_max_recognition_distance": "Maksimalus atpažinimo atstumas",
"machine_learning_max_recognition_distance_description": "",
"machine_learning_min_detection_score": "Minimalus aptikimo balas",
"machine_learning_min_detection_score_description": "",
"machine_learning_min_recognized_faces": "Mažiausias atpažintų veidų skaičius",
"machine_learning_min_recognized_faces_description": "Mažiausias atpažintų veidų skaičius asmeniui, kurį reikia sukurti. Tai padidinus, veido atpažinimas tampa tikslesnis, bet padidėja tikimybė, kad veidas žmogui nepriskirtas.",
"machine_learning_settings": "Mašininio mokymosi nustatymai",
@ -158,7 +152,6 @@
"metadata_settings": "Metaduomenų nustatymai",
"metadata_settings_description": "Tvarkyti metaduomenų nustatymus",
"migration_job": "Migracija",
"migration_job_description": "",
"no_paths_added": "Keliai nepridėti",
"no_pattern_added": "Šablonas nepridėtas",
"note_apply_storage_label_previous_assets": "Pastaba: norėdami pritaikyti saugyklos etiketę seniau įkeltiems ištekliams, paleiskite",
@ -191,12 +184,6 @@
"oauth_settings": "OAuth",
"oauth_settings_description": "Tvarkyti OAuth prisijungimo nustatymus",
"oauth_settings_more_details": "Detaliau apie šią funkciją galite paskaityti <link>dokumentacijoje</link>.",
"oauth_storage_label_claim": "",
"oauth_storage_label_claim_description": "",
"oauth_storage_quota_claim": "",
"oauth_storage_quota_claim_description": "",
"oauth_storage_quota_default": "",
"oauth_storage_quota_default_description": "",
"offline_paths": "Nepasiekiami adresai",
"offline_paths_description": "Šie rezultatai gali būti dėl rankinio failų ištrynimo, kurie nėra išorinės bibliotekos dalis.",
"password_enable_description": "Prisijungti su el. paštu ir slaptažodžiu",
@ -217,93 +204,42 @@
"server_settings_description": "Tvarkyti serverio nustatymus",
"server_welcome_message": "Sveikinimo pranešimas",
"server_welcome_message_description": "Žinutė, rodoma prisijungimo puslapyje.",
"sidecar_job_description": "",
"slideshow_duration_description": "Sekundžių skaičius, kiek viena nuotrauka rodoma",
"smart_search_job_description": "Vykdykite mašininį mokymąsi bibliotekos elementų išmaniajai paieškai",
"storage_template_enable_description": "",
"storage_template_hash_verification_enabled": "",
"storage_template_hash_verification_enabled_description": "",
"storage_template_migration_job": "",
"storage_template_settings": "",
"storage_template_settings_description": "",
"system_settings": "Sistemos nustatymai",
"tag_cleanup_job": "Žymų išvalymas",
"theme_custom_css_settings": "Individualizuotas CSS",
"theme_custom_css_settings_description": "",
"theme_settings": "Temos nustatymai",
"theme_settings_description": "",
"thumbnail_generation_job": "Generuoti miniatiūras",
"thumbnail_generation_job_description": "Didelių, mažų ir neryškių miniatiūrų generavimas kiekvienam bibliotekos elementui, taip pat miniatiūrų generavimas kiekvienam asmeniui",
"transcoding_acceleration_api": "Spartinimo API",
"transcoding_acceleration_api_description": "",
"transcoding_acceleration_nvenc": "NVENC (reikalinga NVIDIA GPU)",
"transcoding_acceleration_qsv": "",
"transcoding_acceleration_rkmpp": "",
"transcoding_acceleration_vaapi": "VAAPI",
"transcoding_accepted_audio_codecs": "",
"transcoding_accepted_audio_codecs_description": "",
"transcoding_accepted_containers": "Priimami konteineriai",
"transcoding_accepted_video_codecs": "",
"transcoding_accepted_video_codecs_description": "",
"transcoding_advanced_options_description": "Parinktys, kurių daugelis naudotojų keisti neturėtų",
"transcoding_audio_codec": "Garso kodekas",
"transcoding_audio_codec_description": "Opus yra aukščiausios kokybės variantas, tačiau turi mažesnį suderinamumą su senesniais įrenginiais ar programine įranga.",
"transcoding_bitrate_description": "Vaizdo įrašai viršija maksimalią leistiną bitų spartą arba nėra priimtino formato",
"transcoding_constant_quality_mode": "Pastovios kokybės režimas",
"transcoding_constant_quality_mode_description": "",
"transcoding_constant_rate_factor": "",
"transcoding_constant_rate_factor_description": "",
"transcoding_disabled_description": "",
"transcoding_hardware_acceleration": "Techninės įrangos spartinimas",
"transcoding_hardware_acceleration_description": "",
"transcoding_hardware_decoding": "Aparatinis dekodavimas",
"transcoding_hardware_decoding_setting_description": "",
"transcoding_hevc_codec": "HEVC kodekas",
"transcoding_max_b_frames": "",
"transcoding_max_b_frames_description": "",
"transcoding_max_bitrate": "Maksimalus bitų srautas",
"transcoding_max_bitrate_description": "",
"transcoding_max_keyframe_interval": "",
"transcoding_max_keyframe_interval_description": "",
"transcoding_optimal_description": "",
"transcoding_preferred_hardware_device": "",
"transcoding_preferred_hardware_device_description": "",
"transcoding_preset_preset": "",
"transcoding_preset_preset_description": "",
"transcoding_reference_frames": "",
"transcoding_reference_frames_description": "",
"transcoding_required_description": "",
"transcoding_settings": "",
"transcoding_settings_description": "",
"transcoding_target_resolution": "",
"transcoding_target_resolution_description": "Didesnės skiriamosios gebos gali išsaugoti daugiau detalių, tačiau jas koduoti užtrunka ilgiau, failų dydžiai yra didesni ir gali sumažėti programos jautrumas.",
"transcoding_temporal_aq": "",
"transcoding_temporal_aq_description": "",
"transcoding_threads": "",
"transcoding_threads_description": "",
"transcoding_tone_mapping": "",
"transcoding_tone_mapping_description": "",
"transcoding_transcode_policy": "",
"transcoding_two_pass_encoding": "",
"transcoding_two_pass_encoding_setting_description": "",
"transcoding_video_codec": "Video kodekas",
"transcoding_video_codec_description": "",
"trash_enabled_description": "Įgalinti šiukšliadėžės funkcijas",
"trash_number_of_days": "Dienų skaičius",
"trash_number_of_days_description": "",
"trash_settings": "Šiukšliadėžės nustatymai",
"trash_settings_description": "Tvarkyti šiukšliadėžės nustatymus",
"untracked_files": "Nesekami failai",
"untracked_files_description": "Šie failai aplikacijos nesekami. Jie galėjo atsirasti dėl nepavykusio perkėlimo, nutraukto įkėlimo ar palikti per klaidą",
"user_delete_delay_settings": "Ištrynimo delsa",
"user_delete_delay_settings_description": "",
"user_management": "Naudotojų valdymas",
"user_password_has_been_reset": "Naudotojo slaptažodis buvo iš naujo nustatytas:",
"user_restore_description": "Naudotojo <b>{user}</b> paskyra bus atkurta.",
"user_settings": "Naudotojo nustatymai",
"user_settings_description": "Valdyti naudotojo nustatymus",
"user_successfully_removed": "Naudotojas {email} sėkmingai pašalintas.",
"version_check_enabled_description": "",
"version_check_settings": "Versijos tikrinimas",
"version_check_settings_description": "Įjungti/išjungti naujos versijos pranešimus",
"video_conversion_job": "Vaizdo įrašų konvertavimas",
@ -312,7 +248,6 @@
"admin_email": "Administratoriaus el. paštas",
"admin_password": "Administratoriaus slaptažodis",
"administration": "Administravimas",
"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",
@ -370,7 +305,6 @@
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"app_settings": "Programos nustatymai",
"appears_in": "",
"archive": "Archyvas",
"archive_or_unarchive_photo": "Archyvuoti arba išarchyvuoti nuotrauką",
"archive_page_no_archived_assets": "No archived assets found",
@ -488,7 +422,6 @@
"backup_manual_title": "Upload status",
"backup_options_page_title": "Backup options",
"backup_setting_subtitle": "Manage background and foreground upload settings",
"backward": "",
"birthdate_saved": "Sėkmingai išsaugota gimimo data",
"blurred_background": "Neryškus fonas",
"bugs_and_feature_requests": "Klaidų ir funkcijų užklausos",
@ -527,7 +460,6 @@
"change_expiration_time": "Pakeisti galiojimo trukmę",
"change_location": "Pakeisti vietovę",
"change_name": "Pakeisti vardą",
"change_name_successfully": "",
"change_password": "Pakeisti slaptažodį",
"change_password_description": "Tai arba pirmas kartas, kai jungiatės prie sistemos, arba buvo pateikta užklausa pakeisti jūsų slaptažodį. Prašome įvesti naują slaptažodį žemiau.",
"change_password_form_confirm_password": "Confirm Password",
@ -570,7 +502,6 @@
"confirm_admin_password": "Patvirtinti administratoriaus slaptažodį",
"confirm_delete_shared_link": "Ar tikrai norite ištrinti šią bendrinimo nuorodą?",
"confirm_password": "Patvirtinti slaptažodį",
"contain": "",
"context": "Kontekstas",
"continue": "Tęsti",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
@ -592,8 +523,6 @@
"copy_password": "Kopijuoti slaptažodį",
"copy_to_clipboard": "Kopijuoti į iškarpinę",
"country": "Šalis",
"cover": "",
"covers": "",
"create": "Sukurti",
"create_album": "Sukurti albumą",
"create_album_page_untitled": "Untitled",
@ -615,24 +544,20 @@
"curated_object_page_title": "Things",
"current_device": "Dabartinis įrenginys",
"current_server_address": "Current server address",
"custom_locale": "",
"custom_locale_description": "Formatuoti datas ir skaičius pagal kalbą ir regioną",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"dark": "",
"date_after": "Data po",
"date_and_time": "Data ir laikas",
"date_before": "Data prieš",
"date_format": "E, LLL d, y • h:mm a",
"date_of_birth_saved": "Gimimo data sėkmingai išsaugota",
"date_range": "",
"day": "Diena",
"deduplicate_all": "Šalinti visus dublikatus",
"deduplication_criteria_1": "Failo dydis baitais",
"deduplication_criteria_2": "EXIF metaduomenų įrašų skaičius",
"deduplication_info": "Dublikatų šalinimo informacija",
"deduplication_info_description": "Automatinis elementų parinkimas ir masinis dublikatų šalinimas atliekamas atsižvelgiant į:",
"default_locale": "",
"default_locale_description": "Formatuoti datas ir skaičius pagal jūsų naršyklės lokalę",
"delete": "Ištrinti",
"delete_album": "Ištrinti albumą",
@ -665,13 +590,10 @@
"discover": "Atrasti",
"dismiss_all_errors": "Nepaisyti visų klaidų",
"dismiss_error": "Nepaisyti klaidos",
"display_options": "",
"display_order": "Atvaizdavimo tvarka",
"display_original_photos": "Rodyti originalias nuotraukas",
"display_original_photos_setting_description": "",
"do_not_show_again": "Daugiau nerodyti šio pranešimo",
"documentation": "Dokumentacija",
"done": "",
"download": "Atsisiųsti",
"download_canceled": "Download canceled",
"download_complete": "Download complete",
@ -711,7 +633,6 @@
"edit_title": "Redaguoti antraštę",
"edit_user": "Redaguoti naudotoją",
"edited": "Redaguota",
"editor": "",
"email": "El. paštas",
"empty_folder": "This folder is empty",
"empty_trash": "Ištuštinti šiukšliadėžę",
@ -763,56 +684,37 @@
"unable_to_create_library": "Nepavyko sukurti bibliotekos",
"unable_to_create_user": "Nepavyko sukurti naudotojo",
"unable_to_delete_album": "Nepavyksta ištrinti albumo",
"unable_to_delete_asset": "",
"unable_to_delete_exclusion_pattern": "Nepavyksta ištrinti išimčių šablono",
"unable_to_delete_import_path": "Nepavyksta ištrinti importavimo kelio",
"unable_to_delete_shared_link": "Nepavyko ištrinti bendrinimo nuorodos",
"unable_to_delete_user": "Nepavyksta ištrinti naudotojo",
"unable_to_edit_exclusion_pattern": "Nepavyksta redaguoti išimčių šablono",
"unable_to_edit_import_path": "Nepavyksta redaguoti išimčių kelio",
"unable_to_empty_trash": "",
"unable_to_enter_fullscreen": "Nepavyksta pereiti į viso ekrano režimą",
"unable_to_exit_fullscreen": "Nepavyksta išeiti iš viso ekrano režimo",
"unable_to_get_shared_link": "Nepavyko gauti bendrinimo nuorodos",
"unable_to_hide_person": "Nepavyksta paslėpti žmogaus",
"unable_to_link_oauth_account": "Nepavyko susieti su OAuth paskyra",
"unable_to_load_album": "Nepavyksta užkrauti albumo",
"unable_to_load_asset_activity": "",
"unable_to_load_items": "",
"unable_to_load_liked_status": "",
"unable_to_log_out_all_devices": "Nepavyksta atjungti visų įrenginių",
"unable_to_log_out_device": "Nepavyksta atjungti įrenginio",
"unable_to_login_with_oauth": "Nepavyko prisijungti su OAuth",
"unable_to_play_video": "Nepavyksta paleisti vaizdo įrašo",
"unable_to_refresh_user": "Nepavyksta atnaujinti naudotojo",
"unable_to_remove_album_users": "",
"unable_to_remove_api_key": "Nepavyko pašalinti API rakto",
"unable_to_remove_assets_from_shared_link": "Nepavyko iš bendrinimo nuorodos pašalinti elementų",
"unable_to_remove_deleted_assets": "Nepavyko pašalinti nepasiekiamų elementų",
"unable_to_remove_library": "Nepavyksta pašalinti bibliotekos",
"unable_to_remove_partner": "Nepavyksta pašalinti partnerio",
"unable_to_remove_reaction": "Nepavyksta pašalinti reakcijos",
"unable_to_repair_items": "",
"unable_to_reset_password": "",
"unable_to_resolve_duplicate": "Nepavyko sutvarkyti dublikatų",
"unable_to_restore_assets": "",
"unable_to_restore_trash": "",
"unable_to_restore_user": "",
"unable_to_save_album": "",
"unable_to_save_name": "",
"unable_to_save_profile": "Nepavyko išsaugoti profilio",
"unable_to_save_settings": "Nepavyksta išsaugoti nustatymų",
"unable_to_scan_libraries": "Nepavyksta nuskaityti bibliotekų",
"unable_to_scan_library": "Nepavyksta nuskaityti bibliotekos",
"unable_to_set_feature_photo": "Nepavyksta nustatyti mėgstamiausios nuotraukos",
"unable_to_set_profile_picture": "Nepavyksta nustatyti profilio nuotraukos",
"unable_to_submit_job": "",
"unable_to_trash_asset": "Nepavyko perkelti į šiukšliadėžę",
"unable_to_unlink_account": "",
"unable_to_update_library": "",
"unable_to_update_location": "",
"unable_to_update_settings": "",
"unable_to_update_user": "",
"unable_to_upload_file": "Nepavyksta įkelti failo"
},
"exif": "Exif",
@ -831,7 +733,6 @@
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!",
"experimental_settings_title": "Experimental",
"expire_after": "",
"expired": "Nebegalioja",
"expires_date": "Nebegalios už {date}",
"explore": "Naršyti",
@ -850,27 +751,19 @@
"favorite_or_unfavorite_photo": "Įtraukti prie arba pašalinti iš mėgstamiausių",
"favorites": "Mėgstamiausi",
"favorites_page_no_favorites": "No favorite assets found",
"feature_photo_updated": "",
"features": "Funkcijos",
"features_setting_description": "Valdyti aplikacijos funkcijas",
"file_name": "Failo pavadinimas",
"file_name_or_extension": "Failo pavadinimas arba plėtinys",
"filename": "",
"filetype": "Failo tipas",
"filter": "Filter",
"filter_people": "Filtruoti žmones",
"fix_incorrect_match": "",
"folder": "Folder",
"folder_not_found": "Folder not found",
"folders": "Aplankai",
"folders_feature_description": "Peržiūrėkite failų sistemoje esančias nuotraukas ir vaizdo įrašus aplankų rodinyje",
"forward": "",
"general": "",
"get_help": "Gauti pagalbos",
"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",
"getting_started": "",
"go_back": "",
"go_to_search": "",
"grant_permission": "Grant permission",
"group_albums_by": "Grupuoti albumus pagal...",
"group_no": "Negrupuoti",
@ -906,7 +799,6 @@
"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": "",
"hour": "Valanda",
"ignore_icloud_photos": "Ignore iCloud photos",
"ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
@ -922,11 +814,9 @@
"include_archived": "Įtraukti archyvuotus",
"include_shared_albums": "Įtraukti bendrinamus albumus",
"include_shared_partner_assets": "Įtraukti partnerio pasidalintus elementus",
"individual_share": "",
"info": "Informacija",
"interval": {
"day_at_onepm": "Kiekvieną dieną 13:00",
"hours": "",
"night_at_midnight": "Kiekvieną vidurnaktį",
"night_at_twoam": "Kiekvieną naktį 02:00"
},
@ -955,7 +845,6 @@
"library_page_sort_created": "Created date",
"library_page_sort_last_modified": "Last modified",
"library_page_sort_title": "Album title",
"light": "",
"link_options": "Nuorodų parinktys",
"link_to_oauth": "Susieti su OAuth",
"linked_oauth_account": "Susieta OAuth paskyra",
@ -1000,9 +889,7 @@
"logout_all_device_confirmation": "Ar tikrai norite atsijungti iš visų įrenginių?",
"logout_this_device_confirmation": "Ar tikrai norite atsijungti iš šio prietaiso?",
"longitude": "Ilguma",
"look": "",
"loop_videos": "Kartoti vaizdo įrašus",
"loop_videos_description": "",
"make": "Gamintojas",
"manage_shared_links": "Bendrinimo nuorodų tvarkymas",
"manage_sharing_with_partners": "Valdyti dalijimąsi su partneriais",
@ -1019,7 +906,6 @@
"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_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",
@ -1086,15 +972,11 @@
"no_assets_message": "SPUSTELĖKITE NORĖDAMI ĮKELTI PIRMĄJĄ NUOTRAUKĄ",
"no_assets_to_show": "No assets to show",
"no_duplicates_found": "Dublikatų nerasta.",
"no_exif_info_available": "",
"no_explore_results_message": "Įkelkite daugiau nuotraukų ir tyrinėkite savo kolekciją.",
"no_favorites_message": "",
"no_libraries_message": "Sukurkite išorinę biblioteką nuotraukoms ir vaizdo įrašams peržiūrėti",
"no_name": "Be vardo",
"no_places": "",
"no_results": "Nerasta",
"no_results_description": "Pabandykite sinonimą arba bendresnį raktažodį",
"no_shared_albums_message": "",
"not_in_any_album": "Nė viename albume",
"not_selected": "Not selected",
"notes": "Pastabos",
@ -1120,7 +1002,6 @@
"or": "arba",
"organize_your_library": "Tvarkykite savo biblioteką",
"original": "Originalas",
"other": "",
"other_devices": "Kiti įrenginiai",
"other_variables": "Kiti kintamieji",
"owned": "Nuosavi",
@ -1137,19 +1018,12 @@
"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_sharing": "",
"partners": "Partneriai",
"password": "Slaptažodis",
"password_does_not_match": "Slaptažodis nesutampa",
"password_required": "Reikalingas slaptažodis",
"password_reset_success": "Slaptažodis sėkmingai atkurtas",
"past_durations": {
"days": "",
"hours": "",
"years": ""
},
"path": "Kelias",
"pattern": "",
"pause": "Sustabdyti",
"pause_memories": "Pristabdyti atsiminimus",
"paused": "Sustabdyta",
@ -1158,11 +1032,8 @@
"people_edits_count": "{count, plural, one {Redaguotas # asmuo} few {Redaguoti # asmenys} other {Redaguota # asmenų}}",
"people_feature_description": "Peržiūrėkite nuotraukas ir vaizdo įrašus sugrupuotus pagal asmenis",
"people_sidebar_description": "Rodyti asmenų rodinio nuorodą šoninėje juostoje",
"permanent_deletion_warning": "",
"permanent_deletion_warning_setting_description": "",
"permanently_delete": "Ištrinti visam laikui",
"permanently_delete_assets_count": "Visam laikui ištrinti {count, plural, one {# elementą} few {# elementus} other {# elementų}}",
"permanently_deleted_asset": "",
"permanently_deleted_assets_count": "Visam laikui {count, plural, one {ištrintas # elementas} few {ištrinti # elementai} other {ištrinta # elementų}}",
"permission_onboarding_back": "Back",
"permission_onboarding_continue_anyway": "Continue anyway",
@ -1176,22 +1047,11 @@
"photos_and_videos": "Nuotraukos ir vaizdo įrašai",
"photos_count": "{count, plural, one {{count, number} nuotrauka} few {{count, number} nuotraukos} other {{count, number} nuotraukų}}",
"photos_from_previous_years": "Ankstesnių metų nuotraukos",
"pick_a_location": "",
"place": "Vieta",
"places": "Vietos",
"play": "",
"play_memories": "Leisti atsiminimus",
"play_motion_photo": "",
"play_or_pause_video": "",
"port": "",
"preferences_settings_subtitle": "Manage the app's preferences",
"preferences_settings_title": "Preferences",
"preset": "",
"preview": "",
"previous": "",
"previous_memory": "",
"previous_or_next_photo": "",
"primary": "",
"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.",
@ -1202,7 +1062,6 @@
"profile_image_of_user": "{user} profilio nuotrauka",
"profile_picture_set": "Profilio nuotrauka nustatyta.",
"public_album": "Viešas albumas",
"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}",
@ -1237,10 +1096,6 @@
"rating": "Įvertinimas žvaigždutėmis",
"rating_count": "{count, plural, one {# įvertinimas} few {# įvertinimai} other {# įvertinimų}}",
"rating_description": "Rodyti EXIF įvertinimus informacijos skydelyje",
"reaction_options": "",
"read_changelog": "",
"recent": "",
"recent_searches": "",
"recently_added": "Recently added",
"recently_added_page_title": "Recently Added",
"refresh": "Atnaujinti",
@ -1255,7 +1110,6 @@
"refreshing_metadata": "Perkraunami metaduomenys",
"remove": "Pašalinti",
"remove_assets_shared_link_confirmation": "Ar tikrai norite pašalinti {count, plural, one {# elementą} few {# elementus} other {# elementų}} iš šios bendrinimo nuorodos?",
"remove_deleted_assets": "",
"remove_from_album": "Pašalinti iš albumo",
"remove_from_favorites": "Pašalinti iš mėgstamiausių",
"remove_from_shared_link": "Pašalinti iš bendrinimo nuorodos",
@ -1273,16 +1127,12 @@
"replace_with_upload": "Pakeisti naujai įkeltu failu",
"require_password": "Reikalauti slaptažodžio",
"reset": "Atstatyti",
"reset_password": "",
"reset_people_visibility": "",
"resolve_duplicates": "Sutvarkyti dublikatus",
"resolved_all_duplicates": "Sutvarkyti visi dublikatai",
"restore": "Atkurti",
"restore_all": "Atkurti visus",
"restore_user": "Atkurti naudotoją",
"retry_upload": "",
"review_duplicates": "Peržiūrėti dublikatus",
"role": "",
"save": "Išsaugoti",
"save_to_gallery": "Save to gallery",
"saved_api_key": "Išsaugotas API raktas",
@ -1294,14 +1144,10 @@
"scan_library": "Skenuoti",
"scan_settings": "Skenavimo nustatymai",
"search": "Ieškoti",
"search_albums": "",
"search_by_context": "Ieškoti pagal kontekstą",
"search_by_description_example": "Žygio diena Sapoje",
"search_by_filename": "Ieškoti pagal failo pavadinimą arba plėtinį",
"search_by_filename_example": "pvz. IMG_1234.JPG arba PNG",
"search_camera_make": "",
"search_camera_model": "",
"search_city": "",
"search_country": "Ieškoti šalies...",
"search_filter_apply": "Apply filter",
"search_filter_camera_title": "Select camera type",
@ -1316,7 +1162,6 @@
"search_filter_media_type": "Media Type",
"search_filter_media_type_title": "Select media type",
"search_filter_people_title": "Select people",
"search_for_existing_person": "",
"search_no_more_result": "No more results",
"search_no_people_named": "Nėra žmonių vardu „{name}“",
"search_no_result": "No results found, try a different search term or combination",
@ -1335,25 +1180,17 @@
"search_places": "Ieškoti vietų",
"search_result_page_new_search_hint": "New Search",
"search_settings": "Ieškoti nustatymų",
"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_tags": "Ieškoti žymų...",
"search_timezone": "",
"search_type": "Paieškos tipas",
"search_your_photos": "Ieškoti nuotraukų",
"searching_locales": "",
"second": "",
"select_album_cover": "",
"select_all": "",
"select_all_duplicates": "Pasirinkti visus dublikatus",
"select_avatar_color": "Pasirinkti avataro spalvą",
"select_face": "Pasirinkti veidą",
"select_featured_photo": "Pasirinkti rodomą nuotrauką",
"select_keep_all": "Visus pažymėti \"Palikti\"",
"select_library_owner": "Pasirinkti bibliotekos savininką",
"select_new_face": "",
"select_photos": "",
"select_trash_all": "Visus pažymėti \"Išmesti\"",
"select_user_for_sharing_page_err_album": "Failed to create album",
"selected": "Pasirinkta",
@ -1368,7 +1205,6 @@
"server_stats": "Serverio statistika",
"server_version": "Serverio versija",
"set": "Nustatyti",
"set_as_album_cover": "",
"set_as_profile_picture": "Nustatyti kaip profilio nuotrauką",
"set_date_of_birth": "Nustatyti gimimo datą",
"set_profile_picture": "Nustatyti profilio nuotrauką",
@ -1398,7 +1234,6 @@
"setting_video_viewer_original_video_title": "Force original video",
"settings": "Nustatymai",
"settings_require_restart": "Please restart Immich to apply this setting",
"settings_saved": "",
"share": "Dalintis",
"share_add_photos": "Add photos",
"share_assets_selected": "{} selected",
@ -1411,8 +1246,6 @@
"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_by": "",
"shared_by_you": "",
"shared_intent_upload_button_progress_text": "{} / {} Uploaded",
"shared_link_app_bar_title": "Shared Links",
"shared_link_clipboard_copied_massage": "Copied to clipboard",
@ -1458,20 +1291,15 @@
"show_album_options": "Rodyti albumo parinktis",
"show_file_location": "Rodyti rinkmenos vietą",
"show_gallery": "Rodyti galeriją",
"show_hidden_people": "",
"show_in_timeline": "Rodyti laiko skalėje",
"show_in_timeline_setting_description": "Rodyti šio naudotojo nuotraukas ir vaizdo įrašus mano laiko skalėje",
"show_keyboard_shortcuts": "",
"show_metadata": "Rodyti metaduomenis",
"show_or_hide_info": "Rodyti arba slėpti informaciją",
"show_password": "Rodyti slaptažodį",
"show_person_options": "",
"show_progress_bar": "",
"show_search_options": "Rodyti paieškos parinktis",
"show_slideshow_transition": "Rodyti perėjimą tarp skaidrių",
"show_supporter_badge": "Rėmėjo ženklelis",
"show_supporter_badge_description": "Rodyti rėmėjo ženklelį",
"shuffle": "",
"sidebar": "Šoninė juosta",
"sidebar_display_description": "Rodyti rodinio nuorodą šoninėje juostoje",
"sign_out": "Atsijungti",
@ -1480,7 +1308,6 @@
"skip_to_content": "Pereiti prie turinio",
"slideshow": "Skaidrių peržiūra",
"slideshow_settings": "Skaidrių peržiūros nustatymai",
"sort_albums_by": "",
"sort_created": "Sukūrimo data",
"sort_modified": "Keitimo data",
"sort_oldest": "Seniausia nuotrauka",
@ -1492,20 +1319,14 @@
"stack_select_one_photo": "Pasirinkti pagrindinę grupės nuotrauką",
"stack_selected_photos": "Grupuoti pasirinktas nuotraukas",
"stacked_assets_count": "{count, plural, one {Sugrupuotas # elementas} few {Sugrupuoti # elementai} other {Sugrupuota # elementų}}",
"stacktrace": "",
"start": "Pradėti",
"start_date": "Pradžios data",
"state": "",
"status": "Statusas",
"stop_motion_photo": "",
"storage": "Saugykla",
"storage_label": "",
"storage_usage": "Naudojama {used} iš {available}",
"submit": "Pateikti",
"suggestions": "",
"sunrise_on_the_beach": "Saulėtekis paplūdimyje",
"support_and_feedback": "Palaikymas ir atsiliepimai",
"swap_merge_direction": "",
"sync": "Sinchronizuoti",
"sync_albums": "Sync albums",
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
@ -1519,8 +1340,6 @@
"tags": "Žymos",
"template": "Šablonas",
"theme": "Tema",
"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.",
@ -1541,9 +1360,6 @@
"to_change_password": "Pakeisti slaptažodį",
"to_favorite": "Įtraukti prie mėgstamiausių",
"to_trash": "Išmesti",
"toggle_settings": "",
"toggle_theme": "",
"total_usage": "",
"trash": "Šiukšliadėžė",
"trash_all": "Perkelti visus į šiukšliadėžę",
"trash_count": "Perkelti {count, number} į šiukšliadėžę",
@ -1561,8 +1377,6 @@
"unarchive": "Išarchyvuoti",
"unarchived_count": "{count, plural, other {# išarchyvuota}}",
"unfavorite": "Pašalinti iš mėgstamiausių",
"unhide_person": "",
"unknown": "",
"unknown_year": "Nežinomi metai",
"unlink_oauth": "Atsieti OAuth",
"unlinked_oauth_account": "Atsieta OAuth paskyra",
@ -1574,7 +1388,6 @@
"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",
@ -1597,7 +1410,6 @@
"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ą",
"username": "Naudotojo vardas",
@ -1625,8 +1437,6 @@
"view_all_users": "Peržiūrėti visus naudotojus",
"view_in_timeline": "Žiūrėti laiko skalėje",
"view_links": "Žiūrėti nuorodas",
"view_next_asset": "",
"view_previous_asset": "",
"view_stack": "Peržiūrėti grupę",
"viewer_remove_from_stack": "Remove from Stack",
"viewer_stack_use_as_main_asset": "Use as Main Asset",

View File

@ -56,10 +56,6 @@
"face_detection": "Seju noteikšana",
"image_format": "Formāts",
"image_format_description": "WebP veido mazākus failus nekā JPEG, taču to kodēšana ir lēnāka.",
"image_prefer_embedded_preview": "",
"image_prefer_embedded_preview_setting_description": "",
"image_prefer_wide_gamut": "",
"image_prefer_wide_gamut_setting_description": "",
"image_quality": "Kvalitāte",
"image_resolution": "Izšķirtspēja",
"image_settings": "Attēla Iestatījumi",
@ -70,98 +66,43 @@
"job_settings_description": "Uzdevumu izpildes vienlaicīguma pārvaldība",
"job_status": "Uzdevumu statuss",
"library_deleted": "Bibliotēka dzēsta",
"library_scanning": "",
"library_scanning_description": "",
"library_scanning_enable_description": "",
"library_settings": "",
"library_settings_description": "Ārējo bibliotēku iestatījumu pārvaldība",
"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 modelis",
"machine_learning_duplicate_detection": "Dublikātu noteikšana",
"machine_learning_duplicate_detection_enabled_description": "",
"machine_learning_duplicate_detection_setting_description": "",
"machine_learning_enabled_description": "",
"machine_learning_facial_recognition": "Seju atpazīšana",
"machine_learning_facial_recognition_description": "",
"machine_learning_facial_recognition_model": "Seju atpazīšanas modelis",
"machine_learning_facial_recognition_model_description": "",
"machine_learning_facial_recognition_setting_description": "",
"machine_learning_max_detection_distance": "",
"machine_learning_max_detection_distance_description": "",
"machine_learning_max_recognition_distance": "",
"machine_learning_max_recognition_distance_description": "",
"machine_learning_min_detection_score": "",
"machine_learning_min_detection_score_description": "",
"machine_learning_min_recognized_faces": "",
"machine_learning_min_recognized_faces_description": "",
"machine_learning_settings": "Mašīnmācīšanās iestatījumi",
"machine_learning_settings_description": "Mašīnmācīšanās funkciju un iestatījumu pārvaldība",
"machine_learning_smart_search": "Viedā meklēšana",
"machine_learning_smart_search_description": "",
"machine_learning_smart_search_enabled_description": "",
"machine_learning_url_description": "Mašīnmācīšanās servera URL",
"manage_concurrency": "Vienlaicīgas darbības pārvaldība",
"manage_log_settings": "Žurnāla iestatījumu pārvaldība",
"map_dark_style": "",
"map_enable_description": "",
"map_gps_settings": "Kartes un GPS iestatījumi",
"map_gps_settings_description": "Karšu un GPS (apgrieztās ģeokodēšanas) iestatījumu pārvaldība",
"map_light_style": "",
"map_manage_reverse_geocoding_settings": "<link>Reversās ģeokodēšanas</link> iestatījumu pārvaldība",
"map_reverse_geocoding": "",
"map_reverse_geocoding_enable_description": "",
"map_reverse_geocoding_settings": "",
"map_settings": "Karte",
"map_settings_description": "Kartes iestatījumu pārvaldība",
"map_style_description": "",
"metadata_extraction_job": "Metadatu iegūšana",
"metadata_extraction_job_description": "",
"metadata_settings": "Metadatu iestatījumi",
"metadata_settings_description": "Metadatu iestatījumu pārvaldība",
"migration_job": "Migrācija",
"migration_job_description": "",
"no_paths_added": "Nav pievienots neviens ceļš",
"no_pattern_added": "Nav pievienots neviens izslēgšanas šablons",
"note_cannot_be_changed_later": "PIEZĪME: Vēlāk to vairs nevar mainīt!",
"notification_email_from_address": "No adreses",
"notification_email_from_address_description": "Sūtītāja e-pasta adrese, piemēram: “Immich foto serveris <noreply@example.com>”",
"notification_email_host_description": "",
"notification_email_ignore_certificate_errors": "Ignorēt sertifikātu kļūdas",
"notification_email_ignore_certificate_errors_description": "Ignorēt TLS sertifikāta apstiprināšanas kļūdas (nav ieteicams)",
"notification_email_password_description": "",
"notification_email_port_description": "e-pasta servera ports (piemēram, 25, 465 vai 587)",
"notification_email_sent_test_email_button": "Nosūtīt testa e-pastu un saglabāt",
"notification_email_setting_description": "",
"notification_email_test_email": "Nosūtīt testa e-pastu",
"notification_email_test_email_failed": "",
"notification_email_test_email_sent": "",
"notification_email_username_description": "",
"notification_enable_email_notifications": "",
"notification_settings": "Paziņojumu iestatījumi",
"notification_settings_description": "Paziņojumu iestatījumu, tostarp e-pasta, pārvaldība",
"oauth_auto_launch": "",
"oauth_auto_launch_description": "",
"oauth_auto_register": "",
"oauth_auto_register_description": "",
"oauth_button_text": "Pogas teksts",
"oauth_enable_description": "Pieslēgties ar OAuth",
"oauth_mobile_redirect_uri": "",
"oauth_mobile_redirect_uri_override": "",
"oauth_mobile_redirect_uri_override_description": "",
"oauth_settings": "OAuth",
"oauth_settings_description": "OAuth pieteikšanās iestatījumu pārvaldība",
"oauth_storage_label_claim": "",
"oauth_storage_label_claim_description": "",
"oauth_storage_quota_claim": "",
"oauth_storage_quota_claim_description": "",
"oauth_storage_quota_default": "Noklusējuma krātuves kvota (GiB)",
"oauth_storage_quota_default_description": "",
"password_enable_description": "Pieteikšanās ar e-pasta adresi un paroli",
"password_settings": "Pieteikšanās ar paroli",
"password_settings_description": "Pieteikšanās ar paroli iestatījumu pārvaldība",
@ -172,105 +113,42 @@
"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…",
"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": "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": "",
"storage_template_date_time_sample": "Laika paraugs {date}",
"storage_template_enable_description": "",
"storage_template_hash_verification_enabled": "",
"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: <b>{length, number}</b>/{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": "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",
"thumbnail_generation_job_description": "",
"transcoding_acceleration_api": "Paātrināšanas API",
"transcoding_acceleration_api_description": "",
"transcoding_acceleration_nvenc": "NVENC (nepieciešams NVIDIA GPU)",
"transcoding_acceleration_qsv": "Quick Sync (nepieciešams 7. paaudzes vai jaunāks Intel procesors)",
"transcoding_acceleration_rkmpp": "RKMPP (tikai Rockchip SOC)",
"transcoding_acceleration_vaapi": "VAAPI",
"transcoding_accepted_audio_codecs": "",
"transcoding_accepted_audio_codecs_description": "",
"transcoding_accepted_video_codecs": "",
"transcoding_accepted_video_codecs_description": "",
"transcoding_advanced_options_description": "Lielākajai daļai lietotāju nevajadzētu mainīt šīs opcijas",
"transcoding_audio_codec": "Audio kodeks",
"transcoding_audio_codec_description": "",
"transcoding_bitrate_description": "",
"transcoding_codecs_learn_more": "Lai uzzinātu vairāk par šeit lietoto terminoloģiju, skatiet FFmpeg dokumentāciju par <h264-link>H.264 kodeku</h264-link>, <hevc-link>HEVC kodeku</hevc-link> un <vp9-link>VP9 kodeku</vp9-link>.",
"transcoding_constant_quality_mode": "",
"transcoding_constant_quality_mode_description": "",
"transcoding_constant_rate_factor": "",
"transcoding_constant_rate_factor_description": "",
"transcoding_disabled_description": "",
"transcoding_hardware_acceleration": "",
"transcoding_hardware_acceleration_description": "",
"transcoding_hardware_decoding": "",
"transcoding_hardware_decoding_setting_description": "",
"transcoding_hevc_codec": "",
"transcoding_max_b_frames": "",
"transcoding_max_b_frames_description": "",
"transcoding_max_bitrate": "",
"transcoding_max_bitrate_description": "",
"transcoding_max_keyframe_interval": "",
"transcoding_max_keyframe_interval_description": "",
"transcoding_optimal_description": "",
"transcoding_preferred_hardware_device": "",
"transcoding_preferred_hardware_device_description": "",
"transcoding_preset_preset": "",
"transcoding_preset_preset_description": "",
"transcoding_reference_frames": "",
"transcoding_reference_frames_description": "",
"transcoding_required_description": "",
"transcoding_settings": "",
"transcoding_settings_description": "",
"transcoding_target_resolution": "",
"transcoding_target_resolution_description": "",
"transcoding_temporal_aq": "",
"transcoding_temporal_aq_description": "",
"transcoding_threads": "Pavedieni",
"transcoding_threads_description": "",
"transcoding_tone_mapping": "",
"transcoding_tone_mapping_description": "",
"transcoding_transcode_policy": "",
"transcoding_two_pass_encoding": "",
"transcoding_two_pass_encoding_setting_description": "",
"transcoding_video_codec": "Video kodeks",
"transcoding_video_codec_description": "",
"trash_enabled_description": "",
"trash_number_of_days": "Dienu skaits",
"trash_number_of_days_description": "",
"trash_settings": "",
"trash_settings_description": "Atkritnes iestatījumu pārvaldība",
"user_delete_delay_settings": "",
"user_delete_delay_settings_description": "",
"user_management": "Lietotāju pārvaldība",
"user_password_has_been_reset": "Lietotāja parole ir atiestatīta:",
"user_restore_description": "<b>{user}</b> konts tiks atjaunots.",
"user_settings": "",
"user_settings_description": "Lietotāju iestatījumu pārvaldība",
"version_check_enabled_description": "Ieslēgt versijas pārbaudi",
"version_check_implications": "Versiju pārbaudes funkcija ir atkarīga no periodiskas saziņas ar github.com",
"version_check_settings": "Versijas pārbaude",
"version_check_settings_description": "",
"video_conversion_job_description": ""
"version_check_settings": "Versijas pārbaude"
},
"admin_email": "Administratora e-pasts",
"admin_password": "Administratora parole",
@ -290,21 +168,18 @@
"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",
"album_info_card_backup_album_excluded": "NEIEKĻAUTS",
"album_info_card_backup_album_included": "IEKĻAUTS",
"album_info_updated": "Albuma informācija atjaunināta",
"album_leave": "Pamest albumu?",
"album_name": "Albuma nosaukums",
"album_options": "",
"album_remove_user": "Noņemt lietotāju?",
"album_thumbnail_card_item": "1 vienums",
"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}",
"album_user_removed": "Noņēma {user}",
"album_viewer_appbar_delete_confirm": "Vai tiešām vēlaties dzēst šo albumu no sava konta?",
@ -331,10 +206,7 @@
"app_bar_signout_dialog_content": "Vai tiešām vēlaties izrakstīties?",
"app_bar_signout_dialog_ok": "Jā",
"app_bar_signout_dialog_title": "Izrakstīties",
"app_settings": "",
"appears_in": "",
"archive": "Arhīvs",
"archive_or_unarchive_photo": "",
"archive_page_no_archived_assets": "Nav atrasts neviens arhivēts aktīvs",
"archive_page_title": "Arhīvs ({count})",
"archive_size": "Arhīva izmērs",
@ -351,7 +223,6 @@
"asset_list_layout_sub_title": "Izvietojums",
"asset_list_settings_subtitle": "Fotorežģa izkārtojuma iestatījumi",
"asset_list_settings_title": "Fotorežģis",
"asset_offline": "",
"asset_restored_successfully": "Asset restored successfully",
"asset_uploading": "Augšupielādē…",
"asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
@ -431,10 +302,8 @@
"backup_manual_title": "Augšupielādes statuss",
"backup_options_page_title": "Dublēšanas iestatījumi",
"backup_setting_subtitle": "Manage background and foreground upload settings",
"backward": "",
"birthdate_saved": "Dzimšanas datums veiksmīgi saglabāts",
"birthdate_set_description": "Dzimšanas datums tiek izmantots, lai aprēķinātu šīs personas vecumu fotogrāfijas uzņemšanas brīdī.",
"blurred_background": "",
"bugs_and_feature_requests": "Kļūdas un funkciju pieprasījumi",
"build": "Būvējums",
"build_image": "Būvējuma attēls",
@ -456,14 +325,9 @@
"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",
"camera": "",
"camera_brand": "",
"camera_model": "",
"cancel": "Atcelt",
"cancel_search": "",
"canceled": "Canceled",
"cannot_merge_people": "Nevar apvienot cilvēkus",
"cannot_update_the_description": "",
"change_date": "Mainīt datumu",
"change_display_order": "Change display order",
"change_expiration_time": "Izmainīt derīguma termiņu",
@ -477,17 +341,13 @@
"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",
"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_logs": "",
"choose_matching_people_to_merge": "Izvēlies atbilstošus cilvēkus apvienošanai",
"city": "Pilsēta",
"clear": "Notīrīt",
"clear_all": "Notīrīt visu",
"clear_message": "",
"clear_value": "Notīrīt vērtību",
"client_cert_dialog_msg_confirm": "OK",
"client_cert_enter_password": "Enter Password",
@ -504,16 +364,12 @@
"color": "Krāsa",
"color_theme": "Krāsu tēma",
"comment_deleted": "Komentārs dzēsts",
"comment_options": "",
"comments_are_disabled": "",
"common_create_new_album": "Izveidot jaunu albumu",
"common_server_error": "Lūdzu, pārbaudiet tīkla savienojumu, pārliecinieties, vai serveris ir sasniedzams un aplikācijas/servera versijas ir saderīgas.",
"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": "{count} vienumi · Koplietoti",
@ -525,17 +381,8 @@
"control_bottom_app_bar_share_link": "Share Link",
"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": "Kopēšanas kļūda",
"copy_file_path": "",
"copy_image": "",
"copy_link": "",
"copy_link_to_clipboard": "",
"copy_password": "",
"copy_to_clipboard": "",
"country": "Valsts",
"cover": "",
"covers": "",
"create": "Izveidot",
"create_album": "Izveidot albumu",
"create_album_page_untitled": "Bez nosaukuma",
@ -548,17 +395,12 @@
"create_shared_album_page_share_add_assets": "PIEVIENOT AKTĪVUS",
"create_shared_album_page_share_select_photos": "Fotoattēlu Izvēle",
"create_user": "Izveidot lietotāju",
"created": "",
"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": "Datums pēc",
"date_and_time": "Datums un Laiks",
"date_before": "Datums pirms",
@ -567,8 +409,6 @@
"date_range": "Datumu diapazons",
"day": "Diena",
"deduplication_criteria_1": "Attēla izmērs baitos",
"default_locale": "",
"default_locale_description": "",
"delete": "Dzēst",
"delete_album": "Dzēst albumu",
"delete_dialog_alert": "Šie vienumi tiks neatgriezeniski dzēsti no Immich un jūsu ierīces",
@ -593,15 +433,8 @@
"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": "Attēlošanas secība",
"display_original_photos": "",
"display_original_photos_setting_description": "",
"documentation": "Dokumentācija",
"done": "Gatavs",
"download": "Lejupielādēt",
@ -624,13 +457,10 @@
"downloading_asset_filename": "Lejupielādē failu {filename}",
"downloading_media": "Downloading media",
"duplicates": "Dublikāti",
"duration": "",
"edit": "Labot",
"edit_album": "Labot albumu",
"edit_avatar": "",
"edit_date": "Labot datumu",
"edit_date_and_time": "Labot datumu un laiku",
"edit_exclusion_pattern": "",
"edit_faces": "Labot sejas",
"edit_import_path": "Labot importa ceļu",
"edit_import_paths": "Labot importa ceļus",
@ -650,66 +480,18 @@
"email_notifications": "E-pasta paziņojumi",
"empty_folder": "This folder is empty",
"empty_trash": "Iztukšot atkritni",
"enable": "",
"enabled": "",
"end_date": "",
"enqueued": "Enqueued",
"enter_wifi_name": "Enter WiFi name",
"error": "",
"error_change_sort_album": "Failed to change album sort order",
"error_loading_image": "",
"error_saving_image": "Kļūda: {error}",
"errors": {
"cant_get_faces": "Nevar iegūt sejas",
"cant_search_people": "Neizdevās veikt peronu meklēšanu",
"failed_to_create_album": "Neizdevās izveidot albumu",
"unable_to_add_album_users": "",
"unable_to_add_comment": "",
"unable_to_add_partners": "",
"unable_to_change_album_user_role": "",
"unable_to_change_date": "",
"unable_to_change_location": "",
"unable_to_create_admin_account": "",
"unable_to_create_library": "",
"unable_to_create_user": "Neizdevās izveidot lietotāju",
"unable_to_delete_album": "",
"unable_to_delete_asset": "",
"unable_to_delete_user": "Neizdevās dzēst lietotāju",
"unable_to_empty_trash": "",
"unable_to_enter_fullscreen": "",
"unable_to_exit_fullscreen": "",
"unable_to_hide_person": "Neizdevās paslēpt personu",
"unable_to_load_album": "",
"unable_to_load_asset_activity": "",
"unable_to_load_items": "",
"unable_to_load_liked_status": "",
"unable_to_play_video": "",
"unable_to_refresh_user": "",
"unable_to_remove_album_users": "",
"unable_to_remove_library": "",
"unable_to_remove_partner": "",
"unable_to_remove_reaction": "",
"unable_to_repair_items": "",
"unable_to_reset_password": "",
"unable_to_resolve_duplicate": "",
"unable_to_restore_assets": "",
"unable_to_restore_trash": "",
"unable_to_restore_user": "",
"unable_to_save_album": "",
"unable_to_save_date_of_birth": "Neizdevās saglabāt dzimšanas datumu",
"unable_to_save_name": "",
"unable_to_save_profile": "",
"unable_to_save_settings": "",
"unable_to_scan_libraries": "",
"unable_to_scan_library": "",
"unable_to_set_profile_picture": "",
"unable_to_submit_job": "",
"unable_to_trash_asset": "",
"unable_to_unlink_account": "",
"unable_to_update_library": "",
"unable_to_update_location": "",
"unable_to_update_settings": "",
"unable_to_update_user": ""
"unable_to_save_date_of_birth": "Neizdevās saglabāt dzimšanas datumu"
},
"exif_bottom_sheet_description": "Pievienot Aprakstu...",
"exif_bottom_sheet_details": "INFORMĀCIJA",
@ -721,7 +503,6 @@
"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ā",
"experimental_settings_new_asset_list_title": "Iespējot eksperimentālo fotorežģi",
"experimental_settings_subtitle": "Izmanto uzņemoties risku!",
@ -729,38 +510,21 @@
"expire_after": "Derīguma termiņš beidzas pēc",
"expired": "Derīguma termiņš beidzās",
"explore": "Izpētīt",
"extension": "",
"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",
"failed": "Failed",
"failed_to_load_assets": "Failed to load assets",
"failed_to_load_folder": "Failed to load folder",
"favorite": "Izlase",
"favorite_or_unfavorite_photo": "",
"favorites": "Izlase",
"favorites_page_no_favorites": "Nav atrasti iecienītākie aktīvi",
"feature_photo_updated": "",
"features_setting_description": "Lietotnes funkciju pārvaldība",
"file_name": "",
"file_name_or_extension": "",
"filename": "",
"filetype": "",
"filter": "Filter",
"filter_people": "",
"fix_incorrect_match": "",
"folder": "Folder",
"folder_not_found": "Folder not found",
"folders": "Mapes",
"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",
"getting_started": "",
"go_back": "",
"go_to_search": "",
"grant_permission": "Grant permission",
"group_albums_by": "",
"haptic_feedback_switch": "Iestatīt haptisku reakciju",
"haptic_feedback_title": "Haptiska Reakcija",
"has_quota": "Ir kvota",
@ -770,9 +534,7 @@
"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",
"hide_gallery": "",
"hide_named_person": "Paslēpt personu {name}",
"hide_password": "",
"hide_person": "Paslēpt personu",
"home_page_add_to_album_conflicts": "Pievienoja {added} aktīvus albumam {album}. {failed} aktīvi jau ir albumā.",
"home_page_add_to_album_err_local": "Albumiem vēl nevar pievienot lokālos aktīvus, notiek izlaišana",
@ -788,8 +550,6 @@
"home_page_first_time_notice": "Ja šī ir pirmā reize, kad izmantojat aplikāciju, lūdzu, izvēlieties dublējuma albumu(s), lai laika skala varētu aizpildīt fotoattēlus un videoklipus albumā(os).",
"home_page_share_err_local": "Caur saiti nevarēja kopīgot lokālos aktīvus, notiek izlaišana",
"home_page_upload_err_limit": "Vienlaikus var augšupielādēt ne vairāk kā 30 aktīvus, notiek izlaišana",
"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",
"image": "Attēls",
@ -804,12 +564,9 @@
"in_archive": "Arhīvā",
"include_archived": "Iekļaut arhivētos",
"include_shared_albums": "Iekļaut koplietotos albumus",
"include_shared_partner_assets": "",
"individual_share": "",
"info": "Informācija",
"interval": {
"day_at_onepm": "Katru dienu 13.00",
"hours": "",
"night_at_midnight": "Katru dienu pusnaktī",
"night_at_twoam": "Katru dienu 2.00 naktī"
},
@ -830,20 +587,14 @@
"let_others_respond": "Ļaut citiem atbildēt",
"level": "Līmenis",
"library": "Bibliotēka",
"library_options": "",
"library_page_device_albums": "Albumi ierīcē",
"library_page_new_album": "Jauns albums",
"library_page_sort_asset_count": "Daudzums ar aktīviem",
"library_page_sort_created": "Jaunākais izveidotais",
"library_page_sort_last_modified": "Pēdējo reizi modificēts",
"library_page_sort_title": "Albuma virsraksts",
"light": "",
"link_options": "",
"link_to_oauth": "",
"linked_oauth_account": "",
"list": "Saraksts",
"loading": "Ielādē",
"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",
@ -854,7 +605,6 @@
"location_picker_longitude_error": "Ievadiet korektu ģeogrāfisko garumu",
"location_picker_longitude_hint": "Ievadiet savu ģeogrāfisko garumu šeit",
"log_out": "Izrakstīties",
"log_out_all_devices": "",
"login_disabled": "Pieslēgšanās ir atslēgta",
"login_form_api_exception": "API izņēmums. Lūdzu, pārbaudiet servera URL un mēģiniet vēlreiz.",
"login_form_back_button_text": "Atpakaļ",
@ -874,12 +624,10 @@
"login_form_save_login": "Palikt pieteiktam",
"login_form_server_empty": "Ieraksties servera URL.",
"login_form_server_error": "Nevarēja izveidot savienojumu ar serveri.",
"login_has_been_disabled": "",
"login_password_changed_error": "Atjaunojot paroli radās kļūda",
"login_password_changed_success": "Parole veiksmīgi atjaunota",
"longitude": "Ģeogrāfiskais garums",
"look": "Izskats",
"loop_videos": "",
"loop_videos_description": "Iespējot, lai automātiski videoklips tiktu cikliski palaists detaļu skatītājā.",
"make": "Firma",
"manage_shared_links": "Kopīgoto saišu pārvaldība",
@ -919,7 +667,6 @@
"memories": "Atmiņas",
"memories_all_caught_up": "Šobrīd, tas arī viss",
"memories_check_back_tomorrow": "Priekš vairāk atmiņām atgriezieties rītdien.",
"memories_setting_description": "",
"memories_start_over": "Sākt no jauna",
"memories_swipe_to_close": "Pavelciet uz augšu, lai aizvērtu",
"memories_year_ago": "A year ago",
@ -955,25 +702,19 @@
"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": "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",
"no_duplicates_found": "Dublikāti netika atrasti.",
"no_exif_info_available": "Nav pieejama exif informācija",
"no_explore_results_message": "",
"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",
"no_shared_albums_message": "",
"not_in_any_album": "Nav nevienā albumā",
"not_selected": "Not selected",
"notes": "Piezīmes",
@ -988,7 +729,6 @@
"official_immich_resources": "Oficiālie Immich resursi",
"offline": "Bezsaistē",
"ok": "Labi",
"oldest_first": "",
"on_this_device": "On this device",
"online": "Tiešsaistē",
"only_favorites": "Tikai izlase",
@ -997,7 +737,6 @@
"open_the_search_filters": "Atvērt meklēšanas filtrus",
"options": "Iestatījumi",
"or": "vai",
"organize_your_library": "",
"original": "oriģināls",
"other": "Citi",
"other_devices": "Citas ierīces",
@ -1013,29 +752,11 @@
"partner_page_select_partner": "Izvēlēties partneri",
"partner_page_shared_to_title": "Kopīgots uz",
"partner_page_stop_sharing_content": "{partner} vairs nevarēs piekļūt jūsu fotoattēliem.",
"partner_sharing": "",
"partners": "Partneri",
"password": "Parole",
"password_does_not_match": "Parole nesakrīt",
"password_required": "",
"password_reset_success": "",
"past_durations": {
"days": "",
"hours": "",
"years": ""
},
"path": "Ceļš",
"pattern": "",
"pause": "",
"pause_memories": "",
"paused": "",
"pending": "",
"people": "Cilvēki",
"people_sidebar_description": "",
"permanent_deletion_warning": "",
"permanent_deletion_warning_setting_description": "",
"permanently_delete": "",
"permanently_deleted_asset": "",
"permission_onboarding_back": "Atpakaļ",
"permission_onboarding_continue_anyway": "Tomēr turpināt",
"permission_onboarding_get_started": "Darba sākšana",
@ -1047,22 +768,11 @@
"person": "Persona",
"photos": "Fotoattēli",
"photos_from_previous_years": "Fotogrāfijas no iepriekšējiem gadiem",
"pick_a_location": "",
"place": "",
"places": "Vietas",
"play": "",
"play_memories": "",
"play_motion_photo": "",
"play_or_pause_video": "",
"port": "Ports",
"preferences_settings_subtitle": "Manage the app's preferences",
"preferences_settings_title": "Iestatījumi",
"preset": "",
"preview": "Priekšskatījums",
"previous": "",
"previous_memory": "",
"previous_or_next_photo": "",
"primary": "",
"privacy": "Privātums",
"profile": "Profils",
"profile_drawer_app_logs": "Žurnāli",
@ -1072,8 +782,6 @@
"profile_drawer_github": "GitHub",
"profile_drawer_server_out_of_date_major": "Serveris ir novecojis. Lūdzu atjaunojiet to uz jaunāko lielo versiju",
"profile_drawer_server_out_of_date_minor": "Serveris ir novecojis. Lūdzu atjaunojiet to uz jaunāko mazo versiju",
"profile_picture_set": "",
"public_share": "",
"purchase_button_never_show_again": "Nekad vairs nerādīt",
"purchase_button_reminder": "Atgādināt man pēc 30 dienām",
"purchase_button_remove_key": "Noņemt atslēgu",
@ -1091,33 +799,20 @@
"purchase_server_title": "Serveris",
"purchase_settings_server_activated": "Servera produkta atslēgu pārvalda administrators",
"rating_clear": "Noņemt vērtējumu",
"reaction_options": "",
"read_changelog": "Lasīt izmaiņu sarakstu",
"recent": "",
"recent_searches": "",
"recently_added": "Recently added",
"recently_added_page_title": "Nesen Pievienotais",
"refresh": "",
"refreshed": "",
"refreshes_every_file": "",
"remove": "Noņemt",
"remove_deleted_assets": "",
"remove_from_album": "Noņemt no albuma",
"remove_from_favorites": "Noņemt no izlases",
"remove_from_shared_link": "",
"remove_user": "Noņemt lietotāju",
"removed_api_key": "Noņēma API atslēgu: {name}",
"removed_from_archive": "Noņēma no arhīva",
"removed_from_favorites": "Noņēma no izlases",
"rename": "Pārsaukt",
"repair": "Remonts",
"repair_no_results_message": "",
"replace_with_upload": "Aizstāt ar augšupielādi",
"require_password": "",
"require_user_to_change_password_on_first_login": "Pieprasīt lietotājam mainīt paroli pēc pirmās pieteikšanās",
"reset": "",
"reset_password": "",
"reset_people_visibility": "",
"resolve_duplicates": "Atrisināt dublēšanās gadījumus",
"resolved_all_duplicates": "Visi dublikāti ir atrisināti",
"restore": "Atjaunot",
@ -1136,16 +831,9 @@
"saved_settings": "Iestatījumi saglabāti",
"say_something": "Teikt kaut ko",
"scaffold_body_error_occurred": "Radās kļūda",
"scan_all_libraries": "",
"scan_settings": "",
"search": "Meklēt",
"search_albums": "Meklēt albumus",
"search_by_context": "",
"search_by_filename_example": "piemēram, IMG_1234.JPG vai PNG",
"search_camera_make": "",
"search_camera_model": "",
"search_city": "",
"search_country": "",
"search_filter_apply": "Lietot filtru",
"search_filter_camera_title": "Select camera type",
"search_filter_date": "Date",
@ -1159,7 +847,6 @@
"search_filter_media_type": "Media Type",
"search_filter_media_type_title": "Select media type",
"search_filter_people_title": "Select people",
"search_for_existing_person": "",
"search_no_more_result": "No more results",
"search_no_people": "Nav cilvēku",
"search_no_people_named": "Nav cilvēku ar vārdu \"{name}\"",
@ -1176,40 +863,22 @@
"search_page_your_activity": "Jūsu aktivitāte",
"search_page_your_map": "Jūsu Karte",
"search_people": "Meklēt cilvēkus",
"search_places": "",
"search_result_page_new_search_hint": "Jauns Meklējums",
"search_state": "",
"search_suggestion_list_smart_search_hint_1": "Viedā meklēšana ir iespējota pēc noklusējuma, lai meklētu metadatus, izmantojiet sintaksi",
"search_suggestion_list_smart_search_hint_2": "m:jūsu-meklēšanas-frāze",
"search_timezone": "",
"search_type": "",
"search_your_photos": "Meklēt Jūsu fotoattēlus",
"searching_locales": "",
"second": "Sekunde",
"select_album_cover": "Izvēlieties albuma vāciņu",
"select_all": "",
"select_all_duplicates": "Atlasīt visus dublikātus",
"select_avatar_color": "",
"select_face": "",
"select_featured_photo": "",
"select_library_owner": "",
"select_new_face": "",
"select_photos": "Fotoattēlu Izvēle",
"select_user_for_sharing_page_err_album": "Neizdevās izveidot albumu",
"selected": "",
"send_message": "",
"server_endpoint": "Server Endpoint",
"server_info_box_app_version": "Aplikācijas Versija",
"server_info_box_server_url": "Servera URL",
"server_online": "Serveris tiešsaistē",
"server_stats": "Servera statistika",
"server_version": "Servera versija",
"set": "",
"set_as_album_cover": "",
"set_as_profile_picture": "",
"set_date_of_birth": "Iestatīt dzimšanas datumu",
"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ē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",
@ -1235,7 +904,6 @@
"setting_video_viewer_original_video_title": "Force original video",
"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",
@ -1249,8 +917,6 @@
"shared_album_section_people_action_leave": "Noņemt lietotāju no albuma",
"shared_album_section_people_action_remove_user": "Noņemt lietotāju no albuma",
"shared_album_section_people_title": "CILVĒKI",
"shared_by": "",
"shared_by_you": "",
"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ē",
@ -1286,7 +952,6 @@
"sharing_page_album": "Kopīgotie albumi",
"sharing_page_description": "Izveidojiet koplietojamus albumus, lai kopīgotu fotoattēlus un videoklipus ar Jūsu tīkla lietotājiem.",
"sharing_page_empty_list": "TUKŠS SARAKSTS",
"sharing_sidebar_description": "",
"sharing_silver_appbar_create_shared_album": "Izveidot kopīgotu albumu",
"sharing_silver_appbar_share_partner": "Dalīties ar partneri",
"show_album_options": "Rādīt albuma iespējas",
@ -1296,21 +961,10 @@
"show_file_location": "Rādīt faila atrašanās vietu",
"show_gallery": "Rādīt galeriju",
"show_hidden_people": "Rādīt paslēptos cilvēkus",
"show_in_timeline": "",
"show_in_timeline_setting_description": "",
"show_keyboard_shortcuts": "",
"show_metadata": "Rādīt metadatus",
"show_or_hide_info": "",
"show_password": "",
"show_person_options": "",
"show_progress_bar": "",
"show_search_options": "",
"show_supporter_badge": "Atbalstītāja nozīmīte",
"show_supporter_badge_description": "Rādīt atbalstītāja nozīmīti",
"shuffle": "",
"sign_up": "",
"size": "Izmērs",
"skip_to_content": "",
"slideshow": "Slīdrāde",
"slideshow_settings": "Slīdrādes iestatījumi",
"sort_albums_by": "Kārtot albumus pēc...",
@ -1322,30 +976,21 @@
"sort_title": "Nosaukums",
"source": "Pirmkods",
"stack": "Apvienot kaudzē",
"stack_selected_photos": "",
"stacktrace": "",
"start_date": "",
"state": "Štats",
"status": "Statuss",
"stop_motion_photo": "",
"stop_photo_sharing": "Beigt kopīgot jūsu fotogrāfijas?",
"storage": "Vieta krātuvē",
"storage_label": "",
"storage_usage": "{used} no {available} izmantoti",
"submit": "Iesniegt",
"suggestions": "Ieteikumi",
"sunrise_on_the_beach": "Saullēkts pludmalē",
"support": "Atbalsts",
"support_and_feedback": "Atbalsts un atsauksmes",
"swap_merge_direction": "",
"sync": "Sinhronizēt",
"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",
"template": "",
"theme": "Dizains",
"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": "Failu skaits rindā ({count})",
"theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.",
@ -1360,17 +1005,14 @@
"theme_setting_three_stage_loading_subtitle": "Trīspakāpju ielāde var palielināt ielādēšanas veiktspēju, bet izraisa ievērojami lielāku tīkla noslodzi",
"theme_setting_three_stage_loading_title": "Iespējot trīspakāpju ielādi",
"they_will_be_merged_together": "Tās tiks apvienotas",
"time_based_memories": "",
"timezone": "Laika zona",
"to_archive": "Arhivēt",
"to_change_password": "Mainīt paroli",
"toggle_settings": "Pārslēgt iestatījumus",
"toggle_theme": "",
"total_usage": "Kopējais lietojums",
"trash": "Atkritne",
"trash_all": "Dzēst Visu",
"trash_emptied": "Emptied trash",
"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 {days} dienām",
@ -1378,26 +1020,19 @@
"trash_page_restore_all": "Atjaunot Visu",
"trash_page_select_assets_btn": "Atlasīt aktīvus",
"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",
"unknown": "",
"unknown_country": "Nezināma Valsts",
"unknown_year": "Nezināms gads",
"unlimited": "Neierobežots",
"unlink_oauth": "",
"unlinked_oauth_account": "",
"unnamed_album": "Albums bez nosaukuma",
"unsaved_change": "Nesaglabāta izmaiņa",
"unselect_all": "",
"unstack": "At-Stekot",
"up_next": "",
"updated_password": "Parole ir atjaunināta",
"upload": "Augšupielādēt",
"upload_concurrency": "",
"upload_dialog_info": "Vai vēlaties veikt izvēlētā(-o) aktīva(-u) dublējumu uz servera?",
"upload_dialog_title": "Augšupielādēt Aktīvu",
"upload_status_duplicates": "Dublikāti",
@ -1405,7 +1040,6 @@
"upload_status_uploaded": "Augšupielādēts",
"upload_to_immich": "Augšupielādēt Immich ({count})",
"uploading": "Uploading",
"url": "",
"usage": "Lietojums",
"use_current_connection": "use current connection",
"user": "Lietotājs",
@ -1416,9 +1050,7 @@
"username": "Lietotājvārds",
"users": "Lietotāji",
"utilities": "Rīki",
"validate": "",
"validate_endpoint_error": "Please enter a valid URL",
"variables": "",
"version": "Versija",
"version_announcement_message": "Sveiki! Ir pieejama jauna Immich versija. Lūdzu, veltiet laiku, lai izlasītu <link>laidiena piezīmes</link> 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",
@ -1429,20 +1061,15 @@
"version_history": "Versiju vēsture",
"version_history_item": "{version} uzstādīta {date}",
"video": "Videoklips",
"video_hover_setting_description": "",
"videos": "Videoklipi",
"view_album": "Skatīt Albumu",
"view_all": "Apskatīt visu",
"view_all_users": "Skatīt visus lietotājus",
"view_links": "",
"view_next_asset": "",
"view_previous_asset": "",
"viewer_remove_from_stack": "Noņemt no Steka",
"viewer_stack_use_as_main_asset": "Izmantot kā Galveno Aktīvu",
"viewer_unstack": "At-Stekot",
"waiting": "Gaida",
"week": "Nedēļa",
"welcome_to_immich": "",
"wifi_name": "WiFi Name",
"year": "Gads",
"years_ago": "Pirms {years, plural, one {# gada} other {# gadiem}}",

File diff suppressed because it is too large Load Diff

View File

@ -301,7 +301,6 @@
"transcoding_reference_frames_description": "கொடுக்கப்பட்ட சட்டகத்தை சுருக்கும்போது குறிப்பிட வேண்டிய பிரேம்களின் எண்ணிக்கை. அதிக மதிப்புகள் சுருக்க செயல்திறனை மேம்படுத்துகின்றன, ஆனால் குறியாக்கத்தை மெதுவாக்குகின்றன. 0 இந்த மதிப்பை தானாக அமைக்கிறது.",
"transcoding_required_description": "ஏற்றுக்கொள்ளப்பட்ட வடிவத்தில் இல்லாத வீடியோக்கள் மட்டுமே",
"transcoding_settings": "வீடியோ டிரான்ச்கோடிங் அமைப்புகள்",
"transcoding_settings_description": "",
"transcoding_target_resolution": "இலக்கு தீர்மானம்",
"transcoding_target_resolution_description": "அதிக தீர்மானங்கள் அதிக விவரங்களை பாதுகாக்க முடியும், ஆனால் குறியாக்க அதிக நேரம் எடுக்கும், பெரிய கோப்பு அளவுகளைக் கொண்டிருக்கலாம், மேலும் பயன்பாட்டு மறுமொழியைக் குறைக்கலாம்.",
"transcoding_temporal_aq": "தம்போர்ல்",
@ -314,7 +313,6 @@
"transcoding_transcode_policy_description": "ஒரு வீடியோ எப்போது மாற்றப்பட வேண்டும் என்பதற்கான கொள்கை. எச்.டி.ஆர் வீடியோக்கள் எப்போதும் டிரான்ச்கோட் செய்யப்படும் (டிரான்ச்கோடிங் முடக்கப்பட்டிருந்தால் தவிர).",
"transcoding_two_pass_encoding": "இரண்டு-பாச் குறியாக்கம்",
"transcoding_two_pass_encoding_setting_description": "சிறந்த குறியாக்கப்பட்ட வீடியோக்களை உருவாக்க இரண்டு பாச்களில் டிரான்ச்கோட். மேக்ச் பிட்ரேட் இயக்கப்பட்டிருக்கும்போது (H.264 மற்றும் HEVC உடன் வேலை செய்ய இது தேவைப்படுகிறது), இந்த பயன்முறை அதிகபட்ச பிட்ரேட்டை அடிப்படையாகக் கொண்ட பிட்ரேட் வரம்பைப் பயன்படுத்துகிறது மற்றும் CRF ஐ புறக்கணிக்கிறது. VP9 ஐப் பொறுத்தவரை, அதிகபட்ச பிட்ரேட் முடக்கப்பட்டிருந்தால் CRF ஐப் பயன்படுத்தலாம்.",
"transcoding_video_codec": "",
"transcoding_video_codec_description": "VP9 அதிக செயல்திறன் மற்றும் வலை பொருந்தக்கூடிய தன்மையைக் கொண்டுள்ளது, ஆனால் டிரான்ச்கோடிற்கு அதிக நேரம் எடுக்கும். HEVC இதேபோல் செயல்படுகிறது, ஆனால் குறைந்த வலை பொருந்தக்கூடிய தன்மையைக் கொண்டுள்ளது. H.264 பரவலாக இணக்கமானது மற்றும் டிரான்ச்கோடு விரைவானது, ஆனால் மிகப் பெரிய கோப்புகளை உருவாக்குகிறது. ஏ.வி 1 மிகவும் திறமையான கோடெக் ஆனால் பழைய சாதனங்களில் உதவி இல்லை.",
"trash_enabled_description": "குப்பை அம்சங்களை இயக்கவும்",
"trash_number_of_days": "நாட்களின் எண்ணிக்கை",

View File

@ -1700,7 +1700,6 @@
"stack_duplicates": "นำสิ่งที่ซ้ำมาซ้อนอยู่ด้วยกัน",
"stack_select_one_photo": "เลือกรูปหลักหนึ่งรูปสำหรับรูปที่ซ้อนกันนี้",
"stack_selected_photos": "ซ้อนรูปที่ถูกเลือก",
"stacktrace": "",
"start": "เริ่มต้น",
"start_date": "วันที่เริ่ม",
"state": "รัฐ",

View File

@ -269,6 +269,7 @@ class AlbumsPage extends HookConsumerWidget {
],
),
),
resizeToAvoidBottomInset: false,
);
}
}

View File

@ -167,6 +167,7 @@ class TabControllerPage extends HookConsumerWidget {
onPopInvokedWithResult: (didPop, _) =>
!didPop ? tabsRouter.setActiveIndex(0) : null,
child: Scaffold(
resizeToAvoidBottomInset: false,
body: isScreenLandscape
? Row(
children: [

View File

@ -523,7 +523,7 @@ class SearchPage extends HookConsumerWidget {
}
return Scaffold(
resizeToAvoidBottomInset: true,
resizeToAvoidBottomInset: false,
appBar: AppBar(
automaticallyImplyLeading: true,
actions: [

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
@ -76,39 +75,32 @@ class ImmichImage extends StatelessWidget {
);
}
final imageProviderInstance = ImmichImage.imageProvider(
asset: asset,
width: context.width,
height: context.height,
);
return OctoImage(
fadeInDuration: const Duration(milliseconds: 0),
fadeOutDuration: const Duration(milliseconds: 200),
fadeOutDuration: const Duration(milliseconds: 100),
placeholderBuilder: (context) {
if (placeholder != null) {
// Use the gray box placeholder
return placeholder!;
}
// No placeholder
return const SizedBox();
},
image: ImmichImage.imageProvider(
asset: asset,
width: context.width,
height: context.height,
),
image: imageProviderInstance,
width: width,
height: height,
fit: fit,
errorBuilder: (context, error, stackTrace) {
if (error is PlatformException &&
error.code == "The asset not found!") {
debugPrint(
"Asset ${asset?.localId} does not exist anymore on device!",
);
} else {
debugPrint(
"Error getting thumb for assetId=${asset?.localId}: $error",
);
}
imageProviderInstance.evict();
return Icon(
Icons.image_not_supported_outlined,
color: context.primaryColor,
size: 32,
color: Colors.red[200],
);
},
);

View File

@ -77,15 +77,28 @@ class ImmichThumbnail extends HookConsumerWidget {
);
}
final thumbnailProviderInstance = ImmichThumbnail.imageProvider(
asset: asset,
userId: userId,
);
customErrorBuilder(BuildContext ctx, Object error, StackTrace? stackTrace) {
thumbnailProviderInstance.evict();
final originalErrorWidgetBuilder =
blurHashErrorBuilder(blurhash, fit: fit);
return originalErrorWidgetBuilder(ctx, error, stackTrace);
}
return OctoImage.fromSet(
placeholderFadeInDuration: Duration.zero,
fadeInDuration: Duration.zero,
fadeOutDuration: const Duration(milliseconds: 100),
octoSet: blurHashOrPlaceholder(blurhash),
image: ImmichThumbnail.imageProvider(
asset: asset,
userId: userId,
octoSet: OctoSet(
placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit),
errorBuilder: customErrorBuilder,
),
image: thumbnailProviderInstance,
width: width,
height: height,
fit: fit,

View File

@ -494,8 +494,8 @@ Class | Method | HTTP request | Description
- [TemplateDto](doc//TemplateDto.md)
- [TemplateResponseDto](doc//TemplateResponseDto.md)
- [TestEmailResponseDto](doc//TestEmailResponseDto.md)
- [TimeBucketResponseDto](doc//TimeBucketResponseDto.md)
- [TimeBucketSize](doc//TimeBucketSize.md)
- [TimeBucketAssetResponseDto](doc//TimeBucketAssetResponseDto.md)
- [TimeBucketsResponseDto](doc//TimeBucketsResponseDto.md)
- [ToneMapping](doc//ToneMapping.md)
- [TranscodeHWAccel](doc//TranscodeHWAccel.md)
- [TranscodePolicy](doc//TranscodePolicy.md)

View File

@ -289,8 +289,8 @@ part 'model/tags_update.dart';
part 'model/template_dto.dart';
part 'model/template_response_dto.dart';
part 'model/test_email_response_dto.dart';
part 'model/time_bucket_response_dto.dart';
part 'model/time_bucket_size.dart';
part 'model/time_bucket_asset_response_dto.dart';
part 'model/time_buckets_response_dto.dart';
part 'model/tone_mapping.dart';
part 'model/transcode_hw_accel.dart';
part 'model/transcode_policy.dart';

View File

@ -19,8 +19,6 @@ class TimelineApi {
/// Performs an HTTP 'GET /timeline/bucket' operation and returns the [Response].
/// Parameters:
///
/// * [TimeBucketSize] size (required):
///
/// * [String] timeBucket (required):
///
/// * [String] albumId:
@ -33,6 +31,10 @@ class TimelineApi {
///
/// * [AssetOrder] order:
///
/// * [num] page:
///
/// * [num] pageSize:
///
/// * [String] personId:
///
/// * [String] tagId:
@ -44,7 +46,7 @@ class TimelineApi {
/// * [bool] withPartners:
///
/// * [bool] withStacked:
Future<Response> getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
Future<Response> getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, num? page, num? pageSize, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/timeline/bucket';
@ -70,10 +72,15 @@ class TimelineApi {
if (order != null) {
queryParams.addAll(_queryParams('', 'order', order));
}
if (page != null) {
queryParams.addAll(_queryParams('', 'page', page));
}
if (pageSize != null) {
queryParams.addAll(_queryParams('', 'pageSize', pageSize));
}
if (personId != null) {
queryParams.addAll(_queryParams('', 'personId', personId));
}
queryParams.addAll(_queryParams('', 'size', size));
if (tagId != null) {
queryParams.addAll(_queryParams('', 'tagId', tagId));
}
@ -107,8 +114,6 @@ class TimelineApi {
/// Parameters:
///
/// * [TimeBucketSize] size (required):
///
/// * [String] timeBucket (required):
///
/// * [String] albumId:
@ -121,6 +126,10 @@ class TimelineApi {
///
/// * [AssetOrder] order:
///
/// * [num] page:
///
/// * [num] pageSize:
///
/// * [String] personId:
///
/// * [String] tagId:
@ -132,8 +141,8 @@ class TimelineApi {
/// * [bool] withPartners:
///
/// * [bool] withStacked:
Future<List<AssetResponseDto>?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
Future<TimeBucketAssetResponseDto?> getTimeBucket(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, num? page, num? pageSize, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, page: page, pageSize: pageSize, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@ -141,11 +150,8 @@ class TimelineApi {
// 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<AssetResponseDto>') as List)
.cast<AssetResponseDto>()
.toList(growable: false);
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TimeBucketAssetResponseDto',) as TimeBucketAssetResponseDto;
}
return null;
}
@ -153,8 +159,6 @@ class TimelineApi {
/// Performs an HTTP 'GET /timeline/buckets' operation and returns the [Response].
/// Parameters:
///
/// * [TimeBucketSize] size (required):
///
/// * [String] albumId:
///
/// * [bool] isFavorite:
@ -176,7 +180,7 @@ class TimelineApi {
/// * [bool] withPartners:
///
/// * [bool] withStacked:
Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
Future<Response> getTimeBucketsWithHttpInfo({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/timeline/buckets';
@ -205,7 +209,6 @@ class TimelineApi {
if (personId != null) {
queryParams.addAll(_queryParams('', 'personId', personId));
}
queryParams.addAll(_queryParams('', 'size', size));
if (tagId != null) {
queryParams.addAll(_queryParams('', 'tagId', tagId));
}
@ -238,8 +241,6 @@ class TimelineApi {
/// Parameters:
///
/// * [TimeBucketSize] size (required):
///
/// * [String] albumId:
///
/// * [bool] isFavorite:
@ -261,8 +262,8 @@ class TimelineApi {
/// * [bool] withPartners:
///
/// * [bool] withStacked:
Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
Future<List<TimeBucketsResponseDto>?> getTimeBuckets({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketsWithHttpInfo( albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@ -271,8 +272,8 @@ class TimelineApi {
// 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<TimeBucketResponseDto>') as List)
.cast<TimeBucketResponseDto>()
return (await apiClient.deserializeAsync(responseBody, 'List<TimeBucketsResponseDto>') as List)
.cast<TimeBucketsResponseDto>()
.toList(growable: false);
}

View File

@ -634,10 +634,10 @@ class ApiClient {
return TemplateResponseDto.fromJson(value);
case 'TestEmailResponseDto':
return TestEmailResponseDto.fromJson(value);
case 'TimeBucketResponseDto':
return TimeBucketResponseDto.fromJson(value);
case 'TimeBucketSize':
return TimeBucketSizeTypeTransformer().decode(value);
case 'TimeBucketAssetResponseDto':
return TimeBucketAssetResponseDto.fromJson(value);
case 'TimeBucketsResponseDto':
return TimeBucketsResponseDto.fromJson(value);
case 'ToneMapping':
return ToneMappingTypeTransformer().decode(value);
case 'TranscodeHWAccel':

View File

@ -139,9 +139,6 @@ String parameterToString(dynamic value) {
if (value is SyncRequestType) {
return SyncRequestTypeTypeTransformer().encode(value).toString();
}
if (value is TimeBucketSize) {
return TimeBucketSizeTypeTransformer().encode(value).toString();
}
if (value is ToneMapping) {
return ToneMappingTypeTransformer().encode(value).toString();
}

View File

@ -0,0 +1,241 @@
//
// 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 TimeBucketAssetResponseDto {
/// Returns a new [TimeBucketAssetResponseDto] instance.
TimeBucketAssetResponseDto({
this.city = const [],
this.country = const [],
this.duration = const [],
this.id = const [],
this.isFavorite = const [],
this.isImage = const [],
this.isTrashed = const [],
this.livePhotoVideoId = const [],
this.localDateTime = const [],
this.ownerId = const [],
this.projectionType = const [],
this.ratio = const [],
this.stack = const [],
this.thumbhash = const [],
this.visibility = const [],
});
List<String?> city;
List<String?> country;
List<String?> duration;
List<String> id;
List<bool> isFavorite;
List<bool> isImage;
List<bool> isTrashed;
List<String?> livePhotoVideoId;
List<String> localDateTime;
List<String> ownerId;
List<String?> projectionType;
List<num> ratio;
/// (stack ID, stack asset count) tuple
List<List<String>?> stack;
List<String?> thumbhash;
List<AssetVisibility> visibility;
@override
bool operator ==(Object other) => identical(this, other) || other is TimeBucketAssetResponseDto &&
_deepEquality.equals(other.city, city) &&
_deepEquality.equals(other.country, country) &&
_deepEquality.equals(other.duration, duration) &&
_deepEquality.equals(other.id, id) &&
_deepEquality.equals(other.isFavorite, isFavorite) &&
_deepEquality.equals(other.isImage, isImage) &&
_deepEquality.equals(other.isTrashed, isTrashed) &&
_deepEquality.equals(other.livePhotoVideoId, livePhotoVideoId) &&
_deepEquality.equals(other.localDateTime, localDateTime) &&
_deepEquality.equals(other.ownerId, ownerId) &&
_deepEquality.equals(other.projectionType, projectionType) &&
_deepEquality.equals(other.ratio, ratio) &&
_deepEquality.equals(other.stack, stack) &&
_deepEquality.equals(other.thumbhash, thumbhash) &&
_deepEquality.equals(other.visibility, visibility);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(city.hashCode) +
(country.hashCode) +
(duration.hashCode) +
(id.hashCode) +
(isFavorite.hashCode) +
(isImage.hashCode) +
(isTrashed.hashCode) +
(livePhotoVideoId.hashCode) +
(localDateTime.hashCode) +
(ownerId.hashCode) +
(projectionType.hashCode) +
(ratio.hashCode) +
(stack.hashCode) +
(thumbhash.hashCode) +
(visibility.hashCode);
@override
String toString() => 'TimeBucketAssetResponseDto[city=$city, country=$country, duration=$duration, id=$id, isFavorite=$isFavorite, isImage=$isImage, isTrashed=$isTrashed, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, ownerId=$ownerId, projectionType=$projectionType, ratio=$ratio, stack=$stack, thumbhash=$thumbhash, visibility=$visibility]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'city'] = this.city;
json[r'country'] = this.country;
json[r'duration'] = this.duration;
json[r'id'] = this.id;
json[r'isFavorite'] = this.isFavorite;
json[r'isImage'] = this.isImage;
json[r'isTrashed'] = this.isTrashed;
json[r'livePhotoVideoId'] = this.livePhotoVideoId;
json[r'localDateTime'] = this.localDateTime;
json[r'ownerId'] = this.ownerId;
json[r'projectionType'] = this.projectionType;
json[r'ratio'] = this.ratio;
json[r'stack'] = this.stack;
json[r'thumbhash'] = this.thumbhash;
json[r'visibility'] = this.visibility;
return json;
}
/// Returns a new [TimeBucketAssetResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static TimeBucketAssetResponseDto? fromJson(dynamic value) {
upgradeDto(value, "TimeBucketAssetResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return TimeBucketAssetResponseDto(
city: json[r'city'] is Iterable
? (json[r'city'] as Iterable).cast<String>().toList(growable: false)
: const [],
country: json[r'country'] is Iterable
? (json[r'country'] as Iterable).cast<String>().toList(growable: false)
: const [],
duration: json[r'duration'] is Iterable
? (json[r'duration'] as Iterable).cast<String>().toList(growable: false)
: const [],
id: json[r'id'] is Iterable
? (json[r'id'] as Iterable).cast<String>().toList(growable: false)
: const [],
isFavorite: json[r'isFavorite'] is Iterable
? (json[r'isFavorite'] as Iterable).cast<bool>().toList(growable: false)
: const [],
isImage: json[r'isImage'] is Iterable
? (json[r'isImage'] as Iterable).cast<bool>().toList(growable: false)
: const [],
isTrashed: json[r'isTrashed'] is Iterable
? (json[r'isTrashed'] as Iterable).cast<bool>().toList(growable: false)
: const [],
livePhotoVideoId: json[r'livePhotoVideoId'] is Iterable
? (json[r'livePhotoVideoId'] as Iterable).cast<String>().toList(growable: false)
: const [],
localDateTime: json[r'localDateTime'] is Iterable
? (json[r'localDateTime'] as Iterable).cast<String>().toList(growable: false)
: const [],
ownerId: json[r'ownerId'] is Iterable
? (json[r'ownerId'] as Iterable).cast<String>().toList(growable: false)
: const [],
projectionType: json[r'projectionType'] is Iterable
? (json[r'projectionType'] as Iterable).cast<String>().toList(growable: false)
: const [],
ratio: json[r'ratio'] is Iterable
? (json[r'ratio'] as Iterable).cast<num>().toList(growable: false)
: const [],
stack: json[r'stack'] is List
? (json[r'stack'] as List).map((e) =>
e == null ? null : (e as List).cast<String>()
).toList()
: const [],
thumbhash: json[r'thumbhash'] is Iterable
? (json[r'thumbhash'] as Iterable).cast<String>().toList(growable: false)
: const [],
visibility: AssetVisibility.listFromJson(json[r'visibility']),
);
}
return null;
}
static List<TimeBucketAssetResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <TimeBucketAssetResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = TimeBucketAssetResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, TimeBucketAssetResponseDto> mapFromJson(dynamic json) {
final map = <String, TimeBucketAssetResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = TimeBucketAssetResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of TimeBucketAssetResponseDto-objects as value to a dart map
static Map<String, List<TimeBucketAssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<TimeBucketAssetResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = TimeBucketAssetResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'city',
'country',
'duration',
'id',
'isFavorite',
'isImage',
'isTrashed',
'livePhotoVideoId',
'localDateTime',
'ownerId',
'projectionType',
'ratio',
'thumbhash',
'visibility',
};
}

View File

@ -1,85 +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 TimeBucketSize {
/// Instantiate a new enum with the provided [value].
const TimeBucketSize._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const DAY = TimeBucketSize._(r'DAY');
static const MONTH = TimeBucketSize._(r'MONTH');
/// List of all possible values in this [enum][TimeBucketSize].
static const values = <TimeBucketSize>[
DAY,
MONTH,
];
static TimeBucketSize? fromJson(dynamic value) => TimeBucketSizeTypeTransformer().decode(value);
static List<TimeBucketSize> listFromJson(dynamic json, {bool growable = false,}) {
final result = <TimeBucketSize>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = TimeBucketSize.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [TimeBucketSize] to String,
/// and [decode] dynamic data back to [TimeBucketSize].
class TimeBucketSizeTypeTransformer {
factory TimeBucketSizeTypeTransformer() => _instance ??= const TimeBucketSizeTypeTransformer._();
const TimeBucketSizeTypeTransformer._();
String encode(TimeBucketSize data) => data.value;
/// Decodes a [dynamic value][data] to a TimeBucketSize.
///
/// 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.
TimeBucketSize? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'DAY': return TimeBucketSize.DAY;
case r'MONTH': return TimeBucketSize.MONTH;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [TimeBucketSizeTypeTransformer] instance.
static TimeBucketSizeTypeTransformer? _instance;
}

View File

@ -10,9 +10,9 @@
part of openapi.api;
class TimeBucketResponseDto {
/// Returns a new [TimeBucketResponseDto] instance.
TimeBucketResponseDto({
class TimeBucketsResponseDto {
/// Returns a new [TimeBucketsResponseDto] instance.
TimeBucketsResponseDto({
required this.count,
required this.timeBucket,
});
@ -22,7 +22,7 @@ class TimeBucketResponseDto {
String timeBucket;
@override
bool operator ==(Object other) => identical(this, other) || other is TimeBucketResponseDto &&
bool operator ==(Object other) => identical(this, other) || other is TimeBucketsResponseDto &&
other.count == count &&
other.timeBucket == timeBucket;
@ -33,7 +33,7 @@ class TimeBucketResponseDto {
(timeBucket.hashCode);
@override
String toString() => 'TimeBucketResponseDto[count=$count, timeBucket=$timeBucket]';
String toString() => 'TimeBucketsResponseDto[count=$count, timeBucket=$timeBucket]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -42,15 +42,15 @@ class TimeBucketResponseDto {
return json;
}
/// Returns a new [TimeBucketResponseDto] instance and imports its values from
/// Returns a new [TimeBucketsResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static TimeBucketResponseDto? fromJson(dynamic value) {
upgradeDto(value, "TimeBucketResponseDto");
static TimeBucketsResponseDto? fromJson(dynamic value) {
upgradeDto(value, "TimeBucketsResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return TimeBucketResponseDto(
return TimeBucketsResponseDto(
count: mapValueOfType<int>(json, r'count')!,
timeBucket: mapValueOfType<String>(json, r'timeBucket')!,
);
@ -58,11 +58,11 @@ class TimeBucketResponseDto {
return null;
}
static List<TimeBucketResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <TimeBucketResponseDto>[];
static List<TimeBucketsResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <TimeBucketsResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = TimeBucketResponseDto.fromJson(row);
final value = TimeBucketsResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
@ -71,12 +71,12 @@ class TimeBucketResponseDto {
return result.toList(growable: growable);
}
static Map<String, TimeBucketResponseDto> mapFromJson(dynamic json) {
final map = <String, TimeBucketResponseDto>{};
static Map<String, TimeBucketsResponseDto> mapFromJson(dynamic json) {
final map = <String, TimeBucketsResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = TimeBucketResponseDto.fromJson(entry.value);
final value = TimeBucketsResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
@ -85,14 +85,14 @@ class TimeBucketResponseDto {
return map;
}
// maps a json object with a list of TimeBucketResponseDto-objects as value to a dart map
static Map<String, List<TimeBucketResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<TimeBucketResponseDto>>{};
// maps a json object with a list of TimeBucketsResponseDto-objects as value to a dart map
static Map<String, List<TimeBucketsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<TimeBucketsResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = TimeBucketResponseDto.listFromJson(entry.value, growable: growable,);
map[entry.key] = TimeBucketsResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;

View File

@ -1,5 +1,5 @@
#!/usr/bin/env bash
OPENAPI_GENERATOR_VERSION=v7.8.0
OPENAPI_GENERATOR_VERSION=v7.12.0
# usage: ./bin/generate-open-api.sh
@ -8,6 +8,7 @@ function dart {
cd ./templates/mobile/serialization/native
wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache
patch --no-backup-if-mismatch -u native_class.mustache <native_class.mustache.patch
patch --no-backup-if-mismatch -u native_class.mustache <native_class_nullable_items_in_arrays.patch
cd ../../
wget -O api.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/api.mustache

View File

@ -7284,6 +7284,24 @@
"$ref": "#/components/schemas/AssetOrder"
}
},
{
"name": "page",
"required": false,
"in": "query",
"schema": {
"minimum": 1,
"type": "number"
}
},
{
"name": "pageSize",
"required": false,
"in": "query",
"schema": {
"minimum": 1,
"type": "number"
}
},
{
"name": "personId",
"required": false,
@ -7293,14 +7311,6 @@
"type": "string"
}
},
{
"name": "size",
"required": true,
"in": "query",
"schema": {
"$ref": "#/components/schemas/TimeBucketSize"
}
},
{
"name": "tagId",
"required": false,
@ -7357,10 +7367,7 @@
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/AssetResponseDto"
},
"type": "array"
"$ref": "#/components/schemas/TimeBucketAssetResponseDto"
}
}
},
@ -7437,14 +7444,6 @@
"type": "string"
}
},
{
"name": "size",
"required": true,
"in": "query",
"schema": {
"$ref": "#/components/schemas/TimeBucketSize"
}
},
{
"name": "tagId",
"required": false,
@ -7494,7 +7493,7 @@
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/TimeBucketResponseDto"
"$ref": "#/components/schemas/TimeBucketsResponseDto"
},
"type": "array"
}
@ -14069,7 +14068,131 @@
],
"type": "object"
},
"TimeBucketResponseDto": {
"TimeBucketAssetResponseDto": {
"properties": {
"city": {
"items": {
"nullable": true,
"type": "string"
},
"type": "array"
},
"country": {
"items": {
"nullable": true,
"type": "string"
},
"type": "array"
},
"duration": {
"items": {
"nullable": true,
"type": "string"
},
"type": "array"
},
"id": {
"items": {
"type": "string"
},
"type": "array"
},
"isFavorite": {
"items": {
"type": "boolean"
},
"type": "array"
},
"isImage": {
"items": {
"type": "boolean"
},
"type": "array"
},
"isTrashed": {
"items": {
"type": "boolean"
},
"type": "array"
},
"livePhotoVideoId": {
"items": {
"nullable": true,
"type": "string"
},
"type": "array"
},
"localDateTime": {
"items": {
"type": "string"
},
"type": "array"
},
"ownerId": {
"items": {
"type": "string"
},
"type": "array"
},
"projectionType": {
"items": {
"nullable": true,
"type": "string"
},
"type": "array"
},
"ratio": {
"items": {
"type": "number"
},
"type": "array"
},
"stack": {
"description": "(stack ID, stack asset count) tuple",
"items": {
"items": {
"type": "string"
},
"maxItems": 2,
"minItems": 2,
"nullable": true,
"type": "array"
},
"type": "array"
},
"thumbhash": {
"items": {
"nullable": true,
"type": "string"
},
"type": "array"
},
"visibility": {
"items": {
"$ref": "#/components/schemas/AssetVisibility"
},
"type": "array"
}
},
"required": [
"city",
"country",
"duration",
"id",
"isFavorite",
"isImage",
"isTrashed",
"livePhotoVideoId",
"localDateTime",
"ownerId",
"projectionType",
"ratio",
"thumbhash",
"visibility"
],
"type": "object"
},
"TimeBucketsResponseDto": {
"properties": {
"count": {
"type": "integer"
@ -14084,13 +14207,6 @@
],
"type": "object"
},
"TimeBucketSize": {
"enum": [
"DAY",
"MONTH"
],
"type": "string"
},
"ToneMapping": {
"enum": [
"hable",

View File

@ -32,7 +32,7 @@ class {{{classname}}} {
{{/required}}
{{/isNullable}}
{{/isEnum}}
{{{datatypeWithEnum}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isNullable}} {{{name}}};
{{#isArray}}{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}<{{{items.dataType}}}{{#items.isNullable}}?{{/items.isNullable}}>{{/isArray}}{{^isArray}}{{{datatypeWithEnum}}}{{/isArray}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isNullable}} {{{name}}};
{{/vars}}
@override

View File

@ -0,0 +1,13 @@
diff --git a/open-api/templates/mobile/serialization/native/native_class.mustache b/open-api/templates/mobile/serialization/native/native_class.mustache
index 9a7b1439b..9f40d5b0b 100644
--- a/open-api/templates/mobile/serialization/native/native_class.mustache
+++ b/open-api/templates/mobile/serialization/native/native_class.mustache
@@ -32,7 +32,7 @@ class {{{classname}}} {
{{/required}}
{{/isNullable}}
{{/isEnum}}
- {{{datatypeWithEnum}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isNullable}} {{{name}}};
+ {{#isArray}}{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}<{{{items.dataType}}}{{#items.isNullable}}?{{/items.isNullable}}>{{/isArray}}{{^isArray}}{{{datatypeWithEnum}}}{{/isArray}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isNullable}} {{{name}}};
{{/vars}}
@override

View File

@ -1420,7 +1420,25 @@ export type TagBulkAssetsResponseDto = {
export type TagUpdateDto = {
color?: string | null;
};
export type TimeBucketResponseDto = {
export type TimeBucketAssetResponseDto = {
city: (string | null)[];
country: (string | null)[];
duration: (string | null)[];
id: string[];
isFavorite: boolean[];
isImage: boolean[];
isTrashed: boolean[];
livePhotoVideoId: (string | null)[];
localDateTime: string[];
ownerId: string[];
projectionType: (string | null)[];
ratio: number[];
/** (stack ID, stack asset count) tuple */
stack?: (string[] | null)[];
thumbhash: (string | null)[];
visibility: AssetVisibility[];
};
export type TimeBucketsResponseDto = {
count: number;
timeBucket: string;
};
@ -3367,14 +3385,15 @@ export function tagAssets({ id, bulkIdsDto }: {
body: bulkIdsDto
})));
}
export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, size, tagId, timeBucket, userId, visibility, withPartners, withStacked }: {
export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, page, pageSize, personId, tagId, timeBucket, userId, visibility, withPartners, withStacked }: {
albumId?: string;
isFavorite?: boolean;
isTrashed?: boolean;
key?: string;
order?: AssetOrder;
page?: number;
pageSize?: number;
personId?: string;
size: TimeBucketSize;
tagId?: string;
timeBucket: string;
userId?: string;
@ -3384,15 +3403,16 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: AssetResponseDto[];
data: TimeBucketAssetResponseDto;
}>(`/timeline/bucket${QS.query(QS.explode({
albumId,
isFavorite,
isTrashed,
key,
order,
page,
pageSize,
personId,
size,
tagId,
timeBucket,
userId,
@ -3403,14 +3423,13 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers
...opts
}));
}
export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, size, tagId, userId, visibility, withPartners, withStacked }: {
export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, tagId, userId, visibility, withPartners, withStacked }: {
albumId?: string;
isFavorite?: boolean;
isTrashed?: boolean;
key?: string;
order?: AssetOrder;
personId?: string;
size: TimeBucketSize;
tagId?: string;
userId?: string;
visibility?: AssetVisibility;
@ -3419,7 +3438,7 @@ export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, per
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: TimeBucketResponseDto[];
data: TimeBucketsResponseDto[];
}>(`/timeline/buckets${QS.query(QS.explode({
albumId,
isFavorite,
@ -3427,7 +3446,6 @@ export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, per
key,
order,
personId,
size,
tagId,
userId,
visibility,
@ -3921,7 +3939,3 @@ export enum OAuthTokenEndpointAuthMethod {
ClientSecretPost = "client_secret_post",
ClientSecretBasic = "client_secret_basic"
}
export enum TimeBucketSize {
Day = "DAY",
Month = "MONTH"
}

View File

@ -72,7 +72,9 @@ class SqlGenerator {
await rm(this.options.targetDir, { force: true, recursive: true });
await mkdir(this.options.targetDir);
process.env.DB_HOSTNAME = 'localhost';
if (!process.env.DB_HOSTNAME) {
process.env.DB_HOSTNAME = 'localhost';
}
const { database, cls, otel } = new ConfigRepository().getEnv();
const moduleFixture = await Test.createTestingModule({

View File

@ -1,8 +1,7 @@
import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { Controller, Get, Header, Query } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto';
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
import { TimeBucketAssetDto, TimeBucketAssetResponseDto, TimeBucketDto } from 'src/dtos/time-bucket.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { TimelineService } from 'src/services/timeline.service';
@ -14,13 +13,15 @@ export class TimelineController {
@Get('buckets')
@Authenticated({ permission: Permission.ASSET_READ, sharedLink: true })
getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto) {
return this.service.getTimeBuckets(auth, dto);
}
@Get('bucket')
@Authenticated({ permission: Permission.ASSET_READ, sharedLink: true })
getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
return this.service.getTimeBucket(auth, dto) as Promise<AssetResponseDto[]>;
@ApiOkResponse({ type: TimeBucketAssetResponseDto })
@Header('Content-Type', 'application/json')
getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto) {
return this.service.getTimeBucket(auth, dto);
}
}

View File

@ -13,6 +13,7 @@ import {
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { hexOrBufferToBase64 } from 'src/utils/bytes';
import { mimeTypes } from 'src/utils/mime-types';
export class SanitizedAssetResponseDto {
@ -140,15 +141,6 @@ const mapStack = (entity: { stack?: Stack | null }) => {
};
};
// if an asset is jsonified in the DB before being returned, its buffer fields will be hex-encoded strings
export const hexOrBufferToBase64 = (encoded: string | Buffer) => {
if (typeof encoded === 'string') {
return Buffer.from(encoded.slice(2), 'hex').toString('base64');
}
return encoded.toString('base64');
};
export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): AssetResponseDto {
const { stripMetadata = false, withStack = false } = options;
@ -192,7 +184,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
tags: entity.tags?.map((tag) => mapTag(tag)),
people: peopleWithFaces(entity.faces),
unassignedFaces: entity.faces?.filter((face) => !face.person).map((a) => mapFacesWithoutPerson(a)),
checksum: hexOrBufferToBase64(entity.checksum),
checksum: hexOrBufferToBase64(entity.checksum)!,
stack: withStack ? mapStack(entity) : undefined,
isOffline: entity.isOffline,
hasMetadata: true,

View File

@ -1,15 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { IsEnum, IsInt, IsString, Min } from 'class-validator';
import { AssetOrder, AssetVisibility } from 'src/enum';
import { TimeBucketSize } from 'src/repositories/asset.repository';
import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation';
export class TimeBucketDto {
@IsNotEmpty()
@IsEnum(TimeBucketSize)
@ApiProperty({ enum: TimeBucketSize, enumName: 'TimeBucketSize' })
size!: TimeBucketSize;
@ValidateUUID({ optional: true })
userId?: string;
@ -46,9 +41,75 @@ export class TimeBucketDto {
export class TimeBucketAssetDto extends TimeBucketDto {
@IsString()
timeBucket!: string;
@IsInt()
@Min(1)
@Optional()
page?: number;
@IsInt()
@Min(1)
@Optional()
pageSize?: number;
}
export class TimeBucketResponseDto {
export class TimelineStackResponseDto {
id!: string;
primaryAssetId!: string;
assetCount!: number;
}
export class TimeBucketAssetResponseDto {
id!: string[];
ownerId!: string[];
ratio!: number[];
isFavorite!: boolean[];
@ApiProperty({ enum: AssetVisibility, enumName: 'AssetVisibility', isArray: true })
visibility!: AssetVisibility[];
isTrashed!: boolean[];
isImage!: boolean[];
@ApiProperty({ type: 'array', items: { type: 'string', nullable: true } })
thumbhash!: (string | null)[];
localDateTime!: string[];
@ApiProperty({ type: 'array', items: { type: 'string', nullable: true } })
duration!: (string | null)[];
@ApiProperty({
type: 'array',
items: {
type: 'array',
items: { type: 'string' },
minItems: 2,
maxItems: 2,
nullable: true,
},
description: '(stack ID, stack asset count) tuple',
})
stack?: ([string, string] | null)[];
@ApiProperty({ type: 'array', items: { type: 'string', nullable: true } })
projectionType!: (string | null)[];
@ApiProperty({ type: 'array', items: { type: 'string', nullable: true } })
livePhotoVideoId!: (string | null)[];
@ApiProperty({ type: 'array', items: { type: 'string', nullable: true } })
city!: (string | null)[];
@ApiProperty({ type: 'array', items: { type: 'string', nullable: true } })
country!: (string | null)[];
}
export class TimeBucketsResponseDto {
@ApiProperty({ type: 'string' })
timeBucket!: string;

View File

@ -235,14 +235,14 @@ limit
with
"assets" as (
select
date_trunc($1, "localDateTime" at time zone 'UTC') at time zone 'UTC' as "timeBucket"
date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' as "timeBucket"
from
"assets"
where
"assets"."deletedAt" is null
and (
"assets"."visibility" = $2
or "assets"."visibility" = $3
"assets"."visibility" = $1
or "assets"."visibility" = $2
)
)
select
@ -256,47 +256,112 @@ order by
"timeBucket" desc
-- AssetRepository.getTimeBucket
select
"assets".*,
to_json("exif") as "exifInfo",
to_json("stacked_assets") as "stack"
from
"assets"
left join "exif" on "assets"."id" = "exif"."assetId"
left join "asset_stack" on "asset_stack"."id" = "assets"."stackId"
left join lateral (
with
"cte" as (
select
"asset_stack".*,
count("stacked") as "assetCount"
"assets"."duration",
"assets"."id",
"assets"."visibility",
"assets"."isFavorite",
assets.type = 'IMAGE' as "isImage",
assets."deletedAt" is null as "isTrashed",
"assets"."livePhotoVideoId",
"assets"."localDateTime",
"assets"."ownerId",
"assets"."status",
encode("assets"."thumbhash", 'base64') as "thumbhash",
"exif"."city",
"exif"."country",
"exif"."projectionType",
coalesce(
case
when exif."exifImageHeight" = 0
or exif."exifImageWidth" = 0 then 1
when "exif"."orientation" in ('5', '6', '7', '8', '-90', '90') then round(
exif."exifImageHeight"::numeric / exif."exifImageWidth"::numeric,
3
)
else round(
exif."exifImageWidth"::numeric / exif."exifImageHeight"::numeric,
3
)
end,
1
) as "ratio",
"stack"
from
"assets" as "stacked"
"assets"
inner join "exif" on "assets"."id" = "exif"."assetId"
left join lateral (
select
array[stacked."stackId"::text, count('stacked')::text] as "stack"
from
"assets" as "stacked"
where
"stacked"."stackId" = "assets"."stackId"
and "stacked"."deletedAt" is null
and "stacked"."visibility" != $1
group by
"stacked"."stackId"
) as "stacked_assets" on true
where
"stacked"."stackId" = "asset_stack"."id"
and "stacked"."deletedAt" is null
and "stacked"."visibility" != $1
group by
"asset_stack"."id"
) as "stacked_assets" on "asset_stack"."id" is not null
where
(
"asset_stack"."primaryAssetId" = "assets"."id"
or "assets"."stackId" is null
"assets"."deletedAt" is null
and (
"assets"."visibility" = $2
or "assets"."visibility" = $3
)
and date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' = $4
and (
"assets"."visibility" = $5
or "assets"."visibility" = $6
)
and not exists (
select
from
"asset_stack"
where
"asset_stack"."id" = "assets"."stackId"
and "asset_stack"."primaryAssetId" != "assets"."id"
)
order by
"assets"."localDateTime" desc
),
"agg" as (
select
coalesce(array_agg("city"), '{}') as "city",
coalesce(array_agg("country"), '{}') as "country",
coalesce(array_agg("duration"), '{}') as "duration",
coalesce(array_agg("id"), '{}') as "id",
coalesce(array_agg("visibility"), '{}') as "visibility",
coalesce(array_agg("isFavorite"), '{}') as "isFavorite",
coalesce(array_agg("isImage"), '{}') as "isImage",
coalesce(array_agg("isTrashed"), '{}') as "isTrashed",
coalesce(array_agg("livePhotoVideoId"), '{}') as "livePhotoVideoId",
coalesce(array_agg("localDateTime"), '{}') as "localDateTime",
coalesce(array_agg("ownerId"), '{}') as "ownerId",
coalesce(array_agg("projectionType"), '{}') as "projectionType",
coalesce(array_agg("ratio"), '{}') as "ratio",
coalesce(array_agg("status"), '{}') as "status",
coalesce(array_agg("thumbhash"), '{}') as "thumbhash",
coalesce(json_agg("stack"), '[]') as "stack"
from
"cte"
)
and "assets"."deletedAt" is null
and (
"assets"."visibility" = $2
or "assets"."visibility" = $3
)
and date_trunc($4, "localDateTime" at time zone 'UTC') at time zone 'UTC' = $5
order by
"assets"."localDateTime" desc
select
to_json(agg)::text as "assets"
from
"agg"
-- AssetRepository.getDuplicates
with
"duplicates" as (
select
"assets"."duplicateId",
jsonb_agg("asset") as "assets"
json_agg(
"asset"
order by
"assets"."localDateTime" asc
) as "assets"
from
"assets"
left join lateral (
@ -323,7 +388,7 @@ with
from
"duplicates"
where
jsonb_array_length("assets") = $3
json_array_length("assets") = $3
),
"removed_unique" as (
update "assets"

View File

@ -68,7 +68,6 @@ export interface AssetBuilderOptions {
}
export interface TimeBucketOptions extends AssetBuilderOptions {
size: TimeBucketSize;
order?: AssetOrder;
}
@ -539,7 +538,7 @@ export class AssetRepository {
.with('assets', (qb) =>
qb
.selectFrom('assets')
.select(truncatedDate<Date>(options.size).as('timeBucket'))
.select(truncatedDate<Date>(TimeBucketSize.MONTH).as('timeBucket'))
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
.$if(options.visibility === undefined, withDefaultVisibility)
@ -581,53 +580,126 @@ export class AssetRepository {
);
}
@GenerateSql({ params: [DummyValue.TIME_BUCKET, { size: TimeBucketSize.MONTH, withStacked: true }] })
async getTimeBucket(timeBucket: string, options: TimeBucketOptions) {
return this.db
.selectFrom('assets')
.selectAll('assets')
.$call(withExif)
.$if(!!options.albumId, (qb) =>
@GenerateSql({
params: [DummyValue.TIME_BUCKET, { withStacked: true }],
})
getTimeBucket(timeBucket: string, options: TimeBucketOptions) {
const query = this.db
.with('cte', (qb) =>
qb
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
.where('albums_assets_assets.albumsId', '=', options.albumId!),
.selectFrom('assets')
.innerJoin('exif', 'assets.id', 'exif.assetId')
.select((eb) => [
'assets.duration',
'assets.id',
'assets.visibility',
'assets.isFavorite',
sql`assets.type = 'IMAGE'`.as('isImage'),
sql`assets."deletedAt" is null`.as('isTrashed'),
'assets.livePhotoVideoId',
'assets.localDateTime',
'assets.ownerId',
'assets.status',
eb.fn('encode', ['assets.thumbhash', sql.lit('base64')]).as('thumbhash'),
'exif.city',
'exif.country',
'exif.projectionType',
eb.fn
.coalesce(
eb
.case()
.when(sql`exif."exifImageHeight" = 0 or exif."exifImageWidth" = 0`)
.then(eb.lit(1))
.when('exif.orientation', 'in', sql<string>`('5', '6', '7', '8', '-90', '90')`)
.then(sql`round(exif."exifImageHeight"::numeric / exif."exifImageWidth"::numeric, 3)`)
.else(sql`round(exif."exifImageWidth"::numeric / exif."exifImageHeight"::numeric, 3)`)
.end(),
eb.lit(1),
)
.as('ratio'),
])
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
.$if(options.visibility == undefined, withDefaultVisibility)
.$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!))
.where(truncatedDate(TimeBucketSize.MONTH), '=', timeBucket.replace(/^[+-]/, ''))
.$if(!!options.albumId, (qb) =>
qb.where((eb) =>
eb.exists(
eb
.selectFrom('albums_assets_assets')
.whereRef('albums_assets_assets.assetsId', '=', 'assets.id')
.where('albums_assets_assets.albumsId', '=', asUuid(options.albumId!)),
),
),
)
.$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
.$if(options.visibility == undefined, withDefaultVisibility)
.$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!))
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
.$if(!!options.withStacked, (qb) =>
qb
.where((eb) =>
eb.not(
eb.exists(
eb
.selectFrom('asset_stack')
.whereRef('asset_stack.id', '=', 'assets.stackId')
.whereRef('asset_stack.primaryAssetId', '!=', 'assets.id'),
),
),
)
.leftJoinLateral(
(eb) =>
eb
.selectFrom('assets as stacked')
.select(sql`array[stacked."stackId"::text, count('stacked')::text]`.as('stack'))
.whereRef('stacked.stackId', '=', 'assets.stackId')
.where('stacked.deletedAt', 'is', null)
.where('stacked.visibility', '!=', AssetVisibility.ARCHIVE)
.groupBy('stacked.stackId')
.as('stacked_assets'),
(join) => join.onTrue(),
)
.select('stack'),
)
.$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
.$if(options.isDuplicate !== undefined, (qb) =>
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
)
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!))
.orderBy('assets.localDateTime', options.order ?? 'desc'),
)
.$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
.$if(!!options.withStacked, (qb) =>
.with('agg', (qb) =>
qb
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
.where((eb) =>
eb.or([eb('asset_stack.primaryAssetId', '=', eb.ref('assets.id')), eb('assets.stackId', 'is', null)]),
)
.leftJoinLateral(
(eb) =>
eb
.selectFrom('assets as stacked')
.selectAll('asset_stack')
.select((eb) => eb.fn.count(eb.table('stacked')).as('assetCount'))
.whereRef('stacked.stackId', '=', 'asset_stack.id')
.where('stacked.deletedAt', 'is', null)
.where('stacked.visibility', '!=', AssetVisibility.ARCHIVE)
.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').$castTo<Stack | null>()).as('stack')),
.selectFrom('cte')
.select((eb) => [
eb.fn.coalesce(eb.fn('array_agg', ['city']), sql.lit('{}')).as('city'),
eb.fn.coalesce(eb.fn('array_agg', ['country']), sql.lit('{}')).as('country'),
eb.fn.coalesce(eb.fn('array_agg', ['duration']), sql.lit('{}')).as('duration'),
eb.fn.coalesce(eb.fn('array_agg', ['id']), sql.lit('{}')).as('id'),
eb.fn.coalesce(eb.fn('array_agg', ['visibility']), sql.lit('{}')).as('visibility'),
eb.fn.coalesce(eb.fn('array_agg', ['isFavorite']), sql.lit('{}')).as('isFavorite'),
eb.fn.coalesce(eb.fn('array_agg', ['isImage']), sql.lit('{}')).as('isImage'),
// TODO: isTrashed is redundant as it will always be all true or false depending on the options
eb.fn.coalesce(eb.fn('array_agg', ['isTrashed']), sql.lit('{}')).as('isTrashed'),
eb.fn.coalesce(eb.fn('array_agg', ['livePhotoVideoId']), sql.lit('{}')).as('livePhotoVideoId'),
eb.fn.coalesce(eb.fn('array_agg', ['localDateTime']), sql.lit('{}')).as('localDateTime'),
eb.fn.coalesce(eb.fn('array_agg', ['ownerId']), sql.lit('{}')).as('ownerId'),
eb.fn.coalesce(eb.fn('array_agg', ['projectionType']), sql.lit('{}')).as('projectionType'),
eb.fn.coalesce(eb.fn('array_agg', ['ratio']), sql.lit('{}')).as('ratio'),
eb.fn.coalesce(eb.fn('array_agg', ['status']), sql.lit('{}')).as('status'),
eb.fn.coalesce(eb.fn('array_agg', ['thumbhash']), sql.lit('{}')).as('thumbhash'),
])
.$if(!!options.withStacked, (qb) =>
qb.select((eb) => eb.fn.coalesce(eb.fn('json_agg', ['stack']), sql.lit('[]')).as('stack')),
),
)
.$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
.$if(options.isDuplicate !== undefined, (qb) =>
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
)
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!))
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
.$if(options.visibility == undefined, withDefaultVisibility)
.$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!))
.where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, ''))
.orderBy('assets.localDateTime', options.order ?? 'desc')
.execute();
.selectFrom('agg')
.select(sql<string>`to_json(agg)::text`.as('assets'));
return query.executeTakeFirstOrThrow();
}
@GenerateSql({ params: [DummyValue.UUID] })
@ -649,10 +721,7 @@ export class AssetRepository {
)
.select('assets.duplicateId')
.select((eb) =>
eb
.fn('jsonb_agg', [eb.table('asset')])
.$castTo<MapAsset[]>()
.as('assets'),
eb.fn.jsonAgg('asset').orderBy('assets.localDateTime', 'asc').$castTo<MapAsset[]>().as('assets'),
)
.where('assets.ownerId', '=', asUuid(userId))
.where('assets.duplicateId', 'is not', null)
@ -666,7 +735,7 @@ export class AssetRepository {
qb
.selectFrom('duplicates')
.select('duplicateId')
.where((eb) => eb(eb.fn('jsonb_array_length', ['assets']), '=', 1)),
.where((eb) => eb(eb.fn('json_array_length', ['assets']), '=', 1)),
)
.with('removed_unique', (qb) =>
qb
@ -677,7 +746,7 @@ export class AssetRepository {
)
.selectFrom('duplicates')
.selectAll()
// TODO: compare with filtering by jsonb_array_length > 1
// TODO: compare with filtering by json_array_length > 1
.where(({ not, exists }) =>
not(exists((eb) => eb.selectFrom('unique').whereRef('unique.duplicateId', '=', 'duplicates.duplicateId'))),
)

View File

@ -209,7 +209,7 @@ export class MediaRepository {
index: stream.index,
codecType: stream.codec_type,
codecName: stream.codec_name,
frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames),
bitrate: this.parseInt(stream.bit_rate),
})),
};
}

View File

@ -1235,7 +1235,7 @@ describe(MediaService.name, () => {
expect(mocks.media.transcode).not.toHaveBeenCalled();
});
it('should transcode the longest stream', async () => {
it('should transcode the highest bitrate video stream', async () => {
mocks.logger.isLevelEnabled.mockReturnValue(false);
mocks.media.probe.mockResolvedValue(probeStub.multipleVideoStreams);
@ -1249,7 +1249,27 @@ describe(MediaService.name, () => {
'upload/encoded-video/user-id/as/se/asset-id.mp4',
expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-map 0:0', '-map 0:1']),
outputOptions: expect.arrayContaining(['-map 0:1', '-map 0:3']),
twoPass: false,
}),
);
});
it('should transcode the highest bitrate audio stream', async () => {
mocks.logger.isLevelEnabled.mockReturnValue(false);
mocks.media.probe.mockResolvedValue(probeStub.multipleAudioStreams);
await sut.handleVideoConversion({ id: assetStub.video.id });
expect(mocks.media.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: false });
expect(mocks.systemMetadata.get).toHaveBeenCalled();
expect(mocks.storage.mkdirSync).toHaveBeenCalled();
expect(mocks.media.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-map 0:0', '-map 0:2']),
twoPass: false,
}),
);
@ -1780,7 +1800,7 @@ describe(MediaService.name, () => {
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
'-map 0:1',
'-map 0:3',
'-v verbose',
'-vf scale=-2:720',
'-preset 12',
@ -1901,7 +1921,7 @@ describe(MediaService.name, () => {
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
'-map 0:1',
'-map 0:3',
'-g 256',
'-v verbose',
'-vf hwupload_cuda,scale_cuda=-2:720:format=nv12',
@ -2072,7 +2092,7 @@ describe(MediaService.name, () => {
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
'-map 0:1',
'-map 0:3',
'-bf 7',
'-refs 5',
'-g 256',
@ -2294,7 +2314,7 @@ describe(MediaService.name, () => {
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
'-map 0:1',
'-map 0:3',
'-g 256',
'-v verbose',
'-vf hwupload=extra_hw_frames=64,scale_vaapi=-2:720:mode=hq:out_range=pc:format=nv12',
@ -2581,7 +2601,7 @@ describe(MediaService.name, () => {
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
'-map 0:1',
'-map 0:3',
'-g 256',
'-v verbose',
'-vf scale_rkrga=-2:720:format=nv12:afbc=1:async_depth=4',

View File

@ -547,7 +547,7 @@ export class MediaService extends BaseService {
private getMainStream<T extends VideoStreamInfo | AudioStreamInfo>(streams: T[]): T {
return streams
.filter((stream) => stream.codecName !== 'unknown')
.sort((stream1, stream2) => stream2.frameCount - stream1.frameCount)[0];
.sort((stream1, stream2) => stream2.bitrate - stream1.bitrate)[0];
}
private getTranscodeTarget(

View File

@ -4,7 +4,7 @@ import { DateTime } from 'luxon';
import { Writable } from 'node:stream';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
import { SessionSyncCheckpoints } from 'src/db';
import { AssetResponseDto, hexOrBufferToBase64, mapAsset } from 'src/dtos/asset-response.dto';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import {
AssetDeltaSyncDto,
@ -18,6 +18,7 @@ import { AssetVisibility, DatabaseAction, EntityType, Permission, SyncEntityType
import { BaseService } from 'src/services/base.service';
import { SyncAck } from 'src/types';
import { getMyPartnerIds } from 'src/utils/asset.util';
import { hexOrBufferToBase64 } from 'src/utils/bytes';
import { setIsEqual } from 'src/utils/set';
import { fromAck, serialize } from 'src/utils/sync';

View File

@ -1,10 +1,7 @@
import { BadRequestException } from '@nestjs/common';
import { AssetVisibility } from 'src/enum';
import { TimeBucketSize } from 'src/repositories/asset.repository';
import { TimelineService } from 'src/services/timeline.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
describe(TimelineService.name, () => {
@ -19,13 +16,10 @@ describe(TimelineService.name, () => {
it("should return buckets if userId and albumId aren't set", async () => {
mocks.asset.getTimeBuckets.mockResolvedValue([{ timeBucket: 'bucket', count: 1 }]);
await expect(
sut.getTimeBuckets(authStub.admin, {
size: TimeBucketSize.DAY,
}),
).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }]));
await expect(sut.getTimeBuckets(authStub.admin, {})).resolves.toEqual(
expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }]),
);
expect(mocks.asset.getTimeBuckets).toHaveBeenCalledWith({
size: TimeBucketSize.DAY,
userIds: [authStub.admin.user.id],
});
});
@ -34,35 +28,34 @@ describe(TimelineService.name, () => {
describe('getTimeBucket', () => {
it('should return the assets for a album time bucket if user has album.read', async () => {
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
const json = `[{ id: ['asset-id'] }]`;
mocks.asset.getTimeBucket.mockResolvedValue({ assets: json });
await expect(
sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
await expect(sut.getTimeBucket(authStub.admin, { timeBucket: 'bucket', albumId: 'album-id' })).resolves.toEqual(
json,
);
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id']));
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
albumId: 'album-id',
});
});
it('should return the assets for a archive time bucket if user has archive.read', async () => {
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
const json = `[{ id: ['asset-id'] }]`;
mocks.asset.getTimeBucket.mockResolvedValue({ assets: json });
await expect(
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
visibility: AssetVisibility.ARCHIVE,
userId: authStub.admin.user.id,
}),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
).resolves.toEqual(json);
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith(
'bucket',
expect.objectContaining({
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
visibility: AssetVisibility.ARCHIVE,
userIds: [authStub.admin.user.id],
@ -71,20 +64,19 @@ describe(TimelineService.name, () => {
});
it('should include partner shared assets', async () => {
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
const json = `[{ id: ['asset-id'] }]`;
mocks.asset.getTimeBucket.mockResolvedValue({ assets: json });
mocks.partner.getAll.mockResolvedValue([]);
await expect(
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
visibility: AssetVisibility.TIMELINE,
userId: authStub.admin.user.id,
withPartners: true,
}),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
).resolves.toEqual(json);
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
visibility: AssetVisibility.TIMELINE,
withPartners: true,
@ -93,62 +85,37 @@ describe(TimelineService.name, () => {
});
it('should check permissions to read tag', async () => {
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
const json = `[{ id: ['asset-id'] }]`;
mocks.asset.getTimeBucket.mockResolvedValue({ assets: json });
mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-123']));
await expect(
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
userId: authStub.admin.user.id,
tagId: 'tag-123',
}),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
).resolves.toEqual(json);
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
size: TimeBucketSize.DAY,
tagId: 'tag-123',
timeBucket: 'bucket',
userIds: [authStub.admin.user.id],
});
});
it('should strip metadata if showExif is disabled', async () => {
mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-id']));
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
const auth = factory.auth({ sharedLink: { showExif: false } });
const buckets = await sut.getTimeBucket(auth, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
visibility: AssetVisibility.ARCHIVE,
albumId: 'album-id',
});
expect(buckets).toEqual([expect.objectContaining({ id: 'asset-id' })]);
expect(buckets[0]).not.toHaveProperty('exif');
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
visibility: AssetVisibility.ARCHIVE,
albumId: 'album-id',
});
});
it('should return the assets for a library time bucket if user has library.read', async () => {
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
const json = `[{ id: ['asset-id'] }]`;
mocks.asset.getTimeBucket.mockResolvedValue({ assets: json });
await expect(
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
userId: authStub.admin.user.id,
}),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
).resolves.toEqual(json);
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith(
'bucket',
expect.objectContaining({
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
userIds: [authStub.admin.user.id],
}),
@ -158,7 +125,6 @@ describe(TimelineService.name, () => {
it('should throw an error if withParners is true and visibility true or undefined', async () => {
await expect(
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
visibility: AssetVisibility.ARCHIVE,
withPartners: true,
@ -168,7 +134,6 @@ describe(TimelineService.name, () => {
await expect(
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
visibility: undefined,
withPartners: true,
@ -180,7 +145,6 @@ describe(TimelineService.name, () => {
it('should throw an error if withParners is true and isFavorite is either true or false', async () => {
await expect(
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isFavorite: true,
withPartners: true,
@ -190,7 +154,6 @@ describe(TimelineService.name, () => {
await expect(
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isFavorite: false,
withPartners: true,
@ -202,7 +165,6 @@ describe(TimelineService.name, () => {
it('should throw an error if withParners is true and isTrash is true', async () => {
await expect(
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isTrashed: true,
withPartners: true,

View File

@ -1,7 +1,6 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketsResponseDto } from 'src/dtos/time-bucket.dto';
import { AssetVisibility, Permission } from 'src/enum';
import { TimeBucketOptions } from 'src/repositories/asset.repository';
import { BaseService } from 'src/services/base.service';
@ -9,22 +8,20 @@ import { getMyPartnerIds } from 'src/utils/asset.util';
@Injectable()
export class TimelineService extends BaseService {
async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketsResponseDto[]> {
await this.timeBucketChecks(auth, dto);
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
return this.assetRepository.getTimeBuckets(timeBucketOptions);
return await this.assetRepository.getTimeBuckets(timeBucketOptions);
}
async getTimeBucket(
auth: AuthDto,
dto: TimeBucketAssetDto,
): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> {
// pre-jsonified response
async getTimeBucket(auth: AuthDto, dto: TimeBucketAssetDto): Promise<string> {
await this.timeBucketChecks(auth, dto);
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions);
return !auth.sharedLink || auth.sharedLink?.showExif
? assets.map((asset) => mapAsset(asset, { withStack: true, auth }))
: assets.map((asset) => mapAsset(asset, { stripMetadata: true, auth }));
const timeBucketOptions = await this.buildTimeBucketOptions(auth, { ...dto });
// TODO: use id cursor for pagination
const bucket = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions);
return bucket.assets;
}
private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {

View File

@ -89,7 +89,7 @@ export interface VideoStreamInfo {
export interface AudioStreamInfo {
index: number;
codecName?: string;
frameCount: number;
bitrate: number;
}
export interface VideoFormat {
@ -361,11 +361,7 @@ export type JobItem =
| { name: JobName.NOTIFY_SIGNUP; data: INotifySignupJob }
// Version check
| { name: JobName.VERSION_CHECK; data: IBaseJob }
// Memories
| { name: JobName.MEMORIES_CLEANUP; data?: IBaseJob }
| { name: JobName.MEMORIES_CREATE; data?: IBaseJob };
| { name: JobName.VERSION_CHECK; data: IBaseJob };
export type VectorExtension = DatabaseExtension.VECTOR | DatabaseExtension.VECTORS;

View File

@ -22,3 +22,12 @@ export function asHumanReadable(bytes: number, precision = 1): string {
return `${remainder.toFixed(magnitude == 0 ? 0 : precision)} ${units[magnitude]}`;
}
// if an asset is jsonified in the DB before being returned, its buffer fields will be hex-encoded strings
export const hexOrBufferToBase64 = (encoded: string | Buffer) => {
if (typeof encoded === 'string') {
return Buffer.from(encoded.slice(2), 'hex').toString('base64');
}
return encoded.toString('base64');
};

View File

@ -271,7 +271,7 @@ export function withTags(eb: ExpressionBuilder<DB, 'assets'>) {
}
export function truncatedDate<O>(size: TimeBucketSize) {
return sql<O>`date_trunc(${size}, "localDateTime" at time zone 'UTC') at time zone 'UTC'`;
return sql<O>`date_trunc(${sql.lit(size)}, "localDateTime" at time zone 'UTC') at time zone 'UTC'`;
}
export function withTagId<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagId: string) {
@ -285,6 +285,7 @@ export function withTagId<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagId: str
),
);
}
const joinDeduplicationPlugin = new DeduplicateJoinsPlugin();
/** TODO: This should only be used for search-related queries, not as a general purpose query builder */

View File

@ -14,7 +14,6 @@ import { LoggingRepository } from 'src/repositories/logging.repository';
import { bootstrapTelemetry } from 'src/repositories/telemetry.repository';
import { ApiService } from 'src/services/api.service';
import { isStartUpError, useSwagger } from 'src/utils/misc';
async function bootstrap() {
process.title = 'immich-api';

View File

@ -251,6 +251,10 @@ export const assetStub = {
duplicateId: null,
isOffline: false,
stack: null,
orientation: '',
projectionType: null,
height: 3840,
width: 2160,
visibility: AssetVisibility.TIMELINE,
}),

View File

@ -21,7 +21,7 @@ const probeStubDefaultVideoStream: VideoStreamInfo[] = [
},
];
const probeStubDefaultAudioStream: AudioStreamInfo[] = [{ index: 1, codecName: 'mp3', frameCount: 100 }];
const probeStubDefaultAudioStream: AudioStreamInfo[] = [{ index: 3, codecName: 'mp3', bitrate: 100 }];
const probeStubDefault: VideoInfo = {
format: probeStubDefaultFormat,
@ -40,23 +40,42 @@ export const probeStub = {
height: 1080,
width: 400,
codecName: 'hevc',
frameCount: 100,
frameCount: 1,
rotation: 0,
isHDR: false,
bitrate: 0,
bitrate: 100,
pixelFormat: 'yuv420p',
},
{
index: 1,
height: 1080,
width: 400,
codecName: 'h7000',
frameCount: 99,
codecName: 'hevc',
frameCount: 2,
rotation: 0,
isHDR: false,
bitrate: 0,
bitrate: 101,
pixelFormat: 'yuv420p',
},
{
index: 2,
height: 1080,
width: 400,
codecName: 'h7000',
frameCount: 3,
rotation: 0,
isHDR: false,
bitrate: 99,
pixelFormat: 'yuv420p',
},
],
}),
multipleAudioStreams: Object.freeze<VideoInfo>({
...probeStubDefault,
audioStreams: [
{ index: 0, codecName: 'mp3', bitrate: 100 },
{ index: 1, codecName: 'mp3', bitrate: 101 },
{ index: 2, codecName: 'mp3', bitrate: 102 },
],
}),
noHeight: Object.freeze<VideoInfo>({
@ -200,13 +219,13 @@ export const probeStub = {
}),
audioStreamAac: Object.freeze<VideoInfo>({
...probeStubDefault,
audioStreams: [{ index: 1, codecName: 'aac', frameCount: 100 }],
audioStreams: [{ index: 1, codecName: 'aac', bitrate: 100 }],
}),
audioStreamUnknown: Object.freeze<VideoInfo>({
...probeStubDefault,
audioStreams: [
{ index: 0, codecName: 'aac', frameCount: 100 },
{ index: 1, codecName: 'unknown', frameCount: 200 },
{ index: 0, codecName: 'aac', bitrate: 100 },
{ index: 1, codecName: 'unknown', bitrate: 200 },
],
}),
matroskaContainer: Object.freeze<VideoInfo>({

View File

@ -0,0 +1,6 @@
{
"name": "typescript-sdk",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

24
web/package-lock.json generated
View File

@ -11,7 +11,7 @@
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@immich/ui": "^0.22.0",
"@immich/ui": "^0.22.1",
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.11.5",
@ -1341,15 +1341,15 @@
"link": true
},
"node_modules/@immich/ui": {
"version": "0.22.0",
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.22.0.tgz",
"integrity": "sha512-bBx9hPy7/VECZPcEiBGty6Lu9jmD4vJf6VL2ud+LHLQcpZebv4FVFZzzVFf7ctBwooYJWTEfWZTPNgAo0rbQtQ==",
"version": "0.22.1",
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.22.1.tgz",
"integrity": "sha512-/QdqctBit+eX8QZgTL4PlgS7l6/NCGXeDjR6kQNLOVBPhbjkmtwqsvZ+RymYClcHAEhutXOKRhnlQU9mNLC/SA==",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@mdi/js": "^7.4.47",
"bits-ui": "^1.0.0-next.46",
"tailwind-merge": "^2.5.4",
"tailwind-variants": "^0.3.0"
"tailwind-variants": "^1.0.0"
},
"peerDependencies": {
"svelte": "^5.0.0"
@ -9479,12 +9479,12 @@
}
},
"node_modules/tailwind-variants": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-0.3.1.tgz",
"integrity": "sha512-krn67M3FpPwElg4FsZrOQd0U26o7UDH/QOkK8RNaiCCrr052f6YJPBUfNKnPo/s/xRzNPtv1Mldlxsg8Tb46BQ==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-1.0.0.tgz",
"integrity": "sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==",
"license": "MIT",
"dependencies": {
"tailwind-merge": "2.5.4"
"tailwind-merge": "3.0.2"
},
"engines": {
"node": ">=16.x",
@ -9495,9 +9495,9 @@
}
},
"node_modules/tailwind-variants/node_modules/tailwind-merge": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
"integrity": "sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz",
"integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==",
"license": "MIT",
"funding": {
"type": "github",

View File

@ -28,7 +28,7 @@
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@immich/ui": "^0.22.0",
"@immich/ui": "^0.22.1",
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.11.5",

View File

@ -1,5 +1,6 @@
@import 'tailwindcss';
@import '@immich/ui/theme/default.css';
/* @import '/usr/ui/dist/theme/default.css'; */
@config '../tailwind.config.js';

View File

@ -30,7 +30,7 @@
<div class="relative mx-auto font-mono text-2xl font-semibold">
<span class="text-gray-400 dark:text-gray-600">{zeros()}</span><span>{value}</span>
{#if unit}
<Code color="muted" class="absolute -top-5 end-2 font-light">{unit}</Code>
<Code color="muted" class="absolute -top-5 end-1 font-light">{unit}</Code>
{/if}
</div>
</div>

View File

@ -1,13 +1,12 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
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, getNotificationTemplateAdmin } from '@immich/sdk';
import { Button } from '@immich/ui';
import { Button, Modal, ModalBody } from '@immich/ui';
import { mdiEyeOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
@ -112,15 +111,17 @@
</div>
{#if htmlPreview}
<FullScreenModal title={$t('admin.template_email_preview')} onClose={closePreviewModal} width="wide">
<div style="position:relative; width:100%; height:90vh; overflow: hidden">
<iframe
title={$t('admin.template_email_preview')}
srcdoc={htmlPreview}
style="width: 100%; height: 100%; border: none; overflow:hidden; position: absolute; top: 0; left: 0;"
></iframe>
</div>
</FullScreenModal>
<Modal title={$t('admin.template_email_preview')} onClose={closePreviewModal} size="medium">
<ModalBody>
<div style="position:relative; width:100%; height:90vh; overflow: hidden">
<iframe
title={$t('admin.template_email_preview')}
srcdoc={htmlPreview}
style="width: 100%; height: 100%; border: none; overflow:hidden; position: absolute; top: 0; left: 0;"
></iframe>
</div>
</ModalBody>
</Modal>
{/if}
</form>
</div>

View File

@ -1,7 +1,6 @@
<script lang="ts">
import { clickOutside } from '$lib/actions/click-outside';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import type Map from '$lib/components/shared-components/map/map.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
import { timeToLoadTheMap } from '$lib/constants';
@ -11,7 +10,7 @@
import { delay } from '$lib/utils/asset-utils';
import { navigate } from '$lib/utils/navigation';
import { getAlbumInfo, type AlbumResponseDto, type MapMarkerResponseDto } from '@immich/sdk';
import { LoadingSpinner } from '@immich/ui';
import { LoadingSpinner, Modal, ModalBody } from '@immich/ui';
import { mdiMapOutline } from '@mdi/js';
import { onDestroy, onMount } from 'svelte';
import { t } from 'svelte-i18n';
@ -112,31 +111,33 @@
{#if albumMapViewManager.isInMapView}
<div use:clickOutside={{ onOutclick: closeMap }}>
<FullScreenModal title={$t('map')} width="wide" onClose={closeMap}>
<div class="flex flex-col w-full h-full gap-2">
<div class="h-[500px] min-h-[300px] w-full">
{#await import('../shared-components/map/map.svelte')}
{#await delay(timeToLoadTheMap) then}
<!-- show the loading spinner only if loading the map takes too much time -->
<div class="flex items-center justify-center h-full w-full">
<LoadingSpinner />
</div>
<Modal title={$t('map')} size="medium" onClose={closeMap}>
<ModalBody>
<div class="flex flex-col w-full h-full gap-2 border border-gray-300 dark:border-light rounded-2xl">
<div class="h-[500px] min-h-[300px] w-full">
{#await import('../shared-components/map/map.svelte')}
{#await delay(timeToLoadTheMap) then}
<!-- show the loading spinner only if loading the map takes too much time -->
<div class="flex items-center justify-center h-full w-full">
<LoadingSpinner />
</div>
{/await}
{:then { default: Map }}
<Map
bind:this={mapElement}
center={undefined}
{zoom}
clickable={false}
bind:mapMarkers
onSelect={onViewAssets}
showSettings={false}
rounded
/>
{/await}
{:then { default: Map }}
<Map
bind:this={mapElement}
center={undefined}
{zoom}
clickable={false}
bind:mapMarkers
onSelect={onViewAssets}
showSettings={false}
rounded
/>
{/await}
</div>
</div>
</div>
</FullScreenModal>
</ModalBody>
</Modal>
</div>
<Portal target="body">

View File

@ -2,7 +2,6 @@
import Icon from '$lib/components/elements/icon.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
@ -16,6 +15,7 @@
type AlbumResponseDto,
type UserResponseDto,
} from '@immich/sdk';
import { Modal, ModalBody } from '@immich/ui';
import { mdiArrowDownThin, mdiArrowUpThin, mdiDotsVertical, mdiPlus } from '@mdi/js';
import { findKey } from 'lodash-es';
import { t } from 'svelte-i18n';
@ -115,79 +115,81 @@
</script>
{#if !selectedRemoveUser}
<FullScreenModal title={$t('options')} {onClose}>
<div class="items-center justify-center">
<div class="py-2">
<h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2>
<div class="grid p-2 gap-y-2">
{#if order}
<SettingDropdown
title={$t('display_order')}
options={Object.values(options)}
selectedOption={options[order]}
onToggle={handleToggle}
<Modal title={$t('options')} {onClose} size="small">
<ModalBody>
<div class="items-center justify-center">
<div class="py-2">
<h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2>
<div class="grid p-2 gap-y-2">
{#if order}
<SettingDropdown
title={$t('display_order')}
options={Object.values(options)}
selectedOption={options[order]}
onToggle={handleToggle}
/>
{/if}
<SettingSwitch
title={$t('comments_and_likes')}
subtitle={$t('let_others_respond')}
checked={album.isActivityEnabled}
onToggle={onToggleEnabledActivity}
/>
{/if}
<SettingSwitch
title={$t('comments_and_likes')}
subtitle={$t('let_others_respond')}
checked={album.isActivityEnabled}
onToggle={onToggleEnabledActivity}
/>
</div>
</div>
<div class="py-2">
<div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div>
<div class="p-2">
<button type="button" class="flex items-center gap-2" onclick={onShowSelectSharedUser}>
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
<div><Icon path={mdiPlus} size="25" /></div>
</div>
<div>{$t('invite_people')}</div>
</button>
<div class="flex items-center gap-2 py-2 mt-2">
<div>
<UserAvatar {user} size="md" />
</div>
<div class="w-full">{user.name}</div>
<div>{$t('owner')}</div>
</div>
</div>
<div class="py-2">
<div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div>
<div class="p-2">
<button type="button" class="flex items-center gap-2" onclick={onShowSelectSharedUser}>
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
<div><Icon path={mdiPlus} size="25" /></div>
</div>
<div>{$t('invite_people')}</div>
</button>
{#each album.albumUsers as { user, role } (user.id)}
<div class="flex items-center gap-2 py-2">
<div class="flex items-center gap-2 py-2 mt-2">
<div>
<UserAvatar {user} size="md" />
</div>
<div class="w-full">{user.name}</div>
{#if role === AlbumUserRole.Viewer}
{$t('role_viewer')}
{:else}
{$t('role_editor')}
{/if}
{#if user.id !== album.ownerId}
<ButtonContextMenu icon={mdiDotsVertical} size="20" title={$t('options')}>
{#if role === AlbumUserRole.Viewer}
<MenuOption
onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Editor)}
text={$t('allow_edits')}
/>
{:else}
<MenuOption
onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Viewer)}
text={$t('disallow_edits')}
/>
{/if}
<!-- Allow deletion for non-owners -->
<MenuOption onClick={() => handleMenuRemove(user)} text={$t('remove')} />
</ButtonContextMenu>
{/if}
<div>{$t('owner')}</div>
</div>
{/each}
{#each album.albumUsers as { user, role } (user.id)}
<div class="flex items-center gap-2 py-2">
<div>
<UserAvatar {user} size="md" />
</div>
<div class="w-full">{user.name}</div>
{#if role === AlbumUserRole.Viewer}
{$t('role_viewer')}
{:else}
{$t('role_editor')}
{/if}
{#if user.id !== album.ownerId}
<ButtonContextMenu icon={mdiDotsVertical} size="20" title={$t('options')}>
{#if role === AlbumUserRole.Viewer}
<MenuOption
onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Editor)}
text={$t('allow_edits')}
/>
{:else}
<MenuOption
onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Viewer)}
text={$t('disallow_edits')}
/>
{/if}
<!-- Allow deletion for non-owners -->
<MenuOption onClick={() => handleMenuRemove(user)} text={$t('remove')} />
</ButtonContextMenu>
{/if}
</div>
{/each}
</div>
</div>
</div>
</div>
</FullScreenModal>
</ModalBody>
</Modal>
{/if}
{#if selectedRemoveUser}

View File

@ -40,7 +40,7 @@
onblur={handleUpdateName}
class="w-[99%] mb-2 border-b-2 border-transparent text-2xl md:text-4xl lg:text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned
? 'hover:border-gray-400'
: 'hover:border-transparent'} focus:border-b-2 focus:border-immich-primary focus:outline-none bg-light dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray"
: 'hover:border-transparent'} focus:border-b-2 focus:border-immich-primary focus:outline-none bg-light dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray placeholder:text-primary/90"
type="text"
bind:value={newAlbumName}
disabled={!isOwned}

View File

@ -1,5 +1,6 @@
<script lang="ts">
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { AssetAction } from '$lib/constants';
import { modalManager } from '$lib/managers/modal-manager.svelte';
import { keepThisDeleteOthers } from '$lib/utils/asset-utils';

View File

@ -5,7 +5,7 @@
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 } from '@immich/sdk';
import { AssetVisibility, updateAssets } from '@immich/sdk';
import { mdiEyeOffOutline, mdiFolderMoveOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { OnAction, PreAction } from './action';
@ -17,7 +17,7 @@
}
let { asset, onAction, preAction }: Props = $props();
const isLocked = asset.visibility === Visibility.Locked;
const isLocked = asset.visibility === AssetVisibility.Locked;
const toggleLockedVisibility = async () => {
const isConfirmed = await modalManager.showDialog({

View File

@ -50,7 +50,7 @@
{#each tags as tag (tag.id)}
<div class="flex group transition-all">
<a
class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary roudned-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary rounded-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
href={encodeURI(`${AppRoute.TAGS}/?path=${tag.value}`)}
>
<p class="text-sm">

View File

@ -5,7 +5,7 @@
import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
import { timeToSeconds } from '$lib/utils/date-time';
import { getAltText } from '$lib/utils/thumbnail-util';
import { AssetMediaSize, Visibility } from '@immich/sdk';
import { AssetMediaSize, AssetVisibility } from '@immich/sdk';
import {
mdiArchiveArrowDownOutline,
mdiCameraBurst,
@ -285,7 +285,7 @@
</div>
{/if}
{#if !authManager.key && showArchiveIcon && asset.visibility === Visibility.Archive}
{#if !authManager.key && showArchiveIcon && asset.visibility === AssetVisibility.Archive}
<div class={['absolute start-2', asset.isFavorite ? 'bottom-10' : 'bottom-2']}>
<Icon path={mdiArchiveArrowDownOutline} size="24" class="text-white" />
</div>

View File

@ -1,9 +1,8 @@
<script lang="ts">
import AlbumCover from '$lib/components/album-page/album-cover.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { handleError } from '$lib/utils/handle-error';
import { updateAlbumInfo, type AlbumResponseDto } from '@immich/sdk';
import { Button } from '@immich/ui';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiRenameOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@ -47,29 +46,33 @@
};
</script>
<FullScreenModal icon={mdiRenameOutline} title={$t('edit_album')} width="wide" {onClose}>
<form {onsubmit} autocomplete="off" id="edit-album-form">
<div class="flex items-center">
<div class="hidden sm:flex">
<AlbumCover {album} class="h-[200px] w-[200px] m-4 shadow-lg" />
</div>
<div class="grow">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">{$t('name')}</label>
<input class="immich-form-input" id="name" type="text" bind:value={albumName} />
<Modal icon={mdiRenameOutline} title={$t('edit_album')} size="medium" {onClose}>
<ModalBody>
<form {onsubmit} autocomplete="off" id="edit-album-form">
<div class="flex items-center">
<div class="hidden sm:flex">
<AlbumCover {album} class="h-[200px] w-[200px] m-4 shadow-lg" />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="description">{$t('description')}</label>
<textarea class="immich-form-input" id="description" bind:value={description}></textarea>
<div class="grow">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">{$t('name')}</label>
<input class="immich-form-input" id="name" type="text" bind:value={albumName} />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="description">{$t('description')}</label>
<textarea class="immich-form-input" id="description" bind:value={description}></textarea>
</div>
</div>
</div>
</form>
</ModalBody>
<ModalFooter>
<div class="flex gap-2 w-full">
<Button shape="round" color="secondary" fullWidth onclick={() => onCancel?.()}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth disabled={isSubmitting} form="edit-album-form">{$t('save')}</Button>
</div>
</form>
{#snippet stickyBottom()}
<Button shape="round" color="secondary" fullWidth onclick={() => onCancel?.()}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth disabled={isSubmitting} form="edit-album-form">{$t('save')}</Button>
{/snippet}
</FullScreenModal>
</ModalFooter>
</Modal>

View File

@ -1,9 +1,8 @@
<script lang="ts">
import { Button } from '@immich/ui';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiFolderRemove } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
interface Props {
exclusionPattern: string;
@ -42,37 +41,40 @@
};
</script>
<FullScreenModal title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={onCancel}>
<form {onsubmit} autocomplete="off" id="add-exclusion-pattern-form">
<p class="py-5 text-sm">
{$t('admin.exclusion_pattern_description')}
<br /><br />
{$t('admin.add_exclusion_pattern_description')}
</p>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="exclusionPattern">{$t('pattern')}</label>
<input
class="immich-form-input"
id="exclusionPattern"
name="exclusionPattern"
type="text"
bind:value={exclusionPattern}
/>
</div>
<div class="mt-8 flex w-full gap-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">{$t('errors.exclusion_pattern_already_exists')}</p>
<Modal size="small" title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={onCancel}>
<ModalBody>
<form {onsubmit} autocomplete="off" id="add-exclusion-pattern-form">
<p class="py-5 text-sm">
{$t('admin.exclusion_pattern_description')}
<br /><br />
{$t('admin.add_exclusion_pattern_description')}
</p>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="exclusionPattern">{$t('pattern')}</label>
<input
class="immich-form-input"
id="exclusionPattern"
name="exclusionPattern"
type="text"
bind:value={exclusionPattern}
/>
</div>
<div class="mt-8 flex w-full gap-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">{$t('errors.exclusion_pattern_already_exists')}</p>
{/if}
</div>
</form>
</ModalBody>
<ModalFooter>
<div class="flex gap-2 w-full">
<Button shape="round" color="secondary" fullWidth onclick={onCancel}>{$t('cancel')}</Button>
{#if isEditing}
<Button shape="round" color="danger" fullWidth onclick={onDelete}>{$t('delete')}</Button>
{/if}
<Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="add-exclusion-pattern-form"
>{submitText}</Button
>
</div>
</form>
{#snippet stickyBottom()}
<Button shape="round" color="secondary" fullWidth onclick={onCancel}>{$t('cancel')}</Button>
{#if isEditing}
<Button shape="round" color="danger" fullWidth onclick={onDelete}>{$t('delete')}</Button>
{/if}
<Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="add-exclusion-pattern-form"
>{submitText}</Button
>
{/snippet}
</FullScreenModal>
</ModalFooter>
</Modal>

View File

@ -1,6 +1,5 @@
<script lang="ts">
import { Button } from '@immich/ui';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiFolderSync } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@ -46,29 +45,33 @@
};
</script>
<FullScreenModal {title} icon={mdiFolderSync} onClose={onCancel}>
<form {onsubmit} autocomplete="off" id="library-import-path-form">
<p class="py-5 text-sm">{$t('admin.library_import_path_description')}</p>
<Modal {title} icon={mdiFolderSync} onClose={onCancel} size="small">
<ModalBody>
<form {onsubmit} autocomplete="off" id="library-import-path-form">
<p class="py-5 text-sm">{$t('admin.library_import_path_description')}</p>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="path">{$t('path')}</label>
<input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} />
</div>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="path">{$t('path')}</label>
<input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} />
</div>
<div class="mt-8 flex w-full gap-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">{$t('errors.import_path_already_exists')}</p>
<div class="mt-8 flex w-full gap-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">{$t('errors.import_path_already_exists')}</p>
{/if}
</div>
</form>
</ModalBody>
<ModalFooter>
<div class="flex gap-2 w-full">
<Button shape="round" color="secondary" fullWidth onclick={onCancel}>{cancelText}</Button>
{#if isEditing}
<Button shape="round" color="danger" fullWidth onclick={onDelete}>{$t('delete')}</Button>
{/if}
<Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="library-import-path-form"
>{submitText}</Button
>
</div>
</form>
{#snippet stickyBottom()}
<Button shape="round" color="secondary" fullWidth onclick={onCancel}>{cancelText}</Button>
{#if isEditing}
<Button shape="round" color="danger" fullWidth onclick={onDelete}>{$t('delete')}</Button>
{/if}
<Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="library-import-path-form"
>{submitText}</Button
>
{/snippet}
</FullScreenModal>
</ModalFooter>
</Modal>

View File

@ -1,7 +1,6 @@
<script lang="ts">
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import type { LibraryResponseDto } from '@immich/sdk';
import { Button, Field, Input } from '@immich/ui';
import { Button, Field, Input, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiRenameOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@ -21,15 +20,19 @@
};
</script>
<form {onsubmit} autocomplete="off">
<FullScreenModal icon={mdiRenameOutline} title={$t('rename')} onClose={onCancel}>
<Field label={$t('name')}>
<Input bind:value={newName} />
</Field>
<Modal icon={mdiRenameOutline} title={$t('rename')} onClose={onCancel} size="small">
<ModalBody>
<form {onsubmit} autocomplete="off" id="rename-library-form">
<Field label={$t('name')}>
<Input bind:value={newName} />
</Field>
</form>
</ModalBody>
{#snippet stickyBottom()}
<ModalFooter>
<div class="flex gap-2 w-full">
<Button shape="round" fullWidth color="secondary" onclick={onCancel}>{$t('cancel')}</Button>
<Button shape="round" fullWidth type="submit">{$t('save')}</Button>
{/snippet}
</FullScreenModal>
</form>
<Button shape="round" fullWidth type="submit" form="rename-library-form">{$t('save')}</Button>
</div>
</ModalFooter>
</Modal>

View File

@ -2,11 +2,10 @@
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import { user } from '$lib/stores/user.store';
import { searchUsersAdmin } from '@immich/sdk';
import { Button } from '@immich/ui';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiFolderSync } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
interface Props {
onCancel: () => void;
@ -30,15 +29,19 @@
};
</script>
<FullScreenModal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={onCancel}>
<form {onsubmit} autocomplete="off" id="select-library-owner-form">
<p class="p-5 text-sm">{$t('admin.note_cannot_be_changed_later')}</p>
<Modal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={onCancel} size="small">
<ModalBody>
<form {onsubmit} autocomplete="off" id="select-library-owner-form">
<p class="p-5 text-sm">{$t('admin.note_cannot_be_changed_later')}</p>
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
</form>
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
</form>
</ModalBody>
{#snippet stickyBottom()}
<Button shape="round" color="secondary" fullWidth onclick={onCancel}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth form="select-library-owner-form">{$t('create')}</Button>
{/snippet}
</FullScreenModal>
<ModalFooter>
<div class="flex gap-2 w-full">
<Button shape="round" color="secondary" fullWidth onclick={onCancel}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth form="select-library-owner-form">{$t('create')}</Button>
</div>
</ModalFooter>
</Modal>

View File

@ -1,13 +1,12 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import { getAllTags, upsertTags, type TagResponseDto } from '@immich/sdk';
import { Button } from '@immich/ui';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiClose, mdiTag } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { SvelteSet } from 'svelte/reactivity';
import Combobox, { type ComboBoxOption } from '../shared-components/combobox.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
interface Props {
onTag: (tagIds: string[]) => void;
@ -52,48 +51,52 @@
};
</script>
<FullScreenModal title={$t('tag_assets')} icon={mdiTag} onClose={onCancel}>
<form {onsubmit} autocomplete="off" id="create-tag-form">
<div class="my-4 flex flex-col gap-2">
<Combobox
onSelect={handleSelect}
label={$t('tag')}
{allowCreate}
defaultFirstOption
options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))}
placeholder={$t('search_tags')}
/>
<Modal size="small" title={$t('tag_assets')} icon={mdiTag} onClose={onCancel}>
<ModalBody>
<form {onsubmit} autocomplete="off" id="create-tag-form">
<div class="my-4 flex flex-col gap-2">
<Combobox
onSelect={handleSelect}
label={$t('tag')}
{allowCreate}
defaultFirstOption
options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))}
placeholder={$t('search_tags')}
/>
</div>
</form>
<section class="flex flex-wrap pt-2 gap-1">
{#each selectedIds as tagId (tagId)}
{@const tag = tagMap[tagId]}
{#if tag}
<div class="flex group transition-all">
<span
class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary roudned-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
>
<p class="text-sm">
{tag.value}
</p>
</span>
<button
type="button"
class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-e-full place-items-center place-content-center pe-2 ps-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
title="Remove tag"
onclick={() => handleRemove(tagId)}
>
<Icon path={mdiClose} />
</button>
</div>
{/if}
{/each}
</section>
</ModalBody>
<ModalFooter>
<div class="flex w-full gap-2">
<Button shape="round" fullWidth color="secondary" onclick={onCancel}>{$t('cancel')}</Button>
<Button type="submit" shape="round" fullWidth form="create-tag-form" {disabled}>{$t('tag_assets')}</Button>
</div>
</form>
<section class="flex flex-wrap pt-2 gap-1">
{#each selectedIds as tagId (tagId)}
{@const tag = tagMap[tagId]}
{#if tag}
<div class="flex group transition-all">
<span
class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary roudned-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
>
<p class="text-sm">
{tag.value}
</p>
</span>
<button
type="button"
class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-e-full place-items-center place-content-center pe-2 ps-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
title="Remove tag"
onclick={() => handleRemove(tagId)}
>
<Icon path={mdiClose} />
</button>
</div>
{/if}
{/each}
</section>
{#snippet stickyBottom()}
<Button shape="round" fullWidth color="secondary" onclick={onCancel}>{$t('cancel')}</Button>
<Button type="submit" shape="round" fullWidth form="create-tag-form" {disabled}>{$t('tag_assets')}</Button>
{/snippet}
</FullScreenModal>
</ModalFooter>
</Modal>

View File

@ -314,8 +314,9 @@
/>
{#if assetInteraction.selectionActive}
<div class="sticky top-0">
<div class="sticky top-0 z-1">
<AssetSelectControlBar
forceDark
assets={assetInteraction.selectedAssets}
clearSelect={() => cancelMultiselect(assetInteraction)}
>
@ -605,6 +606,7 @@
</section>
{/if}
</section>
{#if current}
<!-- GALLERY VIEWER -->
<section class="bg-immich-dark-gray p-4">

View File

@ -2,7 +2,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 { AssetVisibility } 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';
@ -24,12 +24,12 @@
const { clearSelect, getOwnedAssets } = getAssetControlContext();
const handleArchive = async () => {
const isArchived = unarchive ? Visibility.Timeline : Visibility.Archive;
const isArchived = unarchive ? AssetVisibility.Timeline : AssetVisibility.Archive;
const assets = [...getOwnedAssets()].filter((asset) => asset.visibility !== isArchived);
loading = true;
const ids = await archiveAssets(assets, isArchived as unknown as AssetVisibility);
const ids = await archiveAssets(assets, isArchived as AssetVisibility);
if (ids) {
onArchive?.(ids, isArchived);
onArchive?.(ids, isArchived ? AssetVisibility.Archive : AssetVisibility.Timeline);
clearSelect();
}
loading = false;

View File

@ -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 @@
});
</script>
<ControlAppBar onClose={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
<ControlAppBar onClose={clearSelect} {forceDark} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
{#snippet leading()}
<div class="font-medium text-immich-primary dark:text-immich-dark-primary">
<div
class="font-medium {forceDark ? 'text-immich-dark-primary' : 'text-immich-primary dark:text-immich-dark-primary'}"
>
<p class="block sm:hidden">{assets.length}</p>
<p class="hidden sm:block">{$t('selected_count', { values: { count: assets.length } })}</p>
</div>

View File

@ -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);
</script>
<FullScreenModal title={shared ? $t('add_to_shared_album') : $t('add_to_album')} {onClose}>
<div class="mb-2 flex max-h-[400px] flex-col">
{#if loading}
<!-- eslint-disable-next-line svelte/require-each-key -->
{#each { length: 3 } as _}
<div class="flex animate-pulse gap-4 px-6 py-2">
<div class="h-12 w-12 rounded-xl bg-slate-200"></div>
<div class="flex flex-col items-start justify-center gap-2">
<span class="h-4 w-36 animate-pulse bg-slate-200"></span>
<div class="flex animate-pulse gap-1">
<span class="h-3 w-8 bg-slate-200"></span>
<span class="h-3 w-20 bg-slate-200"></span>
<Modal title={shared ? $t('add_to_shared_album') : $t('add_to_album')} {onClose} size="small">
<ModalBody>
<div class="mb-2 flex max-h-[400px] flex-col">
{#if loading}
<!-- eslint-disable-next-line svelte/require-each-key -->
{#each { length: 3 } as _}
<div class="flex animate-pulse gap-4 px-6 py-2">
<div class="h-12 w-12 rounded-xl bg-slate-200"></div>
<div class="flex flex-col items-start justify-center gap-2">
<span class="h-4 w-36 animate-pulse bg-slate-200"></span>
<div class="flex animate-pulse gap-1">
<span class="h-3 w-8 bg-slate-200"></span>
<span class="h-3 w-20 bg-slate-200"></span>
</div>
</div>
</div>
</div>
{/each}
{:else}
<input
class="border-b-4 border-immich-bg px-6 py-2 text-2xl focus:border-immich-primary dark:border-immich-dark-gray dark:focus:border-immich-dark-primary"
placeholder={$t('search')}
{onkeydown}
bind:value={search}
use:initInput
/>
<div class="immich-scrollbar overflow-y-auto">
<!-- eslint-disable-next-line svelte/require-each-key -->
{#each albumModalRows as row}
{#if row.type === AlbumModalRowType.NEW_ALBUM}
<NewAlbumListItem selected={row.selected || false} {onNewAlbum} searchQuery={search} />
{:else if row.type === AlbumModalRowType.SECTION}
<p class="px-5 py-3 text-xs">{row.text}</p>
{:else if row.type === AlbumModalRowType.MESSAGE}
<p class="px-5 py-1 text-sm">{row.text}</p>
{:else if row.type === AlbumModalRowType.ALBUM_ITEM && row.album}
<AlbumListItem
album={row.album}
selected={row.selected || false}
searchQuery={search}
onAlbumClick={handleAlbumClick(row.album)}
/>
{/if}
{/each}
</div>
{/if}
</div>
</FullScreenModal>
{:else}
<input
class="border-b-4 border-immich-bg px-6 py-2 text-2xl focus:border-immich-primary dark:border-immich-dark-gray dark:focus:border-immich-dark-primary"
placeholder={$t('search')}
{onkeydown}
bind:value={search}
use:initInput
/>
<div class="immich-scrollbar overflow-y-auto">
<!-- eslint-disable-next-line svelte/require-each-key -->
{#each albumModalRows as row}
{#if row.type === AlbumModalRowType.NEW_ALBUM}
<NewAlbumListItem selected={row.selected || false} {onNewAlbum} searchQuery={search} />
{:else if row.type === AlbumModalRowType.SECTION}
<p class="px-5 py-3 text-xs">{row.text}</p>
{:else if row.type === AlbumModalRowType.MESSAGE}
<p class="px-5 py-1 text-sm">{row.text}</p>
{:else if row.type === AlbumModalRowType.ALBUM_ITEM && row.album}
<AlbumListItem
album={row.album}
selected={row.selected || false}
searchQuery={search}
onAlbumClick={handleAlbumClick(row.album)}
/>
{/if}
{/each}
</div>
{/if}
</div>
</ModalBody>
</Modal>

View File

@ -38,6 +38,10 @@
buttonClass?: string | undefined;
hideContent?: boolean;
children?: Snippet;
offset?: {
x: number;
y: number;
};
} & HTMLAttributes<HTMLDivElement>;
let {
@ -51,6 +55,7 @@
buttonClass = undefined,
hideContent = false,
children,
offset,
...restProps
}: Props = $props();
@ -186,13 +191,14 @@
]}
>
<ContextMenu
{...contextMenuPosition}
{direction}
ariaActiveDescendant={$selectedIdStore}
ariaLabelledBy={buttonId}
bind:menuElement={menuContainer}
id={menuId}
isVisible={isOpen}
x={contextMenuPosition.x - (offset?.x ?? 0)}
y={contextMenuPosition.y + (offset?.y ?? 0)}
>
{@render children?.()}
</ContextMenu>

View File

@ -66,7 +66,7 @@
let buttonClass = $derived(forceDark ? 'hover:text-immich-dark-gray' : undefined);
</script>
<div in:fly={{ y: 10, duration: 200 }} class="absolute top-0 w-full bg-transparent z-1">
<div in:fly={{ y: 10, duration: 200 }} class="absolute top-0 w-full bg-transparent">
<nav
id="asset-selection-app-bar"
class={[
@ -77,7 +77,7 @@
appBarBorder,
'mx-2 my-2 place-items-center rounded-lg p-2 max-md:p-0 transition-all',
tailwindClasses,
forceDark ? 'bg-immich-dark-gray text-white' : 'bg-subtle dark:bg-immich-dark-gray',
forceDark ? 'bg-immich-dark-gray! text-white' : 'bg-subtle dark:bg-immich-dark-gray',
]}
>
<div class="flex place-items-center sm:gap-6 justify-self-start dark:text-immich-dark-fg">

View File

@ -29,5 +29,5 @@
{#if title}
<h2 class="text-xl font-medium my-4">{title}</h2>
{/if}
<p class="text-immich-text-gray-500 dark:text-immich-dark-fg font-light">{text}</p>
<p class="text-immich-text-gray-500 dark:text-immich-dark-fg font-light text-center">{text}</p>
</svelte:element>

View File

@ -1,107 +0,0 @@
<script lang="ts">
import { clickOutside } from '$lib/actions/click-outside';
import { focusTrap } from '$lib/actions/focus-trap';
import ModalHeader from '$lib/components/shared-components/modal-header.svelte';
import { generateId } from '$lib/utils/generate-id';
import type { Snippet } from 'svelte';
import { fade } from 'svelte/transition';
interface Props {
onClose: () => void;
title: string;
/**
* If true, the logo will be displayed next to the modal title.
*/
showLogo?: boolean;
/**
* Optional icon to display next to the modal title, if `showLogo` is false.
*/
icon?: string | undefined;
/**
* Sets the width of the modal.
*
* - `wide`: 48rem
* - `narrow`: 28rem
* - `auto`: fits the width of the modal content, up to a maximum of 32rem
*/
width?: 'extra-wide' | 'wide' | 'narrow' | 'auto';
stickyBottom?: Snippet;
children?: Snippet;
}
let {
onClose,
title,
showLogo = false,
icon = undefined,
width = 'narrow',
stickyBottom,
children,
}: Props = $props();
/**
* Unique identifier for the modal.
*/
let id: string = generateId();
let titleId = $derived(`${id}-title`);
let isStickyBottom = $derived(!!stickyBottom);
let modalWidth = $state<string>();
$effect(() => {
switch (width) {
case 'extra-wide': {
modalWidth = 'w-4xl';
break;
}
case 'wide': {
modalWidth = 'w-3xl';
break;
}
case 'narrow': {
modalWidth = 'w-md';
break;
}
default: {
modalWidth = 'sm:max-w-4xl';
}
}
});
</script>
<section
role="presentation"
in:fade={{ duration: 100 }}
out:fade={{ duration: 100 }}
class="fixed start-0 top-0 flex h-dvh w-dvw place-content-center place-items-center bg-black/40"
onkeydown={(event) => {
event.stopPropagation();
}}
use:focusTrap
>
<div
class="flex flex-col max-h-[min(95dvh,60rem)] max-w-[95vw] {modalWidth} overflow-hidden rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg pt-3 pb-4"
use:clickOutside={{ onOutclick: onClose, onEscape: onClose }}
tabindex="-1"
aria-modal="true"
aria-labelledby={titleId}
>
<div class="immich-scrollbar overflow-y-auto pt-1" class:pb-4={isStickyBottom}>
<ModalHeader id={titleId} {title} {showLogo} {icon} {onClose} />
<div class="px-5 pt-0 mb-5">
{@render children?.()}
</div>
</div>
{#if isStickyBottom}
<div
class="flex flex-col sm:flex-row justify-end w-full gap-2 sm:gap-4 sticky pt-4 px-5 bg-immich-bg dark:bg-immich-dark-gray border-t border-gray-200 dark:border-gray-500"
>
{@render stickyBottom?.()}
</div>
{/if}
</div>
</section>

View File

@ -14,7 +14,7 @@
<svg {viewBox} class={cssClass}>
<title>{$t('immich_logo')}</title>
{#if !noText}
<g class="st0 dark:fill-[#accbfa]">
<g class="st0 dark:fill-[#accbfa] fill-[#4251b0]">
<path
d="M268.73,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
C257.04,68.36,262.39,63.18,268.73,63.18z M258.88,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
@ -94,9 +94,6 @@
</svg>
<style>
.st0 {
fill: #4251b0;
}
.st1 {
fill: #fa2921;
}

View File

@ -39,7 +39,7 @@
in:fade={{ duration: 100 }}
out:fade={{ duration: 100 }}
id="notification-panel"
class="absolute right-[25px] top-[70px] z-1 w-[min(360px,100vw-50px)] rounded-3xl bg-gray-100 border border-gray-200 shadow-lg dark:border dark:border-immich-dark-gray dark:bg-immich-dark-gray text-light"
class="absolute right-[25px] top-[70px] z-1 w-[min(360px,100vw-50px)] rounded-3xl bg-gray-100 border border-gray-200 shadow-lg dark:border dark:border-light dark:bg-immich-dark-gray text-light px-2"
use:focusTrap
>
<Stack class="max-h-[500px]">

View File

@ -1,9 +1,8 @@
<script lang="ts">
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { user } from '$lib/stores/user.store';
import { handleError } from '$lib/utils/handle-error';
import { createProfileImage, type AssetResponseDto } from '@immich/sdk';
import { Button } from '@immich/ui';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import domtoimage from 'dom-to-image';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@ -89,16 +88,17 @@
};
</script>
<FullScreenModal title={$t('set_profile_picture')} width="auto" {onClose}>
<div class="flex place-items-center items-center justify-center">
<div
class="relative flex aspect-square w-[250px] overflow-hidden rounded-full border-4 border-immich-primary bg-immich-dark-primary dark:border-immich-dark-primary dark:bg-immich-primary"
>
<PhotoViewer bind:element={imgElement} {asset} />
<Modal size="small" title={$t('set_profile_picture')} {onClose}>
<ModalBody>
<div class="flex place-items-center items-center justify-center">
<div
class="relative flex aspect-square w-[250px] overflow-hidden rounded-full border-4 border-immich-primary bg-immich-dark-primary dark:border-immich-dark-primary dark:bg-immich-primary"
>
<PhotoViewer bind:element={imgElement} {asset} />
</div>
</div>
</div>
{#snippet stickyBottom()}
</ModalBody>
<ModalFooter>
<Button fullWidth shape="round" onclick={handleSetProfilePicture}>{$t('set_as_profile_picture')}</Button>
{/snippet}
</FullScreenModal>
</ModalFooter>
</Modal>

View File

@ -464,7 +464,7 @@
class={[
{ 'border-b-2': isDragging },
{ 'rounded-bl-md': !isDragging },
'bg-light truncate opacity-85 pointer-events-none absolute end-0 min-w-20 max-w-64 w-fit rounded-ss-md border-immich-primary py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg',
'bg-light truncate opacity-85 pointer-events-none absolute end-0 min-w-20 max-w-64 w-fit rounded-ss-md border-immich-primary py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg z-1',
]}
style:top="{hoverY + 2}px"
>
@ -506,7 +506,7 @@
{#if assetStore.scrolling && scrollHoverLabel && !isHover}
<p
transition:fade={{ duration: 200 }}
class="truncate pointer-events-none absolute end-0 bottom-0 min-w-20 max-w-64 w-fit rounded-tl-md border-b-2 border-immich-primary bg-subtle/70 py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg"
class="truncate pointer-events-none absolute end-0 bottom-0 min-w-20 max-w-64 w-fit rounded-tl-md border-b-2 border-immich-primary bg-subtle/90 z-1 py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg"
>
{scrollHoverLabel}
</p>

View File

@ -64,8 +64,8 @@
</script>
<div
class="border rounded-2xl my-4 px-6 py-4 transition-all {isOpen
? 'border-primary/40 dark:border-primary/50 shadow-md'
class="border-2 rounded-2xl border-primary/20 my-4 px-6 py-4 transition-all {isOpen
? 'border-primary/60 shadow-md'
: ''}"
bind:this={accordionElement}
>

View File

@ -19,6 +19,8 @@
</div>
<style lang="postcss">
@reference "tailwindcss";
.supporter-effect::after {
@apply absolute inset-0 rounded-lg opacity-0 transition-opacity content-[''];
}

View File

@ -2,9 +2,8 @@
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import { websocketStore } from '$lib/stores/websocket';
import type { ServerVersionResponseDto } from '@immich/sdk';
import { Button } from '@immich/ui';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { t } from 'svelte-i18n';
import FullScreenModal from './full-screen-modal.svelte';
let showModal = $state(false);
@ -39,33 +38,38 @@
</script>
{#if showModal}
<FullScreenModal title="🎉 {$t('new_version_available')}" onClose={() => (showModal = false)}>
<div>
<FormatMessage key="version_announcement_message">
{#snippet children({ tag, message })}
{#if tag === 'link'}
<span class="font-medium underline">
<a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer">
{message}
</a>
</span>
{:else if tag === 'code'}
<code>{message}</code>
{/if}
{/snippet}
</FormatMessage>
</div>
<Modal size="small" title="🎉 {$t('new_version_available')}" onClose={() => (showModal = false)} icon={false}>
<ModalBody>
<div>
<FormatMessage key="version_announcement_message">
{#snippet children({ tag, message })}
{#if tag === 'link'}
<span class="font-medium underline">
<a
href="https://github.com/immich-app/immich/releases/latest"
target="_blank"
rel="noopener noreferrer"
>
{message}
</a>
</span>
{:else if tag === 'code'}
<code>{message}</code>
{/if}
{/snippet}
</FormatMessage>
</div>
<div class="mt-4 font-medium">{$t('version_announcement_closing')}</div>
<div class="mt-4 font-medium">{$t('version_announcement_closing')}</div>
<div class="font-sm mt-8">
<code>{$t('server_version')}: {serverVersion}</code>
<br />
<code>{$t('latest_version')}: {releaseVersion}</code>
</div>
{#snippet stickyBottom()}
<div class="font-sm mt-8">
<code>{$t('server_version')}: {serverVersion}</code>
<br />
<code>{$t('latest_version')}: {releaseVersion}</code>
</div>
</ModalBody>
<ModalFooter>
<Button fullWidth shape="round" onclick={onAcknowledge}>{$t('acknowledge')}</Button>
{/snippet}
</FullScreenModal>
</ModalFooter>
</Modal>
{/if}

View File

@ -1,9 +1,8 @@
<script lang="ts">
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { SettingInputFieldType } from '$lib/constants';
import { Button } from '@immich/ui';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import {
mdiArrowDownThin,
mdiArrowUpThin,
@ -65,37 +64,40 @@
};
</script>
<FullScreenModal title={$t('slideshow_settings')} onClose={() => onClose()}>
<div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary">
<SettingDropdown
title={$t('direction')}
options={Object.values(navigationOptions)}
selectedOption={navigationOptions[tempSlideshowNavigation]}
onToggle={(option) => {
tempSlideshowNavigation = handleToggle(option, navigationOptions) || tempSlideshowNavigation;
}}
/>
<SettingDropdown
title={$t('look')}
options={Object.values(lookOptions)}
selectedOption={lookOptions[tempSlideshowLook]}
onToggle={(option) => {
tempSlideshowLook = handleToggle(option, lookOptions) || tempSlideshowLook;
}}
/>
<SettingSwitch title={$t('show_progress_bar')} bind:checked={tempShowProgressBar} />
<SettingSwitch title={$t('show_slideshow_transition')} bind:checked={tempSlideshowTransition} />
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('duration')}
description={$t('admin.slideshow_duration_description')}
min={1}
bind:value={tempSlideshowDelay}
/>
</div>
{#snippet stickyBottom()}
<Button color="secondary" shape="round" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button fullWidth color="primary" shape="round" onclick={applyChanges}>{$t('confirm')}</Button>
{/snippet}
</FullScreenModal>
<Modal size="small" title={$t('slideshow_settings')} onClose={() => onClose()}>
<ModalBody>
<div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary">
<SettingDropdown
title={$t('direction')}
options={Object.values(navigationOptions)}
selectedOption={navigationOptions[tempSlideshowNavigation]}
onToggle={(option) => {
tempSlideshowNavigation = handleToggle(option, navigationOptions) || tempSlideshowNavigation;
}}
/>
<SettingDropdown
title={$t('look')}
options={Object.values(lookOptions)}
selectedOption={lookOptions[tempSlideshowLook]}
onToggle={(option) => {
tempSlideshowLook = handleToggle(option, lookOptions) || tempSlideshowLook;
}}
/>
<SettingSwitch title={$t('show_progress_bar')} bind:checked={tempShowProgressBar} />
<SettingSwitch title={$t('show_slideshow_transition')} bind:checked={tempSlideshowTransition} />
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('duration')}
description={$t('admin.slideshow_duration_description')}
min={1}
bind:value={tempSlideshowDelay}
/>
</div>
</ModalBody>
<ModalFooter>
<div class="flex gap-2 w-full">
<Button color="secondary" shape="round" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button fullWidth color="primary" shape="round" onclick={applyChanges}>{$t('confirm')}</Button>
</div>
</ModalFooter>
</Modal>

View File

@ -126,7 +126,7 @@
maxlength="1"
bind:this={pinCodeInputElements[index]}
id="pin-code-{index}"
class="h-12 w-10 rounded-xl border-2 border-suble dark:border-gray-700 bg-transparent text-center text-lg font-medium focus:border-immich-primary focus:ring-primary dark:focus:border-primary font-mono bg-white dark:bg-light"
class="h-12 w-10 rounded-xl border-2 border-suble dark:border-gray-700 text-center text-lg font-medium focus:border-immich-primary focus:ring-primary dark:focus:border-primary font-mono bg-white dark:bg-light"
bind:value={pinValues[index]}
onkeydown={handleKeydown}
oninput={(event) => handleInput(event, index)}

View File

@ -2,7 +2,6 @@
import AlbumSharedLink from '$lib/components/album-page/album-shared-link.svelte';
import Dropdown from '$lib/components/elements/dropdown.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { AppRoute } from '$lib/constants';
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
import { makeSharedLinkUrl } from '$lib/utils';
@ -15,7 +14,7 @@
type SharedLinkResponseDto,
type UserResponseDto,
} from '@immich/sdk';
import { Button, Link, Stack, Text } from '@immich/ui';
import { Button, Link, Modal, ModalBody, Stack, Text } from '@immich/ui';
import { mdiCheck, mdiEye, mdiLink, mdiPencil } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@ -76,63 +75,22 @@
{#if sharedLinkUrl}
<QrCodeModal title={$t('view_link')} onClose={() => (sharedLinkUrl = '')} value={sharedLinkUrl} />
{:else}
<FullScreenModal title={$t('share')} showLogo {onClose}>
{#if Object.keys(selectedUsers).length > 0}
<div class="mb-2 py-2 sticky">
<p class="text-xs font-medium">{$t('selected')}</p>
<div class="my-2">
{#each Object.values(selectedUsers) as { user } (user.id)}
{#key user.id}
<div class="flex place-items-center gap-4 p-4">
<div
class="flex h-10 w-10 items-center justify-center rounded-full border bg-immich-dark-success text-3xl text-white dark:border-immich-dark-gray dark:bg-immich-dark-success"
>
<Icon path={mdiCheck} size={24} />
</div>
<Modal size="small" title={$t('share')} {onClose}>
<ModalBody>
{#if Object.keys(selectedUsers).length > 0}
<div class="mb-2 py-2 sticky">
<p class="text-xs font-medium">{$t('selected')}</p>
<div class="my-2">
{#each Object.values(selectedUsers) as { user } (user.id)}
{#key user.id}
<div class="flex place-items-center gap-4 p-4">
<div
class="flex h-10 w-10 items-center justify-center rounded-full border bg-immich-dark-success text-3xl text-white dark:border-immich-dark-gray dark:bg-immich-dark-success"
>
<Icon path={mdiCheck} size={24} />
</div>
<!-- <UserAvatar {user} size="md" /> -->
<div class="text-start grow">
<p class="text-immich-fg dark:text-immich-dark-fg">
{user.name}
</p>
<p class="text-xs">
{user.email}
</p>
</div>
<Dropdown
title={$t('role')}
options={roleOptions}
render={({ title, icon }) => ({ title, icon })}
onSelect={({ value }) => handleChangeRole(user, value)}
/>
</div>
{/key}
{/each}
</div>
</div>
{/if}
{#if users.length + Object.keys(selectedUsers).length === 0}
<p class="p-5 text-sm">
{$t('album_share_no_users')}
</p>
{/if}
<div class="immich-scrollbar max-h-[500px] overflow-y-auto">
{#if users.length > 0 && users.length !== Object.keys(selectedUsers).length}
<Text>{$t('users')}</Text>
<div class="my-2">
{#each users as user (user.id)}
{#if !Object.keys(selectedUsers).includes(user.id)}
<div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl">
<button
type="button"
onclick={() => handleToggle(user)}
class="flex w-full place-items-center gap-4 p-4"
>
<UserAvatar {user} size="md" />
<!-- <UserAvatar {user} size="md" /> -->
<div class="text-start grow">
<p class="text-immich-fg dark:text-immich-dark-fg">
{user.name}
@ -141,53 +99,96 @@
{user.email}
</p>
</div>
</button>
</div>
{/if}
{/each}
<Dropdown
title={$t('role')}
options={roleOptions}
render={({ title, icon }) => ({ title, icon })}
onSelect={({ value }) => handleChangeRole(user, value)}
/>
</div>
{/key}
{/each}
</div>
</div>
{/if}
</div>
{#if users.length > 0}
<div class="py-3">
<Button
size="small"
fullWidth
shape="round"
disabled={Object.keys(selectedUsers).length === 0}
onclick={() =>
onClose({
action: 'sharedUsers',
data: Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })),
})}>{$t('add')}</Button
>
{#if users.length + Object.keys(selectedUsers).length === 0}
<p class="p-5 text-sm">
{$t('album_share_no_users')}
</p>
{/if}
<div class="immich-scrollbar max-h-[500px] overflow-y-auto">
{#if users.length > 0 && users.length !== Object.keys(selectedUsers).length}
<Text>{$t('users')}</Text>
<div class="my-2">
{#each users as user (user.id)}
{#if !Object.keys(selectedUsers).includes(user.id)}
<div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl">
<button
type="button"
onclick={() => handleToggle(user)}
class="flex w-full place-items-center gap-4 p-4"
>
<UserAvatar {user} size="md" />
<div class="text-start grow">
<p class="text-immich-fg dark:text-immich-dark-fg">
{user.name}
</p>
<p class="text-xs">
{user.email}
</p>
</div>
</button>
</div>
{/if}
{/each}
</div>
{/if}
</div>
{/if}
<hr class="my-4" />
<Stack gap={6}>
{#if sharedLinks.length > 0}
<div class="flex justify-between items-center">
<Text>{$t('shared_links')}</Text>
<Link href={AppRoute.SHARED_LINKS} class="text-sm">{$t('view_all')}</Link>
{#if users.length > 0}
<div class="py-3">
<Button
size="small"
fullWidth
shape="round"
disabled={Object.keys(selectedUsers).length === 0}
onclick={() =>
onClose({
action: 'sharedUsers',
data: Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })),
})}>{$t('add')}</Button
>
</div>
<Stack gap={4}>
{#each sharedLinks as sharedLink (sharedLink.id)}
<AlbumSharedLink {album} {sharedLink} onViewQrCode={() => handleViewQrCode(sharedLink)} />
{/each}
</Stack>
{/if}
<Button
leadingIcon={mdiLink}
size="small"
shape="round"
fullWidth
onclick={() => onClose({ action: 'sharedLink' })}>{$t('create_link')}</Button
>
</Stack>
</FullScreenModal>
<hr class="my-4" />
<Stack gap={6}>
{#if sharedLinks.length > 0}
<div class="flex justify-between items-center">
<Text>{$t('shared_links')}</Text>
<Link href={AppRoute.SHARED_LINKS} class="text-sm">{$t('view_all')}</Link>
</div>
<Stack gap={4}>
{#each sharedLinks as sharedLink (sharedLink.id)}
<AlbumSharedLink {album} {sharedLink} onViewQrCode={() => handleViewQrCode(sharedLink)} />
{/each}
</Stack>
{/if}
<Button
leadingIcon={mdiLink}
size="small"
shape="round"
fullWidth
onclick={() => onClose({ action: 'sharedLink' })}>{$t('create_link')}</Button
>
</Stack>
</ModalBody>
</Modal>
{/if}

View File

@ -1,6 +1,6 @@
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { resetSavedUser, user } from '$lib/stores/user.store';
import { Visibility } from '@immich/sdk';
import { AssetVisibility } from '@immich/sdk';
import { timelineAssetFactory } from '@test-data/factories/asset-factory';
import { userAdminFactory } from '@test-data/factories/user-factory';
@ -13,10 +13,10 @@ describe('AssetInteraction', () => {
it('calculates derived values from selection', () => {
assetInteraction.selectAsset(
timelineAssetFactory.build({ isFavorite: true, visibility: Visibility.Archive, isTrashed: true }),
timelineAssetFactory.build({ isFavorite: true, visibility: AssetVisibility.Archive, isTrashed: true }),
);
assetInteraction.selectAsset(
timelineAssetFactory.build({ isFavorite: true, visibility: Visibility.Timeline, isTrashed: false }),
timelineAssetFactory.build({ isFavorite: true, visibility: AssetVisibility.Timeline, isTrashed: false }),
);
expect(assetInteraction.selectionActive).toBe(true);

View File

@ -1,6 +1,6 @@
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
import { user } from '$lib/stores/user.store';
import { Visibility, type UserAdminResponseDto } from '@immich/sdk';
import { AssetVisibility, type UserAdminResponseDto } from '@immich/sdk';
import { SvelteSet } from 'svelte/reactivity';
import { fromStore } from 'svelte/store';
@ -21,7 +21,7 @@ export class AssetInteraction {
private userId = $derived(this.user.current?.id);
isAllTrashed = $derived(this.selectedAssets.every((asset) => asset.isTrashed));
isAllArchived = $derived(this.selectedAssets.every((asset) => asset.visibility === Visibility.Archive));
isAllArchived = $derived(this.selectedAssets.every((asset) => asset.visibility === AssetVisibility.Archive));
isAllFavorite = $derived(this.selectedAssets.every((asset) => asset.isFavorite));
isAllUserOwned = $derived(this.selectedAssets.every((asset) => asset.ownerId === this.userId));

View File

@ -1,8 +1,8 @@
import { sdkMock } from '$lib/__mocks__/sdk.mock';
import { AbortError } from '$lib/utils';
import { TimeBucketSize, type AssetResponseDto } from '@immich/sdk';
import { assetFactory, timelineAssetFactory } from '@test-data/factories/asset-factory';
import { AssetStore } from './assets-store.svelte';
import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory';
import { AssetStore, type TimelineAsset } from './assets-store.svelte';
describe('AssetStore', () => {
beforeEach(() => {
@ -11,18 +11,22 @@ describe('AssetStore', () => {
describe('init', () => {
let assetStore: AssetStore;
const bucketAssets: Record<string, AssetResponseDto[]> = {
'2024-03-01T00:00:00.000Z': assetFactory
const bucketAssets: Record<string, TimelineAsset[]> = {
'2024-03-01T00:00:00.000Z': timelineAssetFactory
.buildList(1)
.map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })),
'2024-02-01T00:00:00.000Z': assetFactory
'2024-02-01T00:00:00.000Z': timelineAssetFactory
.buildList(100)
.map((asset) => ({ ...asset, localDateTime: '2024-02-01T00:00:00.000Z' })),
'2024-01-01T00:00:00.000Z': assetFactory
'2024-01-01T00:00:00.000Z': timelineAssetFactory
.buildList(3)
.map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })),
};
const bucketAssetsResponse: Record<string, TimeBucketAssetResponseDto> = Object.fromEntries(
Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]),
);
beforeEach(async () => {
assetStore = new AssetStore();
sdkMock.getTimeBuckets.mockResolvedValue([
@ -30,13 +34,14 @@ describe('AssetStore', () => {
{ count: 100, timeBucket: '2024-02-01T00:00:00.000Z' },
{ count: 3, timeBucket: '2024-01-01T00:00:00.000Z' },
]);
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket]));
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssetsResponse[timeBucket]));
await assetStore.updateViewport({ width: 1588, height: 1000 });
});
it('should load buckets in viewport', () => {
expect(sdkMock.getTimeBuckets).toBeCalledTimes(1);
expect(sdkMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.Month });
expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(2);
});
@ -48,29 +53,31 @@ describe('AssetStore', () => {
expect(plainBuckets).toEqual(
expect.arrayContaining([
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-03-01T00:00:00.000Z', bucketHeight: 185.5 }),
expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 12_016 }),
expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 286 }),
]),
);
});
it('calculates timeline height', () => {
expect(assetStore.timelineHeight).toBe(5103.333_333_333_333);
expect(assetStore.timelineHeight).toBe(12_487.5);
});
});
describe('loadBucket', () => {
let assetStore: AssetStore;
const bucketAssets: Record<string, AssetResponseDto[]> = {
'2024-01-03T00:00:00.000Z': assetFactory
const bucketAssets: Record<string, TimelineAsset[]> = {
'2024-01-03T00:00:00.000Z': timelineAssetFactory
.buildList(1)
.map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })),
'2024-01-01T00:00:00.000Z': assetFactory
'2024-01-01T00:00:00.000Z': timelineAssetFactory
.buildList(3)
.map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })),
};
const bucketAssetsResponse: Record<string, TimeBucketAssetResponseDto> = Object.fromEntries(
Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]),
);
beforeEach(async () => {
assetStore = new AssetStore();
sdkMock.getTimeBuckets.mockResolvedValue([
@ -82,7 +89,7 @@ describe('AssetStore', () => {
if (signal?.aborted) {
throw new AbortError();
}
return bucketAssets[timeBucket];
return bucketAssetsResponse[timeBucket];
});
await assetStore.updateViewport({ width: 1588, height: 0 });
});
@ -296,7 +303,9 @@ describe('AssetStore', () => {
});
it('removes asset from bucket', () => {
const [assetOne, assetTwo] = timelineAssetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' });
const [assetOne, assetTwo] = timelineAssetFactory.buildList(2, {
localDateTime: '2024-01-20T12:00:00.000Z',
});
assetStore.addAssets([assetOne, assetTwo]);
assetStore.removeAssets([assetOne.id]);
@ -342,17 +351,20 @@ describe('AssetStore', () => {
describe('getLaterAsset', () => {
let assetStore: AssetStore;
const bucketAssets: Record<string, AssetResponseDto[]> = {
'2024-03-01T00:00:00.000Z': assetFactory
const bucketAssets: Record<string, TimelineAsset[]> = {
'2024-03-01T00:00:00.000Z': timelineAssetFactory
.buildList(1)
.map((asset) => ({ ...asset, localDateTime: '2024-03-01T00:00:00.000Z' })),
'2024-02-01T00:00:00.000Z': assetFactory
'2024-02-01T00:00:00.000Z': timelineAssetFactory
.buildList(6)
.map((asset) => ({ ...asset, localDateTime: '2024-02-01T00:00:00.000Z' })),
'2024-01-01T00:00:00.000Z': assetFactory
'2024-01-01T00:00:00.000Z': timelineAssetFactory
.buildList(3)
.map((asset) => ({ ...asset, localDateTime: '2024-01-01T00:00:00.000Z' })),
};
const bucketAssetsResponse: Record<string, TimeBucketAssetResponseDto> = Object.fromEntries(
Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]),
);
beforeEach(async () => {
assetStore = new AssetStore();
@ -361,8 +373,7 @@ describe('AssetStore', () => {
{ count: 6, timeBucket: '2024-02-01T00:00:00.000Z' },
{ count: 3, timeBucket: '2024-01-01T00:00:00.000Z' },
]);
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket]));
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssetsResponse[timeBucket]));
await assetStore.updateViewport({ width: 1588, height: 1000 });
});

View File

@ -1,4 +1,5 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import { locale } from '$lib/stores/preferences.store';
import { CancellableTask } from '$lib/utils/cancellable-task';
import {
@ -15,10 +16,8 @@ import {
getAssetInfo,
getTimeBucket,
getTimeBuckets,
TimeBucketSize,
Visibility,
type AssetResponseDto,
type AssetStackResponseDto,
type TimeBucketAssetResponseDto,
} from '@immich/sdk';
import { clamp, debounce, isEqual, throttle } from 'lodash-es';
import { DateTime } from 'luxon';
@ -32,6 +31,7 @@ const {
} = TUNABLES;
type AssetApiGetTimeBucketsRequest = Parameters<typeof getTimeBuckets>[0];
export type AssetStoreOptions = Omit<AssetApiGetTimeBucketsRequest, 'size'> & {
timelineAlbumId?: string;
deferInit?: boolean;
@ -77,7 +77,7 @@ export type TimelineAsset = {
ratio: number;
thumbhash: string | null;
localDateTime: string;
visibility: Visibility;
visibility: AssetVisibility;
isFavorite: boolean;
isTrashed: boolean;
isVideo: boolean;
@ -86,12 +86,11 @@ export type TimelineAsset = {
duration: string | null;
projectionType: string | null;
livePhotoVideoId: string | null;
text: {
city: string | null;
country: string | null;
people: string[];
};
city: string | null;
country: string | null;
people: string[];
};
class IntersectingAsset {
// --- public ---
readonly #group: AssetDateGroup;
@ -123,9 +122,11 @@ class IntersectingAsset {
this.asset = asset;
}
}
type AssetOperation = (asset: TimelineAsset) => { remove: boolean };
type MoveAsset = { asset: TimelineAsset; year: number; month: number };
export class AssetDateGroup {
// --- public
readonly bucket: AssetBucket;
@ -168,6 +169,7 @@ export class AssetDateGroup {
getFirstAsset() {
return this.intersetingAssets[0]?.asset;
}
getRandomAsset() {
const random = Math.floor(Math.random() * this.intersetingAssets.length);
return this.intersetingAssets[random];
@ -245,6 +247,7 @@ export interface Viewport {
width: number;
height: number;
}
export type ViewportXY = Viewport & {
x: number;
y: number;
@ -252,11 +255,46 @@ export type ViewportXY = Viewport & {
class AddContext {
lookupCache: {
[dayOfMonth: number]: AssetDateGroup;
[year: number]: { [month: number]: { [day: number]: AssetDateGroup } };
} = {};
unprocessedAssets: TimelineAsset[] = [];
changedDateGroups = new Set<AssetDateGroup>();
newDateGroups = new Set<AssetDateGroup>();
getDateGroup(year: number, month: number, day: number): AssetDateGroup | undefined {
return this.lookupCache[year]?.[month]?.[day];
}
setDateGroup(dateGroup: AssetDateGroup, year: number, month: number, day: number) {
if (!this.lookupCache[year]) {
this.lookupCache[year] = {};
}
if (!this.lookupCache[year][month]) {
this.lookupCache[year][month] = {};
}
this.lookupCache[year][month][day] = dateGroup;
}
get existingDateGroups() {
return this.changedDateGroups.difference(this.newDateGroups);
}
get updatedBuckets() {
const updated = new Set<AssetBucket>();
for (const group of this.changedDateGroups) {
updated.add(group.bucket);
}
return updated;
}
get bucketsWithNewDateGroups() {
const updated = new Set<AssetBucket>();
for (const group of this.newDateGroups) {
updated.add(group.bucket);
}
return updated;
}
sort(bucket: AssetBucket, sortOrder: AssetOrder = AssetOrder.Desc) {
for (const group of this.changedDateGroups) {
group.sortAssets(sortOrder);
@ -269,6 +307,7 @@ class AddContext {
}
}
}
export class AssetBucket {
// --- public ---
#intersecting: boolean = $state(false);
@ -333,6 +372,7 @@ export class AssetBucket {
this.handleLoadError,
);
}
set intersecting(newValue: boolean) {
const old = this.#intersecting;
if (old !== newValue) {
@ -418,52 +458,74 @@ export class AssetBucket {
};
}
// note - if the assets are not part of this bucket, they will not be added
addAssets(bucketResponse: AssetResponseDto[]) {
addAssets(bucketAssets: TimeBucketAssetResponseDto) {
const addContext = new AddContext();
for (const asset of bucketResponse) {
const timelineAsset = toTimelineAsset(asset);
const people: string[] = [];
for (let i = 0; i < bucketAssets.id.length; i++) {
const timelineAsset: TimelineAsset = {
city: bucketAssets.city[i],
country: bucketAssets.country[i],
duration: bucketAssets.duration[i],
id: bucketAssets.id[i],
visibility: bucketAssets.visibility[i],
isFavorite: bucketAssets.isFavorite[i],
isImage: bucketAssets.isImage[i],
isTrashed: bucketAssets.isTrashed[i],
isVideo: !bucketAssets.isImage[i],
livePhotoVideoId: bucketAssets.livePhotoVideoId[i],
localDateTime: bucketAssets.localDateTime[i],
ownerId: bucketAssets.ownerId[i],
people,
projectionType: bucketAssets.projectionType[i],
ratio: bucketAssets.ratio[i],
stack: bucketAssets.stack?.[i]
? {
id: bucketAssets.stack[i]![0],
primaryAssetId: bucketAssets.id[i],
assetCount: Number.parseInt(bucketAssets.stack[i]![1]),
}
: null,
thumbhash: bucketAssets.thumbhash[i],
};
this.addTimelineAsset(timelineAsset, addContext);
}
for (const group of addContext.existingDateGroups) {
group.sortAssets(this.#sortOrder);
}
if (addContext.newDateGroups.size > 0) {
this.sortDateGroups();
}
addContext.sort(this, this.#sortOrder);
return addContext.unprocessedAssets;
}
addTimelineAsset(timelineAsset: TimelineAsset, addContext: AddContext) {
const { id, localDateTime } = timelineAsset;
const { localDateTime } = timelineAsset;
const date = DateTime.fromISO(localDateTime).toUTC();
const month = date.get('month');
const year = date.get('year');
// If the timeline asset does not belong to the current bucket, mark it as unprocessed
if (this.month !== month || this.year !== year) {
addContext.unprocessedAssets.push(timelineAsset);
return;
}
const day = date.get('day');
let dateGroup: AssetDateGroup | undefined = addContext.lookupCache[day] || this.findDateGroupByDay(day);
let dateGroup = addContext.getDateGroup(year, month, day) || this.findDateGroupByDay(day);
if (dateGroup) {
// Cache the found date group for future lookups
addContext.lookupCache[day] = dateGroup;
addContext.setDateGroup(dateGroup, year, month, day);
} else {
// Create a new date group if none exists for the given day
dateGroup = new AssetDateGroup(this, this.dateGroups.length, date, day);
this.dateGroups.push(dateGroup);
addContext.lookupCache[day] = dateGroup;
addContext.setDateGroup(dateGroup, year, month, day);
addContext.newDateGroups.add(dateGroup);
}
// Check for duplicate assets in the date group
if (dateGroup.intersetingAssets.some((a) => a.id === id)) {
console.error(`Ignoring attempt to add duplicate asset ${id} to ${dateGroup.groupTitle}`);
return;
}
// Add the timeline asset to the date group
const intersectingAsset = new IntersectingAsset(dateGroup, timelineAsset);
dateGroup.intersetingAssets.push(intersectingAsset);
addContext.changedDateGroups.add(dateGroup);
@ -517,6 +579,7 @@ export class AssetBucket {
}
}
}
get bucketHeight() {
return this.#bucketHeight;
}
@ -953,7 +1016,6 @@ export class AssetStore {
async #initialiazeTimeBuckets() {
const timebuckets = await getTimeBuckets({
...this.#options,
size: TimeBucketSize.Month,
key: authManager.key,
});
@ -1163,7 +1225,7 @@ export class AssetStore {
{
...this.#options,
timeBucket: bucketDate,
size: TimeBucketSize.Month,
key: authManager.key,
},
{ signal },
@ -1174,12 +1236,11 @@ export class AssetStore {
{
albumId: this.#options.timelineAlbumId,
timeBucket: bucketDate,
size: TimeBucketSize.Month,
key: authManager.key,
},
{ signal },
);
for (const { id } of albumAssets) {
for (const id of albumAssets.id) {
this.albumAssets.add(id);
}
}
@ -1215,9 +1276,10 @@ export class AssetStore {
if (assets.length === 0) {
return;
}
const updatedBuckets = new Set<AssetBucket>();
const updatedDateGroups = new Set<AssetDateGroup>();
const addContext = new AddContext();
const updatedBuckets = new Set<AssetBucket>();
const bucketCount = this.buckets.length;
for (const asset of assets) {
const utc = DateTime.fromISO(asset.localDateTime).toUTC().startOf('month');
const year = utc.get('year');
@ -1228,20 +1290,26 @@ export class AssetStore {
bucket = new AssetBucket(this, utc, 1, this.#options.order);
this.buckets.push(bucket);
}
const addContext = new AddContext();
bucket.addTimelineAsset(asset, addContext);
addContext.sort(bucket, this.#options.order);
updatedBuckets.add(bucket);
}
this.buckets.sort((a, b) => {
return a.year === b.year ? b.month - a.month : b.year - a.year;
});
for (const dateGroup of updatedDateGroups) {
dateGroup.sortAssets(this.#options.order);
if (this.buckets.length !== bucketCount) {
this.buckets.sort((a, b) => {
return a.year === b.year ? b.month - a.month : b.year - a.year;
});
}
for (const bucket of updatedBuckets) {
for (const group of addContext.existingDateGroups) {
group.sortAssets(this.#options.order);
}
for (const bucket of addContext.bucketsWithNewDateGroups) {
bucket.sortDateGroups();
}
for (const bucket of addContext.updatedBuckets) {
bucket.sortDateGroups();
this.#updateGeometry(bucket, { invalidateHeight: true });
}
@ -1559,7 +1627,7 @@ export class AssetStore {
isExcluded(asset: TimelineAsset) {
return (
isMismatched(this.#options.visibility, asset.visibility as unknown as AssetVisibility) ||
isMismatched(this.#options.visibility, asset.visibility) ||
isMismatched(this.#options.isFavorite, asset.isFavorite) ||
isMismatched(this.#options.isTrashed, asset.isTrashed)
);

View File

@ -1,7 +1,7 @@
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
import type { AssetStore, TimelineAsset } from '$lib/stores/assets-store.svelte';
import type { StackResponse } from '$lib/utils/asset-utils';
import { deleteAssets as deleteBulk, Visibility } from '@immich/sdk';
import { AssetVisibility, deleteAssets as deleteBulk } from '@immich/sdk';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
import { handleError } from './handle-error';
@ -11,7 +11,7 @@ export type OnRestore = (ids: string[]) => void;
export type OnLink = (assets: { still: TimelineAsset; motion: TimelineAsset }) => void;
export type OnUnlink = (assets: { still: TimelineAsset; motion: TimelineAsset }) => void;
export type OnAddToAlbum = (ids: string[], albumId: string) => void;
export type OnArchive = (ids: string[], visibility: Visibility) => void;
export type OnArchive = (ids: string[], visibility: AssetVisibility) => void;
export type OnFavorite = (ids: string[], favorite: boolean) => void;
export type OnStack = (result: StackResponse) => void;
export type OnUnstack = (assets: TimelineAsset[]) => void;

View File

@ -1,6 +1,6 @@
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
import { getAltText } from '$lib/utils/thumbnail-util';
import { Visibility } from '@immich/sdk';
import { AssetVisibility } from '@immich/sdk';
import { init, register, waitLocale } from 'svelte-i18n';
interface Person {
@ -62,7 +62,7 @@ describe('getAltText', () => {
ratio: 1,
thumbhash: null,
localDateTime: '2024-01-01T12:00:00.000Z',
visibility: Visibility.Timeline,
visibility: AssetVisibility.Timeline,
isFavorite: false,
isTrashed: false,
isVideo,
@ -71,11 +71,9 @@ describe('getAltText', () => {
duration: null,
projectionType: null,
livePhotoVideoId: null,
text: {
city: city ?? null,
country: country ?? null,
people: people?.map((person: Person) => person.name) ?? [],
},
city: city ?? null,
country: country ?? null,
people: people?.map((person: Person) => person.name) ?? [],
};
getAltText.subscribe((fn) => {

View File

@ -41,19 +41,18 @@ export function getThumbnailSize(assetCount: number, viewWidth: number): number
export const getAltText = derived(t, ($t) => {
return (asset: TimelineAsset) => {
const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' }, { locale: get(locale) });
const { city, country, people: names } = asset.text;
const hasPlace = city && country;
const hasPlace = asset.city && asset.country;
const peopleCount = names.length;
const peopleCount = asset.people.length;
const isVideo = asset.isVideo;
const values = {
date,
city,
country,
person1: names[0],
person2: names[1],
person3: names[2],
city: asset.city,
country: asset.country,
person1: asset.people[0],
person2: asset.people[1],
person3: asset.people[2],
isVideo,
additionalCount: peopleCount > 3 ? peopleCount - 2 : 0,
};

View File

@ -2,7 +2,8 @@ import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
import { locale } from '$lib/stores/preferences.store';
import { getAssetRatio } from '$lib/utils/asset-utils';
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
import { AssetTypeEnum, AssetVisibility, type AssetResponseDto } from '@immich/sdk';
import { memoize } from 'lodash-es';
import { DateTime, type LocaleOptions } from 'luxon';
import { get } from 'svelte/store';
@ -65,17 +66,12 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
if (isTimelineAsset(unknownAsset)) {
return unknownAsset;
}
const assetResponse = unknownAsset as AssetResponseDto;
const assetResponse = unknownAsset;
const { width, height } = getAssetRatio(assetResponse);
const ratio = width / height;
const city = assetResponse.exifInfo?.city;
const country = assetResponse.exifInfo?.country;
const people = assetResponse.people?.map((person) => person.name) || [];
const text = {
city: city || null,
country: country || null,
people,
};
return {
id: assetResponse.id,
ownerId: assetResponse.ownerId,
@ -83,7 +79,7 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
thumbhash: assetResponse.thumbhash,
localDateTime: assetResponse.localDateTime,
isFavorite: assetResponse.isFavorite,
visibility: assetResponse.visibility,
visibility: assetResponse.isArchived ? AssetVisibility.Archive : AssetVisibility.Timeline,
isTrashed: assetResponse.isTrashed,
isVideo: assetResponse.type == AssetTypeEnum.Video,
isImage: assetResponse.type == AssetTypeEnum.Image,
@ -91,8 +87,10 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
duration: assetResponse.duration || null,
projectionType: assetResponse.exifInfo?.projectionType || null,
livePhotoVideoId: assetResponse.livePhotoVideoId || null,
text,
city: city || null,
country: country || null,
people,
};
};
export const isTimelineAsset = (asset: AssetResponseDto | TimelineAsset): asset is TimelineAsset =>
(asset as TimelineAsset).ratio !== undefined;
export const isTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset): unknownAsset is TimelineAsset =>
(unknownAsset as TimelineAsset).ratio !== undefined;

View File

@ -453,150 +453,6 @@
<div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}>
<div class="relative w-full shrink">
{#if assetInteraction.selectionActive}
<AssetSelectControlBar
assets={assetInteraction.selectedAssets}
clearSelect={() => assetInteraction.clearMultiselect()}
>
<CreateSharedLink />
<SelectAllAssets {assetStore} {assetInteraction} />
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
<AddToAlbum />
<AddToAlbum shared />
</ButtonContextMenu>
{#if assetInteraction.isAllUserOwned}
<FavoriteAction
removeFavorite={assetInteraction.isAllFavorite}
onFavorite={(ids, isFavorite) =>
assetStore.updateAssetOperation(ids, (asset) => {
asset.isFavorite = isFavorite;
return { remove: false };
})}
></FavoriteAction>
{/if}
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem filename="{album.albumName}.zip" />
{#if assetInteraction.isAllUserOwned}
<ChangeDate menuItem />
<ChangeDescription menuItem />
<ChangeLocation menuItem />
{#if assetInteraction.selectedAssets.length === 1}
<MenuOption
text={$t('set_as_album_cover')}
icon={mdiImageOutline}
onClick={() => updateThumbnailUsingCurrentSelection()}
/>
{/if}
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />
{/if}
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
<TagAction menuItem />
{/if}
{#if isOwned || assetInteraction.isAllUserOwned}
<RemoveFromAlbum menuItem bind:album onRemove={handleRemoveAssets} />
{/if}
{#if assetInteraction.isAllUserOwned}
<DeleteAssets menuItem onAssetDelete={handleRemoveAssets} />
{/if}
</ButtonContextMenu>
</AssetSelectControlBar>
{:else}
{#if viewMode === AlbumPageViewMode.VIEW}
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(backUrl)}>
{#snippet trailing()}
{#if isEditor}
<CircleIconButton
title={$t('add_photos')}
onclick={async () => {
assetStore.suspendTransitions = true;
viewMode = AlbumPageViewMode.SELECT_ASSETS;
oldAt = { at: $gridScrollTarget?.at };
await navigate(
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: { at: null } },
{ replaceState: true },
);
}}
icon={mdiImagePlusOutline}
/>
{/if}
{#if isOwned}
<CircleIconButton title={$t('share')} onclick={handleShare} icon={mdiShareVariantOutline} />
{/if}
<AlbumMap {album} />
{#if album.assetCount > 0}
<CircleIconButton title={$t('slideshow')} onclick={handleStartSlideshow} icon={mdiPresentationPlay} />
<CircleIconButton title={$t('download')} onclick={handleDownloadAlbum} icon={mdiFolderDownloadOutline} />
{/if}
{#if isOwned}
<ButtonContextMenu icon={mdiDotsVertical} title={$t('album_options')}>
{#if album.assetCount > 0}
<MenuOption
icon={mdiImageOutline}
text={$t('select_album_cover')}
onClick={() => (viewMode = AlbumPageViewMode.SELECT_THUMBNAIL)}
/>
<MenuOption
icon={mdiCogOutline}
text={$t('options')}
onClick={() => (viewMode = AlbumPageViewMode.OPTIONS)}
/>
{/if}
<MenuOption icon={mdiDeleteOutline} text={$t('delete_album')} onClick={() => handleRemoveAlbum()} />
</ButtonContextMenu>
{/if}
{#if isCreatingSharedAlbum && album.albumUsers.length === 0}
<Button size="small" disabled={album.assetCount === 0} onclick={handleShare}>
{$t('share')}
</Button>
{/if}
{/snippet}
</ControlAppBar>
{/if}
{#if viewMode === AlbumPageViewMode.SELECT_ASSETS}
<ControlAppBar onClose={handleCloseSelectAssets}>
{#snippet leading()}
<p class="text-lg dark:text-immich-dark-fg">
{#if !timelineInteraction.selectionActive}
{$t('add_to_album')}
{:else}
{$t('selected_count', { values: { count: timelineInteraction.selectedAssets.length } })}
{/if}
</p>
{/snippet}
{#snippet trailing()}
<button
type="button"
onclick={handleSelectFromComputer}
class="rounded-lg px-6 py-2 text-sm font-medium text-immich-primary transition-all hover:bg-immich-primary/10 dark:text-immich-dark-primary dark:hover:bg-immich-dark-primary/25"
>
{$t('select_from_computer')}
</button>
<Button size="small" disabled={!timelineInteraction.selectionActive} onclick={handleAddAssets}
>{$t('done')}</Button
>
{/snippet}
</ControlAppBar>
{/if}
{#if viewMode === AlbumPageViewMode.SELECT_THUMBNAIL}
<ControlAppBar onClose={() => (viewMode = AlbumPageViewMode.VIEW)}>
{#snippet leading()}
{$t('select_album_cover')}
{/snippet}
</ControlAppBar>
{/if}
{/if}
<main class="relative h-dvh overflow-hidden px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height)">
<AssetGrid
enableRouting={viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : true}
@ -685,7 +541,7 @@
<button
type="button"
onclick={() => (viewMode = AlbumPageViewMode.SELECT_ASSETS)}
class="mt-5 bg-subtle flex w-full place-items-center gap-6 rounded-md border px-8 py-8 text-immich-fg transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
class="mt-5 bg-subtle flex w-full place-items-center gap-6 rounded-2xl border px-8 py-8 text-immich-fg transition-all hover:bg-gray-100 dark:hover:bg-gray-500/20 hover:text-immich-primary dark:border-none dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
>
<span class="text-text-immich-primary dark:text-immich-dark-primary"
><Icon path={mdiPlus} size="24" />
@ -710,6 +566,150 @@
</div>
{/if}
</main>
{#if assetInteraction.selectionActive}
<AssetSelectControlBar
assets={assetInteraction.selectedAssets}
clearSelect={() => assetInteraction.clearMultiselect()}
>
<CreateSharedLink />
<SelectAllAssets {assetStore} {assetInteraction} />
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
<AddToAlbum />
<AddToAlbum shared />
</ButtonContextMenu>
{#if assetInteraction.isAllUserOwned}
<FavoriteAction
removeFavorite={assetInteraction.isAllFavorite}
onFavorite={(ids, isFavorite) =>
assetStore.updateAssetOperation(ids, (asset) => {
asset.isFavorite = isFavorite;
return { remove: false };
})}
></FavoriteAction>
{/if}
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')} offset={{ x: 175, y: 25 }}>
<DownloadAction menuItem filename="{album.albumName}.zip" />
{#if assetInteraction.isAllUserOwned}
<ChangeDate menuItem />
<ChangeDescription menuItem />
<ChangeLocation menuItem />
{#if assetInteraction.selectedAssets.length === 1}
<MenuOption
text={$t('set_as_album_cover')}
icon={mdiImageOutline}
onClick={() => updateThumbnailUsingCurrentSelection()}
/>
{/if}
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />
{/if}
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
<TagAction menuItem />
{/if}
{#if isOwned || assetInteraction.isAllUserOwned}
<RemoveFromAlbum menuItem bind:album onRemove={handleRemoveAssets} />
{/if}
{#if assetInteraction.isAllUserOwned}
<DeleteAssets menuItem onAssetDelete={handleRemoveAssets} />
{/if}
</ButtonContextMenu>
</AssetSelectControlBar>
{:else}
{#if viewMode === AlbumPageViewMode.VIEW}
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(backUrl)}>
{#snippet trailing()}
{#if isEditor}
<CircleIconButton
title={$t('add_photos')}
onclick={async () => {
assetStore.suspendTransitions = true;
viewMode = AlbumPageViewMode.SELECT_ASSETS;
oldAt = { at: $gridScrollTarget?.at };
await navigate(
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: { at: null } },
{ replaceState: true },
);
}}
icon={mdiImagePlusOutline}
/>
{/if}
{#if isOwned}
<CircleIconButton title={$t('share')} onclick={handleShare} icon={mdiShareVariantOutline} />
{/if}
<AlbumMap {album} />
{#if album.assetCount > 0}
<CircleIconButton title={$t('slideshow')} onclick={handleStartSlideshow} icon={mdiPresentationPlay} />
<CircleIconButton title={$t('download')} onclick={handleDownloadAlbum} icon={mdiFolderDownloadOutline} />
{/if}
{#if isOwned}
<ButtonContextMenu icon={mdiDotsVertical} title={$t('album_options')} offset={{ x: 175, y: 25 }}>
{#if album.assetCount > 0}
<MenuOption
icon={mdiImageOutline}
text={$t('select_album_cover')}
onClick={() => (viewMode = AlbumPageViewMode.SELECT_THUMBNAIL)}
/>
<MenuOption
icon={mdiCogOutline}
text={$t('options')}
onClick={() => (viewMode = AlbumPageViewMode.OPTIONS)}
/>
{/if}
<MenuOption icon={mdiDeleteOutline} text={$t('delete_album')} onClick={() => handleRemoveAlbum()} />
</ButtonContextMenu>
{/if}
{#if isCreatingSharedAlbum && album.albumUsers.length === 0}
<Button size="small" disabled={album.assetCount === 0} onclick={handleShare}>
{$t('share')}
</Button>
{/if}
{/snippet}
</ControlAppBar>
{/if}
{#if viewMode === AlbumPageViewMode.SELECT_ASSETS}
<ControlAppBar onClose={handleCloseSelectAssets}>
{#snippet leading()}
<p class="text-lg dark:text-immich-dark-fg">
{#if !timelineInteraction.selectionActive}
{$t('add_to_album')}
{:else}
{$t('selected_count', { values: { count: timelineInteraction.selectedAssets.length } })}
{/if}
</p>
{/snippet}
{#snippet trailing()}
<button
type="button"
onclick={handleSelectFromComputer}
class="rounded-lg px-6 py-2 text-sm font-medium text-immich-primary transition-all hover:bg-immich-primary/10 dark:text-immich-dark-primary dark:hover:bg-immich-dark-primary/25"
>
{$t('select_from_computer')}
</button>
<Button size="small" disabled={!timelineInteraction.selectionActive} onclick={handleAddAssets}
>{$t('done')}</Button
>
{/snippet}
</ControlAppBar>
{/if}
{#if viewMode === AlbumPageViewMode.SELECT_THUMBNAIL}
<ControlAppBar onClose={() => (viewMode = AlbumPageViewMode.VIEW)}>
{#snippet leading()}
{$t('select_album_cover')}
{/snippet}
</ControlAppBar>
{/if}
{/if}
</div>
{#if album.albumUsers.length > 0 && album && isShowActivity && $user && !$showAssetViewer}
<div class="flex">

View File

@ -40,6 +40,20 @@
};
</script>
<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
<AssetGrid
enableRouting={true}
{assetStore}
{assetInteraction}
removeAction={AssetAction.UNARCHIVE}
onEscape={handleEscape}
>
{#snippet empty()}
<EmptyPlaceholder text={$t('no_archived_assets_message')} />
{/snippet}
</AssetGrid>
</UserPageLayout>
{#if assetInteraction.selectionActive}
<AssetSelectControlBar
assets={assetInteraction.selectedAssets}
@ -73,17 +87,3 @@
</ButtonContextMenu>
</AssetSelectControlBar>
{/if}
<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
<AssetGrid
enableRouting={true}
{assetStore}
{assetInteraction}
removeAction={AssetAction.UNARCHIVE}
onEscape={handleEscape}
>
{#snippet empty()}
<EmptyPlaceholder text={$t('no_archived_assets_message')} />
{/snippet}
</AssetGrid>
</UserPageLayout>

Some files were not shown because too many files have changed in this diff Show More