From 1213f6df106d9c4565ccf3f931b52ea1e58ef772 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 11 Sep 2025 12:02:47 +0200 Subject: [PATCH 01/30] fix: use correct bot in weblate-checks (#21808) --- .github/workflows/weblate-lock.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index 58b11e00cf..b6cbc04442 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -7,6 +7,9 @@ on: permissions: {} +env: + BOT_NAME: immich-push-o-matic + jobs: pre-job: runs-on: ubuntu-latest @@ -39,7 +42,7 @@ jobs: GH_TOKEN: ${{ github.token }} run: | # Then check for APPROVED by the bot, if absent fail - gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json reviews | jq -e '.reviews | map(select(.author.login == "github-actions[bot]" and .state == "APPROVED")) | length > 0' \ + gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json reviews | jq -e '.reviews | map(select(.author.login == env.BOT_NAME and .state == "APPROVED")) | length > 0' \ || (echo "The push-o-matic bot has not approved this PR yet" && exit 1) success-check-lock: From 529b8c285df63445df8c41fbcb6273c4c2f9af0c Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 11 Sep 2025 12:20:49 +0200 Subject: [PATCH 02/30] fix: only run weblate checks on review (#21809) --- .github/workflows/weblate-lock.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index b6cbc04442..e0634b6af2 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -2,8 +2,6 @@ name: Weblate checks on: pull_request_review: - pull_request: - branches: [main] permissions: {} From 03207a13ec13d8fe7a635fb60334fcf0fa15e771 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 11 Sep 2025 12:28:48 +0200 Subject: [PATCH 03/30] chore(web): update translations (#21624) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate-URL: https://hosted.weblate.org/projects/immich/immich/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/be/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/et/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/eu/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/id/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/lv/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/mr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sq/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ta/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/ Translation: Immich/immich Co-authored-by: Andrea Toska Co-authored-by: Aravinth Co-authored-by: Denis Pacquier Co-authored-by: DevServs Co-authored-by: FarSniper Co-authored-by: Fjuro Co-authored-by: Gustavo de León Co-authored-by: Handkep Co-authored-by: Hudio Hizari Co-authored-by: Indrek Haav Co-authored-by: Ivan Amela Caldeiro Co-authored-by: Ivan Dimitrov Co-authored-by: JPar99 Co-authored-by: Jozef Gaal Co-authored-by: Macgyver Co-authored-by: Marcelo Popper Costa Co-authored-by: Matjaž T Co-authored-by: Mārtiņš Bruņenieks Co-authored-by: Oleks Povar Co-authored-by: Pajtim Rexhepi Co-authored-by: Red Cyclops Co-authored-by: Runskrift Co-authored-by: Santiago Co-authored-by: Saurabh Nandedkar Co-authored-by: Sebastian Co-authored-by: Sergey Katsubo Co-authored-by: Simob98 Co-authored-by: Sylvain Pichon Co-authored-by: TV Box Co-authored-by: Taiki M Co-authored-by: Tomi Pöyskö Co-authored-by: Urko Perez Azkarragaurizar Co-authored-by: User 123456789 Co-authored-by: Vegard Fladby Co-authored-by: anton garcias Co-authored-by: gablilli Co-authored-by: pyccl Co-authored-by: shiuh67 Co-authored-by: Максим Горпиніч Co-authored-by: Яўген --- i18n/ar.json | 2 - i18n/be.json | 8 +++ i18n/bg.json | 14 ++--- i18n/ca.json | 18 +++--- i18n/cs.json | 14 ++--- i18n/da.json | 30 +++++----- i18n/de.json | 9 +-- i18n/el.json | 2 - i18n/es.json | 19 +++---- i18n/et.json | 16 +++--- i18n/eu.json | 56 ++++++++++++++++++- i18n/fi.json | 5 +- i18n/fr.json | 18 +++--- i18n/he.json | 8 --- i18n/hi.json | 2 - i18n/hr.json | 2 - i18n/hu.json | 2 - i18n/id.json | 35 ++++++++++-- i18n/it.json | 15 ++--- i18n/ja.json | 12 ++-- i18n/ko.json | 2 - i18n/lv.json | 7 +-- i18n/mr.json | 4 +- i18n/nb_NO.json | 9 +-- i18n/nl.json | 30 +++++----- i18n/pl.json | 20 +++---- i18n/pt.json | 8 --- i18n/pt_BR.json | 14 ++--- i18n/ro.json | 2 - i18n/ru.json | 52 ++++++++---------- i18n/sk.json | 14 ++--- i18n/sl.json | 9 +-- i18n/sq.json | 60 +++++++++++++++++++- i18n/sv.json | 9 +-- i18n/ta.json | 74 +++++++++++++++++++++++-- i18n/tr.json | 46 ++++++++++++---- i18n/uk.json | 118 +++++++++++++++++++--------------------- i18n/vi.json | 2 - i18n/zh_Hant.json | 12 +--- i18n/zh_SIMPLIFIED.json | 14 ++--- 40 files changed, 452 insertions(+), 341 deletions(-) diff --git a/i18n/ar.json b/i18n/ar.json index 10e7da5f33..8a436deba5 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -592,8 +592,6 @@ "backup_setting_subtitle": "ادارة اعدادات التحميل في الخلفية والمقدمة", "backup_settings_subtitle": "إدارة إعدادات التحميل", "backward": "الى الوراء", - "beta_sync": "حالة المزامنة التجريبية", - "beta_sync_subtitle": "ادارة نظام المزامنة الجديد", "biometric_auth_enabled": "المصادقة البايومترية مفعله", "biometric_locked_out": "لقد قفلت عنك المصادقة البيومترية", "biometric_no_options": "لا توجد خيارات بايومترية متوفرة", diff --git a/i18n/be.json b/i18n/be.json index 0ee0eb9806..1f177593df 100644 --- a/i18n/be.json +++ b/i18n/be.json @@ -28,6 +28,8 @@ "add_to_album": "Дадаць у альбом", "add_to_album_bottom_sheet_added": "Дададзена да {album}", "add_to_album_bottom_sheet_already_exists": "Ужо знаходзіцца ў {album}", + "add_to_albums": "Дадаць у альбомы", + "add_to_albums_count": "Дадаць у альбомы ({count})", "add_to_shared_album": "Дадаць у агульны альбом", "add_url": "Дадаць URL", "added_to_archive": "Дададзена ў архіў", @@ -399,8 +401,14 @@ "purchase_button_buy": "Купіць", "purchase_button_buy_immich": "Купіць Immich", "purchase_button_select": "Выбраць", + "readonly_mode_disabled": "Выключаны рэжым толькі для чытання", "readonly_mode_enabled": "Уключаны рэжым толькі для чытання", "reassign": "Перапрызначыць", + "reassing_hint": "Прыпісаць выбраныя актывы існуючай асобе", + "recent": "Нядаўні", + "recent-albums": "Нядаўнія альбомы", + "recent_searches": "Нядаўнія пошукі", + "recently_added": "Нядаўна дададзена", "remove": "Выдаліць", "remove_from_album": "Выдаліць з альбома", "remove_from_favorites": "Выдаліць з абраных", diff --git a/i18n/bg.json b/i18n/bg.json index 27bd6cc78e..8d9727e492 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Управлявай настройките за архивиране в активен и фонов режим", "backup_settings_subtitle": "Управление на настройките за качване", "backward": "Назад", - "beta_sync": "Статус на бета синхронизацията", - "beta_sync_subtitle": "Управление на новата система за синхронизация", "biometric_auth_enabled": "Включена биометрично удостоверяване", "biometric_locked_out": "Няма достъп до биометрично удостоверяване", "biometric_no_options": "Няма биометрична автентикация", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "За да работи тази функция зарежда външни ресурси от Google.", "general": "Общи", - "geolocation_instruction_all_have_location": "Всички обекти от тази дата вече имат данни за местоположение. Опитайте да включите показване на всички обекти или изберете друга дата", "geolocation_instruction_location": "Изберете обект с GPS координати за да използвате тях или изберете място директно от картата", - "geolocation_instruction_no_date": "Изберете дата за да управлявате данните за локация на снимките и видеата от тази дата", - "geolocation_instruction_no_photos": "Не са намерени снимки или видеа от тази дата. Изберете друга дата", "get_help": "Помощ", "get_wifiname_error": "Неуспешно получаване името на Wi-Fi мрежата. Моля, убедете се, че са предоставени нужните разрешения на приложението и има връзка с Wi-Fi", "getting_started": "Как да започнем", @@ -1417,6 +1412,8 @@ "open_the_search_filters": "Отвари филтрите за търсене", "options": "Настройки", "or": "или", + "organize_into_albums": "Organitzar per àlbums", + "organize_into_albums_description": "Posar les fotos existents dins dels àlbums fent servir la configuració de sincronització", "organize_your_library": "Организиране на вашата библиотека", "original": "оригинал", "other": "Други", @@ -1557,6 +1554,7 @@ "purchase_server_description_2": "Статус на поддръжник", "purchase_server_title": "Сървър", "purchase_settings_server_activated": "Продуктовият ключ на сървъра се управлява от администратора", + "query_asset_id": "Buscar item per ID", "queue_status": "В опашка {count} от {total}", "rating": "Оценка със звезди", "rating_clear": "Изчисти оценката", @@ -1735,7 +1733,7 @@ "select_user_for_sharing_page_err_album": "Създаването на албум не бе успешно", "selected": "Избрано", "selected_count": "{count, plural, other {# избрани}}", - "selected_gps_coordinates": "избрани GPS координати", + "selected_gps_coordinates": "Избрани GPS координати", "send_message": "Изпратете съобщение", "send_welcome_email": "Изпратете имейл за добре дошли", "server_endpoint": "Адрес на сървъра", @@ -1846,10 +1844,8 @@ "shift_to_permanent_delete": "Натиснете ⇧, за да изтриете завинаги елемента", "show_album_options": "Показване опции за албум", "show_albums": "Покажи албуми", - "show_all_assets": "Покажи всичко", "show_all_people": "Покажи всички хора", "show_and_hide_people": "Показване и скриване на хора", - "show_assets_without_location": "Покажи обекти без координати", "show_file_location": "Покажи местоположението на файла", "show_gallery": "Покажи галерия", "show_hidden_people": "Показване на скритите хора", @@ -2034,7 +2030,6 @@ "use_biometric": "Използвай биометрия", "use_current_connection": "използвай текущата връзка", "use_custom_date_range": "Използвайте собствен диапазон от дати вместо това", - "use_this_location": "Избери това място", "user": "Потребител", "user_has_been_deleted": "Този потребител е премахнат.", "user_id": "Потребител ИД", @@ -2077,6 +2072,7 @@ "view_next_asset": "Преглед на следващия файл", "view_previous_asset": "Преглед на предишния файл", "view_qr_code": "Виж QR кода", + "view_similar_photos": "Виж подобни снимки", "view_stack": "Покажи в стек", "view_user": "Виж потребителя", "viewer_remove_from_stack": "Премахване от опашката", diff --git a/i18n/ca.json b/i18n/ca.json index 11b9157f56..4d532e0476 100644 --- a/i18n/ca.json +++ b/i18n/ca.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Gestiona la configuració de càrrega en segon pla i en primer pla", "backup_settings_subtitle": "Administra la configuració de pujada", "backward": "Enrere", - "beta_sync": "Estat de la sincronització beta", - "beta_sync_subtitle": "Administra el nou sistema de sincronització", "biometric_auth_enabled": "Autentificació biomètrica activada", "biometric_locked_out": "Esteu bloquejats fora de l'autenticació biomètrica", "biometric_no_options": "No hi ha opcions biomètriques disponibles", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Aquesta funció carrega recursos externs de Google per funcionar.", "general": "General", - "geolocation_instruction_all_have_location": "Tots els actius d'aquesta data ja tenen dades d'ubicació. Prova de mostrar tots els actius o selecciona una data diferent", "geolocation_instruction_location": "Fes click en un element amb coordinades GPS per utilitzar la seva ubicació o selecciona una ubicació des del mapa", - "geolocation_instruction_no_date": "Seleccioneu una data per gestionar dades d'ubicació per a fotos i vídeos d'aquest dia", - "geolocation_instruction_no_photos": "Cap foto o vídeo trobats per a aquesta data. Selecciona una data diferent", "get_help": "Aconseguir ajuda", "get_wifiname_error": "No s'ha pogut obtenir el nom de la Wi-Fi. Assegureu-vos que heu concedit els permisos necessaris i que esteu connectat a una xarxa Wi-Fi", "getting_started": "Començant", @@ -1206,7 +1201,7 @@ "library_options": "Opcions de biblioteca", "library_page_device_albums": "Àlbums al Dispositiu", "library_page_new_album": "Nou àlbum", - "library_page_sort_asset_count": "Nombre d'elements", + "library_page_sort_asset_count": "Quantitat d'elements", "library_page_sort_created": "Creat més recentment", "library_page_sort_last_modified": "Darrera modificació", "library_page_sort_title": "Títol de l'àlbum", @@ -1417,6 +1412,8 @@ "open_the_search_filters": "Obriu els filtres de cerca", "options": "Opcions", "or": "o", + "organize_into_albums": "Organitzar en àlbums", + "organize_into_albums_description": "Posar fotos existents en àlbums utilitzant la configuració de sincronització actual", "organize_your_library": "Organitzeu la llibreria", "original": "original", "other": "Altres", @@ -1557,6 +1554,7 @@ "purchase_server_description_2": "Estat del contribuent", "purchase_server_title": "Servidor", "purchase_settings_server_activated": "La clau de producte del servidor la gestiona l'administrador", + "query_asset_id": "Consulta d'identificació d'actius", "queue_status": "En cua {count}/{total}", "rating": "Valoració", "rating_clear": "Esborrar valoració", @@ -1735,7 +1733,7 @@ "select_user_for_sharing_page_err_album": "Error al crear l'àlbum", "selected": "Seleccionat", "selected_count": "{count, plural, one {# seleccionat} other {# seleccionats}}", - "selected_gps_coordinates": "seleccio de coordinades GPS", + "selected_gps_coordinates": "Seleccio de coordinades GPS", "send_message": "Envia missatge", "send_welcome_email": "Envia correu de benvinguda", "server_endpoint": "Endpoint de Servidor", @@ -1846,10 +1844,8 @@ "shift_to_permanent_delete": "premeu ⇧ per suprimir el recurs permanentment", "show_album_options": "Mostra les opcions d'àlbum", "show_albums": "Mostrar àlbums", - "show_all_assets": "Mostrar tots els elements", "show_all_people": "Veure totes les persones", "show_and_hide_people": "Mostra i amaga persones", - "show_assets_without_location": "Mostra els elements sense ubicació", "show_file_location": "Mostra l'ubicació del fitxer", "show_gallery": "Mostra la galeria", "show_hidden_people": "Mostra persones ocultes", @@ -1879,7 +1875,7 @@ "slideshow_settings": "Configuració de diapositives", "sort_albums_by": "Ordena àlbums per...", "sort_created": "Data de creació", - "sort_items": "Nombre d'elements", + "sort_items": "Quantitat d'elements", "sort_modified": "Data de modificació", "sort_newest": "Foto més nova", "sort_oldest": "Foto més antiga", @@ -2034,7 +2030,6 @@ "use_biometric": "Empra biometria", "use_current_connection": "utilitzar la connexió actual", "use_custom_date_range": "Fes servir un rang de dates personalitzat", - "use_this_location": "Fes clic per utilitzar la ubicació", "user": "Usuari", "user_has_been_deleted": "Aquest usuari ha sigut eliminat.", "user_id": "ID d'usuari", @@ -2077,6 +2072,7 @@ "view_next_asset": "Mostra el següent element", "view_previous_asset": "Mostra l'element anterior", "view_qr_code": "Veure codi QR", + "view_similar_photos": "Veure fotos similars", "view_stack": "Veure la pila", "view_user": "Veure Usuari", "viewer_remove_from_stack": "Elimina de la pila", diff --git a/i18n/cs.json b/i18n/cs.json index e600fc1348..e97d7f4211 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Správa nastavení zálohování na pozadí a na popředí", "backup_settings_subtitle": "Správa nastavení nahrávání", "backward": "Pozpátku", - "beta_sync": "Stav synchronizace (beta)", - "beta_sync_subtitle": "Správa nového systému synchronizace", "biometric_auth_enabled": "Biometrické ověřování je povoleno", "biometric_locked_out": "Jste vyloučeni z biometrického ověřování", "biometric_no_options": "Biometrické možnosti nejsou k dispozici", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Tato funkce načítá externí zdroje z Googlu, aby mohla fungovat.", "general": "Obecné", - "geolocation_instruction_all_have_location": "Všechny položky k tomuto datu již mají údaje o poloze. Zkuste zobrazit všechny položky nebo vyberte jiné datum", "geolocation_instruction_location": "Klikněte na položku s GPS souřadnicemi, abyste mohli použít její polohu, nebo vyberte polohu přímo z mapy", - "geolocation_instruction_no_date": "Vyberte datum, abyste mohli spravovat údaje o poloze pro fotografie a videa z daného dne", - "geolocation_instruction_no_photos": "Pro tento den nebyly nalezeny žádné fotografie ani videa. Vyberte jiné datum pro jejich zobrazení", "get_help": "Získat pomoc", "get_wifiname_error": "Nepodařilo se získat název Wi-Fi. Zkontrolujte, zda jste udělili potřebná oprávnění a zda jste připojeni k Wi-Fi síti", "getting_started": "Začínáme", @@ -1520,7 +1515,7 @@ "profile_drawer_client_out_of_date_minor": "Mobilní aplikace je zastaralá. Aktualizujte ji na nejnovější verzi.", "profile_drawer_client_server_up_to_date": "Klient a server jsou aktuální", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "Režim pouze pro čtení je aktivován. Dvojitým klepnutím na ikonu avatara uživatele režim ukončíte.", + "profile_drawer_readonly_mode": "Režim jen pro čtení. Ukončíte ho dlouhým podržením ikony avataru.", "profile_drawer_server_out_of_date_major": "Server je zastaralý. Aktualizujte na nejnovější hlavní verzi.", "profile_drawer_server_out_of_date_minor": "Server je zastaralý. Aktualizujte je na nejnovější verzi.", "profile_image_of_user": "Profilový obrázek uživatele {user}", @@ -1645,6 +1640,7 @@ "restore_user": "Obnovit uživatele", "restored_asset": "Položka obnovena", "resume": "Pokračovat", + "resume_paused_jobs": "Pokračovat {count, plural, one {v # pozastavené úloze} few {ve # pozastavených úlohách} other {v # pozastavených úlohách}}", "retry_upload": "Opakování nahrávání", "review_duplicates": "Kontrola duplicit", "review_large_files": "Kontrola velkých souborů", @@ -1849,10 +1845,8 @@ "shift_to_permanent_delete": "stiskněte ⇧ pro trvalé odstranění položky", "show_album_options": "Zobrazit možnosti alba", "show_albums": "Zobrazit alba", - "show_all_assets": "Zobrazit všechny položky", "show_all_people": "Zobrazit všechny lidi", "show_and_hide_people": "Zobrazit a skrýt osoby", - "show_assets_without_location": "Zobrazit položky bez polohy", "show_file_location": "Zobrazit umístění souboru", "show_gallery": "Zobrazit galerii", "show_hidden_people": "Zobrazit skryté lidi", @@ -1923,6 +1917,8 @@ "sync_albums_manual_subtitle": "Synchronizovat všechna nahraná videa a fotografie do vybraných záložních alb", "sync_local": "Synchronizovat místní", "sync_remote": "Synchronizovat vzdálené", + "sync_status": "Stav synchronizace", + "sync_status_subtitle": "Zobrazit a spravovat synchronizační systém", "sync_upload_album_setting_subtitle": "Vytvořit a nahrát fotografie a videa do vybraných alb na Immich", "tag": "Značka", "tag_assets": "Přiřadit značku", @@ -1982,6 +1978,7 @@ "trash_page_select_assets_btn": "Vybrat položky", "trash_page_title": "Koš ({count})", "trashed_items_will_be_permanently_deleted_after": "Smazané položky budou trvale odstraněny po {days, plural, one {# dni} other {# dnech}}.", + "troubleshoot": "Diagnostika", "type": "Typ", "unable_to_change_pin_code": "Nelze změnit PIN kód", "unable_to_setup_pin_code": "Nelze nastavit PIN kód", @@ -2037,7 +2034,6 @@ "use_biometric": "Použít biometrické údaje", "use_current_connection": "použít aktuální připojení", "use_custom_date_range": "Použít vlastní rozsah dat", - "use_this_location": "Klikněte pro použití polohy", "user": "Uživatel", "user_has_been_deleted": "Tento uživatel byl smazán.", "user_id": "ID uživatele", diff --git a/i18n/da.json b/i18n/da.json index 847913497a..c45cf4000d 100644 --- a/i18n/da.json +++ b/i18n/da.json @@ -1,8 +1,8 @@ { - "about": "Om", + "about": "Om os", "account": "Konto", "account_settings": "Kontoindstillinger", - "acknowledge": "Godkend", + "acknowledge": "Anerkendelse", "action": "Handling", "action_common_update": "Opdater", "actions": "Handlinger", @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Administrer indstillnger for upload i forgrund og baggrund", "backup_settings_subtitle": "Håndtere upload indstillinger", "backward": "Baglæns", - "beta_sync": "Beta synkroniseringsstatus", - "beta_sync_subtitle": "Håndter det nye synkroniseringssystem", "biometric_auth_enabled": "Biometrisk adgangskontrol slået til", "biometric_locked_out": "Du er låst ude af biometrisk adgangskontrol", "biometric_no_options": "Ingen biometrisk adgangskontrol tilgængelig", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Denne funktion indlæser eksterne ressourcer fra Google for at virke.", "general": "Generel", - "geolocation_instruction_all_have_location": "Alle objekter for denne dato har allerede lokations-data. Prøv at vise alle objekter eller vælg en anden dato", "geolocation_instruction_location": "Klik på et objekt med GPS-koordinater for at bruge dettes position, eller vælg position direkte på kortet", - "geolocation_instruction_no_date": "Vælg en dato, for at administrere lokationsdata på billeder og videoer fra den dag", - "geolocation_instruction_no_photos": "Ingen fotos eller videoer fundet for den dato. Vælg en anden dato for at vise dem", "get_help": "Få hjælp", "get_wifiname_error": "Kunne ikke hente Wi-Fi-navn. Sørg for, at du har givet de nødvendige tilladelser og er forbundet til et Wi-Fi-netværk", "getting_started": "Kom godt i gang", @@ -1417,6 +1412,8 @@ "open_the_search_filters": "Åbn søgefiltre", "options": "Handlinger", "or": "eller", + "organize_into_albums": "Organiser i album", + "organize_into_albums_description": "Sæt eksisterende billeder i albummer ved hjælp af aktuelle synkroniseringsindstillinger", "organize_your_library": "Organisér dit bibliotek", "original": "original", "other": "Andet", @@ -1425,7 +1422,7 @@ "other_variables": "Andre variable", "owned": "Egne", "owner": "Ejer", - "partner": "Partner", + "partner": "Partnerpartner", "partner_can_access": "{partner} kan tilgå", "partner_can_access_assets": "Alle dine billeder og videoer, bortset fra dem i Arkivet og Slettet", "partner_can_access_location": "Stedet, hvor dine billeder blev taget", @@ -1475,7 +1472,7 @@ "permission_onboarding_permission_granted": "Tilladelse givet! Du er nu klar.", "permission_onboarding_permission_limited": "Tilladelse begrænset. For at lade Immich lave sikkerhedskopi og styre hele dit galleri, skal der gives tilladelse til billeder og videoer i indstillinger.", "permission_onboarding_request": "Immich kræver tilliadelse til at se dine billeder og videoer.", - "person": "Person", + "person": "Personperson", "person_age_months": "{months, plural, one {# month} other {# months}} gammel", "person_age_year_months": "1 år, {months, plural, one {# month} other {# months}} gammel", "person_age_years": "{years, plural, other {# years}} gammel", @@ -1518,7 +1515,7 @@ "profile_drawer_client_out_of_date_minor": "Mobilapp er forældet. Opdater venligst til den nyeste mindre version.", "profile_drawer_client_server_up_to_date": "Klient og server er ajour", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "Skrivebeskyttet tilstand aktiveret. Dobbeltklik på bruger avatar ikonet for at afslutte.", + "profile_drawer_readonly_mode": "Skrivebeskyttet tilstand aktiveret. Lang tryk på bruger avatar ikonet for at afslutte.", "profile_drawer_server_out_of_date_major": "Server er forældet. Opdater venligst til den nyeste større version.", "profile_drawer_server_out_of_date_minor": "Server er forældet. Opdater venligst til den nyeste mindre version.", "profile_image_of_user": "Profilbillede af {user}", @@ -1557,6 +1554,7 @@ "purchase_server_description_2": "Supporterstatus", "purchase_server_title": "Server", "purchase_settings_server_activated": "Serverens produktnøgle administreres af administratoren", + "query_asset_id": "Forespørgsels Asset ID", "queue_status": "Kø {count}/{total}", "rating": "Stjernebedømmelse", "rating_clear": "Nulstil vurdering", @@ -1642,6 +1640,7 @@ "restore_user": "Gendan bruger", "restored_asset": "Gendannet mediefilen", "resume": "Genoptag", + "resume_paused_jobs": "Fortsæt {count, plural, one {# paused job} other {# paused jobs}}", "retry_upload": "Forsøg upload igen", "review_duplicates": "Gennemgå dubletter", "review_large_files": "Gennemgå store filer", @@ -1735,7 +1734,7 @@ "select_user_for_sharing_page_err_album": "Fejlede i at oprette et nyt album", "selected": "Valgt", "selected_count": "{count, plural, one {# valgt} other {# valgte}}", - "selected_gps_coordinates": "Valgte GPS-koordinater", + "selected_gps_coordinates": "Udvalgte GPS Koordinater", "send_message": "Send besked", "send_welcome_email": "Send velkomstemail", "server_endpoint": "Server endepunkt", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "tryk på ⇧ for at slette aktiv permanent", "show_album_options": "Vis albumindstillinger", "show_albums": "Vis albummer", - "show_all_assets": "Vis alle objekter", "show_all_people": "Vis alle personer", "show_and_hide_people": "Vis & skjul personer", - "show_assets_without_location": "Vis objekter uden lokation", "show_file_location": "Vis filplacering", "show_gallery": "Vis galleri", "show_hidden_people": "Vis skjulte personer", @@ -1898,7 +1895,7 @@ "start_date": "Startdato", "state": "Stat", "status": "Status", - "stop_casting": "Stop casting", + "stop_casting": "Stop støbning", "stop_motion_photo": "Stopmotionbillede", "stop_photo_sharing": "Stop med at dele dine billeder?", "stop_photo_sharing_description": "{partner} vil ikke længere kunne tilgå dine billeder.", @@ -1920,6 +1917,8 @@ "sync_albums_manual_subtitle": "Synkroniser alle uploadet billeder og videoer til de valgte backupalbummer", "sync_local": "Synkroniser lokalt", "sync_remote": "Synkroniser eksternt", + "sync_status": "Synkroniserings Status", + "sync_status_subtitle": "Se og administrér synkroniseringssystemet", "sync_upload_album_setting_subtitle": "Opret og upload dine billeder og videoer til de valgte albummer i Immich", "tag": "Tag", "tag_assets": "Tag mediefiler", @@ -1979,6 +1978,7 @@ "trash_page_select_assets_btn": "Vælg elementer", "trash_page_title": "Papirkurv ({count})", "trashed_items_will_be_permanently_deleted_after": "Mediefiler i skraldespanden vil blive slettet permanent efter {days, plural, one {# dag} other {# dage}}.", + "troubleshoot": "Fejlfinding", "type": "Type", "unable_to_change_pin_code": "Kunne ikke ændre PIN kode", "unable_to_setup_pin_code": "Kunne ikke sætte PIN kode", @@ -2034,7 +2034,6 @@ "use_biometric": "Brug biometrisk", "use_current_connection": "brug nuværende forbindelse", "use_custom_date_range": "Brug tilpasset datointerval i stedet", - "use_this_location": "Klik for at benytte lokationen", "user": "Bruger", "user_has_been_deleted": "Denne bruger er slettet.", "user_id": "Bruger-ID", @@ -2077,6 +2076,7 @@ "view_next_asset": "Se næste medie", "view_previous_asset": "Se forrige medie", "view_qr_code": "Vis QR kode", + "view_similar_photos": "Se lignende billeder", "view_stack": "Vis stak", "view_user": "Vis bruger", "viewer_remove_from_stack": "Fjern fra stak", diff --git a/i18n/de.json b/i18n/de.json index 7d1cfe3ed6..a655322c23 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Verwaltung der Upload-Einstellungen im Hintergrund und im Vordergrund", "backup_settings_subtitle": "Upload-Einstellungen verwalten", "backward": "Rückwärts", - "beta_sync": "Status der Beta-Synchronisierung", - "beta_sync_subtitle": "Verwalte das neue Synchronisierungssystem", "biometric_auth_enabled": "Biometrische Authentifizierung aktiviert", "biometric_locked_out": "Du bist von der biometrischen Authentifizierung ausgeschlossen", "biometric_no_options": "Keine biometrischen Optionen verfügbar", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Diese Funktion lädt externe Quellen von Google, um zu funktionieren.", "general": "Allgemein", - "geolocation_instruction_all_have_location": "Alle Dateien für dieses Daten enthalten bereits Standortangaben. Versuche alle Dateien anzuzeigen oder wähle ein anderes Datum", "geolocation_instruction_location": "Klicke auf eine Datei mit GPS Koordinaten um diesen Standort zu verwenden oder wähle einen Standort direkt auf der Karte", - "geolocation_instruction_no_date": "Wähle ein Datum um die Standortangaben der Fotos und Videos dieses Datums zu verwalten", - "geolocation_instruction_no_photos": "Keine Fotos oder Videos an diesem Datum gefunden. Wähle ein anderes Datum", "get_help": "Hilfe erhalten", "get_wifiname_error": "WLAN-Name konnte nicht ermittelt werden. Vergewissere dich, dass die erforderlichen Berechtigungen erteilt wurden und du mit einem WLAN-Netzwerk verbunden bist", "getting_started": "Erste Schritte", @@ -1645,6 +1640,7 @@ "restore_user": "Nutzer wiederherstellen", "restored_asset": "Datei wiederhergestellt", "resume": "Fortsetzen", + "resume_paused_jobs": "{count, plural, one {# Aufgabe fortsetzen } other {# Aufgaben fortsetzen}}", "retry_upload": "Upload wiederholen", "review_duplicates": "Duplikate überprüfen", "review_large_files": "Große Dateien überprüfen", @@ -1849,10 +1845,8 @@ "shift_to_permanent_delete": "Drücke ⇧, um die Datei endgültig zu löschen", "show_album_options": "Album-Optionen anzeigen", "show_albums": "Alben anzeigen", - "show_all_assets": "Alle Dateien anzeigen", "show_all_people": "Alle Personen anzeigen", "show_and_hide_people": "Personen ein- & ausblenden", - "show_assets_without_location": "Zeige Dateien ohne Ortsangabe", "show_file_location": "Dateispeicherort anzeigen", "show_gallery": "Galerie anzeigen", "show_hidden_people": "Ausgeblendete Personen anzeigen", @@ -2037,7 +2031,6 @@ "use_biometric": "Biometrie verwenden", "use_current_connection": "aktuelle Verbindung verwenden", "use_custom_date_range": "Stattdessen einen benutzerdefinierten Datumsbereich verwenden", - "use_this_location": "Klicken um Ort zu verwenden", "user": "Nutzer", "user_has_been_deleted": "Dieser Benutzer wurde gelöscht.", "user_id": "Nutzer-ID", diff --git a/i18n/el.json b/i18n/el.json index 1394c24ec9..2dff77a48f 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -594,8 +594,6 @@ "backup_setting_subtitle": "Διαχείριση ρυθμίσεων μεταφόρτωσης στο παρασκήνιο και στο προσκήνιο", "backup_settings_subtitle": "Διαχείριση των ρυθμίσεων μεταφόρτωσης", "backward": "Προς τα πίσω", - "beta_sync": "Κατάσταση Συγχρονισμού Beta (δοκιμαστική)", - "beta_sync_subtitle": "Διαχείριση του νέου συστήματος συγχρονισμού", "biometric_auth_enabled": "Βιομετρική ταυτοποίηση ενεργοποιήθηκε", "biometric_locked_out": "Είστε κλειδωμένοι εκτός της βιομετρικής ταυτοποίησης", "biometric_no_options": "Δεν υπάρχουν διαθέσιμοι τρόποι βιομετρικής ταυτοποίησης", diff --git a/i18n/es.json b/i18n/es.json index cffc32d5bd..0899a86156 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Administra las configuraciones de respaldo en segundo y primer plano", "backup_settings_subtitle": "Configura las opciones de subida", "backward": "Retroceder", - "beta_sync": "Estado de Sincronización Beta", - "beta_sync_subtitle": "Administrar el nuevo sistema de sincronización", "biometric_auth_enabled": "Autentificación biométrica habilitada", "biometric_locked_out": "Estás bloqueado de la autentificación biométrica", "biometric_no_options": "Sin opciones biométricas disponibles", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Esta funcionalidad carga recursos externos desde Google para poder funcionar.", "general": "General", - "geolocation_instruction_all_have_location": "Todos los assets de esta fecha ya tienen datos de ubicación. Prueba mostrando todos los assets o selecciona otra fecha", "geolocation_instruction_location": "Da click en un asset con coordenadas GPS para usar su ubicacion, o selecciona una ubicacion directamente en el mapa", - "geolocation_instruction_no_date": "Seleccione una fecha para administrar los datos de ubicación de las fotos y los videos de ese día", - "geolocation_instruction_no_photos": "No se encontraron fotos ni vídeos para esta fecha. Seleccione otra fecha para mostrarlos", "get_help": "Solicitar ayuda", "get_wifiname_error": "No se pudo obtener el nombre de la red Wi-Fi. Asegúrate de haber concedido los permisos necesarios y de estar conectado a una red Wi-Fi", "getting_started": "Comenzamos", @@ -1417,10 +1412,12 @@ "open_the_search_filters": "Abre los filtros de búsqueda", "options": "Opciones", "or": "o", + "organize_into_albums": "Organizar en álbumes", + "organize_into_albums_description": "Añade fotos existentes en álbumes usando la configuración actual de sincronización", "organize_your_library": "Organiza tu biblioteca", "original": "original", "other": "Otro", - "other_devices": "Otro dispositivo", + "other_devices": "Otros dispositivos", "other_entities": "Otras entidades", "other_variables": "Otras variables", "owned": "Propios", @@ -1557,6 +1554,7 @@ "purchase_server_description_2": "Estado del soporte", "purchase_server_title": "Servidor", "purchase_settings_server_activated": "La clave del producto del servidor la administra el administrador", + "query_asset_id": "Consultar ID de elemento", "queue_status": "Poniendo en cola {count}/{total}", "rating": "Valoración", "rating_clear": "Borrar calificación", @@ -1623,7 +1621,7 @@ "require_password": "Contraseña requerida", "require_user_to_change_password_on_first_login": "Requerir que el usuario cambie la contraseña en el primer inicio de sesión", "rescan": "Volver a escanear", - "reset": "Reiniciar", + "reset": "Restablecer", "reset_password": "Restablecer la contraseña", "reset_people_visibility": "Restablecer la visibilidad de las personas", "reset_pin_code": "Restablecer PIN", @@ -1642,6 +1640,7 @@ "restore_user": "Restaurar usuario", "restored_asset": "Archivo restaurado", "resume": "Continuar", + "resume_paused_jobs": "Reanudar {count, plural, one {# tarea en pausa} other {# tareas en pausa}}", "retry_upload": "Reintentar subida", "review_duplicates": "Revisar duplicados", "review_large_files": "Revisar archivos grandes", @@ -1735,7 +1734,7 @@ "select_user_for_sharing_page_err_album": "Fallo al crear el álbum", "selected": "Seleccionado", "selected_count": "{count, plural, one {# seleccionado} other {# seleccionados}}", - "selected_gps_coordinates": "coordenadas gps seleccionadas", + "selected_gps_coordinates": "Coordenadas GPS seleccionadas", "send_message": "Enviar mensaje", "send_welcome_email": "Enviar correo de bienvenida", "server_endpoint": "Punto final del servidor", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "presiona ⇧ para eliminar permanentemente el archivo", "show_album_options": "Mostrar opciones del álbum", "show_albums": "Mostrar álbumes", - "show_all_assets": "Mostrar todos los assets", "show_all_people": "Mostrar todas las personas", "show_and_hide_people": "Mostrar y ocultar personas", - "show_assets_without_location": "Mostrar assets sin ubicación", "show_file_location": "Mostrar carpeta del archivo", "show_gallery": "Ver galería", "show_hidden_people": "Mostrar personas ocultas", @@ -2034,7 +2031,6 @@ "use_biometric": "Uso biométrico", "use_current_connection": "Usar conexión actual", "use_custom_date_range": "Usa un intervalo de fechas personalizado", - "use_this_location": "Click para usar ubicación", "user": "Usuario", "user_has_been_deleted": "Este usuario ha sido eliminado.", "user_id": "Id. de usuario", @@ -2077,6 +2073,7 @@ "view_next_asset": "Mostrar siguiente elemento", "view_previous_asset": "Mostrar elemento anterior", "view_qr_code": "Ver código QR", + "view_similar_photos": "Ver fotografías similares", "view_stack": "Ver Pila", "view_user": "Ver Usuario", "viewer_remove_from_stack": "Quitar de la pila", diff --git a/i18n/et.json b/i18n/et.json index 2b4e94c3cd..33a003bd54 100644 --- a/i18n/et.json +++ b/i18n/et.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Halda taustal ja esiplaanil üleslaadimise seadeid", "backup_settings_subtitle": "Halda üleslaadimise seadeid", "backward": "Tagasi", - "beta_sync": "Beeta sünkroonimise staatus", - "beta_sync_subtitle": "Halda uut sünkroonimissüsteemi", "biometric_auth_enabled": "Biomeetriline autentimine lubatud", "biometric_locked_out": "Biomeetriline autentimine on blokeeritud", "biometric_no_options": "Biomeetrilisi valikuid ei ole", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "See funktsionaalsus laadib töötamiseks Google'st väliseid ressursse.", "general": "Üldine", - "geolocation_instruction_all_have_location": "Kõigil selle kuupäevaga üksustel on juba asukoht. Proovi kuvada kõiki üksuseid või vali teine kuupäev", "geolocation_instruction_location": "Klõpsa GPS-koordinaatidega üksusel, et kasutada selle asukohta, või vali asukoht otse kaardilt", - "geolocation_instruction_no_date": "Vali kuupäev, et kõigi selle kuupäevaga fotode ja videote asukohti hallata", - "geolocation_instruction_no_photos": "Selle kuupäevaga fotosid ja videosid ei leitud. Vali mõni muu kuupäev", "get_help": "Küsi abi", "get_wifiname_error": "WiFi-võrgu nime ei õnnestunud lugeda. Veendu, et oled andnud vajalikud load ja oled WiFi-võrguga ühendatud", "getting_started": "Alustamine", @@ -1520,7 +1515,7 @@ "profile_drawer_client_out_of_date_minor": "Mobiilirakendus on aegunud. Palun uuenda uusimale väikesele versioonile.", "profile_drawer_client_server_up_to_date": "Klient ja server on uuendatud", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "Kirjutuskaitserežiim sisse lülitatud. Väljumiseks topeltpuuduta avatari ikooni.", + "profile_drawer_readonly_mode": "Kirjutuskaitserežiim sisse lülitatud. Väljumiseks puuduta pikalt avatari ikooni.", "profile_drawer_server_out_of_date_major": "Server on aegunud. Palun uuenda uusimale suurele versioonile.", "profile_drawer_server_out_of_date_minor": "Server on aegunud. Palun uuenda uusimale väikesele versioonile.", "profile_image_of_user": "Kasutaja {user} profiilipilt", @@ -1644,6 +1639,7 @@ "restore_user": "Taasta kasutaja", "restored_asset": "Üksus taastatud", "resume": "Jätka", + "resume_paused_jobs": "Jätka {count, plural, one {# peatatud tööde} other {# peatatud töödet}}", "retry_upload": "Proovi üleslaadimist uuesti", "review_duplicates": "Vaata duplikaadid läbi", "review_large_files": "Vaata suured failid läbi", @@ -1848,10 +1844,8 @@ "shift_to_permanent_delete": "vajuta ⇧, et üksus jäädavalt kustutada", "show_album_options": "Näita albumi valikuid", "show_albums": "Näita albumeid", - "show_all_assets": "Kuva kõik üksused", "show_all_people": "Näita kõiki isikuid", "show_and_hide_people": "Näita ja peida isikuid", - "show_assets_without_location": "Kuva ilma asukohata üksused", "show_file_location": "Näita faili asukohta", "show_gallery": "Näita galeriid", "show_hidden_people": "Kuva peidetud inimesed", @@ -1922,6 +1916,8 @@ "sync_albums_manual_subtitle": "Sünkrooni kõik üleslaaditud videod ja fotod valitud varundusalbumitesse", "sync_local": "Sünkrooni lokaalsed üksused", "sync_remote": "Sünkrooni kaugüksused", + "sync_status": "Sünkroonimise staatus", + "sync_status_subtitle": "Vaata ja halda sünkroonimissüsteemi", "sync_upload_album_setting_subtitle": "Loo ja laadi oma pildid ja videod üles Immich'isse valitud albumitesse", "tag": "Silt", "tag_assets": "Sildista üksuseid", @@ -1959,7 +1955,9 @@ "to_change_password": "Muuda parool", "to_favorite": "Lemmik", "to_login": "Logi sisse", + "to_multi_select": "vali mitu", "to_parent": "Tase üles", + "to_select": "vali", "to_trash": "Prügikasti", "toggle_settings": "Kuva/peida seaded", "total": "Kokku", @@ -1979,6 +1977,7 @@ "trash_page_select_assets_btn": "Vali üksused", "trash_page_title": "Prügikast ({count})", "trashed_items_will_be_permanently_deleted_after": "Prügikasti tõstetud üksused kustutatakse jäädavalt {days, plural, one {# päeva} other {# päeva}} pärast.", + "troubleshoot": "Tõrkeotsing", "type": "Tüüp", "unable_to_change_pin_code": "PIN-koodi muutmine ebaõnnestus", "unable_to_setup_pin_code": "PIN-koodi seadistamine ebaõnnestus", @@ -2034,7 +2033,6 @@ "use_biometric": "Kasuta biomeetriat", "use_current_connection": "kasuta praegust ühendust", "use_custom_date_range": "Kasuta kohandatud kuupäevavahemikku", - "use_this_location": "Klõpsa asukoha kasutamiseks", "user": "Kasutaja", "user_has_been_deleted": "See kasutaja on kustutatud.", "user_id": "Kasutaja ID", diff --git a/i18n/eu.json b/i18n/eu.json index 311619ad90..633476a2d7 100644 --- a/i18n/eu.json +++ b/i18n/eu.json @@ -38,6 +38,58 @@ "admin": { "add_exclusion_pattern_description": "Gehitu baztertze patroiak. *, ** eta ? karakterak erabil ditzazkezu (globbing). Adibideak: \"Raw\" izeneko edozein direktorioko fitxategi guztiak baztertzeko, erabili \"**/Raw/**\". \".tif\" amaitzen diren fitxategi guztiak baztertzeko, erabili \"**/*.tif\". Bide absolutu bat baztertzeko, erabili \"/baztertu/beharreko/bidea/**\".", "admin_user": "Administradore erabiltzailea", - "image_quality": "Kalitatea" - } + "authentication_settings": "Segurtasun Ezarpenak", + "authentication_settings_description": "Kudeatu pasahitza, OAuth edo beste segurtasun konfigurazio bat", + "authentication_settings_disable_all": "Seguru zaude saioa hasteko modu guztiak desgaitu nahi dituzula? Saioa hastea guztiz desgaitua izango da.", + "authentication_settings_reenable": "Berriro gaitzeko, erabili Server Command.", + "backup_onboarding_footer": "Immich-en babes kopiei buruzko informazio gehiago nahi baduzu, mesedez irakurri dokumentazioa.", + "backup_onboarding_title": "Babes Kopiak", + "confirm_delete_library": "Seguru zaude {library} ezabatu nahi duzula?", + "confirm_email_below": "Konfirmatzeko, idatzi \"{email}\" azpian", + "confirm_reprocess_all_faces": "Seguru zaude aurpegi guztiak berriro prozesatu nahi dituzula? Erabakiak jendearen izenak ere borratuko ditu.", + "confirm_user_password_reset": "Seguru zaude {user}-ren pasahitza berrezarri nahi duzula?", + "confirm_user_pin_code_reset": "Seguru zaude {user}-ren PIN kodea berrezarri nahi duzula?", + "create_job": "Gehitu zeregina", + "disable_login": "Desgaitu saio hastea", + "face_detection": "Aurpegi detekzioa", + "failed_job_command": "{command} komandoak hutsegin du {job} zereginerako", + "image_format": "Formatua", + "image_format_description": "WebP ereduak JPEG baino fitxategi txikiagoak sortzen ditu, baina motelagoa da kodifikatzen.", + "image_preview_title": "Aurreikusiaen Konfigurazioa", + "image_quality": "Kalitatea", + "image_settings": "Argazkien Konfigurazioa", + "image_thumbnail_title": "Argazki Txikien Konfigurazioa", + "job_created": "Zeregina sortuta", + "job_settings": "Zereginaren konfigurazioa", + "job_status": "Zereginaren Egoera", + "machine_learning_smart_search_enabled": "Gaitu bilaketa arina", + "manage_log_settings": "Kudeatu erregistroen konfigurazioa", + "map_dark_style": "Beltz estiloa", + "map_gps_settings": "Mapa eta GPS Konfigurazioa", + "map_light_style": "Zuri estiloa", + "map_settings": "Mapa", + "metadata_faces_import_setting": "Gaitu aurpegien inportazioa", + "metadata_settings": "Metadata Konfigurazioa", + "metadata_settings_description": "Kudeatu metadaten konfigurazioa", + "migration_job": "Migrazio" + }, + "advanced_settings_readonly_mode_title": "Irakurri-bakarrik Modua", + "apply_count": "Ezarri ({count, number})", + "assets_added_to_albums_count": "Gehituta {assetTotal, plural, one {# asset} other {# assets}} to {albumTotal, plural, one {# album} other {# albums}}", + "assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} ezin izan da albumetara gehitu", + "assets_were_part_of_albums_count": "{count, plural, one {Asset was} other {Assets were}} dagoeneko albumean dago", + "first": "Lehenengo «Lehenik»", + "gps": "GPS", + "gps_missing": "Ez dago GPS", + "last": "Azkena", + "like": "Gustoko", + "manage_geolocation": "Kudeatu kokapena", + "organize_into_albums": "Albumetan antolatu", + "query_asset_id": "Aztertu aukeratutako ID-a", + "readonly_mode_disabled": "Irakurri-bakarrik modua desgaituta", + "readonly_mode_enabled": "Irakurri-bakarrik modua gaituta", + "selected_gps_coordinates": "GPS Koordenadak Aukeratuta", + "sort_newest": "Argazkirik berriena", + "to_select": "aukeratzeko", + "view_similar_photos": "Ikusi antzeko argazkiak" } diff --git a/i18n/fi.json b/i18n/fi.json index 2ed504b87c..2451b30a4b 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -396,6 +396,7 @@ "advanced_settings_prefer_remote_title": "Suosi etäkuvia", "advanced_settings_proxy_headers_subtitle": "Määritä välityspalvelimen otsikot(proxy headers), jotka Immichin tulisi lähettää jokaisen verkkopyynnön mukana", "advanced_settings_proxy_headers_title": "Välityspalvelimen otsikot", + "advanced_settings_readonly_mode_subtitle": "Aktivoi vain luku -tilan, jolloin valokuvia voi ainoastaan selata. Toiminnot kuten useiden kuvien valitseminen, jakaminen, siirtäminen toistolaitteelle ja poistaminen ovat pois käytöstä. Laita vain luku -tila päälle tai pois päältä päävalikon käyttäjäkuvakkeesta", "advanced_settings_readonly_mode_title": "Vain luku -tila", "advanced_settings_self_signed_ssl_subtitle": "Ohita SSL sertifikaattivarmennus palvelimen päätepisteellä. Vaaditaan self-signed -sertifikaateissa.", "advanced_settings_self_signed_ssl_title": "Salli self-signed SSL -sertifikaatit", @@ -462,6 +463,7 @@ "app_bar_signout_dialog_title": "Kirjaudu ulos", "app_settings": "Sovellusasetukset", "appears_in": "Esiintyy albumeissa", + "apply_count": "Aseta {count, number}", "archive": "Arkisto", "archive_action_prompt": "{count} lisätty arkistoon", "archive_or_unarchive_photo": "Arkistoi kuva tai palauta arkistosta", @@ -595,8 +597,6 @@ "backup_setting_subtitle": "Hallinnoi aktiivisia ja taustalla olevia lähetysasetuksia", "backup_settings_subtitle": "Hallitse lähetysasetuksia", "backward": "Taaksepäin", - "beta_sync": "Betasynkronoinnin tila", - "beta_sync_subtitle": "Hallitse uutta synkronointijärjestelmää", "biometric_auth_enabled": "Biometrinen tunnistautuminen käytössä", "biometric_locked_out": "Sinulta on evätty pääsy biometriseen tunnistautumiseen", "biometric_no_options": "Ei biometrisiä vaihtoehtoja", @@ -1074,6 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Ominaisuus lataa ulkoisia resursseja Googlelta toimiakseen.", "general": "Yleinen", + "geolocation_instruction_location": "Napsauta kuvaa, jossa on GPS-koordinaatit, käyttääksesi sen sijaintia, tai valitse sijainti suoraan kartalta", "get_help": "Hae apua", "get_wifiname_error": "Wi-Fi-verkon nimen hakeminen epäonnistui. Varmista, että olet myöntänyt tarvittavat käyttöoikeudet ja että olet yhteydessä Wi-Fi-verkkoon", "getting_started": "Aloittaminen", diff --git a/i18n/fr.json b/i18n/fr.json index 7d30f4e25f..9bed289cab 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Ajuster les paramètres d'envoi au premier et en arrière-plan", "backup_settings_subtitle": "Gérer les paramètres de téléversement", "backward": "Arrière", - "beta_sync": "Statut de la synchronisation béta", - "beta_sync_subtitle": "Gérer le nouveau système de synchronisation", "biometric_auth_enabled": "Authentification biométrique activée", "biometric_locked_out": "L'authentification biométrique est verrouillé", "biometric_no_options": "Aucune option biométrique disponible", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Diffusion Google Cast", "gcast_enabled_description": "Cette fonctionnalité charge des ressources externes depuis Google pour fonctionner.", "general": "Général", - "geolocation_instruction_all_have_location": "Tous les médias pour cette date ont déjà des données de localisation. Essayez d'afficher tous les médias ou sélectionnez une date différente", "geolocation_instruction_location": "Cliquez sur un média avec des coordonnées GPS pour utiliser sa localisation, ou bien sélectionnez une localisation directement sur la carte", - "geolocation_instruction_no_date": "Sélectionnez une date pour gérer les données de localisation pour les photos et vidéos de ce jour", - "geolocation_instruction_no_photos": "Aucune photo ou vidéo trouvée pour cette date. Sélectionnez une date différente pour en afficher", "get_help": "Obtenir de l'aide", "get_wifiname_error": "Impossible d'obtenir le nom du réseau wifi. Assurez-vous d'avoir donné les permissions nécessaires à l'application et que vous êtes connecté à un réseau wifi", "getting_started": "Commencer", @@ -1520,7 +1515,7 @@ "profile_drawer_client_out_of_date_minor": "L'application mobile est obsolète. Veuillez effectuer la mise à jour vers la dernière version mineure.", "profile_drawer_client_server_up_to_date": "Le client et le serveur sont à jour", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "Mode lecture seule activé. Faites un appui double sur l'image de l'utilisateur pour quitter.", + "profile_drawer_readonly_mode": "Mode lecture seule activé. Faites un appui long sur l'image de l'utilisateur pour quitter.", "profile_drawer_server_out_of_date_major": "Le serveur est obsolète. Veuillez mettre à jour vers la dernière version majeure.", "profile_drawer_server_out_of_date_minor": "Le serveur est obsolète. Veuillez mettre à jour vers la dernière version mineure.", "profile_image_of_user": "Image de profil de {user}", @@ -1559,7 +1554,7 @@ "purchase_server_description_2": "Statut de contributeur", "purchase_server_title": "Serveur", "purchase_settings_server_activated": "La clé du produit pour le Serveur est gérée par l'administrateur", - "query_asset_id": "ID du média requis", + "query_asset_id": "Obtenir l'ID du média", "queue_status": "{count}/{total} en file d'attente", "rating": "Étoile d'évaluation", "rating_clear": "Effacer l'évaluation", @@ -1645,6 +1640,7 @@ "restore_user": "Restaurer l'utilisateur", "restored_asset": "Média restauré", "resume": "Reprendre", + "resume_paused_jobs": "Reprendre {count, plural, one {la tâche en cours} other {les # tâches en cours}}", "retry_upload": "Réessayer l'envoi", "review_duplicates": "Consulter les doublons", "review_large_files": "Consulter les fichiers volumineux", @@ -1849,10 +1845,8 @@ "shift_to_permanent_delete": "appuyez sur ⇧ pour supprimer définitivement le média", "show_album_options": "Afficher les options de l'album", "show_albums": "Montrer les albums", - "show_all_assets": "Montrer tous les médias", "show_all_people": "Montrer toutes les personnes", "show_and_hide_people": "Afficher / Masquer les personnes", - "show_assets_without_location": "Montrer les médias sans localisation", "show_file_location": "Afficher l'emplacement du fichier", "show_gallery": "Afficher la galerie", "show_hidden_people": "Afficher les personnes masquées", @@ -1923,6 +1917,8 @@ "sync_albums_manual_subtitle": "Synchroniser toutes les vidéos et photos envoyées dans les albums sélectionnés", "sync_local": "Synchronisation locale", "sync_remote": "Synchronisation à distance", + "sync_status": "Statut de synchronisation", + "sync_status_subtitle": "Consulter et gérer le système de synchronisation", "sync_upload_album_setting_subtitle": "Créez et envoyez vos photos et vidéos dans les albums sélectionnés sur Immich", "tag": "Étiquette", "tag_assets": "Étiqueter les médias", @@ -1982,6 +1978,7 @@ "trash_page_select_assets_btn": "Sélectionner les éléments", "trash_page_title": "Corbeille ({count})", "trashed_items_will_be_permanently_deleted_after": "Les éléments dans la corbeille seront supprimés définitivement après {days, plural, one {# jour} other {# jours}}.", + "troubleshoot": "Dépannage", "type": "Type", "unable_to_change_pin_code": "Impossible de changer le code PIN", "unable_to_setup_pin_code": "Impossible de définir le code PIN", @@ -2037,7 +2034,6 @@ "use_biometric": "Utiliser l'authentification biométrique", "use_current_connection": "Utiliser le réseau actuel", "use_custom_date_range": "Utilisez une plage de date personnalisée à la place", - "use_this_location": "Cliquez pour utiliser la localisation", "user": "Utilisateur", "user_has_been_deleted": "Cet utilisateur à été supprimé.", "user_id": "ID Utilisateur", @@ -2080,7 +2076,7 @@ "view_next_asset": "Voir le média suivant", "view_previous_asset": "Voir le média précédent", "view_qr_code": "Voir le QR code", - "view_similar_photos": "Voir les photos similaires", + "view_similar_photos": "Afficher les photos similaires", "view_stack": "Afficher la pile", "view_user": "Voir l'utilisateur", "viewer_remove_from_stack": "Retirer de la pile", diff --git a/i18n/he.json b/i18n/he.json index efebd99564..ff79065eab 100644 --- a/i18n/he.json +++ b/i18n/he.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "ניהול הגדרות העלאת רקע וחזית", "backup_settings_subtitle": "נהל הגדרות העלאה", "backward": "אחורה", - "beta_sync": "סטטוס סנכרון (בטא)", - "beta_sync_subtitle": "נהל את מערכת הסנכרון החדשה", "biometric_auth_enabled": "אימות ביומטרי הופעל", "biometric_locked_out": "גישה לאימות הביומטרי נחסמה", "biometric_no_options": "אין אפשרויות זמינות עבור אימות ביומטרי", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "תכונה זאת טוענת משאבים חיצוניים מגוגל בכדי לפעול.", "general": "כללי", - "geolocation_instruction_all_have_location": "לכל הפריטים עבור תאריך זה כבר יש נתוני מיקום. נסה להציג את כל הפריטים או בחר תאריך אחר", "geolocation_instruction_location": "לחץ על פריט עם קואורדינטות GPS כדי להשתמש במיקומו, או בחר מיקום ישירות מהמפה", - "geolocation_instruction_no_date": "בחר תאריך כדי לנהל נתוני מיקום עבור תמונות וסרטונים מאותו יום", - "geolocation_instruction_no_photos": "לא נמצאו תמונות או סרטונים עבור תאריך זה. בחר תאריך אחר כדי להציג אותם", "get_help": "קבל עזרה", "get_wifiname_error": "לא היה ניתן לקבל את שם האינטרנט האלחוטי שלך. יש לודא שהענקת את ההרשאות הדרושות ושאת/ה מחובר/ת לרשת אינטרנט אלחוטי", "getting_started": "תחילת העבודה", @@ -1846,10 +1841,8 @@ "shift_to_permanent_delete": "לחץ ⇧ כדי למחוק תמונה לצמיתות", "show_album_options": "הצג אפשרויות אלבום", "show_albums": "הצג אלבומים", - "show_all_assets": "הצג את כל הפריטים", "show_all_people": "הצג את כל האנשים", "show_and_hide_people": "הצג & הסתר אנשים", - "show_assets_without_location": "הצג פריטים ללא מיקום", "show_file_location": "הצג את מיקום הקובץ", "show_gallery": "הצג גלריה", "show_hidden_people": "הצג אנשים מוסתרים", @@ -2034,7 +2027,6 @@ "use_biometric": "השתמש באימות ביומטרי", "use_current_connection": "השתמש בחיבור נוכחי", "use_custom_date_range": "השתמש בטווח תאריכים מותאם במקום", - "use_this_location": "לחץ כדי להשתמש במיקום", "user": "משתמש", "user_has_been_deleted": "משתמש זה נמחק.", "user_id": "מזהה משתמש", diff --git a/i18n/hi.json b/i18n/hi.json index 9662eb1203..90795e7330 100644 --- a/i18n/hi.json +++ b/i18n/hi.json @@ -585,8 +585,6 @@ "backup_setting_subtitle": "पृष्ठभूमि और अग्रभूमि अपलोड सेटिंग प्रबंधित करें", "backup_settings_subtitle": "अपलोड सेटिंग्स संभालें", "backward": "पिछला", - "beta_sync": "बीटा सिंक स्थिति", - "beta_sync_subtitle": "नए सिंक सिस्टम का प्रबंधन करें", "biometric_auth_enabled": "बायोमेट्रिक प्रमाणीकरण सक्षम", "biometric_locked_out": "आप बायोमेट्रिक प्रमाणीकरण से बाहर हैं", "biometric_no_options": "कोई बायोमेट्रिक विकल्प उपलब्ध नहीं है", diff --git a/i18n/hr.json b/i18n/hr.json index 036078f23a..329ff1438d 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -594,8 +594,6 @@ "backup_setting_subtitle": "Upravljajte postavkama učitavanja u pozadini i prvom planu", "backup_settings_subtitle": "Upravljaj postavkama slanja", "backward": "Unazad", - "beta_sync": "Beta status sinkronizacije", - "beta_sync_subtitle": "Upravljaj novim sustavom sinkronizacije", "biometric_auth_enabled": "Biometrijska autentikacija omogućena", "biometric_locked_out": "Zaključani ste iz biometrijske autentikacije", "biometric_no_options": "Nema dostupnih biometrijskih opcija", diff --git a/i18n/hu.json b/i18n/hu.json index 490833a794..598910789d 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -594,8 +594,6 @@ "backup_setting_subtitle": "A háttérben és előtérben mentés beállításainak kezelése", "backup_settings_subtitle": "Feltöltés beállításai", "backward": "Visszafele", - "beta_sync": "Béta Szinkronizálás Állapota", - "beta_sync_subtitle": "Az új szinkronizálási rendszer kezelése", "biometric_auth_enabled": "Biometrikus azonosítás engedélyezve", "biometric_locked_out": "Ki vagy zárva a biometrikus azonosításból", "biometric_no_options": "Nincsen elérhető biometrikus azonosítás", diff --git a/i18n/id.json b/i18n/id.json index dd9e8e009e..abe57386ac 100644 --- a/i18n/id.json +++ b/i18n/id.json @@ -28,7 +28,9 @@ "add_to_album": "Tambahkan ke album", "add_to_album_bottom_sheet_added": "Ditambahkan ke {album}", "add_to_album_bottom_sheet_already_exists": "Sudah ada di {album}", + "add_to_album_toggle": "Masukkan ke {album} / Batalkan dari {album}", "add_to_albums": "Tambahkan ke album", + "add_to_albums_count": "Tambahkan ke album ({count})", "add_to_shared_album": "Tambahkan ke album terbagi", "add_url": "Tambahkan URL", "added_to_archive": "Ditambahkan ke arsip", @@ -394,6 +396,8 @@ "advanced_settings_prefer_remote_title": "Prioritaskan gambar dari server", "advanced_settings_proxy_headers_subtitle": "Tentukan header proxy yang harus dikirim Immich dengan setiap permintaan jaringan", "advanced_settings_proxy_headers_title": "Tajuk Proksi", + "advanced_settings_readonly_mode_subtitle": "Mengaktifkan mode baca-saja, di mana foto hanya bisa dilihat. Fitur seperti memilih banyak foto, berbagi, cast, dan hapus akan dinonaktifkan. Mode baca-saja bisa diaktifkan/nonaktifkan lewat avatar pengguna di layar utama", + "advanced_settings_readonly_mode_title": "Mode Baca-Saja", "advanced_settings_self_signed_ssl_subtitle": "Melewati verifikasi sertifikat SSL untuk titik akhir server. Diperlukan untuk sertifikat yang ditandatangani sendiri.", "advanced_settings_self_signed_ssl_title": "Izinkan sertifikat SSL yang ditandatangani sendiri", "advanced_settings_sync_remote_deletions_subtitle": "Hapus atau pulihkan aset pada perangkat ini secara otomatis ketika tindakan dilakukan di web", @@ -459,6 +463,7 @@ "app_bar_signout_dialog_title": "Keluar akun", "app_settings": "Pengaturan Aplikasi", "appears_in": "Muncul dalam", + "apply_count": "Terapkan ({count, number})", "archive": "Arsip", "archive_action_prompt": "{count} telah ditambahkan ke Arsip", "archive_or_unarchive_photo": "Arsipkan atau batalkan pengarsipan foto", @@ -498,7 +503,9 @@ "assets": "Aset", "assets_added_count": "{count, plural, one {# aset} other {# aset}} ditambahkan", "assets_added_to_album_count": "Ditambahkan {count, plural, one {# aset} other {# aset}} ke album", + "assets_added_to_albums_count": "Ditambahkan {assetTotal, plural, one {# aset} other {# aset}} ke {albumTotal, plural, one {# album} other {# album}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} tidak dapat ditambahkan ke album", + "assets_cannot_be_added_to_albums": "{count, plural, one {Aset} other {Aset}} tidak dapat ditambahkan ke album mana pun", "assets_count": "{count, plural, one {# aset} other {# aset}}", "assets_deleted_permanently": "{count} aset dihapus secara permanen", "assets_deleted_permanently_from_server": "{count} aset dihapus secara permanen dari server Immich", @@ -515,6 +522,7 @@ "assets_trashed_count": "{count, plural, one {# aset} other {# aset}} dibuang ke sampah", "assets_trashed_from_server": "{count} aset dipindahkan ke sampah dari server Immich", "assets_were_part_of_album_count": "{count, plural, one {Aset telah} other {Aset telah}} menjadi bagian dari album", + "assets_were_part_of_albums_count": "{count, plural, one {Aset sudah} other {Aset sudah}} ada di album", "authorized_devices": "Perangkat Terautentikasi", "automatic_endpoint_switching_subtitle": "Sambungkan secara lokal melalui Wi-Fi yang telah ditetapkan saat tersedia, dan gunakan koneksi alternatif lain", "automatic_endpoint_switching_title": "Peralihan URL otomatis", @@ -589,8 +597,6 @@ "backup_setting_subtitle": "Kelola pengaturan unggahan latar belakang dan latar depan", "backup_settings_subtitle": "Kelola pengaturan unggahan", "backward": "Maju", - "beta_sync": "Status proses sinkronisasi versi beta", - "beta_sync_subtitle": "Kelola sistem sinkronisasi baru", "biometric_auth_enabled": "Autentikasi biometrik diaktifkan", "biometric_locked_out": "Anda terkunci oleh autentikasi biometrik", "biometric_no_options": "Opsi biometrik tidak tersedia", @@ -1057,6 +1063,7 @@ "filter_people": "Saring orang", "filter_places": "Saring tempat", "find_them_fast": "Temukan dengan cepat berdasarkan nama dengan pencarian", + "first": "Pertama", "fix_incorrect_match": "Perbaiki pencocokan salah", "folder": "Berkas", "folder_not_found": "Berkas tidak ditemukan", @@ -1067,12 +1074,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Fitur ini memuat sumber daya eksternal dari Google agar dapat berfungsi.", "general": "Umum", + "geolocation_instruction_location": "Klik aset yang memiliki koordinat GPS untuk menggunakan lokasinya, atau pilih lokasi langsung dari peta", "get_help": "Dapatkan Bantuan", "get_wifiname_error": "Tidak dapat mendapatkan nama Wi-Fi. Pastikan Anda telah memberikan izin yang diperlukan dan terhubung ke jaringan Wi-Fi", "getting_started": "Memulai", "go_back": "Kembali", "go_to_folder": "Pergi ke folder", "go_to_search": "Pergi ke pencarian", + "gps": "GPS", + "gps_missing": "Tidak ada GPS", "grant_permission": "Izinkan", "group_albums_by": "Kelompokkan album berdasarkan...", "group_country": "Kelompokkan berdasarkan negara", @@ -1178,6 +1188,7 @@ "language_search_hint": "Mencari Bahasa...", "language_setting_description": "Pilih bahasa Anda yang disukai", "large_files": "File Besar", + "last": "Terakhir", "last_seen": "Terakhir dilihat", "latest_version": "Versi Terkini", "latitude": "Lintang", @@ -1196,6 +1207,7 @@ "library_page_sort_title": "Judul album", "licenses": "Lisensi", "light": "Terang", + "like": "Suka", "like_deleted": "Suka dihapus", "link_motion_video": "Tautan video gerak", "link_to_oauth": "Tautkan ke OAuth", @@ -1254,6 +1266,7 @@ "main_branch_warning": "Anda menggunakan versi pengembangan; kami sangat menyarankan menggunakan versi rilis!", "main_menu": "Menu utama", "make": "Merek", + "manage_geolocation": "Atur lokasi", "manage_shared_links": "Kelola tautan terbagi", "manage_sharing_with_partners": "Kelola pembagian dengan partner", "manage_the_app_settings": "Kelola pengaturan aplikasi", @@ -1399,6 +1412,8 @@ "open_the_search_filters": "Buka saringan pencarian", "options": "Opsi", "or": "atau", + "organize_into_albums": "Atur ke dalam album", + "organize_into_albums_description": "Masukkan foto lama ke album sesuai pengaturan sinkronisasi", "organize_your_library": "Kelola pustaka Anda", "original": "asli", "other": "Lainnya", @@ -1458,9 +1473,9 @@ "permission_onboarding_permission_limited": "Izin dibatasi. Agai Immich dapat mencadangkan dan mengatur seluruh koleksi galeri, izinkan akses foto dan video pada Setelan.", "permission_onboarding_request": "Immich memerlukan izin untuk melihat foto dan video kamu.", "person": "Orang", - "person_age_months": "{months} bulan", - "person_age_year_months": "1 tahun, {months} bulan", - "person_age_years": "{years} tahun", + "person_age_months": "{months, plural, one {# bulan} other {# bulan}} old", + "person_age_year_months": "1 year, {months, plural, one {# bulan} other {# bulan}} old", + "person_age_years": "{years, plural, other {# tahun}} old", "person_birthdate": "Lahir pada {date}", "person_hidden": "{name}{hidden, select, true { (tersembunyi)} other {}}", "photo_shared_all_users": "Sepertinya Anda membagikan foto Anda dengan semua pengguna atau Anda tidak memiliki pengguna siapa pun untuk dibagikan.", @@ -1500,6 +1515,7 @@ "profile_drawer_client_out_of_date_minor": "Versi app seluler ini sudah kedaluwarsa. Silakan perbarui ke versi minor terbaru.", "profile_drawer_client_server_up_to_date": "Klien dan server menjalankan versi terbaru", "profile_drawer_github": "GitHub", + "profile_drawer_readonly_mode": "Mode baca-saja aktif. Ketuk dua kali ikon avatar pengguna untuk keluar.", "profile_drawer_server_out_of_date_major": "Versi server ini telah kedaluwarsa. Silakan perbarui ke versi major terbaru.", "profile_drawer_server_out_of_date_minor": "Versi server ini telah kedaluwarsa. Silakan perbarui ke versi minor terbaru.", "profile_image_of_user": "Foto profil dari {user}", @@ -1538,6 +1554,7 @@ "purchase_server_description_2": "Status pendukung", "purchase_server_title": "Server", "purchase_settings_server_activated": "Kunci produk server dikelola oleh admin", + "query_asset_id": "ID Aset Kueri", "queue_status": "Antrian {count}/{total}", "rating": "Peringkat bintang", "rating_clear": "Hapus peringkat", @@ -1545,6 +1562,8 @@ "rating_description": "Tampilkan peringkat EXIF pada panel info", "reaction_options": "Opsi reaksi", "read_changelog": "Baca Log Perubahan", + "readonly_mode_disabled": "Mode baca-saja dimatikan", + "readonly_mode_enabled": "Mode baca-saja diaktifkan", "reassign": "Tetapkan ulang", "reassigned_assets_to_existing_person": "Menetapkan ulang {count, plural, one {# aset} other {# aset}} kepada {name, select, null {orang yang sudah ada} other {{name}}}", "reassigned_assets_to_new_person": "Menetapkan ulang {count, plural, one {# aset} other {# aset}} kepada orang baru", @@ -1714,6 +1733,7 @@ "select_user_for_sharing_page_err_album": "Gagal membuat album", "selected": "Dipilih", "selected_count": "{count, plural, other {# dipilih}}", + "selected_gps_coordinates": "Koordinat GPS yang dipilih", "send_message": "Kirim pesan", "send_welcome_email": "Kirim surel selamat datang", "server_endpoint": "Endpoint server", @@ -1857,6 +1877,7 @@ "sort_created": "Tanggal dibuat", "sort_items": "Jumlah item", "sort_modified": "Tanggal diubah", + "sort_newest": "Foto terbaru", "sort_oldest": "Foto terlawas", "sort_people_by_similarity": "Urutkan orang berdasarkan kemiripan", "sort_recent": "Foto paling terkini", @@ -1932,7 +1953,9 @@ "to_change_password": "Ubah kata sandi", "to_favorite": "Favorit", "to_login": "Log masuk", + "to_multi_select": "untuk memilih beberapa", "to_parent": "Ke induk", + "to_select": "untuk memilih", "to_trash": "Sampah", "toggle_settings": "Saklar pengaturan", "total": "Jumlah", @@ -1982,6 +2005,7 @@ "unstacked_assets_count": "Penumpukan {count, plural, one {# aset} other {# aset}} dibatalkan", "untagged": "Tidak ditandai", "up_next": "Berikutnya", + "update_location_action_prompt": "Perbarui lokasi {count} aset yang dipilih dengan:", "updated_at": "Diperbarui", "updated_password": "Kata sandi diperbarui", "upload": "Unggah", @@ -2048,6 +2072,7 @@ "view_next_asset": "Tampilkan aset berikutnya", "view_previous_asset": "Tampilkan aset sebelumnya", "view_qr_code": "Tampilkan kode QR", + "view_similar_photos": "Lihat foto yang mirip", "view_stack": "Tampilkan Tumpukan", "view_user": "Lihat Pengguna", "viewer_remove_from_stack": "Keluarkan dari Tumpukan", diff --git a/i18n/it.json b/i18n/it.json index 0bf48a378c..46405d2f94 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Gestisci le impostazioni di upload in primo piano e in background", "backup_settings_subtitle": "Gestisci le impostazioni di caricamento", "backward": "Indietro", - "beta_sync": "Status sincronizzazione beta", - "beta_sync_subtitle": "Gestisci il nuovo sistema di sincronizzazione", "biometric_auth_enabled": "Autenticazione biometrica attivata", "biometric_locked_out": "Sei stato bloccato dall'autenticazione biometrica", "biometric_no_options": "Nessuna opzione biometrica disponibile", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast Abilitato", "gcast_enabled_description": "Questa funzione carica risorse esterne da Google per poter funzionare.", "general": "Generale", - "geolocation_instruction_all_have_location": "Tutte le risorse per questa data hanno già dati sulla posizione. Prova a mostrare tutte le risorse o seleziona una data diversa", "geolocation_instruction_location": "Fai clic su una risorsa con coordinate GPS per utilizzare la sua posizione oppure seleziona una posizione direttamente dalla mappa", - "geolocation_instruction_no_date": "Seleziona una data per gestire i dati sulla posizione per foto e video di quel giorno", - "geolocation_instruction_no_photos": "Nessuna foto o video trovato per questa data. Seleziona una data diversa per visualizzarli", "get_help": "Chiedi Aiuto", "get_wifiname_error": "Non sono riuscito a recuperare il nome della rete Wi-Fi. Accertati di aver concesso i permessi necessari e di essere connesso ad una rete Wi-Fi", "getting_started": "Iniziamo", @@ -1417,6 +1412,8 @@ "open_the_search_filters": "Apri filtri di ricerca", "options": "Opzioni", "or": "o", + "organize_into_albums": "Organizza all'interno degli albums", + "organize_into_albums_description": "Inserisci le foto esistenti all'interno degli albums utilizzando le attuale impostazioni di sincronizzazione", "organize_your_library": "Organizza la tua libreria", "original": "originale", "other": "Altro", @@ -1557,6 +1554,7 @@ "purchase_server_description_2": "Stato di Contributore", "purchase_server_title": "Server", "purchase_settings_server_activated": "La chiave del prodotto del server è gestita dall'amministratore", + "query_asset_id": "Esegui una query sull'ID dell'asset", "queue_status": "Messi in coda {count}/{total}", "rating": "Valutazione a stelle", "rating_clear": "Crea valutazione", @@ -1642,6 +1640,7 @@ "restore_user": "Ripristina utente", "restored_asset": "Asset ripristinato", "resume": "Riprendi", + "resume_paused_jobs": "Riprendi {count, plural, one {# processo in pausa} other {# i processi in pausa}}", "retry_upload": "Riprova caricamento", "review_duplicates": "Esamina duplicati", "review_large_files": "Revisiona file pesanti", @@ -1735,7 +1734,7 @@ "select_user_for_sharing_page_err_album": "Impossibile nel creare l'album", "selected": "Selezionato", "selected_count": "{count, plural, one {# selezionato} other {# selezionati}}", - "selected_gps_coordinates": "coordinate GPS selezionate", + "selected_gps_coordinates": "Coordinate GPS selezionate", "send_message": "Manda messaggio", "send_welcome_email": "Invia email di benvenuto", "server_endpoint": "Server endpoint", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "premi ⇧ per cancellare definitivamente l'asset", "show_album_options": "Mostra opzioni album", "show_albums": "Mostra gli album", - "show_all_assets": "Mostra tutte le risorse", "show_all_people": "Mostra tutte le persone", "show_and_hide_people": "Mostra & nascondi persone", - "show_assets_without_location": "Mostra risorse senza posizione", "show_file_location": "Mostra percorso file", "show_gallery": "Mostra galleria", "show_hidden_people": "Mostra persone nascoste", @@ -2034,7 +2031,6 @@ "use_biometric": "Usa biometrica", "use_current_connection": "usa la connessione attuale", "use_custom_date_range": "Altrimenti utilizza un intervallo date personalizzato", - "use_this_location": "Clicca per usare la posizione", "user": "Utente", "user_has_been_deleted": "L'utente è stato rimosso.", "user_id": "ID utente", @@ -2077,6 +2073,7 @@ "view_next_asset": "Visualizza risorsa successiva", "view_previous_asset": "Visualizza risorsa precedente", "view_qr_code": "Visualizza Codice QR", + "view_similar_photos": "Visualizza le foto simili", "view_stack": "Visualizza Raggruppamento", "view_user": "Visualizza Utente", "viewer_remove_from_stack": "Rimuovi dalla pila", diff --git a/i18n/ja.json b/i18n/ja.json index b7e484ca52..bed981bb83 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "アップロードに関する設定", "backup_settings_subtitle": "アップロード設定を管理", "backward": "新しい方へ", - "beta_sync": "同期の状態", - "beta_sync_subtitle": "同期の仕組みを管理", "biometric_auth_enabled": "生体認証を有効化しました", "biometric_locked_out": "生体認証により、アクセスできません", "biometric_no_options": "生体認証を利用できません", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "この機能は動作のためにGoogleのリソースを読み込みます。", "general": "一般", - "geolocation_instruction_all_have_location": "この日付のすべての項目に位置情報がすでについています。すべての項目を表示を試みるか別の日付を選択してください", "geolocation_instruction_location": "位置情報付きの項目をクリックして、その位置情報を利用します。あるいは、地図上の地点を直接選ぶことも可能です", - "geolocation_instruction_no_date": "日付を選択して、その日の写真や動画の位置情報を管理しましょう", - "geolocation_instruction_no_photos": "この日付に写真や動画が無いようです。別の日付を選択してみてください", "get_help": "助けを求める", "get_wifiname_error": "Wi-Fiの名前(SSID)が入手できませんでした。Wi-Fiに繋がってるのと必要な権限を許可したか確認してください", "getting_started": "はじめる", @@ -1417,6 +1412,8 @@ "open_the_search_filters": "検索フィルタを開く", "options": "オプション", "or": "または", + "organize_into_albums": "アルバムに追加して整理する", + "organize_into_albums_description": "既存の写真を、現在の同期設定に基づきアルバムに追加する", "organize_your_library": "ライブラリを整理", "original": "オリジナル", "other": "その他", @@ -1557,6 +1554,7 @@ "purchase_server_description_2": "サポーターの状態", "purchase_server_title": "サーバー", "purchase_settings_server_activated": "サーバーのプロダクトキーは管理者に管理されています", + "query_asset_id": "順番待ちの項目ID", "queue_status": "順番待ち中 {count}/{total}", "rating": "星での評価", "rating_clear": "評価を取り消す", @@ -1846,10 +1844,8 @@ "shift_to_permanent_delete": "⇧を押してアセットを完全に削除", "show_album_options": "アルバム設定を表示", "show_albums": "アルバムを表示", - "show_all_assets": "すべての項目を表示", "show_all_people": "全ての人物を表示", "show_and_hide_people": "人物を表示/非表示", - "show_assets_without_location": "位置情報無しの項目を表示", "show_file_location": "ファイルの場所を表示", "show_gallery": "ギャラリーを表示", "show_hidden_people": "非表示の人物を表示", @@ -2034,7 +2030,6 @@ "use_biometric": "生体認証をご利用ください", "use_current_connection": "現在の接続情報を使用", "use_custom_date_range": "代わりにカスタム日付範囲を使用", - "use_this_location": "クリックして位置情報を使う", "user": "ユーザー", "user_has_been_deleted": "このユーザーは削除されました", "user_id": "ユーザーID", @@ -2077,6 +2072,7 @@ "view_next_asset": "次のアセットを見る", "view_previous_asset": "前のアセットを見る", "view_qr_code": "QRコードを見る", + "view_similar_photos": "類似する写真を見る", "view_stack": "ビュースタック", "view_user": "ユーザーを見る", "viewer_remove_from_stack": "スタックから外す", diff --git a/i18n/ko.json b/i18n/ko.json index bed0418c3b..09d215c2a3 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -596,8 +596,6 @@ "backup_setting_subtitle": "백그라운드 및 포그라운드 백업 설정을 관리합니다.", "backup_settings_subtitle": "업로드 설정을 관리합니다.", "backward": "뒤로", - "beta_sync": "동기화 (베타) 상태", - "beta_sync_subtitle": "새 동기화 시스템의 설정을 관리합니다.", "biometric_auth_enabled": "생체 인증이 활성화되었습니다.", "biometric_locked_out": "생체 인증이 일시적으로 비활성화되었습니다.", "biometric_no_options": "사용 가능한 생체 인증 옵션 없음", diff --git a/i18n/lv.json b/i18n/lv.json index 909c2a05d5..617b555a7c 100644 --- a/i18n/lv.json +++ b/i18n/lv.json @@ -391,8 +391,6 @@ "backup_options_page_title": "Dublēšanas iestatījumi", "backup_settings_subtitle": "Pārvaldīt augšupielādes iestatījumus", "backward": "Atpakaļejoši", - "beta_sync": "Beta Sinhronizācijas statuss", - "beta_sync_subtitle": "Pārvaldīt jauno sinhronizācijas sistēmu", "biometric_auth_enabled": "Ieslēgta biometriskā autentifikācija", "biometric_locked_out": "Biometriskā autentifikācija tev ir bloķēta", "biometric_no_options": "Nav pieejamas biometriskās autentifikācijas iespējas", @@ -1163,7 +1161,7 @@ "select_trash_all": "Atzīmēt visus dzēšanai", "select_user_for_sharing_page_err_album": "Neizdevās izveidot albumu", "selected": "Izvēlētie", - "selected_gps_coordinates": "izvēlētās ģeogrāfiskās koordinātas", + "selected_gps_coordinates": "Izvēlētās ģeogrāfiskās koordinātas", "server_info_box_app_version": "Aplikācijas Versija", "server_info_box_server_url": "Servera URL", "server_online": "Serveris tiešsaistē", @@ -1190,6 +1188,7 @@ "setting_notifications_total_progress_subtitle": "Kopējais augšupielādes progress (pabeigti/kopējie faili)", "setting_notifications_total_progress_title": "Rādīt fona dublējuma kopējo progresu", "setting_video_viewer_looping_title": "Cikliski", + "setting_video_viewer_original_video_subtitle": "Straumējot video no servera, izmantot oriģinālu, pat ja ir pieejama pārkodēšana. Tas var izraisīt buferēšanu. Lokāli pieejamie video tiek atskaņoti oriģinālajā kvalitātē, neatkarīgi no šīs iestatījuma.", "settings": "Iestatījumi", "settings_require_restart": "Lūdzu, restartējiet Immich, lai lietotu šo iestatījumu", "setup_pin_code": "Uzstādīt PIN kodu", @@ -1245,7 +1244,6 @@ "sharing_silver_appbar_share_partner": "Dalīties ar partneri", "show_album_options": "Rādīt albuma iespējas", "show_albums": "Rādīt albumus", - "show_all_assets": "Rādīt visus failus", "show_all_people": "Rādīt visas personas", "show_and_hide_people": "Rādīt un slēpt personas", "show_file_location": "Rādīt faila atrašanās vietu", @@ -1404,6 +1402,7 @@ "view_next_asset": "Skatīt nākamo failu", "view_previous_asset": "Skatīt iepriekšējo failu", "view_qr_code": "Skatīt QR kodu", + "view_similar_photos": "Skatīt līdzīgas fotogrāfijas", "view_stack": "Apskatīt kaudzi", "view_user": "Apskatīt lietotāju", "viewer_remove_from_stack": "Noņemt no Steka", diff --git a/i18n/mr.json b/i18n/mr.json index 1af1428d6a..ed31fd14de 100644 --- a/i18n/mr.json +++ b/i18n/mr.json @@ -28,6 +28,8 @@ "add_to_album": "संग्रहात टाका", "add_to_album_bottom_sheet_added": "{album} मध्ये जोडले गेले", "add_to_album_bottom_sheet_already_exists": "आधीच {album} मध्ये आहे", + "add_to_album_toggle": "अल्बमसाठी निवड टॉगल करा", + "add_to_albums": "अल्बममध्ये जोडा", "add_to_shared_album": "सामायिक संग्रहात टाका", "add_url": "URL प्रविष्ट करा", "added_to_archive": "संग्रहित केले", @@ -572,8 +574,6 @@ "backup_setting_subtitle": "बॅकग्राउंड आणि फोरग्राउंड अपलोड सेटिंग्ज व्यवस्थापित करा", "backup_settings_subtitle": "अपलोड सेटिंग्ज व्यवस्थापित करा", "backward": "मागासलेले", - "beta_sync": "बीटा सिंक स्थिती", - "beta_sync_subtitle": "नवीन सिंक प्रणाली व्यवस्थापित करा", "biometric_auth_enabled": "बायोमेट्रिक प्रमाणीकरण चालू आहे", "biometric_locked_out": "आपण बायोमेट्रिक प्रमाणीकरणापासून लॉक आहात", "biometric_no_options": "कोणतेही बायोमेट्रिक पर्याय उपलब्ध नाहीत", diff --git a/i18n/nb_NO.json b/i18n/nb_NO.json index fe676921c3..4e91a2b421 100644 --- a/i18n/nb_NO.json +++ b/i18n/nb_NO.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Administrer opplastingsinnstillinger for bakgrunn og forgrunn", "backup_settings_subtitle": "Håndter opplastingsinnstillinger", "backward": "Bakover", - "beta_sync": "Beta synkroniseringsstatus", - "beta_sync_subtitle": "Håndter det nye synkroniseringssystemet", "biometric_auth_enabled": "Biometrisk autentisering aktivert", "biometric_locked_out": "Du er låst ute av biometrisk verifisering", "biometric_no_options": "Ingen biometriske valg tilgjengelige", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Denne funksjonen laster eksterne ressurser fra Google for å fungere.", "general": "Generelt", - "geolocation_instruction_all_have_location": "Alle objekter for denne datoen har allerede lokasjonsdata. Prøv å vise alle objekter eller velg en annen dato", "geolocation_instruction_location": "Klikk på et objekt med GPS-koordinater for å bruke posisjonen, eller velg en posisjon direkte fra kartet", - "geolocation_instruction_no_date": "Velg en dato for å administrere posisjonsdata for bilder og videoer fra den dagen", - "geolocation_instruction_no_photos": "Ingen bilder eller videoer funnet for denne datoen. Velg en annen dato for å vise dem", "get_help": "Få Hjelp", "get_wifiname_error": "Kunne ikke hente Wi-Fi-navnet. Sørg for at du har gitt de nødvendige tillatelsene og er koblet til et Wi-Fi-nettverk", "getting_started": "Kom i gang", @@ -1645,6 +1640,7 @@ "restore_user": "Gjenopprett bruker", "restored_asset": "Gjenopprettet ressurs", "resume": "Fortsett", + "resume_paused_jobs": "Fortsett {count, plural, one {# paused job} other {# paused jobs}}", "retry_upload": "Prøv opplasting på nytt", "review_duplicates": "Gjennomgå duplikater", "review_large_files": "Se gjennom store filer", @@ -1849,10 +1845,8 @@ "shift_to_permanent_delete": "trykk ⇧ for å slette eiendeler permanent", "show_album_options": "Vis albumalternativer", "show_albums": "Vis album", - "show_all_assets": "Via alle objekter", "show_all_people": "Vis alle mennesker", "show_and_hide_people": "Vis og skjul personer", - "show_assets_without_location": "Vis objekter uten lokasjon", "show_file_location": "Vis filplassering", "show_gallery": "Vis galleri", "show_hidden_people": "Vis skjulte personer", @@ -2037,7 +2031,6 @@ "use_biometric": "Bruk biometri", "use_current_connection": "bruk nåværende tilkobling", "use_custom_date_range": "Bruk egendefinert datoperiode i stedet", - "use_this_location": "Trykk for å bruke lokasjon", "user": "Bruker", "user_has_been_deleted": "Denne brukeren har blitt slettet.", "user_id": "Bruker ID", diff --git a/i18n/nl.json b/i18n/nl.json index 29a60f8844..73eda6f498 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -293,7 +293,7 @@ "theme_settings_description": "Beheer het uiterlijk van de Immich webinterface", "thumbnail_generation_job": "Thumbnail genereren", "thumbnail_generation_job_description": "Genereer grote, kleine en vervaagde thumbnails voor ieder item, en genereer thumbnails voor iedere persoon", - "transcoding_acceleration_api": "Acceleration API", + "transcoding_acceleration_api": "Versnelling API", "transcoding_acceleration_api_description": "De API die met je apparaat zal communiceren om transcodering te versnellen. Deze instelling is 'best effort': wanneer fouten optreden wordt teruggevallen op softwaretranscodering. VP9 kan wel of niet werken, afhankelijk van je hardware.", "transcoding_acceleration_nvenc": "NVENC (vereist NVIDIA GPU)", "transcoding_acceleration_qsv": "Quick Sync (vereist 7e generatie Intel CPU of nieuwer)", @@ -312,7 +312,7 @@ "transcoding_codecs_learn_more": "Om meer te leren over de terminologie die hier wordt gebruikt, bekijk de FFmpeg documentatie voor H.264 codec, HEVC codec en VP9 codec.", "transcoding_constant_quality_mode": "Constante kwaliteit modus", "transcoding_constant_quality_mode_description": "ICQ is beter dan CQP, maar sommige hardware versnellingsmethodes ondersteunen deze modus niet. Als u deze optie instelt, wordt de voorkeur gegeven aan de opgegeven modus bij gebruik van op kwaliteit gebaseerde encoding. Deze optie wordt genegeerd door NVENC omdat het ICQ niet ondersteunt.", - "transcoding_constant_rate_factor": "Constant rate factor (-crf)", + "transcoding_constant_rate_factor": "Constant tarief factor (-ctf)", "transcoding_constant_rate_factor_description": "Niveau voor videokwaliteit. Typische waarden zijn 23 voor H.264, 28 voor HEVC, 31 voor VP9 en 35 voor AV1. Lager is beter, maar produceert grotere bestanden.", "transcoding_disabled_description": "Transcodeer geen video's. Het afspelen kan op sommige clients niet meer werken", "transcoding_encoding_options": "Coderings Opties", @@ -325,7 +325,7 @@ "transcoding_max_b_frames_description": "Hogere waarden verbeteren de compressie efficiëntie, maar vertragen de codering. Is mogelijk niet compatibel met hardwareversnelling op oudere apparaten. 0 schakelt B-frames uit, terwijl -1 deze waarde automatisch instelt.", "transcoding_max_bitrate": "Maximum bitrate", "transcoding_max_bitrate_description": "Het instellen van een maximale bitrate kan de bestandsgrootte voorspelbaarder maken, tegen geringe kosten voor de kwaliteit. Bij 720p zijn de typische waarden 2600 kbit/s voor VP9 of HEVC, of 4500 kbit/s voor H.264. Uitgeschakeld indien ingesteld op 0.", - "transcoding_max_keyframe_interval": "Maximum keyframe interval", + "transcoding_max_keyframe_interval": "Maximale keyframe interval", "transcoding_max_keyframe_interval_description": "Stelt de maximale frameafstand tussen keyframes in. Lagere waarden verslechteren de compressie efficiëntie, maar verbeteren de zoektijden en kunnen de kwaliteit verbeteren in scènes met snelle bewegingen. 0 stelt deze waarde automatisch in.", "transcoding_optimal_description": "Video's met een hogere resolutie dan de doelresolutie of niet in een geaccepteerd formaat", "transcoding_policy": "Transcode beleid", @@ -341,7 +341,7 @@ "transcoding_settings_description": "Beheer welke videos worden getranscodeerd en hoe ze worden verwerkt", "transcoding_target_resolution": "Target resolutie", "transcoding_target_resolution_description": "Hogere resoluties kunnen meer details behouden, maar het coderen ervan duurt langer, de bestandsgrootte is groter en de app reageert mogelijk minder snel.", - "transcoding_temporal_aq": "Temporal AQ", + "transcoding_temporal_aq": "Tijdelijke AQ", "transcoding_temporal_aq_description": "Alleen van toepassing op NVENC. Verhoogt de kwaliteit van scènes met veel details en weinig beweging. Is mogelijk niet compatibel met oudere apparaten.", "transcoding_threads": "Threads", "transcoding_threads_description": "Hogere waarden leiden tot snellere codering, maar laten minder ruimte over voor de server om andere taken te verwerken terwijl deze actief is. Deze waarde mag niet groter zijn dan het aantal CPU cores. Maximaliseert het gebruik als deze is ingesteld op 0.", @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Beheer achtergrond en voorgrond uploadinstellingen", "backup_settings_subtitle": "Beheer upload instellingen", "backward": "Achteruit", - "beta_sync": "Beta Sync Status", - "beta_sync_subtitle": "Beheer het nieuwe synchronisatiesysteem", "biometric_auth_enabled": "Biometrische authenticatie ingeschakeld", "biometric_locked_out": "Biometrische authenticatie is vergrendeld", "biometric_no_options": "Geen biometrische opties beschikbaar", @@ -837,7 +835,7 @@ "download_sucess_android": "Het bestand is gedownload naar DCIM/Immich", "download_waiting_to_retry": "Wachten om opnieuw te proberen", "downloading": "Downloaden", - "downloading_asset_filename": "Item {filename} downloaden...", + "downloading_asset_filename": "Downloading asset {filename}", "downloading_media": "Media aan het downloaden", "drop_files_to_upload": "Zet bestanden ergens neer om ze te uploaden", "duplicates": "Duplicaten", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Deze functie gebruikt externe bronnen van Google om te kunnen werken.", "general": "Algemeen", - "geolocation_instruction_all_have_location": "Alle items voor deze datum hebben al locatiegegevens. Probeer alle items te tonen of selecteer een andere datum", "geolocation_instruction_location": "Klik op een item met GPS coördinaten om de locatie te gebruiken, of selecteer een locatie direct vanaf de kaart", - "geolocation_instruction_no_date": "Selecteer een datum om locatiegegevens te beheren voor foto's en video's van die dag", - "geolocation_instruction_no_photos": "Geen foto's of video's gevonden voor deze datum. Selecteer een andere datum om ze te tonen", "get_help": "Krijg hulp", "get_wifiname_error": "Kon de WiFi-naam niet ophalen. Zorg ervoor dat je de benodigde machtigingen hebt verleend en verbonden bent met een WiFi-netwerk", "getting_started": "Aan de slag", @@ -1417,6 +1412,8 @@ "open_the_search_filters": "Open de zoekfilters", "options": "Opties", "or": "of", + "organize_into_albums": "Organiseren in albums", + "organize_into_albums_description": "Bestaande foto's in albums plaatsen met de huidige synchronisatie-instellingen", "organize_your_library": "Organiseer je bibliotheek", "original": "origineel", "other": "Overige", @@ -1513,7 +1510,7 @@ "primary": "Primair", "privacy": "Privacy", "profile": "Profiel", - "profile_drawer_app_logs": "Logboek", + "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "Mobiele app is verouderd. Werk bij naar de nieuwste hoofdversie.", "profile_drawer_client_out_of_date_minor": "Mobiele app is verouderd. Werk bij naar de nieuwste subversie.", "profile_drawer_client_server_up_to_date": "App en server zijn up-to-date", @@ -1525,7 +1522,7 @@ "profile_picture_set": "Profielfoto ingesteld.", "public_album": "Openbaar album", "public_share": "Openbare deellink", - "purchase_account_info": "Supporter", + "purchase_account_info": "Ondersteuner", "purchase_activated_subtitle": "Bedankt voor het ondersteunen van Immich en open-source software", "purchase_activated_time": "Geactiveerd op {date}", "purchase_activated_title": "Je licentiesleutel is succesvol geactiveerd", @@ -1557,6 +1554,7 @@ "purchase_server_description_2": "Supporterstatus", "purchase_server_title": "Server", "purchase_settings_server_activated": "De licentiesleutel van de server wordt beheerd door de beheerder", + "query_asset_id": "Query Asset ID", "queue_status": "Wachtrij {count}/{total}", "rating": "Sterwaardering", "rating_clear": "Waardering verwijderen", @@ -1695,7 +1693,7 @@ "search_page_motion_photos": "Bewegende foto's", "search_page_no_objects": "Geen objectgegevens beschikbaar", "search_page_no_places": "Geen locatiegegevens beschikbaar", - "search_page_screenshots": "Screenshots", + "search_page_screenshots": "Schermafbeelding", "search_page_search_photos_videos": "Zoek naar je foto's en video's", "search_page_selfies": "Selfies", "search_page_things": "Dingen", @@ -1735,7 +1733,7 @@ "select_user_for_sharing_page_err_album": "Album aanmaken mislukt", "selected": "Geselecteerd", "selected_count": "{count, plural, other {# geselecteerd}}", - "selected_gps_coordinates": "geselecteerde GPS coördinaten", + "selected_gps_coordinates": "Geselecteerde GPS Coördinaten", "send_message": "Bericht versturen", "send_welcome_email": "Stuur welkomstmail", "server_endpoint": "Server-URL", @@ -1846,10 +1844,8 @@ "shift_to_permanent_delete": "druk op ⇧ om items permanent te verwijderen", "show_album_options": "Toon albumopties", "show_albums": "Toon albums", - "show_all_assets": "Toon alle items", "show_all_people": "Toon alle mensen", "show_and_hide_people": "Toon & verberg mensen", - "show_assets_without_location": "Toon items zonder locatie", "show_file_location": "Toon bestandslocatie", "show_gallery": "Toon galerij", "show_hidden_people": "Verbogen mensen weergeven", @@ -2034,7 +2030,6 @@ "use_biometric": "Gebruik biometrische authenticatie", "use_current_connection": "gebruik huidige verbinding", "use_custom_date_range": "Gebruik in plaats daarvan een aangepast datumbereik", - "use_this_location": "Klik om locatie te gebruiken", "user": "Gebruiker", "user_has_been_deleted": "Deze gebruiker is verwijderd.", "user_id": "Gebruikers ID", @@ -2077,6 +2072,7 @@ "view_next_asset": "Bekijk volgende item", "view_previous_asset": "Bekijk vorige item", "view_qr_code": "QR-code bekijken", + "view_similar_photos": "Bekijk vergelijkbare foto's", "view_stack": "Bekijk stapel", "view_user": "Bekijk gebruiker", "viewer_remove_from_stack": "Verwijder van Stapel", diff --git a/i18n/pl.json b/i18n/pl.json index 64fe2ac60f..5a8fd966da 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Zarządzaj ustawieniami przesyłania w tle i na pierwszym planie", "backup_settings_subtitle": "Zarządzanie ustawieniami przesyłania", "backward": "Do tyłu", - "beta_sync": "Status synchronizacji w wersji Beta", - "beta_sync_subtitle": "Zarządzaj nowym systemem synchronizacji", "biometric_auth_enabled": "Włączono logowanie biometryczne", "biometric_locked_out": "Uwierzytelnianie biometryczne jest dla Ciebie zablokowane", "biometric_no_options": "Brak możliwości biometrii", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Ta funkcja , aby działać, ładuje zewnętrzne zasoby z Google.", "general": "Ogólne", - "geolocation_instruction_all_have_location": "Wszystkie zasoby z tego dnia mają już dane o lokalizacji. Spróbuj wyświetlić wszystkie zasoby lub wybierz inną datę", "geolocation_instruction_location": "Kliknij na zasób z współrzędnymi GPS, aby użyć jego lokalizacji, lub wybierz lokalizację bezpośrednio z mapy", - "geolocation_instruction_no_date": "Wybierz datę, aby zarządzać danymi o lokalizacji dla zdjęć i filmów z tego dnia", - "geolocation_instruction_no_photos": "Nie znaleziono żadnych zdjęć ani filmów dla tej daty. Wybierz inną datę, aby je wyświetlić", "get_help": "Pomoc", "get_wifiname_error": "Nie można uzyskać nazwy Wi-Fi. Upewnij się, że udzieliłeś niezbędnych uprawnień i jesteś połączony z siecią Wi-Fi", "getting_started": "Pierwsze kroki", @@ -1417,6 +1412,8 @@ "open_the_search_filters": "Otwórz filtry wyszukiwania", "options": "Opcje", "or": "lub", + "organize_into_albums": "Uporządkuj w albumy", + "organize_into_albums_description": "Umieść istniejące zdjęcia w albumach przy użyciu bieżących ustawień synchronizacji", "organize_your_library": "Organizuj swoją bibliotekę", "original": "oryginalny", "other": "Inne", @@ -1518,7 +1515,7 @@ "profile_drawer_client_out_of_date_minor": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej pomniejszej wersji.", "profile_drawer_client_server_up_to_date": "Klient i serwer są aktualne", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "Włączono tryb tylko do odczytu. Aby wyjść, należy dwukrotnie dotknąć ikony awatara użytkownika.", + "profile_drawer_readonly_mode": "Włączono tryb tylko do odczytu. Aby wyjść, naciśnij i przytrzymaj ikonę awatara użytkownika.", "profile_drawer_server_out_of_date_major": "Serwer jest nieaktualny. Zaktualizuj do najnowszej głównej wersji.", "profile_drawer_server_out_of_date_minor": "Serwer jest nieaktualny. Zaktualizuj do najnowszej pomniejszej wersji.", "profile_image_of_user": "Zdjęcie profilowe {user}", @@ -1557,6 +1554,7 @@ "purchase_server_description_2": "Status wspierającego", "purchase_server_title": "Serwer", "purchase_settings_server_activated": "Klucz produktu serwera jest zarządzany przez administratora", + "query_asset_id": "Zapytanie o ID zasobu", "queue_status": "Kolejkowanie {count}/{total}", "rating": "Ocena gwiazdkowa", "rating_clear": "Wyczyść ocenę", @@ -1642,6 +1640,7 @@ "restore_user": "Przywróć użytkownika", "restored_asset": "Przywrócony zasób", "resume": "Wznów", + "resume_paused_jobs": "Wznów {count, plural, one {# wstrzymane zadanie} few {# wstrzymane zadania} other {# wstrzymanych zadań}}", "retry_upload": "Prześlij ponownie", "review_duplicates": "Przejrzyj duplikaty", "review_large_files": "Przejrzyj duże pliki", @@ -1735,7 +1734,7 @@ "select_user_for_sharing_page_err_album": "Nie udało się utworzyć albumu", "selected": "Zaznaczone", "selected_count": "{count, plural, other {# wybrane}}", - "selected_gps_coordinates": "wybrane współrzędne GPS", + "selected_gps_coordinates": "Wybrane Współrzędne GPS", "send_message": "Wyślij wiadomość", "send_welcome_email": "Wyślij e-mail powitalny", "server_endpoint": "Punkt końcowy serwera", @@ -1846,10 +1845,8 @@ "shift_to_permanent_delete": "naciśnij ⇧, aby trwale usunąć zasób", "show_album_options": "Pokaż opcje albumu", "show_albums": "Pokaż albumy", - "show_all_assets": "Pokaż wszystkie zasoby", "show_all_people": "Pokaż wszystkie osoby", "show_and_hide_people": "Pokaż lub ukryj osoby", - "show_assets_without_location": "Pokaż zasoby bez lokalizacji", "show_file_location": "Pokaż ścieżkę pliku", "show_gallery": "Wyświetl galerię", "show_hidden_people": "Pokaż ukryte osoby", @@ -1920,6 +1917,8 @@ "sync_albums_manual_subtitle": "Zsynchronizuj wszystkie przesłane filmy i zdjęcia z wybranymi albumami z włączoną kopią zapasową", "sync_local": "Synchronizacja lokalna", "sync_remote": "Synchronizacja zdalna", + "sync_status": "Stan synchronizacji", + "sync_status_subtitle": "Wyświetl i zarządzaj systemem synchronizacji", "sync_upload_album_setting_subtitle": "Twórz i przesyłaj swoje zdjęcia i filmy do wybranych albumów w Immich", "tag": "Etykieta", "tag_assets": "Ustaw etykiety zasobów", @@ -1979,6 +1978,7 @@ "trash_page_select_assets_btn": "Wybierz zasoby", "trash_page_title": "Kosz ({count})", "trashed_items_will_be_permanently_deleted_after": "Wyrzucone zasoby zostaną trwale usunięte po {days, plural, one {jednym dniu} other {# dniach}}.", + "troubleshoot": "Rozwiąż problemy", "type": "Typ", "unable_to_change_pin_code": "Nie można zmienić kodu PIN", "unable_to_setup_pin_code": "Nie można ustawić kodu PIN", @@ -2034,7 +2034,6 @@ "use_biometric": "Użyj biometrii", "use_current_connection": "użyj bieżącego połączenia", "use_custom_date_range": "Zamiast tego użyj niestandardowego zakresu dat", - "use_this_location": "Kliknij, aby użyć lokalizacji", "user": "Użytkownik", "user_has_been_deleted": "Ten użytkownik został usunięty.", "user_id": "ID użytkownika", @@ -2077,6 +2076,7 @@ "view_next_asset": "Wyświetl następny zasób", "view_previous_asset": "Wyświetl poprzedni zasób", "view_qr_code": "Pokaż kod QR", + "view_similar_photos": "Zobacz podobne zdjęcia", "view_stack": "Zobacz Ułożenie", "view_user": "Wyświetl użytkownika", "viewer_remove_from_stack": "Usuń ze stosu", diff --git a/i18n/pt.json b/i18n/pt.json index f43fac797b..f9f3b3d089 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Gerenciar as configurações de envio em primeiro e segundo plano", "backup_settings_subtitle": "Gerir definições de carregamento", "backward": "Para trás", - "beta_sync": "Estado de Sincronização Beta", - "beta_sync_subtitle": "Gerir o novo sistema de sincronização", "biometric_auth_enabled": "Autenticação biométrica ativada", "biometric_locked_out": "Está impedido de utilizar a autenticação biométrica", "biometric_no_options": "Sem opções biométricas disponíveis", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Esta funcionalidade requer o carregamento de recursos externos da Google para poder funcionar.", "general": "Geral", - "geolocation_instruction_all_have_location": "Todos os activos desta data já possuem dados de localização. Tente exibir todos os ativos ou seleccione uma data diferente", "geolocation_instruction_location": "Clique num ativo com coordenadas GPS para usar a sua localização ou seleccione um local diretamente do mapa", - "geolocation_instruction_no_date": "Seleccione uma data para gerir os dados de localização de fotos e vídeos daquele dia", - "geolocation_instruction_no_photos": "Nenhuma foto ou vídeo encontrado para esta data. Seleccione uma data diferente para exibi-los", "get_help": "Obter Ajuda", "get_wifiname_error": "Não foi possível obter o nome do Wi-Fi. Verifique se concedeu as permissões necessárias e se está conectado a uma rede Wi-Fi", "getting_started": "Primeiros Passos", @@ -1846,10 +1841,8 @@ "shift_to_permanent_delete": "Pressione ⇧ para eliminar o ficheiro permanentemente", "show_album_options": "Exibir opções do álbum", "show_albums": "Mostrar álbuns", - "show_all_assets": "Mostrar todos os recursos", "show_all_people": "Mostrar todas as pessoas", "show_and_hide_people": "Mostrar & ocultar pessoas", - "show_assets_without_location": "Mostrar recursos sem localização", "show_file_location": "Exibir localização do ficheiro", "show_gallery": "Exibir galeria", "show_hidden_people": "Exibir pessoas ocultadas", @@ -2034,7 +2027,6 @@ "use_biometric": "Utilizar dados biométricos", "use_current_connection": "usar conexão atual", "use_custom_date_range": "Utilizar um intervalo de datas personalizado", - "use_this_location": "Clique para usar a localização", "user": "Utilizador", "user_has_been_deleted": "Este utilizador for eliminado.", "user_id": "ID do utilizador", diff --git a/i18n/pt_BR.json b/i18n/pt_BR.json index 54e6e0212f..e0c75b692b 100644 --- a/i18n/pt_BR.json +++ b/i18n/pt_BR.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Gerenciar as configurações de envio em primeiro e segundo plano", "backup_settings_subtitle": "Gerenciar configurações de envio", "backward": "Para trás", - "beta_sync": "Status da sincronização Beta", - "beta_sync_subtitle": "Configurar o novo sistema de sincronização", "biometric_auth_enabled": "Autenticação por biometria ativada", "biometric_locked_out": "Sua autenticação por biometria está bloqueada", "biometric_no_options": "Não há opções de biometria disponíveis", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Esta funcionalidade carrega recursos externos do Google para funcionar.", "general": "Geral", - "geolocation_instruction_all_have_location": "Todos arquivos nesta data já possuem dados de localização. Tente exibir todos os arquivos ou selecione uma data diferente", "geolocation_instruction_location": "Selecione um arquivo com as coordenadas de GPS desejada, ou selecione a localização diretamente no mapa", - "geolocation_instruction_no_date": "Selecione uma data para gerenciar os dados de localização das fotos e vídeos daquele dia", - "geolocation_instruction_no_photos": "Nenhuma foto ou vídeo encontrado nesta data. Selecione uma data diferente para ser exibida", "get_help": "Obter Ajuda", "get_wifiname_error": "Não foi possível obter o nome do Wi-Fi. Verifique se concedeu as permissões necessárias e se está conectado a uma rede Wi-Fi", "getting_started": "Primeiros passos", @@ -1417,6 +1412,8 @@ "open_the_search_filters": "Abre os filtros de pesquisa", "options": "Opções", "or": "ou", + "organize_into_albums": "Organizar em álbuns", + "organize_into_albums_description": "Colocar imagens existentes em álbuns usando as configurações de sincronização atuais", "organize_your_library": "Organize sua biblioteca", "original": "original", "other": "Outro", @@ -1557,6 +1554,7 @@ "purchase_server_description_2": "Status de Contribuidor", "purchase_server_title": "Servidor", "purchase_settings_server_activated": "A chave do produto para servidor é gerenciada pelo administrador", + "query_asset_id": "Consultar ID do Ativo", "queue_status": "Na fila {count} de {total}", "rating": "Estrelas", "rating_clear": "Limpar classificação", @@ -1735,7 +1733,7 @@ "select_user_for_sharing_page_err_album": "Falha ao criar álbum", "selected": "Selecionados", "selected_count": "{count, plural, one {# selecionado} other {# selecionados}}", - "selected_gps_coordinates": "selecione as coordenadas de GPS", + "selected_gps_coordinates": "Coordenadas de GPS Selecionada", "send_message": "Enviar mensagem", "send_welcome_email": "Enviar E-mail de boas vindas", "server_endpoint": "URL do servidor", @@ -1846,10 +1844,8 @@ "shift_to_permanent_delete": "pressione ⇧ para excluir permanentemente o arquivo", "show_album_options": "Exibir opções do álbum", "show_albums": "Exibir álbuns", - "show_all_assets": "Ver todos arquivos", "show_all_people": "Mostrar todas as pessoas", "show_and_hide_people": "Mostrar & ocultar pessoas", - "show_assets_without_location": "Ver arquivos sem localização", "show_file_location": "Exibir local do arquivo", "show_gallery": "Exibir galeria", "show_hidden_people": "Exibir pessoas ocultadas", @@ -2034,7 +2030,6 @@ "use_biometric": "Usar biometria", "use_current_connection": "usar conexão atual", "use_custom_date_range": "Usar intervalo de datas personalizado", - "use_this_location": "Clique para marcar o local", "user": "Usuário", "user_has_been_deleted": "Este usuário foi excluído.", "user_id": "ID do usuário", @@ -2077,6 +2072,7 @@ "view_next_asset": "Ver próximo arquivo", "view_previous_asset": "Ver arquivo anterior", "view_qr_code": "Ver QR Code", + "view_similar_photos": "Ver fotos similares", "view_stack": "Ver grupo", "view_user": "Visualizar usuário", "viewer_remove_from_stack": "Remover do grupo", diff --git a/i18n/ro.json b/i18n/ro.json index 6218a0dff7..fc2ca444c6 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -595,8 +595,6 @@ "backup_setting_subtitle": "Schimbă opțiuni pentru backup în prim-plan și în fundal", "backup_settings_subtitle": "Gestionați setările de încărcare", "backward": "În sens invers", - "beta_sync": "Starea sincronizării Beta", - "beta_sync_subtitle": "Gestionați noul sistem de sincronizare", "biometric_auth_enabled": "Autentificare biometrică activată", "biometric_locked_out": "Sunteți blocați de la autentificare biometrică", "biometric_no_options": "Nu sunt disponibile opțiuni biometrice", diff --git a/i18n/ru.json b/i18n/ru.json index f7f5724e2d..cfab2ca06c 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -409,7 +409,7 @@ "age_year_months": "1 год {months, plural, one {# месяц} many {# месяцев} other {# месяца}}", "age_years": "{years, plural, one {# год} many {# лет} other {# года}}", "album_added": "Альбом добавлен", - "album_added_notification_setting_description": "Получать уведомление по электронной почте, когда вы добавлены к общему альбому", + "album_added_notification_setting_description": "Получать уведомление по электронной почте, когда вам предоставили доступ в общий альбом", "album_cover_updated": "Обложка альбома обновлена", "album_delete_confirmation": "Вы уверены, что хотите удалить альбом {album}?", "album_delete_confirmation_description": "Если альбом был общим, другие пользователи больше не смогут получить к нему доступ.", @@ -426,7 +426,7 @@ "album_search_not_found": "Не найдено альбомов по вашему запросу", "album_share_no_users": "Нет доступных пользователей, с которыми можно поделиться альбомом.", "album_updated": "Альбом обновлён", - "album_updated_setting_description": "Получать уведомление по электронной почте при добавлении новых ресурсов в общий альбом", + "album_updated_setting_description": "Получать уведомление по электронной почте при добавлении новых объектов в общий альбом", "album_user_left": "Вы покинули {album}", "album_user_removed": "Пользователь {user} удален", "album_viewer_appbar_delete_confirm": "Вы уверены, что хотите удалить альбом из своей учетной записи?", @@ -544,7 +544,7 @@ "backup_background_service_current_upload_notification": "Загружается {filename}", "backup_background_service_default_notification": "Поиск новых объектов…", "backup_background_service_error_title": "Ошибка резервного копирования", - "backup_background_service_in_progress_notification": "Резервное копирование ваших объектов…", + "backup_background_service_in_progress_notification": "Резервное копирование объектов…", "backup_background_service_upload_failure_notification": "Ошибка загрузки {filename}", "backup_controller_page_albums": "Резервное копирование альбомов", "backup_controller_page_background_app_refresh_disabled_content": "Включите фоновое обновление приложения в Настройки > Общие > Фоновое обновление приложений, чтобы использовать фоновое резервное копирование.", @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Настройка активного и фонового резервного копирования", "backup_settings_subtitle": "Настройка загрузки объектов", "backward": "Назад", - "beta_sync": "Статус синхронизации", - "beta_sync_subtitle": "Управление новой системой синхронизации", "biometric_auth_enabled": "Биометрическая аутентификация включена", "biometric_locked_out": "Вам закрыт доступ к биометрической аутентификации", "biometric_no_options": "Биометрическая аутентификация недоступна", @@ -637,8 +635,8 @@ "cannot_merge_people": "Невозможно объединить людей", "cannot_undo_this_action": "Это действие нельзя отменить!", "cannot_update_the_description": "Невозможно обновить описание", - "cast": "Транслировать", - "cast_description": "Настройка доступных целей трансляции", + "cast": "Трансляция", + "cast_description": "Выбор доступных способов для трансляции", "change_date": "Изменить дату", "change_description": "Изменить описание", "change_display_order": "Изменить порядок отображения", @@ -827,7 +825,7 @@ "download_failed": "Загрузка не удалась", "download_finished": "Загрузка окончена", "download_include_embedded_motion_videos": "Встроенные видео", - "download_include_embedded_motion_videos_description": "Включить видео, встроенные в живые фото, в виде отдельного файла", + "download_include_embedded_motion_videos_description": "Сохранять видео, встроенные в живые фото, в виде отдельных файлов", "download_notfound": "Загрузка не найдена", "download_paused": "Загрузка приостановлена", "download_settings": "Скачивание", @@ -1070,16 +1068,13 @@ "folder": "Папка", "folder_not_found": "Папка не найдена", "folders": "Папки", - "folders_feature_description": "Просмотр папок с фотографиями и видео в файловой системе", + "folders_feature_description": "Просмотр папок с фото и видео в файловой системе", "forgot_pin_code_question": "Забыли PIN-код?", "forward": "Вперёд", "gcast_enabled": "Google Cast", - "gcast_enabled_description": "Этот функционал требует загрузки внешних ресурсов с серверов Google.", + "gcast_enabled_description": "Для работы требуется загрузка внешних ресурсов с серверов Google.", "general": "Общие", - "geolocation_instruction_all_have_location": "Все объекты в этом периоде уже содержат данные о местоположении. Включите отображение всех объектов или укажите другой период.", "geolocation_instruction_location": "Выберите объект с имеющимися координатами, чтобы использовать их, либо вручную укажите место на карте", - "geolocation_instruction_no_date": "Укажите дату для управления координатами мест съёмки за этот день", - "geolocation_instruction_no_photos": "Не найдено объектов в этом периоде. Укажите другую дату.", "get_help": "Получить помощь", "get_wifiname_error": "Не удалось получить имя Wi-Fi сети. Убедитесь, что вы подключены к сети и предоставили приложению необходимые разрешения", "getting_started": "Старт", @@ -1221,7 +1216,7 @@ "loading": "Загрузка", "loading_search_results_failed": "Загрузка результатов поиска не удалась", "local": "На устройстве", - "local_asset_cast_failed": "Невозможно транслировать объект, который ещё не загружен на сервер", + "local_asset_cast_failed": "Невозможна трансляция объектов, которые ещё не загружены на сервер", "local_assets": "Объекты на устройстве", "local_network": "Локальная сеть", "local_network_sheet_info": "Приложение будет подключаться к серверу по этому адресу, когда устройство подключено к выбранной Wi-Fi сети", @@ -1273,7 +1268,7 @@ "make": "Производитель", "manage_geolocation": "Управление местами съёмки", "manage_shared_links": "Управление публичными ссылками", - "manage_sharing_with_partners": "Управление обменом информацией с партнерами. Эта функция позволяет вашему партнеру видеть ваши фотографии и видеозаписи, кроме тех, которые находятся в Архиве и Корзине", + "manage_sharing_with_partners": "Функция совместного доступа к фото и видео, позволяющая видеть все объекты партнёров, а также предоставлять доступ к своим", "manage_the_app_settings": "Управление настройками приложения", "manage_your_account": "Управление учётной записью", "manage_your_api_keys": "Управление API ключами для взаимодействия с другими программами", @@ -1458,7 +1453,7 @@ "pending": "Ожидает", "people": "Люди", "people_edits_count": "{count, plural, one {Изменён # человек} many {Изменено # человек} other {Изменено # человека}}", - "people_feature_description": "Просмотр фотографий и видео, сгруппированных по людям", + "people_feature_description": "Просмотр фото и видео, сгруппированных по людям", "people_sidebar_description": "Отображать пункт меню \"Люди\" в боковой панели", "permanent_deletion_warning": "Предупреждение об удалении", "permanent_deletion_warning_setting_description": "Предупреждать перед безвозвратным удалением объектов", @@ -1520,7 +1515,7 @@ "profile_drawer_client_out_of_date_minor": "Версия мобильного приложения устарела. Пожалуйста, обновите его.", "profile_drawer_client_server_up_to_date": "Клиент и сервер обновлены", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "Включён режим «только просмотр». Дважды коснитесь значка аватара пользователя, чтобы выйти.", + "profile_drawer_readonly_mode": "Включён режим «только просмотр». Удерживайте значок аватара пользователя для отключения.", "profile_drawer_server_out_of_date_major": "Версия сервера устарела. Пожалуйста, обновите его.", "profile_drawer_server_out_of_date_minor": "Версия сервера устарела. Пожалуйста, обновите его.", "profile_image_of_user": "Изображение профиля {user}", @@ -1561,10 +1556,10 @@ "purchase_settings_server_activated": "Ключом продукта управляет администратор сервера", "query_asset_id": "Идентификатор исходного объекта", "queue_status": "В очереди {count}/{total}", - "rating": "Рейтинг звёзд", + "rating": "Рейтинг", "rating_clear": "Очистить рейтинг", "rating_count": "{count, plural, one {# звезда} many {# звезд} other {# звезды}}", - "rating_description": "Показывать рейтинг в панели информации", + "rating_description": "Система оценки объектов в панели информации", "reaction_options": "Опции реакций", "read_changelog": "Прочитать список изменений", "readonly_mode_disabled": "Режим «только просмотр» отключён", @@ -1645,6 +1640,7 @@ "restore_user": "Восстановить пользователя", "restored_asset": "Восстановленный объект", "resume": "Продолжить", + "resume_paused_jobs": "Возобновить выполнение {count, plural, one {# задачи} other {# задач}}", "retry_upload": "Повторить загрузку", "review_duplicates": "Разбор дубликатов", "review_large_files": "Обзор больших файлов", @@ -1800,7 +1796,7 @@ "shared_by": "Поделился", "shared_by_user": "Владелец: {user}", "shared_by_you": "Вы поделились", - "shared_from_partner": "Фото от {partner}", + "shared_from_partner": "Фото и видео пользователя {partner}", "shared_intent_upload_button_progress_text": "{current} / {total} Загружено", "shared_link_app_bar_title": "Публичные ссылки", "shared_link_clipboard_copied_massage": "Скопировано в буфер обмена", @@ -1849,15 +1845,13 @@ "shift_to_permanent_delete": "нажмите ⇧ чтобы удалить объект навсегда", "show_album_options": "Показать параметры альбома", "show_albums": "Показать альбомы", - "show_all_assets": "Показать все объекты", "show_all_people": "Показать всех людей", "show_and_hide_people": "Показать и скрыть людей", - "show_assets_without_location": "Показать объекты без координат", "show_file_location": "Показать расположение файла", "show_gallery": "Показать галерею", "show_hidden_people": "Показать скрытых людей", "show_in_timeline": "Показать на временной шкале", - "show_in_timeline_setting_description": "Показывайте фото и видео этого пользователя в своей ленте", + "show_in_timeline_setting_description": "Отображать фото и видео этого пользователя в своей ленте", "show_keyboard_shortcuts": "Показать сочетания клавиш", "show_metadata": "Показывать метаданные", "show_or_hide_info": "Показать или скрыть информацию", @@ -1871,7 +1865,7 @@ "show_supporter_badge_description": "Показать значок поддержки", "shuffle": "Перемешать", "sidebar": "Боковая панель", - "sidebar_display_description": "Показывать ссылку на представление в боковой панели", + "sidebar_display_description": "Отображать раздел на боковой панели", "sign_out": "Выход", "sign_up": "Зарегистрироваться", "size": "Размер", @@ -1904,7 +1898,7 @@ "stop_casting": "Остановить трансляцию", "stop_motion_photo": "Покадровая анимация", "stop_photo_sharing": "Закрыть доступ партнёра к вашим фото?", - "stop_photo_sharing_description": "{partner} больше не сможет получить доступ к вашим фотографиям.", + "stop_photo_sharing_description": "Пользователь {partner} больше не имеет доступа к вашим фотографиям.", "stop_sharing_photos_with_user": "Прекратить делиться своими фотографиями с этим пользователем", "storage": "Хранилище", "storage_label": "Метка хранилища", @@ -1923,6 +1917,8 @@ "sync_albums_manual_subtitle": "Синхронизировать все загруженные фото и видео в выбранные альбомы для резервного копирования", "sync_local": "Синхронизировать локально", "sync_remote": "Синхронизация с сервером", + "sync_status": "Статус синхронизации", + "sync_status_subtitle": "Просмотр и управление системой синхронизации", "sync_upload_album_setting_subtitle": "Создавайте и загружайте свои фотографии и видео в выбранные альбомы на сервер Immich", "tag": "Тег", "tag_assets": "Добавить теги", @@ -1937,7 +1933,7 @@ "template": "Шаблон", "theme": "Тема", "theme_selection": "Выбор темы", - "theme_selection_description": "Автоматически устанавливать тему в зависимости от системных настроек вашего браузера", + "theme_selection_description": "Автоматически устанавливать светлую или тёмную тему в зависимости от настроек вашего браузера", "theme_setting_asset_list_storage_indicator_title": "Показать индикатор хранилища на плитках объектов", "theme_setting_asset_list_tiles_per_row_title": "Количество объектов в строке ({count})", "theme_setting_colorful_interface_subtitle": "Добавить оттенок к фону.", @@ -1982,6 +1978,7 @@ "trash_page_select_assets_btn": "Выбранные объекты", "trash_page_title": "Корзина ({count})", "trashed_items_will_be_permanently_deleted_after": "Объекты, хранящиеся в корзине более {days, plural, one {# дня} other {# дней}}, удаляются автоматически.", + "troubleshoot": "Решение проблем", "type": "Тип", "unable_to_change_pin_code": "Ошибка при изменении PIN-кода", "unable_to_setup_pin_code": "Ошибка при создании PIN-кода", @@ -2037,7 +2034,6 @@ "use_biometric": "Использовать биометрию", "use_current_connection": "Использовать текущее подключение", "use_custom_date_range": "Использовать пользовательский диапазон дат", - "use_this_location": "Выбрать это место", "user": "Пользователь", "user_has_been_deleted": "Этот пользователь был удалён.", "user_id": "ID пользователя", @@ -2076,7 +2072,7 @@ "view_in_timeline": "Показать на временной шкале", "view_link": "Показать ссылку", "view_links": "Показать ссылки", - "view_name": "Посмотреть", + "view_name": "Вид", "view_next_asset": "Показать следующий объект", "view_previous_asset": "Показать предыдущий объект", "view_qr_code": "Посмотреть QR код", diff --git a/i18n/sk.json b/i18n/sk.json index f769373f90..3113ce788d 100644 --- a/i18n/sk.json +++ b/i18n/sk.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Spravovať nastavenia odosielania na pozadí a v popredí", "backup_settings_subtitle": "Spravovať nastavenia nahrávania", "backward": "Dozadu", - "beta_sync": "Stav synchronizácie verzie Beta", - "beta_sync_subtitle": "Spravovať nový systém synchronizácie", "biometric_auth_enabled": "Biometrické overovanie je povolené", "biometric_locked_out": "Ste vymknutí z biometrického overovania", "biometric_no_options": "Nie sú k dispozícii žiadne biometrické možnosti", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Táto funkcia načítava externé zdroje zo spoločnosti Google, aby mohla fungovať.", "general": "Všeobecné", - "geolocation_instruction_all_have_location": "Všetky položky pre tento dátum už majú údaje o polohe. Skúste zobraziť všetky položky alebo vyberte iný dátum", "geolocation_instruction_location": "Kliknite na položku s GPS súradnicami, aby ste použili jej polohu, alebo vyberte polohu priamo z mapy", - "geolocation_instruction_no_date": "Vyberte dátum, aby ste mohli spravovať údaje o polohe pre fotografie a videá z daného dňa", - "geolocation_instruction_no_photos": "Pre tento dátum neboli nájdené žiadne fotografie ani videá. Vyberte iný dátum, aby sa zobrazili", "get_help": "Získať pomoc", "get_wifiname_error": "Nepodarilo sa získať názov Wi-Fi siete. Uistite sa, že ste udelili potrebné oprávnenia a ste pripojení k sieti Wi-Fi", "getting_started": "Začíname", @@ -1520,7 +1515,7 @@ "profile_drawer_client_out_of_date_minor": "Mobilná aplikácia je zastaralá. Prosím aktualizujte na najnovšiu verziu.", "profile_drawer_client_server_up_to_date": "Klient a server sú aktuálne", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "Režim iba na čítanie je aktivovaný. Dvojitým ťuknutím na ikonu obrázku používateľa režim opustíte.", + "profile_drawer_readonly_mode": "Režim iba na čítanie je aktivovaný. Dlhým stlačením ikony obrázku používateľa režim opustíte.", "profile_drawer_server_out_of_date_major": "Server je zastaralý. Prosím aktualizujte na najnovšiu verziu.", "profile_drawer_server_out_of_date_minor": "Server je zastaralý. Prosím aktualizujte na najnovšiu verziu.", "profile_image_of_user": "Profilový obrázok používateľa {user}", @@ -1645,6 +1640,7 @@ "restore_user": "Navrátiť používateľa", "restored_asset": "Navrátená položka", "resume": "Pokračovať", + "resume_paused_jobs": "Pokračovať v {count, plural, one {# pozastavenej úlohe} other {# pozastavených úlohách}}", "retry_upload": "Zopakovať nahrávanie", "review_duplicates": "Preskúmať duplikáty", "review_large_files": "Skontrolovať veľké súbory", @@ -1849,10 +1845,8 @@ "shift_to_permanent_delete": "stlačte ⇧ na trvalé vymazanie položky", "show_album_options": "Zobraziť možnosti albumu", "show_albums": "Zobraziť albumy", - "show_all_assets": "Zobraziť všetky položky", "show_all_people": "Zobraziť všetkých ľudí", "show_and_hide_people": "Zobraziť a skryť ľudí", - "show_assets_without_location": "Zobraziť položky bez polohy", "show_file_location": "Zobraziť umiestnenie súboru", "show_gallery": "Zobraziť galériu", "show_hidden_people": "Zobraziť skrytých ľudí", @@ -1923,6 +1917,8 @@ "sync_albums_manual_subtitle": "Synchronizujte všetky nahrané videá a fotografie s vybranými záložnými albumami", "sync_local": "Synchronizovať lokálne", "sync_remote": "Synchronizovať vzdialené", + "sync_status": "Stav synchronizácie", + "sync_status_subtitle": "Zobraziť a spravovať systém synchronizácie", "sync_upload_album_setting_subtitle": "Vytvárajte a nahrávajte svoje fotografie a videá do vybraných albumov na Immich", "tag": "Štítok", "tag_assets": "Pridať štítky", @@ -1982,6 +1978,7 @@ "trash_page_select_assets_btn": "Vybrať médiá", "trash_page_title": "Kôš ({count})", "trashed_items_will_be_permanently_deleted_after": "Položky v koši sa natrvalo vymažú po {days, plural, one {# dni} other {# dňoch}}.", + "troubleshoot": "Riešenie problémov", "type": "Typ", "unable_to_change_pin_code": "Nie je možné zmeniť PIN kód", "unable_to_setup_pin_code": "Nie je možné nastaviť PIN kód", @@ -2037,7 +2034,6 @@ "use_biometric": "Použiť biometrické údaje", "use_current_connection": "použiť aktuálne pripojenie", "use_custom_date_range": "Použiť radšej vlastný rozsah dátumov", - "use_this_location": "Kliknutím použite polohu", "user": "Používateľ", "user_has_been_deleted": "Tento používateľ bol vymazaný.", "user_id": "ID používateľa", diff --git a/i18n/sl.json b/i18n/sl.json index e456c015b0..851d0290b5 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Upravljaj nastavitve nalaganja v ozadju in ospredju", "backup_settings_subtitle": "Upravljanje nastavitev nalaganja", "backward": "Nazaj", - "beta_sync": "Stanje sinhronizacije beta različice", - "beta_sync_subtitle": "Upravljanje novega sistema sinhronizacije", "biometric_auth_enabled": "Biometrična avtentikacija omogočena", "biometric_locked_out": "Biometrična avtentikacija vam je onemogočena", "biometric_no_options": "Biometrične možnosti niso na voljo", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Ta funkcija za delovanje nalaga zunanje vire iz Googla.", "general": "Splošno", - "geolocation_instruction_all_have_location": "Vsa sredstva za ta datum že imajo podatke o lokaciji. Poskusite prikazati vsa sredstva ali izberite drug datum", "geolocation_instruction_location": "Kliknite na sredstvo z GPS koordinatami, da uporabite njegovo lokacijo, ali pa izberite lokacijo neposredno na zemljevidu", - "geolocation_instruction_no_date": "Izberite datum za upravljanje podatkov o lokaciji za fotografije in videoposnetke s tega dne", - "geolocation_instruction_no_photos": "Za ta datum ni bilo najdenih fotografij ali videoposnetkov. Izberite drug datum, da jih prikažete", "get_help": "Poiščite pomoč", "get_wifiname_error": "Imena Wi-Fi ni bilo mogoče dobiti. Prepričajte se, da ste podelili potrebna dovoljenja in ste povezani v omrežje Wi-Fi", "getting_started": "Začetek", @@ -1645,6 +1640,7 @@ "restore_user": "Obnovi uporabnika", "restored_asset": "Obnovljeno sredstvo", "resume": "Nadaljuj", + "resume_paused_jobs": "Nadaljuj {count, plural, one {# zaustavljeno opravilo} two {# zaustavljeni opravili} few {# zaustavljena opravila} other {# zaustavljenih opravil}}", "retry_upload": "Poskusite znova naložiti", "review_duplicates": "Pregled dvojnikov", "review_large_files": "Pregled velikih datotek", @@ -1849,10 +1845,8 @@ "shift_to_permanent_delete": "pritisni ⇧ za trajno brisanje sredstva", "show_album_options": "Prikaži možnosti albuma", "show_albums": "Prikaži albume", - "show_all_assets": "Prikaži vsa sredstva", "show_all_people": "Prikaži vse osebe", "show_and_hide_people": "Prikaži & skrij osebe", - "show_assets_without_location": "Prikaži sredstva brez lokacije", "show_file_location": "Pokaži lokacijo datoteke", "show_gallery": "Prikaži galerijo", "show_hidden_people": "Prikaži skrite osebe", @@ -2037,7 +2031,6 @@ "use_biometric": "Uporabite biometrične podatke", "use_current_connection": "uporabi trenutno povezavo", "use_custom_date_range": "Namesto tega uporabite časovno obdobje po meri", - "use_this_location": "Kliknite za uporabo lokacije", "user": "Uporabnik", "user_has_been_deleted": "Ta uporabnik je bil izbrisan.", "user_id": "ID uporabnika", diff --git a/i18n/sq.json b/i18n/sq.json index 0967ef424b..de7c5faa27 100644 --- a/i18n/sq.json +++ b/i18n/sq.json @@ -1 +1,59 @@ -{} +{ + "about": "Rreth", + "account": "Llogari", + "account_settings": "Cilësimet e Llogarisë", + "acknowledge": "Prano", + "action": "Aksion", + "action_common_update": "Përditëso", + "actions": "Aksione", + "active": "Aktiv", + "activity": "Aktivitet", + "activity_changed": "Aktiviteti është {enabled, select, true {aktivizuar} other {çaktivizuar}}", + "add": "Shto", + "add_a_description": "Shto një përshkrim", + "add_a_location": "Shto një vendndodhje", + "add_a_name": "Shto një emër", + "add_a_title": "Shto një titull", + "add_birthday": "Shto një ditëlindje", + "add_endpoint": "Shto një endpoint", + "add_exclusion_pattern": "Shto model përjashtimi", + "add_import_path": "Shto vënd importimi", + "add_location": "Shto vendndodhje", + "add_more_users": "Shto më shumë përdorues", + "add_partner": "Shto partner", + "add_path": "Shto path", + "add_photos": "Shto foto", + "add_tag": "Shto tag", + "add_to": "Shto në…", + "add_to_album": "Shto në album", + "add_to_album_bottom_sheet_added": "Shtuar në {album}", + "add_to_album_bottom_sheet_already_exists": "Existon në {album}", + "add_to_album_toggle": "Aktivizo/çaktivizo zgjedhjen për {album}", + "add_to_albums": "Shto në albume", + "add_to_albums_count": "Shto në albume ({count})", + "add_to_shared_album": "Shto në album të hapur", + "add_url": "Shto URL", + "added_to_archive": "Shtuar në arkiv", + "added_to_favorites": "Shtuar tek të preferuarat", + "added_to_favorites_count": "Shtuar {count, number} në të preferuarat", + "admin": { + "add_exclusion_pattern_description": "Shto modele përjashtimi. Mbështetet globimi duke përdorur *, ** dhe ?. Për të injoruar të gjithë skedarët në çdo drejtori të quajtur \"Raw\", përdorni \"**/Raw/**\". Për të injoruar të gjithë skedarët që mbarojnë me \".tif\", përdorni \"**/*.tif\". Për të injoruar një shteg absolut, përdorni \"/path/to/ignore/**\".", + "admin_user": "Përdorues Administrator", + "asset_offline_description": "Ky aset i bibliotekës së jashtme nuk gjendet më në disk dhe është zhvendosur në koshin e plehrave. Nëse skedari është zhvendosur brenda bibliotekës, kontrolloni kronologjinë tuaj për asetin e ri përkatës. Për të rivendosur këtë aset, sigurohuni që shtegu i skedarit më poshtë të jetë i arritshëm nga Immich dhe skanoni bibliotekën.", + "authentication_settings": "Cilësimet e vërtetimit të përdoruesit", + "authentication_settings_description": "Manaxho passwordin, OAuth, dhe cilësime të tjera të", + "authentication_settings_disable_all": "Je i sigurt që dëshiron të çaktivizosh të gjitha metodat e hyrjes? Hyrja do të çaktivizohet plotësisht.", + "authentication_settings_reenable": "Për ta riaktivizuar, përdorni një Komandë Serveri.", + "background_task_job": "Detyrat në Sfond", + "backup_database": "Krijo demp të databaseit", + "backup_database_enable_description": "Aktivizo demp-et e bazës së të dhënave", + "backup_keep_last_amount": "Sasia e deponive të mëparshme për t'u mbajtur", + "backup_onboarding_1_description": "kopje në cloud ose në një vendndodhje tjetër fizike.", + "backup_onboarding_2_description": "kopje lokale në pajisje të ndryshme. Kjo përfshin skedarët kryesorë dhe një kopje rezervë të këtyre skedarëve lokalisht.", + "backup_onboarding_3_description": "kopje totale të të dhënave tuaja, duke përfshirë skedarët origjinalë. Kjo përfshin 1 kopje jashtë faqes dhe 2 kopje lokale.", + "backup_onboarding_description": "Rekomandohet një strategji 3-2-1 për ruajtjen e të dhënave tuaja. Duhet të ruani kopje të fotove/videove të ngarkuara, si dhe të bazës së të dhënave të Immich për një zgjidhje gjithëpërfshirëse të ruajtjes së të dhënave.", + "backup_onboarding_footer": "Për më shumë informacion për të krijuar një kopje rezervë të Immich, ju lutem referouni tek dokumentimi.", + "backup_onboarding_parts_title": "Një kopje rezervë 3-2-1 ka:", + "backup_onboarding_title": "Kopje rezervë" + } +} diff --git a/i18n/sv.json b/i18n/sv.json index 5b663c4d02..5ea85d2307 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Hantera inställningar för för- och bakgrundsuppladdning", "backup_settings_subtitle": "Hantera uppladdningsinställningar", "backward": "Bakåt", - "beta_sync": "Synkroniseringsstatus(BETA)", - "beta_sync_subtitle": "Hantera det nya synkroniseringssystemet", "biometric_auth_enabled": "Biometrisk autentisering aktiverad", "biometric_locked_out": "Du är utelåst från biometrisk autentisering", "biometric_no_options": "Inga biometriska alternativ tillgängliga", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Denna funktion läser in externa resurser från Google för att fungera.", "general": "Allmänt", - "geolocation_instruction_all_have_location": "Alla tillgångar för detta datum har redan platsdata. Försök att visa alla tillgångar eller välj ett annat datum", "geolocation_instruction_location": "Klicka på en tillgång med GPS-koordinater för att använda dess plats, eller välj en plats direkt från kartan", - "geolocation_instruction_no_date": "Välj ett datum för att hantera platsdata för foton och videor från den dagen", - "geolocation_instruction_no_photos": "Inga foton eller videor hittades för detta datum. Välj ett annat datum för att visa dem", "get_help": "Få hjälp", "get_wifiname_error": "Kunde inte hämta Wi-Fi-namn. Säkerställ att du tillåtit nödvändiga rättigheter och är ansluten till ett Wi-Fi-nätverk", "getting_started": "Komma igång", @@ -1645,6 +1640,7 @@ "restore_user": "Återställ användare", "restored_asset": "Återställ tillgång", "resume": "Återuppta", + "resume_paused_jobs": "Återuppta {count, plural, one {# pausat jobb} other {# pausade jobb}}", "retry_upload": "Ladda upp igen", "review_duplicates": "Granska dubbletter", "review_large_files": "Granska stora filer", @@ -1849,10 +1845,8 @@ "shift_to_permanent_delete": "tryck på ⇧ för att permanent radera tillgången", "show_album_options": "Visa albumalternativ", "show_albums": "Visa album", - "show_all_assets": "Visa alla tillgångar", "show_all_people": "Visa alla personer", "show_and_hide_people": "Visa & göm personer", - "show_assets_without_location": "Visa tillgångar utan plats", "show_file_location": "Visa sökväg", "show_gallery": "Visa galleri", "show_hidden_people": "Visa gömda personer", @@ -2037,7 +2031,6 @@ "use_biometric": "Använd biometri", "use_current_connection": "Använd aktuell anslutning", "use_custom_date_range": "Använd anpassat datumintervall istället", - "use_this_location": "Klicka för att använda plats", "user": "Användare", "user_has_been_deleted": "Den här användaren har raderats.", "user_id": "Användar-ID", diff --git a/i18n/ta.json b/i18n/ta.json index d2b20ed7c2..96bec1f2f0 100644 --- a/i18n/ta.json +++ b/i18n/ta.json @@ -167,6 +167,8 @@ "map_settings": "மேப் & ஜிபிஎஸ் (GPS) அமைப்புகள்", "map_settings_description": "மேப் அமைப்புகளை நிர்வகிக்கவும்", "map_style_description": "style.json மேப் தீமுக்கான URL", + "memory_cleanup_job": "நினைவகத்தை சுத்தம் செய்தல்", + "memory_generate_job": "நினைவக உருவாக்கம்", "metadata_extraction_job": "மெட்டாடேட்டாவைப் பிரித்தெடுக்கவும்", "metadata_extraction_job_description": "ஜிபிஎஸ் மற்றும் தெளிவுத்திறன் போன்ற ஒவ்வொரு சொத்திலிருந்தும் மெட்டாடேட்டா தகவலைப் பிரித்தெடுக்கவும்", "metadata_faces_import_setting": "முக இறக்குமதியை இயக்கவும்", @@ -175,6 +177,20 @@ "metadata_settings_description": "மேனிலை தரவு அமைப்புகளை நிர்வகிக்கவும்", "migration_job": "இடம்பெயர்தல்", "migration_job_description": "புகைப்படங்கள் மற்றும் முகங்களுக்கான சிறுபடங்களை (தம்ப்னெயில்) சமீபத்திய கோப்புறை அமைப்பிற்கு மாற்றவும்", + "nightly_tasks_cluster_faces_setting_description": "புதிதாகக் கண்டறியப்பட்ட முகங்களில் முக அங்கீகாரத்தை இயக்கு", + "nightly_tasks_cluster_new_faces_setting": "புதிய முகங்களைக் தொகுதிபடுத்து", + "nightly_tasks_database_cleanup_setting": "தரவுத்தளத்தை சுத்தம் செய்யும் பணிகள்", + "nightly_tasks_database_cleanup_setting_description": "தரவுத்தளத்திலிருந்து பழைய, காலாவதியான தரவை சுத்தம் செய்யவும்", + "nightly_tasks_generate_memories_setting": "நினைவுகளை உருவாக்கு", + "nightly_tasks_generate_memories_setting_description": "சொத்துக்களிலிருந்து புதிய நினைவுகளை உருவாக்கு", + "nightly_tasks_missing_thumbnails_setting": "விடுபட்ட சிறுபடங்களை உருவாக்கு", + "nightly_tasks_missing_thumbnails_setting_description": "சிறுபட உருவாக்கத்திற்காக சிறுபடங்கள் இல்லாமல் சொத்துக்களை வரிசைப்படுத்தவும்", + "nightly_tasks_settings": "இரவு நேரப் பணிகள் அமைப்புகள்", + "nightly_tasks_settings_description": "இரவு நேர பணிகளை நிர்வகி", + "nightly_tasks_start_time_setting": "தொடக்க நேரம்", + "nightly_tasks_start_time_setting_description": "சர்வர் இரவு நேர பணிகளை இயக்கத் தொடங்கும் நேரம்", + "nightly_tasks_sync_quota_usage_setting": "ஒத்திசைவு ஒதுக்கீடு பயன்பாடு", + "nightly_tasks_sync_quota_usage_setting_description": "தற்போதைய பயன்பாட்டின் அடிப்படையில், பயனர் சேமிப்பக ஒதுக்கீட்டைப் புதுப்பிக்கவும்", "no_paths_added": "ஃபோல்ட்டர் பாதைகள் சேர்க்கப்படவில்லை", "no_pattern_added": "பேட்டர்ன்் சேர்க்கப்படவில்லை", "note_apply_storage_label_previous_assets": "குறிப்பு: முன்பு பதிவேற்றிய படங்களுக்கு சேமிப்பக லேபிளைப் பயன்படுத்த, இதை இயக்கவும்", @@ -200,11 +216,14 @@ "oauth_auto_register": "தானியங்கு பதிவு", "oauth_auto_register_description": "OAuth உடன் உள்நுழைந்த பிறகு தானாகவே புதிய பயனர்களைப் பதிவுசெய்யவும்", "oauth_button_text": "பட்டன் உரை", + "oauth_client_secret_description": "அவசியம், OAuth வழங்குநரால் PKCE (குறியீட்டுப் பரிமாற்றத்திற்கான ஆதார விசை) ஆதரிக்கப்படாவிட்டால்", "oauth_enable_description": "OAuth மூலம் உள்நுழைக", "oauth_mobile_redirect_uri": "மொபைல் வழிமாற்று URI", "oauth_mobile_redirect_uri_override": "மொபைல் வழிமாற்று URI மேலெழுதுதல்", - "oauth_mobile_redirect_uri_override_description": "'app.immich:/' தவறான வழிமாற்று URI ஆக இருக்கும்போது இயக்கவும்.", - "oauth_settings": "Oauth", + "oauth_mobile_redirect_uri_override_description": "''{callback}'' போன்ற மொபைல் URI ஐ OAuth வழங்குநர் அனுமதிக்காதபோது இயக்கவும்", + "oauth_role_claim": "பதவி உரிமைகோரல்", + "oauth_role_claim_description": "இந்தக் கோரிக்கையின் இருப்பின் அடிப்படையில் தானாகவே நிர்வாகி அணுகலை வழங்கவும். கோரிக்கையில் 'பயனர்' அல்லது 'நிர்வாகி' இருக்கலாம்.", + "oauth_settings": "ஓஆத்", "oauth_settings_description": "OAuth உள்நுழைவு அமைப்புகளை நிர்வகிக்கவும்", "oauth_settings_more_details": "இந்த அம்சத்தைப் பற்றிய கூடுதல் விவரங்களுக்கு, டாக்ஸ் ஐப் பார்க்கவும்.", "oauth_storage_label_claim": "சேமிப்பக லேபிள் உரிமைகோரல்", @@ -212,7 +231,9 @@ "oauth_storage_quota_claim": "சேமிப்பக ஒதுக்கீடு உரிமைகோரல்", "oauth_storage_quota_claim_description": "இந்த உரிமைகோரலின் மதிப்பிற்கு பயனரின் சேமிப்பக ஒதுக்கீட்டை தானாக அமைக்கவும்.", "oauth_storage_quota_default": "இயல்புநிலை சேமிப்பக ஒதுக்கீடு (GiB)", - "oauth_storage_quota_default_description": "GiB இல் உள்ள ஒதுக்கீடு எந்த உரிமைகோரலும் வழங்கப்படாதபோது பயன்படுத்தப்படும் (வரம்பற்ற ஒதுக்கீட்டிற்கு 0 ஐ உள்ளிடவும்).", + "oauth_storage_quota_default_description": "GiB இல் உள்ள ஒதுக்கீடு எந்த உரிமைகோரலும் வழங்கப்படாதபோது பயன்படுத்தப்படும் .", + "oauth_timeout": "கோரிக்கை நேரம் முடிந்தது", + "oauth_timeout_description": "கோரிக்கைகளுக்கான காலக்கெடு மில்லி வினாடிகளில்", "password_enable_description": "மின்னஞ்சல் மற்றும் கடவுச்சொல் மூலம் உள்நுழையவும்", "password_settings": "கடவுச்சொல் உள்நுழைவு", "password_settings_description": "கடவுச்சொல் உள்நுழைவு அமைப்புகளை நிர்வகிக்கவும்", @@ -250,6 +271,7 @@ "storage_template_migration_info": "டெம்ப்ளேட் மாற்றங்கள் புதிய படங்களுக்கு மட்டுமே பொருந்தும். முன்பு பதிவேற்றிய படங்களுக்கு டெம்ப்ளேட்டைப் பயன்படுத்த, {job} ஐ இயக்கவும்.", "storage_template_migration_job": "ஸ்டோரேஜ் டெம்ப்ளேட் இடம்பெயர்வு வேலை", "storage_template_more_details": "இந்த அம்சத்தைப் பற்றிய கூடுதல் விவரங்களுக்கு, Storage Template மற்றும் அதன் தாக்கங்கள் ஐப் பார்க்கவும்", + "storage_template_onboarding_description_v2": "இயக்கப்பட்டால், இந்த அம்சம் பயனர் வரையறுக்கப்பட்ட டெம்ப்ளேட்டின் அடிப்படையில் கோப்புகளை தானாக ஒழுங்கமைக்கும். மேலும் தகவலுக்கு, ஆவணங்கள் ஐப் பார்க்கவும்.", "storage_template_path_length": "தோராயமான பாதை நீள வரம்பு: {length, number}/{limit, number}", "storage_template_settings": "ஸ்டோரேஜ் டெம்ப்ளேட்", "storage_template_settings_description": "பதிவேற்ற புகைப்படங்களின் கோப்புறை அமைப்பு மற்றும் கோப்பு பெயரை நிர்வகிக்கவும்", @@ -293,6 +315,8 @@ "transcoding_constant_rate_factor": "நிலையான வீத காரணி (-crf)", "transcoding_constant_rate_factor_description": "வீடியோ தர நிலை. வழக்கமான மதிப்புகள் H.264 க்கு 23, HEVC க்கு 28, VP9 க்கு 31, மற்றும் AV1 க்கு 35 ஆகும். குறைவானது சிறந்தது, ஆனால் பெரிய கோப்புகளை உருவாக்குகிறது.", "transcoding_disabled_description": "எந்த வீடியோக்களையும் டிரான்ச்கோட் செய்யாதீர்கள், சில வாடிக்கையாளர்களின் பிளேபேக்கை உடைக்கலாம்", + "transcoding_encoding_options": "குறியீட்டு விருப்பங்கள்", + "transcoding_encoding_options_description": "குறியிடப்பட்ட வீடியோக்களுக்கான கோடெக்குகள், தெளிவுத்திறன், தரம் மற்றும் பிற விருப்பங்களை அமைக்கவும்", "transcoding_hardware_acceleration": "வன்பொருள் முடுக்கம்", "transcoding_hardware_acceleration_description": "சோதனை; மிக வேகமாக, ஆனால் அதே பிட்ரேட்டில் குறைந்த தகுதி இருக்கும்", "transcoding_hardware_decoding": "வன்பொருள் டிகோடிங்", @@ -304,6 +328,8 @@ "transcoding_max_keyframe_interval": "அதிகபட்ச கீஃப்ரேம் இடைவெளி", "transcoding_max_keyframe_interval_description": "கீஃப்ரேம்களுக்கு இடையில் அதிகபட்ச பிரேம் தூரத்தை அமைக்கிறது. குறைந்த மதிப்புகள் சுருக்க செயல்திறனை மோசமாக்குகின்றன, ஆனால் தேடல் நேரங்களை மேம்படுத்துகின்றன, மேலும் வேகமான இயக்கத்துடன் காட்சிகளில் தரத்தை மேம்படுத்தலாம். 0 இந்த மதிப்பை தானாக அமைக்கிறது.", "transcoding_optimal_description": "இலக்கு தீர்மானத்தை விட உயர்ந்த வீடியோக்கள் அல்லது ஏற்றுக்கொள்ளப்பட்ட வடிவத்தில் இல்லை", + "transcoding_policy": "குறிமாற்றக் கொள்கை", + "transcoding_policy_description": "ஒரு வீடியோ எப்போது குறிமாற்றம் செய்யப்படும் என்பதை அமைக்கவும்", "transcoding_preferred_hardware_device": "விருப்பமான வன்பொருள் சாதனம்", "transcoding_preferred_hardware_device_description": "VAAPI மற்றும் QSV க்கு மட்டுமே பொருந்தும். வன்பொருள் டிரான்ச்கோடிங்கிற்கு பயன்படுத்தப்படும் ட்ரை முனையை அமைக்கிறது.", "transcoding_preset_preset": "முன்னமைக்கப்பட்ட (-பிரசெட்)", @@ -312,6 +338,7 @@ "transcoding_reference_frames_description": "கொடுக்கப்பட்ட சட்டகத்தை சுருக்கும்போது குறிப்பிட வேண்டிய பிரேம்களின் எண்ணிக்கை. அதிக மதிப்புகள் சுருக்க செயல்திறனை மேம்படுத்துகின்றன, ஆனால் குறியாக்கத்தை மெதுவாக்குகின்றன. 0 இந்த மதிப்பை தானாக அமைக்கிறது.", "transcoding_required_description": "ஏற்றுக்கொள்ளப்பட்ட வடிவத்தில் இல்லாத வீடியோக்கள் மட்டுமே", "transcoding_settings": "வீடியோ டிரான்ச்கோடிங் அமைப்புகள்", + "transcoding_settings_description": "எந்த வீடியோக்களை டிரான்ஸ்கோட் செய்ய வேண்டும், அவற்றை எவ்வாறு செயலாக்க வேண்டும் என்பதை நிர்வகிக்கவும்", "transcoding_target_resolution": "இலக்கு தீர்மானம்", "transcoding_target_resolution_description": "அதிக தீர்மானங்கள் அதிக விவரங்களை பாதுகாக்க முடியும், ஆனால் குறியாக்க அதிக நேரம் எடுக்கும், பெரிய கோப்பு அளவுகளைக் கொண்டிருக்கலாம், மேலும் பயன்பாட்டு மறுமொழியைக் குறைக்கலாம்.", "transcoding_temporal_aq": "தம்போர்ல்", @@ -324,18 +351,23 @@ "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": "நாட்களின் எண்ணிக்கை", "trash_number_of_days_description": "சொத்துக்களை நிரந்தரமாக அகற்றுவதற்கு முன் குப்பைத்தொட்டியில் வைத்திருக்க நாட்கள் எண்ணிக்கை", "trash_settings": "குப்பை அமைப்புகள்", "trash_settings_description": "குப்பை அமைப்புகளை நிர்வகிக்கவும்", + "unlink_all_oauth_accounts": "அனைத்து OAuth கணக்குகளின் இணைப்பையும் நீக்கு", + "unlink_all_oauth_accounts_description": "புதிய வழங்குநருக்கு மாறுவதற்கு முன், அனைத்து OAuth கணக்குகளின் இணைப்பையும் நீக்க நினைவில் கொள்ளுங்கள்.", + "unlink_all_oauth_accounts_prompt": "எல்லா OAuth கணக்குகளின் இணைப்பையும் நீக்க விரும்புகிறீர்களா? இது ஒவ்வொரு பயனருக்கும் OAuth ஐடியை மீட்டமைக்கும், மேலும் இதை திரும்பப் பெறு முடியாது.", "user_cleanup_job": "பயனர் தூய்மைப்படுத்துதல்", "user_delete_delay": " {user} இன் கணக்கு மற்றும் சொத்துக்கள் {தாமதம், பன்மை, ஒன்று {# நாள்} மற்ற {# நாட்கள்}} இல் நிரந்தர நீக்க திட்டமிடப்படும்.", "user_delete_delay_settings": "தாமதத்தை நீக்கு", "user_delete_delay_settings_description": "எண் of days after நீக்கும் பெறுநர் permanently நீக்கு a user's account and assets. நீக்குவதற்கு தயாராக இருக்கும் பயனர்களைச் சரிபார்க்க பயனர் நீக்குதல் வேலை நள்ளிரவில் இயங்குகிறது. இந்த அமைப்பில் மாற்றங்கள் அடுத்த மரணதண்டனையில் மதிப்பீடு செய்யப்படும்.", "user_delete_immediately": " {user} இன் கணக்கு மற்றும் சொத்துக்கள் நிரந்தர நீக்குதலுக்காக வரிசையில் நிற்கப்படும் உடனடியாக .", "user_delete_immediately_checkbox": "உடனடியாக நீக்க பயனர் மற்றும் சொத்துக்கள்", + "user_details": "பயனர் விவரங்கள்", "user_management": "பயனர் மேலாண்மை", "user_password_has_been_reset": "பயனரின் கடவுச்சொல் மீட்டமைக்கப்பட்டுள்ளது:", "user_password_reset_description": "தயவுசெய்து தற்காலிக கடவுச்சொல்லை பயனருக்கு வழங்கவும், அவர்களின் அடுத்த உள்நுழைவில் கடவுச்சொல்லை மாற்ற வேண்டும் என்று அவர்களுக்குத் தெரிவிக்கவும்.", @@ -355,6 +387,18 @@ "admin_password": "நிர்வாகி கடவுச்சொல்", "administration": "நிர்வாகம்", "advanced": "மேம்பட்ட", + "advanced_settings_beta_timeline_subtitle": "புதிய பயன்பாட்டு அனுபவத்தை முயற்சிக்கவும்", + "advanced_settings_beta_timeline_title": "பீட்டா காலவரிசை", + "advanced_settings_enable_alternate_media_filter_subtitle": "மாற்று அளவுகோல்களின் அடிப்படையில் ஒத்திசைவின் போது மீடியாவை வடிகட்ட இந்த விருப்பத்தைப் பயன்படுத்தவும். எல்லா ஆல்பங்களையும் ஆப்ஸ் கண்டறிவதில் சிக்கல்கள் இருந்தால் மட்டுமே இதை முயற்சிக்கவும்.", + "advanced_settings_enable_alternate_media_filter_title": "[பரிசோதனைக்கு உட்பட்டது] மாற்று சாதன ஆல்ப ஒத்திசைவு வடிப்பானைப் பயன்படுத்தவும்", + "advanced_settings_log_level_title": "பதிவு நிலை: {level}", + "advanced_settings_prefer_remote_subtitle": "சில சாதனங்கள் உள் சொத்துக்களிலிருந்து சிறுபடங்களை ஏற்றுவதில் மிகவும் மெதுவாக இருக்கும். அதற்கு பதிலாக சர்வர் படங்களை ஏற்ற இந்த அமைப்பைச் செயல்படுத்தவும்.", + "advanced_settings_prefer_remote_title": "ரிமோட் படங்களுக்கு முன்னுரிமை கொடு", + "advanced_settings_proxy_headers_title": "ப்ராக்ஸி தலைப்புகள்", + "advanced_settings_readonly_mode_subtitle": "புகைப்படங்களை மட்டும் பார்க்கக்கூடிய படிக்க மட்டும் பயன்முறையை இயக்குகிறது, பல படங்களைத் தேர்ந்தெடுப்பது, பகிர்தல், அனுப்புதல், நீக்குதல் போன்ற அனைத்தும் முடக்கப்பட்டுள்ளன. பிரதான திரையில் இருந்து பயனர் அவதார் வழியாக படிக்க மட்டும் என்பதை இயக்கு/முடக்கு", + "advanced_settings_readonly_mode_title": "படிக்க மட்டுமேயான பயன்முறை", + "advanced_settings_self_signed_ssl_title": "சுய கையொப்பமிட்ட SSL சான்றிதழ்களை அனுமதி", + "advanced_settings_sync_remote_deletions_subtitle": "இணையத்தில் நடவடிக்கை எடுக்கப்படும்போது, இந்தச் சாதனத்தில் உள்ள ஒரு சொத்தை தானாகவே நீக்கவும் அல்லது மீட்டெடுக்கவும்", "age_months": "அகவை {மாதங்கள், பன்மை, ஒன்று {# மாதம்} மற்ற {# மாதங்கள்}}", "age_year_months": "அகவை 1 அகவை, {மாதங்கள், பன்மை, ஒன்று {# மாதம்} மற்ற {# மாதங்கள்}}", "age_years": "{ஆண்டுகள், பன்மை, பிற {வயது #}}", @@ -1269,12 +1313,21 @@ "upload_status_errors": "பிழைகள்", "upload_status_uploaded": "பதிவேற்றப்பட்டது", "upload_success": "வெற்றியைப் பதிவேற்றவும், புதிய பதிவேற்ற சொத்துக்களைக் காண பக்கத்தைப் புதுப்பிக்கவும்.", + "upload_to_immich": "இம்மிச்சிற்கு பதிவேற்று ({count})", + "uploading": "பதிவேற்றுகிறது", + "uploading_media": "மீடியாவைப் பதிவேற்றுகிறது", "url": "முகவரி", "usage": "பயன்பாடு", + "use_biometric": "பயோமெட்ரிக்கைப் பயன்படுத்தவும்", + "use_current_connection": "தற்போதைய இணைப்பைப் பயன்படுத்தவும்", "use_custom_date_range": "அதற்கு பதிலாக தனிப்பயன் தேதி வரம்பைப் பயன்படுத்தவும்", "user": "பயனர்", + "user_has_been_deleted": "இந்தப் பயனர் நீக்கப்பட்டார்.", "user_id": "பயனர் ஐடி", "user_liked": "{user} விரும்பினார் {வகை, தேர்ந்தெடு, புகைப்படம் {this photo} வீடியோ {this video} சொத்து {this asset} பிற {it}}", + "user_pin_code_settings": "பின் குறியீடு", + "user_pin_code_settings_description": "உங்கள் பின் குறியீட்டை நிர்வகிக்கவும்", + "user_privacy": "பயனர் தனியுரிமை", "user_purchase_settings": "வாங்க", "user_purchase_settings_description": "உங்கள் வாங்குதலை நிர்வகிக்கவும்", "user_role_set": "{user} {பாத்திரமாக அமைக்கவும்", @@ -1283,12 +1336,14 @@ "user_usage_stats_description": "கணக்கு உபயோகப் புள்ளிவிவரங்களைப் பார்க்க", "username": "பயனர்பெயர்", "users": "பயனர்கள்", + "users_added_to_album_count": "ஆல்பத்தில் {எண்ணிக்கை, பன்மை, ஒன்று{# user} மற்றவை{# users}} சேர்க்கப்பட்டது", "utilities": "பயன்பாடுகள்", "validate": "சரிபார்க்கவும்", + "validate_endpoint_error": "தயவுசெய்து ஒரு செல்லுபடியாகும் URL ஐ உள்ளிடவும்", "variables": "மாறிகள்", "version": "பதிப்பு", "version_announcement_closing": "உங்கள் நண்பர், அலெக்ச்", - "version_announcement_message": "ஆய்! இம்மியின் புதிய பதிப்பு கிடைக்கிறது. எந்தவொரு தவறான கருத்துக்களையும் தடுக்க உங்கள் அமைப்பு புதுப்பித்த நிலையில் இருப்பதை உறுதிசெய்ய <இணைப்பு> வெளியீட்டுக் குறிப்புகள் ஐப் படிக்க சிறிது நேரம் ஒதுக்குங்கள், குறிப்பாக நீங்கள் காவற்கோபுரத்தைப் பயன்படுத்தினால் அல்லது உங்கள் இம்மிச் நிகழ்வை தானாகவே புதுப்பிப்பதைக் கையாளும் எந்தவொரு பொறிமுறையையும் பயன்படுத்தினால்.", + "version_announcement_message": "வணக்கம்! இம்மியின் புதிய பதிப்பு கிடைக்கிறது. எந்தவொரு தவறான கருத்துக்களையும் தடுக்க உங்கள் அமைப்பு புதுப்பித்த நிலையில் இருப்பதை உறுதிசெய்ய வெளியீட்டுக் குறிப்புகள் ஐப் படிக்க சிறிது நேரம் ஒதுக்குங்கள், குறிப்பாக நீங்கள் காவற்கோபுரத்தைப் பயன்படுத்தினால் அல்லது உங்கள் இம்மிச் நிகழ்வை தானாகவே புதுப்பிப்பதைக் கையாளும் எந்தவொரு பொறிமுறையையும் பயன்படுத்தினால்.", "version_history": "பதிப்பு வரலாறு", "version_history_item": "{version} இல் {date} நிறுவப்பட்டது", "video": "ஒளிதோற்றம்", @@ -1300,21 +1355,32 @@ "view_album": "ஆல்பத்தைக் காண்க", "view_all": "அனைத்தையும் காண்க", "view_all_users": "அனைத்து பயனர்களையும் காண்க", + "view_details": "விவரங்களைப் பார்", "view_in_timeline": "காலவரிசையில் காண்க", + "view_link": "இணைப்பைக் காண்க", "view_links": "இணைப்புகளைக் காண்க", "view_name": "பார்வை", "view_next_asset": "அடுத்த சொத்தை காண்க", "view_previous_asset": "முந்தைய சொத்தைப் பார்க்கவும்", + "view_qr_code": "QR குறியீட்டைக் காட்டு", + "view_similar_photos": "இதே போன்ற புகைப்படங்களைக் காட்டு", "view_stack": "காண்க அடுக்கு", + "view_user": "பயனரைப் பார்க்கவும்", + "viewer_remove_from_stack": "அடுக்கிலிருந்து அகற்று", + "viewer_stack_use_as_main_asset": "பிரதான சொத்தாகப் பயன்படுத்தவும்", + "viewer_unstack": "அடுக்கை நீக்கு", "visibility_changed": "{எண்ணிக்கை, பன்மை, ஒன்று {# நபர்} மற்ற {# நபர்கள்} க்கு க்கு தெரிவுநிலை மாற்றப்பட்டது", "waiting": "காத்திருக்கிறது", "warning": "எச்சரிக்கை", "week": "வாரம்", "welcome": "வரவேற்கிறோம்", "welcome_to_immich": "இம்மிச்சிற்கு வருக", + "wifi_name": "வைஃபை பெயர்", + "wrong_pin_code": "தவறான பின் குறியீடு", "year": "ஆண்டு", "years_ago": "{ஆண்டுகள், பன்மை, ஒன்று {# ஆண்டு} மற்ற {# ஆண்டுகள்}}} முன்பு", "yes": "ஆம்", "you_dont_have_any_shared_links": "உங்களிடம் பகிரப்பட்ட இணைப்புகள் எதுவும் இல்லை", + "your_wifi_name": "உங்கள் வைஃபை பெயர்", "zoom_image": "பெரிதாக்க படம்" } diff --git a/i18n/tr.json b/i18n/tr.json index f9355564ad..c73f1ce1cb 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -11,7 +11,7 @@ "activity_changed": "Etkinlik {enabled, select, true {etkin} other {devre dışı}}", "add": "Ekle", "add_a_description": "Açıklama ekle", - "add_a_location": "Lokasyon ekle", + "add_a_location": "Konum ekle", "add_a_name": "İsim ekle", "add_a_title": "Başlık ekle", "add_birthday": "Doğum günü ekle", @@ -28,6 +28,7 @@ "add_to_album": "Albüme ekle", "add_to_album_bottom_sheet_added": "{album} albümüne eklendi", "add_to_album_bottom_sheet_already_exists": "Zaten {album} albümüne ekli", + "add_to_album_toggle": "{album} için seçimi değiştir", "add_to_albums": "Albümlere ekle", "add_to_albums_count": "{count} albümlerine ekle", "add_to_shared_album": "Paylaşılan albüme ekle", @@ -44,7 +45,7 @@ "authentication_settings_disable_all": "Tüm giriş yöntemlerini devre dışı bırakmak istediğinize emin misiniz? Giriş yapma fonksiyonu tamamen devre dışı bırakılacak.", "authentication_settings_reenable": "Yeniden aktif etmek için Sunucu Komutu'nu kullanın.", "background_task_job": "Arka Plan Görevleri", - "backup_database": "Veritabanı yığını oluştur", + "backup_database": "Veritabanı Yığını Oluştur", "backup_database_enable_description": "Veritabanı yığınlarını etkinleştir", "backup_keep_last_amount": "Tutulması gereken geçmiş yığını miktarı", "backup_onboarding_1_description": "bulutta veya başka bir fiziksel konumda bulunan yedek kopya.", @@ -54,7 +55,7 @@ "backup_onboarding_footer": "Immich'i yedekleme hakkında daha fazla bilgi için lütfen belgelere bakın.", "backup_onboarding_parts_title": "3-2-1 yedekleme şunları içerir:", "backup_onboarding_title": "Yedeklemeler", - "backup_settings": "Veritabanı yığını ayarları", + "backup_settings": "Veritabanı Yığını Ayarları", "backup_settings_description": "Veritabanı döküm ayarlarını yönet.", "cleared_jobs": "{job} için işler temizlendi", "config_set_by_file": "Ayarlar şuanda config dosyası tarafından ayarlanmıştır", @@ -395,6 +396,8 @@ "advanced_settings_prefer_remote_title": "Uzak görüntüleri tercih et", "advanced_settings_proxy_headers_subtitle": "Immich'in her ağ isteğiyle birlikte göndermesi gereken proxy header'ları tanımlayın", "advanced_settings_proxy_headers_title": "Proxy Header'lar", + "advanced_settings_readonly_mode_subtitle": "Fotoğrafların yalnızca görüntülenebildiği salt okunur modu etkinleştirir; birden fazla görüntü seçme, paylaşma, aktarma, silme gibi işlemler devre dışı bırakılır. Ana ekrandan kullanıcı avatarı aracılığıyla salt okunur modu Etkinleştirin/Devre dışı bırakın", + "advanced_settings_readonly_mode_title": "Salt okunur Mod", "advanced_settings_self_signed_ssl_subtitle": "Sunucu uç noktası için SSL sertifika doğrulamasını atlar. Kendinden imzalı sertifikalar için gereklidir.", "advanced_settings_self_signed_ssl_title": "Kendi kendine imzalanmış SSL sertifikalarına izin ver", "advanced_settings_sync_remote_deletions_subtitle": "Web üzerinde işlem yapıldığında, bu aygıttaki varlığı otomatik olarak sil veya geri yükle", @@ -460,6 +463,7 @@ "app_bar_signout_dialog_title": "Çıkış", "app_settings": "Uygulama Ayarları", "appears_in": "Şurada görünür", + "apply_count": "Uygula ({count, number})", "archive": "Arşiv", "archive_action_prompt": "{count} arşive eklendi", "archive_or_unarchive_photo": "Fotoğrafı arşivle/arşivden çıkar", @@ -499,7 +503,9 @@ "assets": "Varlıklar", "assets_added_count": "{count, plural, one {# varlık eklendi} other {# varlık eklendi}}", "assets_added_to_album_count": "{count, plural, one {# varlık} other {# varlık}} albüme eklendi", + "assets_added_to_albums_count": "Eklendi {assetTotal, plural, one {# asset} other {# assets}} to {albumTotal, plural, one {# album} other {# albums}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Varlık} other {Varlıklar}} albüme eklenemiyor", + "assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} hiçbir albüme eklenemez", "assets_count": "{count, plural, one {# varlık} other {# varlıklar}}", "assets_deleted_permanently": "{count} öğe kalıcı olarak silindi", "assets_deleted_permanently_from_server": "{count} öğe kalıcı olarak Immich sunucusundan silindi", @@ -516,6 +522,7 @@ "assets_trashed_count": "{count, plural, one {# varlık} other {# varlıklar}} çöp kutusuna taşındı", "assets_trashed_from_server": "{count} öğe Immich sunucusunda çöpe atıldı", "assets_were_part_of_album_count": "{count, plural, one {Varlık zaten} other {Varlıklar zaten}} albümün parçasıydı", + "assets_were_part_of_albums_count": "{count, plural, one {Asset was} other {Assets were}} albümlerin zaten bir parçası", "authorized_devices": "Yetki Verilmiş Cihazlar", "automatic_endpoint_switching_subtitle": "Belirlenmiş Wi-Fi ağına bağlıyken yerel olarak bağlanıp başka yerlerde alternatif bağlantıyı kullan", "automatic_endpoint_switching_title": "Otomatik URL değiştirme", @@ -590,8 +597,6 @@ "backup_setting_subtitle": "Arka planda ve ön planda yükleme ayarlarını düzenle", "backup_settings_subtitle": "Yükleme ayarlarını yönet", "backward": "Geriye doğru", - "beta_sync": "Beta Senkronizasyon Durumu", - "beta_sync_subtitle": "Yeni senkronizasyon sistemini yönetin", "biometric_auth_enabled": "Biyometrik kimlik doğrulama etkin", "biometric_locked_out": "Biyometrik kimlik doğrulaması kilitli", "biometric_no_options": "Biyometrik seçenek yok", @@ -843,6 +848,7 @@ "edit_date": "Tarihi Düzenle", "edit_date_and_time": "Tarih ve zamanı düzenleyin", "edit_date_and_time_action_prompt": "{count} tarih ve zaman düzenlendi", + "edit_date_and_time_by_offset": "Tarihi ofset ile değiştir", "edit_date_and_time_by_offset_interval": "Yeni tarih aralığı: {from}'dan {to}'a kadar", "edit_description": "Açıklamayı düzenle", "edit_description_prompt": "Lütfen yeni bir açıklama seçin:", @@ -1068,12 +1074,15 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Bu özellik, çalışabilmek için Google'dan harici kaynaklar yükler.", "general": "Genel", + "geolocation_instruction_location": "GPS koordinatları olan bir varlığı tıklayarak konumunu kullanın veya haritadan doğrudan bir konum seçin", "get_help": "Yardım Al", "get_wifiname_error": "Wi-Fi adı alınamadı. Gerekli izinleri verdiğinizden ve bir Wi-Fi ağına bağlı olduğunuzdan emin olun", "getting_started": "Başlarken", "go_back": "Geri git", "go_to_folder": "Klasöre git", "go_to_search": "Aramaya git", + "gps": "GPS", + "gps_missing": "GPS yok", "grant_permission": "İzin ver", "group_albums_by": "Albümleri gruplandır...", "group_country": "Ülkeye göre grupla", @@ -1111,7 +1120,7 @@ "home_page_delete_remote_err_local": "Uzaktan silme seçimindeki yerel öğeler atlanıyor", "home_page_favorite_err_local": "Yerel ögeler henüz gözdelere eklenemiyor, atlanıyor", "home_page_favorite_err_partner": "Ortak ögeleri henüz gözdelere eklenemiyor, atlanıyor", - "home_page_first_time_notice": "Uygulamayı ilk kez kullanıyorsanız, zaman çizelgesinin albümlerdeki fotoğraf ve videolar ile oluşturulabilmesi için lütfen yedekleme için albüm(ler) seçtiğinizden emin olun.", + "home_page_first_time_notice": "Uygulamayı ilk kez kullanıyorsanız, zaman çizelgesinin albümlerdeki fotoğraf ve videolar ile oluşturulabilmesi için lütfen yedekleme için albüm seçtiğinizden emin olun", "home_page_locked_error_local": "Yerel varlıklar kilitli klasöre taşınamıyor, atlanıyor", "home_page_locked_error_partner": "Ortak varlıklar kilitli klasöre taşınamıyor, atlanıyor", "home_page_share_err_local": "Yerel öğeler bağlantı ile paylaşılamaz, atlanıyor", @@ -1243,10 +1252,10 @@ "login_form_handshake_exception": "Sunucuda bir El Sıkışma İstisnası vardı. Kendi kendine imzalanmış bir sertifika kullanıyorsanız, ayarlar menüsünden kendi kendine imzalanmış sertifikalara izin verin.", "login_form_password_hint": "parola", "login_form_save_login": "Oturum açık kalsın", - "login_form_server_empty": "Sunucu URL'si girin", + "login_form_server_empty": "Sunucu URL'si girin.", "login_form_server_error": "Sunucuya bağlanılamadı.", "login_has_been_disabled": "Giriş devre dışı bırakıldı.", - "login_password_changed_error": "Parolanız güncellenirken bir hata oluştu.", + "login_password_changed_error": "Parolanız güncellenirken bir hata oluştu", "login_password_changed_success": "Parola güncellendi", "logout_all_device_confirmation": "Tüm cihazlarda oturum kapatmak istediğinizden emin misiniz?", "logout_this_device_confirmation": "Bu cihazda oturum kapatmak istediğinizden emin misiniz?", @@ -1257,6 +1266,7 @@ "main_branch_warning": "Geliştirme sürümü kullanıyorsunuz. Yayınlanan bir sürüm kullanmanızı önemle tavsiye ederiz!", "main_menu": "Ana menü", "make": "Marka", + "manage_geolocation": "Konumu yönet", "manage_shared_links": "Paylaşılan bağlantıları yönet", "manage_sharing_with_partners": "Ortaklarla paylaşımı yönet", "manage_the_app_settings": "Uygulama ayarlarını yönet", @@ -1265,7 +1275,7 @@ "manage_your_devices": "Cihazlarınızı yönetin", "manage_your_oauth_connection": "OAuth bağlantınızı yönetin", "map": "Harita", - "map_assets_in_bounds": "{count} fotoğraf", + "map_assets_in_bounds": "{count, plural, =0 {Bu alanda fotoğraf yok} one {# photo} other {# photos}}", "map_cannot_get_user_location": "Kullanıcının konumu alınamıyor", "map_location_dialog_yes": "Evet", "map_location_picker_page_use_location": "Bu konumu kullan", @@ -1383,6 +1393,7 @@ "oauth": "OAuth", "official_immich_resources": "Resmi Immich Kaynakları", "offline": "Çevrim dışı", + "offset": "Ofset", "ok": "Tamam", "oldest_first": "Eski olan önce", "on_this_device": "Bu cihazda", @@ -1401,6 +1412,8 @@ "open_the_search_filters": "Arama filtrelerini aç", "options": "Seçenekler", "or": "veya", + "organize_into_albums": "Albümler halinde düzenle", + "organize_into_albums_description": "Mevcut senkronizasyon ayarlarını kullanarak mevcut fotoğrafları albümlere ekleyin", "organize_your_library": "Kütüphanenizi düzenleyin", "original": "orijinal", "other": "Diğer", @@ -1460,6 +1473,8 @@ "permission_onboarding_permission_limited": "Sınırlı izin. Immich'in tüm fotoğrav ve videolarınızı yedeklemesine ve yönetmesine izin vermek için Ayarlar'da fotoğraf ve video izinlerini verin.", "permission_onboarding_request": "Immich'in fotoğraflarınızı ve videolarınızı görüntüleyebilmesi için izne ihtiyacı var.", "person": "Kişi", + "person_age_months": "{months, plural, one {# month} other {# months}} eski", + "person_age_year_months": "1 yıl, {months, plural, one {# month} other {# months}} eski", "person_age_years": "{years, plural, other {# sene}} önce", "person_birthdate": "{date} tarihinde doğdu", "person_hidden": "{name}{hidden, select, true { (gizli)} other {}}", @@ -1500,6 +1515,7 @@ "profile_drawer_client_out_of_date_minor": "Mobil uygulama güncel değil. Lütfen en son sürüme güncelleyin.", "profile_drawer_client_server_up_to_date": "Uygulama ve sunucu güncel", "profile_drawer_github": "GitHub", + "profile_drawer_readonly_mode": "Salt okunur mod etkinleştirildi. Çıkmak için kullanıcı avatar simgesine iki kez dokunun.", "profile_drawer_server_out_of_date_major": "Sunucu güncel değil. Lütfen en son ana sürüme güncelleyin.", "profile_drawer_server_out_of_date_minor": "Sunucu güncel değil. Lütfen en son sürüme güncelleyin.", "profile_image_of_user": "{user} kullanıcısının profil resmi", @@ -1538,6 +1554,7 @@ "purchase_server_description_2": "Destekçi statüsü", "purchase_server_title": "Sunucu", "purchase_settings_server_activated": "Sunucu ürün anahtarı, yönetici tarafından yönetilir", + "query_asset_id": "Varlık Kimliği Sorgulama", "queue_status": "Sırada {count}/{total}", "rating": "Derecelendirme", "rating_clear": "Derecelendirmeyi temizle", @@ -1545,6 +1562,8 @@ "rating_description": "EXIF derecelendirmesini bilgi panelinde göster", "reaction_options": "Tepki seçenekleri", "read_changelog": "Değişiklik günlüğünü oku", + "readonly_mode_disabled": "Salt okunur mod devre dışı", + "readonly_mode_enabled": "Salt okunur mod etkin", "reassign": "Yeniden ata", "reassigned_assets_to_existing_person": "{count, plural, one {# dosya} other {# dosya}} {name, select, null {mevcut bir kişiye} other {{name}}} atandı", "reassigned_assets_to_new_person": "{count, plural, one {# dosya} other {# dosya}} yeni bir kişiye atandı", @@ -1687,7 +1706,7 @@ "search_result_page_new_search_hint": "Yeni Arama", "search_settings": "Ayarları ara", "search_state": "Eyalet/İl ara...", - "search_suggestion_list_smart_search_hint_1": "Akıllı arama varsayılan olarak etkindir, meta verileri aramak için syntax kullanın", + "search_suggestion_list_smart_search_hint_1": "Akıllı arama varsayılan olarak etkindir, meta verileri aramak için şu sözdizimini kullanın ", "search_suggestion_list_smart_search_hint_2": "m:meta-veri-araması", "search_tags": "Etiketleri ara...", "search_timezone": "Saat dilimi ara...", @@ -1714,6 +1733,7 @@ "select_user_for_sharing_page_err_album": "Albüm oluşturulamadı", "selected": "Seçildi", "selected_count": "{count, plural, other {# seçildi}}", + "selected_gps_coordinates": "Seçilen GPS Koordinatları", "send_message": "Mesaj gönder", "send_welcome_email": "Hoş geldin e-postası gönder", "server_endpoint": "Sunucu Uç Noktası", @@ -1896,7 +1916,7 @@ "sync_albums_manual_subtitle": "Yüklenmiş fotoğraf ve videoları yedekleme için seçili albümler ile eşzamanlayın", "sync_local": "Yerel Senkronizasyon", "sync_remote": "Uzaktan Senkronizasyon", - "sync_upload_album_setting_subtitle": "Seçili albümleri Immich'te oluşturun ve içindekileri Immich'e yükleyin.", + "sync_upload_album_setting_subtitle": "Fotoğraflarınızı ve videolarınızı oluşturun ve Immich'te seçtiğiniz albümlere yükleyin", "tag": "Etiket", "tag_assets": "Dosyaları etiketle", "tag_created": "Etiket oluşturuldu: {tag}", @@ -1922,7 +1942,7 @@ "theme_setting_system_primary_color_title": "Sistem rengini kullan", "theme_setting_system_theme_switch": "Otomatik (sistem ayarına göre)", "theme_setting_theme_subtitle": "Uygulama teması seç", - "theme_setting_three_stage_loading_subtitle": "Üç aşamalı yükleme, yükleme performansını artırabilir ancak önemli ölçüde daha yüksek ağ yüküne sebep olur.", + "theme_setting_three_stage_loading_subtitle": "Üç aşamalı yükleme, yükleme performansını artırabilir ancak önemli ölçüde daha yüksek ağ yüküne sebep olur", "theme_setting_three_stage_loading_title": "Üç aşamalı yüklemeyi etkinleştir", "they_will_be_merged_together": "Birlikte birleştirilecekler", "third_party_resources": "Üçüncü taraf kaynaklar", @@ -1985,6 +2005,7 @@ "unstacked_assets_count": "{count, plural, one {# dosya} other {# dosya}} yığını kaldırıldı", "untagged": "Etiketlenmemiş", "up_next": "Sıradaki", + "update_location_action_prompt": "Seçilen {count} varlığın konumunu şu şekilde güncelleyin:", "updated_at": "Güncellenme", "updated_password": "Şifreyi güncelle", "upload": "Yükle", @@ -2051,6 +2072,7 @@ "view_next_asset": "Sonraki dosyayı görüntüle", "view_previous_asset": "Önceki dosyayı görüntüle", "view_qr_code": "QR kodu görüntüle", + "view_similar_photos": "Benzer fotoğrafları görüntüle", "view_stack": "Yığını görüntüle", "view_user": "Kullanıcıyı Görüntüle", "viewer_remove_from_stack": "Yığından Kaldır", diff --git a/i18n/uk.json b/i18n/uk.json index 9721728d68..444d163134 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -39,7 +39,7 @@ "admin": { "add_exclusion_pattern_description": "Додайте шаблони виключень. Підстановка з використанням *, ** та ? підтримується. Для ігнорування всіх файлів у будь-якому каталозі з ім'ям «Raw», використовуйте \"**/Raw/**\". Для ігнорування всіх файлів, що закінчуються на \".tif\", використовуйте \"**/*.tif\". Для ігнорування абсолютного шляху використовуйте \"/path/to/ignore/**\".", "admin_user": "Адміністратор", - "asset_offline_description": "Цей файл зовнішньої бібліотеки не знайдено на диску і був переміщений до смітника. Якщо файл був переміщений у межах бібліотеки, перевірте свою стрічку на наявність нового відповідного файлу. Щоб відновити цей файл, переконайтеся, що шлях до файлу доступний для Immich, і проскануйте бібліотеку.", + "asset_offline_description": "Цей файл зовнішньої бібліотеки не знайдено на диску і був переміщений до кошика. Якщо файл був переміщений у межах бібліотеки, перевірте свою стрічку на наявність нового відповідного файлу. Щоб відновити цей файл, переконайтеся, що шлях до файлу доступний для Immich, і проскануйте бібліотеку.", "authentication_settings": "Налаштування аутентифікації", "authentication_settings_description": "Управління паролями, OAuth та іншими налаштуваннями аутентифікації", "authentication_settings_disable_all": "Ви впевнені, що хочете вимкнути всі методи входу? Вхід буде повністю вимкнений.", @@ -70,14 +70,14 @@ "cron_expression_description": "Встановіть інтервал сканування, використовуючи формат cron. Для отримання додаткової інформації зверніться до напр. Crontab Guru", "cron_expression_presets": "Попередні налаштування cron виразів", "disable_login": "Вимкнути вхід", - "duplicate_detection_job_description": "Запустити машинне навчання на активах для виявлення схожих зображень. Залежить від інтелектуального пошуку", + "duplicate_detection_job_description": "Запустити машинне навчання на ресурсах для виявлення схожих зображень. Використовує інтелектуальний пошук", "exclusion_pattern_description": "Шаблони виключень дозволяють ігнорувати файли та папки під час сканування вашої бібліотеки. Це корисно, якщо у вас є папки, які містять файли, які ви не хочете імпортувати, наприклад, RAW-файли.", "external_library_management": "Керування зовнішніми бібліотеками", "face_detection": "Виявлення обличчя", "face_detection_description": "Виявлення облич на медіафайлах за допомогою машинного навчання. Для відео обробляється лише ескіз. \"Оновити\" повторно обробляє всі файли. \"Скинути\" додатково очищає всі поточні дані про обличчя. \"Відсутні\" ставить у чергу файли, які ще не були оброблені. Виявлені обличчя будуть поставлені в чергу для розпізнавання після завершення виявлення, групуючи їх у вже існуючих або нових людей.", "facial_recognition_job_description": "Групування виявлених облич у людей. Цей крок виконується після завершення виявлення облич. \"Скинути\" повторно кластеризує всі обличчя. \"Відсутні\" ставить у чергу обличчя, яким ще не призначено людину.", "failed_job_command": "Команда {command} не виконалася для завдання: {job}", - "force_delete_user_warning": "ПОПЕРЕДЖЕННЯ: Це негайно призведе до видалення користувача і всіх активів. Цю дію не можна скасувати, і файли не можна буде відновити.", + "force_delete_user_warning": "ПОПЕРЕДЖЕННЯ: Це негайно призведе до видалення користувача і всіх ресурсів. Цю дію не можна скасувати, і файли не можна буде відновити.", "image_format": "Формат", "image_format_description": "Формат WebP виробляє меньші файлів, ніж JPEG, але його кодування вимагає більше часу.", "image_fullsize_description": "Повнорозмірне зображення з видаленими метаданими, які використовуються під час збільшення", @@ -261,7 +261,7 @@ "sidecar_job_description": "Виявлення або синхронізація метаданих додатків з файлової системи", "slideshow_duration_description": "Кількість секунд для відображення кожного зображення", "smart_search_job_description": "Запуск машинного навчання для ресурсів для підтримки розумного пошуку", - "storage_template_date_time_description": "Позначка часу створення активу використовується для інформації про дату й час", + "storage_template_date_time_description": "Позначка часу створення ресурсу використовується для інформації про дату й час", "storage_template_date_time_sample": "Час вибірки {date}", "storage_template_enable_description": "Ввімкнути механізм шаблонів сховища", "storage_template_hash_verification_enabled": "Увімкнено перевірку хешу", @@ -353,11 +353,11 @@ "transcoding_two_pass_encoding_setting_description": "Транскодування за двома проходами для отримання кращих закодованих відео. Коли ввімкнено максимальний бітрейт (необхідний для роботи з H.264 та HEVC), цей режим використовує діапазон бітрейту, заснований на максимальному бітрейті, і ігнорує CRF. Для VP9 можна використовувати CRF, якщо вимкнено максимальний бітрейт.", "transcoding_video_codec": "Відеокодек", "transcoding_video_codec_description": "VP9 має високу ефективність і сумісність з вебом, але потребує більше часу на транскодування. HEVC працює схоже, але має меншу сумісність з вебом. H.264 має широку сумісність і швидко транскодується, але створює значно більші файли. AV1 - найефективніший кодек, але не підтримується на старіших пристроях.", - "trash_enabled_description": "Увімкнення смітника", + "trash_enabled_description": "Увімкнення кошика", "trash_number_of_days": "Кількість днів", - "trash_number_of_days_description": "Кількість днів, щоб залишити ресурси в смітнику перед остаточним їх видаленням", - "trash_settings": "Налаштування смітника", - "trash_settings_description": "Керування налаштуваннями смітника", + "trash_number_of_days_description": "Кількість днів, протягом якої залишати ресурси в кошику перед їх остаточним видаленням", + "trash_settings": "Налаштування кошика", + "trash_settings_description": "Керування налаштуваннями кошика", "unlink_all_oauth_accounts": "Від’єднати всі облікові записи OAuth", "unlink_all_oauth_accounts_description": "Не забудьте від’єднати всі облікові записи OAuth перед переходом до нового постачальника.", "unlink_all_oauth_accounts_prompt": "Ви впевнені, що хочете від’єднати всі облікові записи OAuth? Це скине ідентифікатор OAuth для кожного користувача, і цю дію не можна буде скасувати.", @@ -389,7 +389,7 @@ "advanced": "Розширені", "advanced_settings_beta_timeline_subtitle": "Випробуйте новий інтерфейс застосунку", "advanced_settings_beta_timeline_title": "Бета-версія стрічки", - "advanced_settings_enable_alternate_media_filter_subtitle": "Використовуйте цей варіант для фільтрації медіафайлів під час синхронізації за альтернативними критеріями. Спробуйте це, якщо у вас виникають проблеми з тим, що додаток не виявляє всі альбоми.", + "advanced_settings_enable_alternate_media_filter_subtitle": "Використовуйте цей варіант для фільтрації медіафайлів під час синхронізації за альтернативними критеріями. Спробуйте це, якщо у вас виникають проблеми з тим, що застосунок не виявляє всі альбоми.", "advanced_settings_enable_alternate_media_filter_title": "[ЕКСПЕРИМЕНТАЛЬНИЙ] Використовуйте альтернативний фільтр синхронізації альбомів пристрою", "advanced_settings_log_level_title": "Рівень логування: {level}", "advanced_settings_prefer_remote_subtitle": "Деякі пристрої вельми повільно завантажують мініатюри із елементів на пристрої. Активуйте цей параметр, щоб завантажувати зображення з серверу.", @@ -491,11 +491,11 @@ "asset_list_layout_sub_title": "Розмітка", "asset_list_settings_subtitle": "Налаштування вигляду сітки фото", "asset_list_settings_title": "Фото-сітка", - "asset_offline": "Актив вимкнено", - "asset_offline_description": "Цей зовнішній актив більше не знайдено на диску. Будь ласка, зверніться до адміністратора Immich за допомогою.", + "asset_offline": "Ресурс офлайн", + "asset_offline_description": "Цей зовнішній ресурс більше не знайдено на диску. Будь ласка, зверніться до адміністратора Immich за допомогою.", "asset_restored_successfully": "Елемент успішно відновлено", "asset_skipped": "Пропущено", - "asset_skipped_in_trash": "У смітнику", + "asset_skipped_in_trash": "У кошику", "asset_uploaded": "Завантажено", "asset_uploading": "Завантаження…", "asset_viewer_settings_subtitle": "Керуйте налаштуваннями переглядача галереї", @@ -503,26 +503,26 @@ "assets": "елементи", "assets_added_count": "Додано {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", "assets_added_to_album_count": "Додано {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}} до альбому", - "assets_added_to_albums_count": "Додано {assetTotal, plural, one {# актив} other {# активи}} до {albumTotal, plural, one {# альбом} other {# альбом}}", + "assets_added_to_albums_count": "Додано {assetTotal, plural, one {# ресурс} other {# ресурси}} до {albumTotal, plural, one {# альбом} other {# альбом}}", "assets_cannot_be_added_to_album_count": "{count, plural, one {Ресурс} other {Ресурси}} не можна додати до альбому", - "assets_cannot_be_added_to_albums": "{count, plural, one {Актив} other {Активи}} не можна додати до жодного з альбомів", + "assets_cannot_be_added_to_albums": "{count, plural, one {Елемент} other {Елементи}} не можна додати до жодного з альбомів", "assets_count": "{count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", "assets_deleted_permanently": "{count} елемент(и) остаточно видалено", "assets_deleted_permanently_from_server": "{count} елемент(и) видалено назавжди з сервера Immich", "assets_downloaded_failed": "{count, plural, one {Завантажено # файл — {error} файл не вдалося} other {Завантажено # файлів — {error} файлів не вдалося}}", "assets_downloaded_successfully": "{count, plural, one {Успішно завантажено # файл} other {Успішно завантажено # файлів}}", - "assets_moved_to_trash_count": "Переміщено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}} у смітник", + "assets_moved_to_trash_count": "Переміщено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}} у кошик", "assets_permanently_deleted_count": "Остаточно видалено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", "assets_removed_count": "Вилучено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", "assets_removed_permanently_from_device": "{count} елемент(и) видалені назавжди з вашого пристрою", - "assets_restore_confirmation": "Ви впевнені, що хочете відновити всі свої активи з смітника? Цю дію не можна скасувати! Зверніть увагу, що будь-які офлайн-активи не можуть бути відновлені таким чином.", + "assets_restore_confirmation": "Ви впевнені, що хочете відновити всі свої елементи з кошика? Цю дію не можна скасувати! Зверніть увагу, що жодні офлайн ресурси не можуть бути відновлені таким чином.", "assets_restored_count": "Відновлено {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", "assets_restored_successfully": "{count} елемент(и) успішно відновлено", "assets_trashed": "{count} елемент(и) поміщено до кошика", - "assets_trashed_count": "Поміщено в смітник {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", + "assets_trashed_count": "Поміщено в кошик {count, plural, one {# ресурс} few {# ресурси} other {# ресурсів}}", "assets_trashed_from_server": "{count} елемент(и) поміщено до кошика на сервері Immich", "assets_were_part_of_album_count": "{count, plural, one {Ресурс був} few {Ресурси були} other {Ресурси були}} вже частиною альбому", - "assets_were_part_of_albums_count": "{count, plural, one {Актив був} other {Активи були}} вже є частиною альбомів", + "assets_were_part_of_albums_count": "{count, plural, one {Елемент вже був} other {Елементи вже були}} частиною альбомів", "authorized_devices": "Авторизовані пристрої", "automatic_endpoint_switching_subtitle": "Підключатися локально через зазначену Wi-Fi мережу, коли це можливо, і використовувати альтернативні з'єднання в інших випадках", "automatic_endpoint_switching_title": "Автоматичне перемикання URL", @@ -597,8 +597,6 @@ "backup_setting_subtitle": "Управління налаштуваннями завантаження у фоновому та активному режимі", "backup_settings_subtitle": "Керування налаштуваннями завантаження", "backward": "Зворотній", - "beta_sync": "Стан бета-синхронізації", - "beta_sync_subtitle": "Налаштування нової системи синхронізації", "biometric_auth_enabled": "Біометрична автентифікація увімкнена", "biometric_locked_out": "Вам закрито доступ до біометричної автентифікації", "biometric_no_options": "Біометричні параметри недоступні", @@ -611,12 +609,12 @@ "build_image": "Версія збірки", "bulk_delete_duplicates_confirmation": "Ви впевнені, що хочете масово видалити {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}}? Це дія залишить найбільший ресурс у кожній групі і остаточно видалить всі інші дублікати. Цю дію неможливо скасувати!", "bulk_keep_duplicates_confirmation": "Ви впевнені, що хочете залишити {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}}? Це дозволить вирішити всі групи дублікатів без видалення чого-небудь.", - "bulk_trash_duplicates_confirmation": "Ви впевнені, що хочете викинути в смітник {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}} масово? Це залишить найбільший ресурс у кожній групі і викине в смітник всі інші дублікати.", + "bulk_trash_duplicates_confirmation": "Ви впевнені, що хочете викинути в кошик {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}} масово? Це залишить найбільший ресурс у кожній групі і викине в кошик всі інші дублікати.", "buy": "Придбайте Immich", "cache_settings_clear_cache_button": "Очистити кеш", "cache_settings_clear_cache_button_title": "Очищає кеш програми. Це суттєво знизить продуктивність програми, доки кеш не буде перебудовано.", "cache_settings_duplicated_assets_clear_button": "ОЧИСТИТИ", - "cache_settings_duplicated_assets_subtitle": "Фото та відео, які додаток ігнорує", + "cache_settings_duplicated_assets_subtitle": "Фото та відео, які ігноруються застосунком", "cache_settings_duplicated_assets_title": "Дубльовані елементи ({count})", "cache_settings_statistics_album": "Бібліотечні мініатюри", "cache_settings_statistics_full": "Повнорзомірні зображення", @@ -656,9 +654,9 @@ "change_pin_code": "Змінити PIN-код", "change_your_password": "Змініть свій пароль", "changed_visibility_successfully": "Видимість успішно змінено", - "check_corrupt_asset_backup": "Перевірити на пошкоджені резервні копії активів", + "check_corrupt_asset_backup": "Перевірити на пошкоджені резервні копії ресурсів", "check_corrupt_asset_backup_button": "Виконати перевірку", - "check_corrupt_asset_backup_description": "Запустіть цю перевірку лише через Wi-Fi та після того, як всі активи будуть завантажені на сервер. Процес може зайняти кілька хвилин.", + "check_corrupt_asset_backup_description": "Запустити цю перевірку лише через Wi-Fi та після того, як всі ресурси будуть завантажені на сервер. Процес може зайняти кілька хвилин.", "check_logs": "Перевірити журнали", "choose_matching_people_to_merge": "Виберіть людей для об'єднання", "city": "Місто", @@ -691,7 +689,7 @@ "completed": "Завершено", "confirm": "Підтвердіть", "confirm_admin_password": "Підтвердити пароль адміністратора", - "confirm_delete_face": "Ви впевнені, що хочете видалити обличчя {name} з активу?", + "confirm_delete_face": "Ви впевнені, що хочете видалити обличчя {name} з елементу?", "confirm_delete_shared_link": "Ви впевнені, що хочете видалити це спільне посилання?", "confirm_keep_this_delete_others": "Усі інші ресурси в стеку буде видалено, окрім цього ресурсу. Ви впевнені, що хочете продовжити?", "confirm_new_pin_code": "Підтвердьте новий PIN-код", @@ -732,7 +730,7 @@ "create_link_to_share_description": "Дозволити перегляд вибраних фотографій за посиланням будь-кому", "create_new": "СТВОРИТИ НОВИЙ", "create_new_person": "Створити нову особу", - "create_new_person_hint": "Призначити обраним активам нову особу", + "create_new_person_hint": "Призначити обраним елементам нову особу", "create_new_user": "Створити нового користувача", "create_shared_album_page_share_add_assets": "ДОДАТИ ЕЛЕМЕНТИ", "create_shared_album_page_share_select_photos": "Вибрати фото", @@ -797,7 +795,7 @@ "delete_tag_confirmation_prompt": "Ви впевнені, що хочете видалити тег {tagName}?", "delete_user": "Видалити користувача", "deleted_shared_link": "Видалено загальне посилання", - "deletes_missing_assets": "Видаляє активи, які відсутні на диску", + "deletes_missing_assets": "Видаляє ресурси, які відсутні на диску", "description": "Опис", "description_input_hint_text": "Додати опис...", "description_input_submit_error": "Помилка оновлення опису, перевірте логи для подробиць", @@ -877,8 +875,8 @@ "email": "Електронна пошта", "email_notifications": "Сповіщення ел. поштою", "empty_folder": "Ця папка порожня", - "empty_trash": "Очистити смітник", - "empty_trash_confirmation": "Ви впевнені, що хочете очистити смітник? Це остаточно видалить всі ресурси в смітнику з Immich.\nЦю дію не можна скасувати!", + "empty_trash": "Очистити кошик", + "empty_trash_confirmation": "Ви впевнені, що хочете очистити кошик? Це остаточно видалить всі ресурси в кошику з Immich.\nЦю дію не можна скасувати!", "enable": "Увімкнути", "enable_backup": "Увімкнути резервне копіювання", "enable_biometric_auth_description": "Введіть свій PIN-код, щоб увімкнути біометричну автентифікацію", @@ -890,7 +888,7 @@ "enter_your_pin_code_subtitle": "Введіть свій PIN-код, щоб отримати доступ до особистої папки", "error": "Помилка", "error_change_sort_album": "Не вдалося змінити порядок сортування альбому", - "error_delete_face": "Помилка при видаленні обличчя з активу", + "error_delete_face": "Помилка при видаленні обличчя з елементу", "error_loading_image": "Помилка завантаження зображення", "error_saving_image": "Помилка: {error}", "error_tag_face_bounding_box": "Помилка під час позначення обличчя – не вдалося отримати координати рамки", @@ -967,7 +965,7 @@ "unable_to_download_files": "Неможливо завантажити файли", "unable_to_edit_exclusion_pattern": "Не вдалося редагувати шаблон виключення", "unable_to_edit_import_path": "Неможливо відредагувати шлях імпорту", - "unable_to_empty_trash": "Неможливо очистити смітник", + "unable_to_empty_trash": "Неможливо очистити кошик", "unable_to_enter_fullscreen": "Неможливо увійти в повноекранний режим", "unable_to_exit_fullscreen": "Неможливо вийти з повноекранного режиму", "unable_to_get_comments_number": "Не вдалося отримати кількість коментарів", @@ -991,7 +989,7 @@ "unable_to_reset_password": "Не вдається скинути пароль", "unable_to_reset_pin_code": "Неможливо скинути PIN-код", "unable_to_resolve_duplicate": "Не вдається вирішити дублікат", - "unable_to_restore_assets": "Неможливо відновити активи", + "unable_to_restore_assets": "Неможливо відновити елементи", "unable_to_restore_trash": "Не вдалося відновити вміст", "unable_to_restore_user": "Не вдається відновити користувача", "unable_to_save_album": "Не вдається зберегти альбом", @@ -1005,7 +1003,7 @@ "unable_to_set_feature_photo": "Не вдалося встановити фотографію на обкладинку", "unable_to_set_profile_picture": "Не вдається встановити зображення профілю", "unable_to_submit_job": "Не вдалося відправити завдання", - "unable_to_trash_asset": "Неможливо вилучити актив", + "unable_to_trash_asset": "Неможливо видалити елемент", "unable_to_unlink_account": "Не вдається відв'язати обліковий запис", "unable_to_unlink_motion_video": "Не вдається від'єднати рухоме відео", "unable_to_update_album_cover": "Неможливо оновити обкладинку альбому", @@ -1043,7 +1041,7 @@ "external": "Зовнішні", "external_libraries": "Зовнішні бібліотеки", "external_network": "Зовнішня мережа", - "external_network_sheet_info": "Коли ви не підключені до переважної мережі Wi-Fi, додаток підключатиметься до сервера через першу з наведених нижче URL-адрес, яку він зможе досягти, починаючи зверху вниз", + "external_network_sheet_info": "Коли ви не підключені до обраної мережі Wi-Fi, застосунок підключатиметься до сервера через першу з наведених нижче URL-адрес, яку він зможе досягти, починаючи зверху вниз", "face_unassigned": "Не призначено", "failed": "Не вдалося", "failed_to_authenticate": "Помилка автентифікації", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast'", "gcast_enabled_description": "Ця функція завантажує зовнішні ресурси з Google для своєї роботи.", "general": "Загальні", - "geolocation_instruction_all_have_location": "Усі об’єкти для цієї дати вже мають дані про місцезнаходження. Спробуйте показати всі об’єкти або виберіть іншу дату", "geolocation_instruction_location": "Натисніть на об'єкт із GPS-координатами, щоб використати його місцезнаходження, або виберіть місцезнаходження безпосередньо на карті", - "geolocation_instruction_no_date": "Виберіть дату для керування даними про місцезнаходження для фотографій і відео за цей день", - "geolocation_instruction_no_photos": "Для цієї дати не знайдено фотографій чи відео. Виберіть іншу дату, щоб показати їх", "get_help": "Отримати допомогу", "get_wifiname_error": "Не вдалося отримати назву Wi-Fi. Переконайтеся, що ви надали необхідні дозволи та підключені до Wi-Fi мережі", "getting_started": "Початок", @@ -1160,7 +1155,7 @@ "in_archive": "В архіві", "include_archived": "Відображати архів", "include_shared_albums": "Включити спільні альбоми", - "include_shared_partner_assets": "Включайте спільні партнерські активи", + "include_shared_partner_assets": "Включайте спільні партнерські ресурси", "individual_share": "Індивідуальний доступ", "individual_shares": "Окремі спільні доступи", "info": "Інформація", @@ -1224,7 +1219,7 @@ "local_asset_cast_failed": "Неможливо транслювати ресурс, який не завантажено на сервер", "local_assets": "Локальні фото та відео", "local_network": "Локальна мережа", - "local_network_sheet_info": "Додаток підключатиметься до сервера через цей URL, коли використовується вказана Wi-Fi мережа", + "local_network_sheet_info": "Застосунок підключатиметься до сервера через цей URL, коли використовується вказана Wi-Fi мережа", "location_permission": "Дозвіл до місцезнаходження", "location_permission_content": "Щоб перемикати мережі у фоновому режимі, Immich має завжди мати доступ до точної геолокації, щоб зчитувати назву Wi-Fi мережі", "location_picker_choose_on_map": "Обрати на мапі", @@ -1335,9 +1330,9 @@ "move_to_lock_folder_action_prompt": "{count} додано до захищеної теки", "move_to_locked_folder": "Перемістити до особистої папки", "move_to_locked_folder_confirmation": "Ці фото та відео буде видалено зі всіх альбомів і їх можна буде переглядати лише в особистій папці", - "moved_to_archive": "Переміщено {count, plural, one {# актив} other {# активів}} в архів", - "moved_to_library": "Переміщено {count, plural, one {# актив} other {# активів}} в бібліотеку", - "moved_to_trash": "Перенесено до смітника", + "moved_to_archive": "Переміщено {count, plural, one {# елемент} other {# елементів}} в архів", + "moved_to_library": "Переміщено {count, plural, one {# елемент} other {# елементів}} в бібліотеку", + "moved_to_trash": "Перенесено до кошика", "multiselect_grid_edit_date_time_err_read_only": "Неможливо редагувати дату елементів лише для читання, пропущено", "multiselect_grid_edit_gps_err_read_only": "Неможливо редагувати місцезнаходження елементів лише для читання, пропущено", "mute_memories": "Приглушити спогади", @@ -1516,11 +1511,11 @@ "privacy": "Конфіденційність", "profile": "Профіль", "profile_drawer_app_logs": "Журнал", - "profile_drawer_client_out_of_date_major": "Мобільний додаток застарів. Будь ласка, оновіть до останньої мажорної версії.", - "profile_drawer_client_out_of_date_minor": "Мобільний додаток застарів. Будь ласка, оновіть до останньої мінорної версії.", + "profile_drawer_client_out_of_date_major": "Мобільний застосунок застарів. Будь ласка, оновіть до останньої мажорної версії.", + "profile_drawer_client_out_of_date_minor": "Мобільний застосунок застарів. Будь ласка, оновіть до останньої мінорної версії.", "profile_drawer_client_server_up_to_date": "Клієнт та сервер — актуальні", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "Режим лише для читання ввімкнено. Двічі торкніться значка аватара користувача, щоб вийти.", + "profile_drawer_readonly_mode": "Режим лише для читання ввімкнено. Щоб вийти, довго натисніть значок аватара користувача.", "profile_drawer_server_out_of_date_major": "Сервер застарів. Будь ласка, оновіть до останньої мажорної версії.", "profile_drawer_server_out_of_date_minor": "Сервер застарів. Будь ласка, оновіть до останньої мінорної версії.", "profile_image_of_user": "Зображення профілю {user}", @@ -1617,7 +1612,7 @@ "removed_from_favorites_count": "{count, plural, other {Видалено #}} з обраних", "removed_memory": "Видалена пам'ять", "removed_photo_from_memory": "Фото видалене з пам'яті", - "removed_tagged_assets": "Видалено тег із {count, plural, one {# активу} other {# активів}}", + "removed_tagged_assets": "Видалено тег із {count, plural, one {# елементу} other {# елементів}}", "rename": "Перейменувати", "repair": "Ремонт", "repair_no_results_message": "Невідстежувані та відсутні файли будуть відображені тут", @@ -1641,10 +1636,11 @@ "resolved_all_duplicates": "Усі дублікати усунуто", "restore": "Відновити", "restore_all": "Відновити все", - "restore_trash_action_prompt": "{count} відновлено зі смітника", + "restore_trash_action_prompt": "{count} відновлено з кошика", "restore_user": "Відновити користувача", - "restored_asset": "Відновлений актив", + "restored_asset": "Відновлений ресурс", "resume": "Продовжити", + "resume_paused_jobs": "Резюме {count, plural, one {# призупинене завдання} other {# призупинені завдання}}", "retry_upload": "Повторити завантаження", "review_duplicates": "Переглянути дублікати", "review_large_files": "Перегляд великих файлів", @@ -1849,10 +1845,8 @@ "shift_to_permanent_delete": "натисніть ⇧ щоб видалити об'єкт назавжди", "show_album_options": "Показати параметри альбому", "show_albums": "Показувати альбоми", - "show_all_assets": "Показати всі ресурси", "show_all_people": "Показати всіх людей", "show_and_hide_people": "Показати та приховати людей", - "show_assets_without_location": "Показати активи без місцезнаходження", "show_file_location": "Показати розташування файлу", "show_gallery": "Показати галерею", "show_hidden_people": "Показати прихованих людей", @@ -1923,6 +1917,8 @@ "sync_albums_manual_subtitle": "Синхронізувати всі завантажені фото та відео у вибрані альбоми для резервного копіювання", "sync_local": "Синхронізувати на пристрої", "sync_remote": "Синхронізувати з сервером", + "sync_status": "Стан синхронізації", + "sync_status_subtitle": "Перегляд та керування системою синхронізації", "sync_upload_album_setting_subtitle": "Створюйте та завантажуйте свої фотографії та відео до вибраних альбомів на сервер Immich", "tag": "Тег", "tag_assets": "Додати теги", @@ -1931,7 +1927,7 @@ "tag_not_found_question": "Не вдається знайти тег? Створити новий тег.", "tag_people": "Тег людей", "tag_updated": "Оновлено тег: {tag}", - "tagged_assets": "Позначено тегом {count, plural, one {# актив} other {# активи}}", + "tagged_assets": "Позначено тегом {count, plural, one {# ресурс} other {# ресурси}}", "tags": "Теги", "tap_to_run_job": "Торкніться, щоб запустити завдання", "template": "Шаблон", @@ -1963,25 +1959,26 @@ "to_multi_select": "для багаторазового вибору", "to_parent": "Повернутись назад", "to_select": "вибрати", - "to_trash": "Смітник", + "to_trash": "Кошик", "toggle_settings": "Перемикання налаштувань", "total": "Усього", "total_usage": "Загальне використання", - "trash": "Смітник", - "trash_action_prompt": "{count} переміщено до смітника", + "trash": "Кошик", + "trash_action_prompt": "{count} переміщено до кошика", "trash_all": "Видалити все", "trash_count": "Видалити {count, number}", - "trash_delete_asset": "Смітник/Видалити ресурс", - "trash_emptied": "Кошик очищений", + "trash_delete_asset": "У кошик/Видалити ресурс", + "trash_emptied": "Кошик очищено", "trash_no_results_message": "Тут з'являтимуться видалені фото та відео.", - "trash_page_delete_all": "Видалити усі", + "trash_page_delete_all": "Видалити усе", "trash_page_empty_trash_dialog_content": "Ви хочете очистити кошик? Ці елементи будуть остаточно видалені з Immich", "trash_page_info": "Поміщені у кошик елементи буде остаточно видалено через {days} днів", - "trash_page_no_assets": "Віддалені елементи відсутні", - "trash_page_restore_all": "Відновити усі", - "trash_page_select_assets_btn": "Вибрані елементи", + "trash_page_no_assets": "Видалені елементи відсутні", + "trash_page_restore_all": "Відновити усе", + "trash_page_select_assets_btn": "Вибрати елементи", "trash_page_title": "Кошик ({count})", "trashed_items_will_be_permanently_deleted_after": "Видалені елементи будуть остаточно видалені через {days, plural, one {# день} few {# дні} many {# днів} other {# днів}}.", + "troubleshoot": "Виправлення неполадок", "type": "Тип", "unable_to_change_pin_code": "Неможливо змінити PIN-код", "unable_to_setup_pin_code": "Неможливо налаштувати PIN-код", @@ -2037,7 +2034,6 @@ "use_biometric": "Використовувати біометрію", "use_current_connection": "використовувати поточне підключення", "use_custom_date_range": "Використовувати користувацький діапазон дат", - "use_this_location": "Натисніть, щоб використовувати місцезнаходження", "user": "Користувач", "user_has_been_deleted": "Користувача видалено.", "user_id": "ID Користувача", diff --git a/i18n/vi.json b/i18n/vi.json index 49a73f022c..fe25f7dab8 100644 --- a/i18n/vi.json +++ b/i18n/vi.json @@ -558,8 +558,6 @@ "backup_setting_subtitle": "Quản lý cài đặt tải lên ở chế độ nền và khi đang mở", "backup_settings_subtitle": "Cài đặt việc tải lên", "backward": "Lùi lại", - "beta_sync": "Trạng thái đồng bộ Beta", - "beta_sync_subtitle": "Hệ thống đồng mới", "biometric_auth_enabled": "Đã bật xác thực sinh trắc học", "biometric_locked_out": "Bạn đã bị khóa xác thực bằng sinh trắc học", "biometric_no_options": "Không có tùy chọn bằng sinh trắc học", diff --git a/i18n/zh_Hant.json b/i18n/zh_Hant.json index 6130716ce6..101d9c6c6e 100644 --- a/i18n/zh_Hant.json +++ b/i18n/zh_Hant.json @@ -579,7 +579,7 @@ "backup_controller_page_start_backup": "開始備份", "backup_controller_page_status_off": "前台自動備份已關閉", "backup_controller_page_status_on": "前台自動備份已開啟", - "backup_controller_page_storage_format": "{used} 中的 {total} 已使用", + "backup_controller_page_storage_format": "{used} / {total} 已使用", "backup_controller_page_to_backup": "要備份的相簿", "backup_controller_page_total_sub": "已選取相簿中的所有不重複的照片與影片", "backup_controller_page_turn_off": "關閉前台備份", @@ -596,8 +596,6 @@ "backup_setting_subtitle": "管理背景與前台上傳設定", "backup_settings_subtitle": "管理上傳設定", "backward": "由舊至新", - "beta_sync": "測試版同步狀態", - "beta_sync_subtitle": "管理新的同步系統", "biometric_auth_enabled": "生物辨識驗證已啟用", "biometric_locked_out": "您已被鎖定無法使用生物辨識驗證", "biometric_no_options": "沒有生物辨識選項可用", @@ -1075,10 +1073,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "此功能需要從 Google 載入外部資源才能正常運作。", "general": "一般", - "geolocation_instruction_all_have_location": "此日期的所有項目已具有位置資料。請嘗試顯示所有項目或選擇其他日期", "geolocation_instruction_location": "點擊具有 GPS 座標的項目以使用其位置,或直接從地圖中選擇地點", - "geolocation_instruction_no_date": "選擇日期以管理該天的照片和影片位置資料", - "geolocation_instruction_no_photos": "此日期沒有找到照片或影片。請選擇其他日期以顯示", "get_help": "線上求助", "get_wifiname_error": "無法取得 Wi-Fi 名稱。請確認您已授予必要的權限,並已連接至 Wi-Fi 網路", "getting_started": "開始使用", @@ -1475,7 +1470,7 @@ "person": "人物", "person_age_months": "{months, plural, one {# 個月} other {# 個月}}前", "person_age_year_months": "1 年 {months, plural, one {# 個月} other {# 個月}}前", - "person_age_years": "{years, plural, other {# 年}}前", + "person_age_years": "{years, plural, other {# 歲}}", "person_birthdate": "生於 {date}", "person_hidden": "{name}{hidden, select, true {(隱藏)} other {}}", "photo_shared_all_users": "看來您與所有使用者分享了照片,或沒有其他使用者可供分享。", @@ -1843,10 +1838,8 @@ "shift_to_permanent_delete": "按 ⇧ 永久刪除檔案", "show_album_options": "顯示相簿選項", "show_albums": "顯示相簿", - "show_all_assets": "顯示所有項目", "show_all_people": "顯示所有人物", "show_and_hide_people": "顯示與隱藏人物", - "show_assets_without_location": "顯示沒有地點的項目", "show_file_location": "顯示文件位置", "show_gallery": "顯示畫廊", "show_hidden_people": "顯示隱藏的人物", @@ -2028,7 +2021,6 @@ "use_biometric": "使用生物辨識", "use_current_connection": "使用目前的連線", "use_custom_date_range": "改用自訂日期範圍", - "use_this_location": "點擊以使用位置", "user": "使用者", "user_has_been_deleted": "此用戶已被刪除。", "user_id": "使用者 ID", diff --git a/i18n/zh_SIMPLIFIED.json b/i18n/zh_SIMPLIFIED.json index 79cc0ee227..b093192632 100644 --- a/i18n/zh_SIMPLIFIED.json +++ b/i18n/zh_SIMPLIFIED.json @@ -597,8 +597,6 @@ "backup_setting_subtitle": "管理后台和前台上传设置", "backup_settings_subtitle": "管理上传设置", "backward": "后退", - "beta_sync": "测试版同步状态", - "beta_sync_subtitle": "管理新的同步系统", "biometric_auth_enabled": "生物识别身份验证已启用", "biometric_locked_out": "您被锁定在生物识别身份验证之外", "biometric_no_options": "没有可用的生物识别选项", @@ -1076,10 +1074,7 @@ "gcast_enabled": "Google Cast 投屏", "gcast_enabled_description": "该功能需要加载来自 Google 的外部资源。", "general": "通用", - "geolocation_instruction_all_have_location": "此日期的所有资产都已具有位置数据。尝试显示所有资产或选择其他日期", "geolocation_instruction_location": "点击带有GPS坐标的资产以使用其位置,或直接从地图上选择位置", - "geolocation_instruction_no_date": "选择一个日期来管理当天照片和视频的位置数据", - "geolocation_instruction_no_photos": "没有找到此日期的照片或视频。选择其他日期显示它们", "get_help": "获取帮助", "get_wifiname_error": "无法获取 Wi-Fi 名称。确保已授予必要的权限,并已连接到 Wi-Fi 网络", "getting_started": "入门", @@ -1520,7 +1515,7 @@ "profile_drawer_client_out_of_date_minor": "客户端有小版本升级,请尽快升级至最新版。", "profile_drawer_client_server_up_to_date": "客户端和服务端都是最新的", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "只读模式已启用。双击用户头像图标退出。", + "profile_drawer_readonly_mode": "只读模式已启用。长按用户头像图标退出。", "profile_drawer_server_out_of_date_major": "服务端有大版本升级,请尽快升级至最新版。", "profile_drawer_server_out_of_date_minor": "服务端有小版本升级,请尽快升级至最新版。", "profile_image_of_user": "{user}的个人资料图片", @@ -1645,6 +1640,7 @@ "restore_user": "恢复用户", "restored_asset": "已恢复项目", "resume": "继续", + "resume_paused_jobs": "继续 {count, plural, one {# 已暂停的作业} other {# 已暂停的作业}}", "retry_upload": "重新上传", "review_duplicates": "检查重复项", "review_large_files": "查看大文件", @@ -1849,10 +1845,8 @@ "shift_to_permanent_delete": "按住 ⇧ Shift 键永久删除项目", "show_album_options": "显示相册选项", "show_albums": "显示相册", - "show_all_assets": "显示所有资产", "show_all_people": "显示所有人物", "show_and_hide_people": "显示和隐藏人物", - "show_assets_without_location": "显示不带GPS定位的资产", "show_file_location": "显示文件位置", "show_gallery": "显示图库", "show_hidden_people": "显示隐藏人物", @@ -1923,6 +1917,8 @@ "sync_albums_manual_subtitle": "将所有上传的视频和照片同步到选定的备份相册", "sync_local": "同步本地", "sync_remote": "同步远程", + "sync_status": "同步状态", + "sync_status_subtitle": "查看和管理同步系统", "sync_upload_album_setting_subtitle": "创建照片和视频并上传到 Immich 上的选定相册中", "tag": "标签", "tag_assets": "标记项目", @@ -1982,6 +1978,7 @@ "trash_page_select_assets_btn": "选择项目", "trash_page_title": "回收站 ({count})", "trashed_items_will_be_permanently_deleted_after": "回收站中的项目将在{days, plural, one {#天} other {#天}}后被永久删除。", + "troubleshoot": "故障排除", "type": "类型", "unable_to_change_pin_code": "无法修改PIN码", "unable_to_setup_pin_code": "无法设置PIN码", @@ -2037,7 +2034,6 @@ "use_biometric": "使用生物识别", "use_current_connection": "使用当前连接", "use_custom_date_range": "自定义日期范围", - "use_this_location": "单击以使用此位置", "user": "用户", "user_has_been_deleted": "此用户已被删除。", "user_id": "用户 ID", From 74e14b6495030f895efe847f7e0990d7d759bd0e Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 11 Sep 2025 13:22:25 +0200 Subject: [PATCH 04/30] chore: 10 minute timeout for translations merge (#21810) --- .github/workflows/merge-translations.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/merge-translations.yml b/.github/workflows/merge-translations.yml index a7906358af..c2392a5b78 100644 --- a/.github/workflows/merge-translations.yml +++ b/.github/workflows/merge-translations.yml @@ -84,7 +84,7 @@ jobs: # So we clean up no matter what set +e - for i in {1..10}; do + for i in {1..100}; do if gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json state | jq -e '.state == "MERGED"'; then echo "PR merged" exit 0 From b4c72fb60926cab9e7941a1c431990cf7accd8ed Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 11 Sep 2025 08:09:58 -0400 Subject: [PATCH 05/30] fix(server): validate token permission (#21802) --- server/src/controllers/auth.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index e865d18f59..636e3a3047 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -49,7 +49,7 @@ export class AuthController { } @Post('validateToken') - @Authenticated() + @Authenticated({ permission: false }) @HttpCode(HttpStatus.OK) validateAccessToken(): ValidateAccessTokenResponseDto { return { authStatus: true }; From e524c59560cb67430cf041ab8a5780f0d5de8e72 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:28:20 +0200 Subject: [PATCH 06/30] chore: @danieldietzler web codeowners (#21813) I don't see web PRs otherwise --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index cd61814ff8..f9687762d0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,5 +1,6 @@ /.github/ @bo0tzz /docker/ @bo0tzz /server/ @danieldietzler +/web/ @danieldietzler /machine-learning/ @mertalev /e2e/ @danieldietzler From 7e6cd48783092656d1859c1bb11e40e958611fca Mon Sep 17 00:00:00 2001 From: trommegutten Date: Thu, 11 Sep 2025 16:00:53 +0200 Subject: [PATCH 07/30] docs: improve and clarify XMP sidecar behavior (#20334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: improve and clarify XMP sidecar behavior - Simplified and reorganized the documentation for XMP sidecars - Clearly separated CLI import vs. external library behavior - Clarified what metadata fields are stored in the database - Documented filename rules and storage behavior - Explained write-back behavior, including permission requirements * Clarify sidecar write-back behavior for external libraries Updated documentation to reflect that Immich does not write metadata to sidecar files in external libraries unless the mount is writable. Mentions silent fail behavior as described in Issue #10538. * Update xmp-sidecars.md * Refactor section 1: clarify XMP fields Immich reads and writes - Rewrote section 1 with a simplified 3-column table: Metadata · Writes to · Reads from - Corrected date field logic with prioritized read order - Clarified that Immich only updates fields that have changed - Removed incorrect mention of dc:title * docs: clarify tag reading priority (TagsList, HierarchicalSubject, IPTC:Keywords) Updated the documentation for tag metadata extraction to clarify the prioritized order in which Immich reads tags from imported media: 1. digiKam:TagsList 2. lr:HierarchicalSubject 3. IPTC:Keywords This reflects the actual logic used in the getTagList() --- docs/docs/features/img/xmp-sidecars.webp | Bin 11118 -> 0 bytes docs/docs/features/xmp-sidecars.md | 67 +++++++++++++++++++++-- 2 files changed, 61 insertions(+), 6 deletions(-) delete mode 100644 docs/docs/features/img/xmp-sidecars.webp diff --git a/docs/docs/features/img/xmp-sidecars.webp b/docs/docs/features/img/xmp-sidecars.webp deleted file mode 100644 index f00b32c730145bb14d391a3504e9517b09d321b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11118 zcmV-!E0NSvNk&FyD*ymjMM6+kP&go3D*ym+kpP_mDir~+06xJ|qf4iyBcZZ%&RFmm z31|Sy=tD67sYwm#ypguvdj?j2gW&`J|Ell!eLH<2f7s}N|I_PB`_D?3{ty5E{r(I8 zq5tCl|NsBs^UoL6>;1o`XP^iF|89-~U+`V3e~A>k z^vCr#+JpSRi+`d2jQd*q1pN{J$NYD?&-cIi|I~l8{nGbI`ak-w{r~2F(tLyeGyci@ zBkj}dzxQMR|GJOw0@$)t15nc~pe@iVD~Lv5GoeqJb36B@S121Q-7t5Gq+FbM@4e_x zYP=30{%_~)zqW0?EDkr;Yt2WJDn5u~74H}4dChD1AzjV{aIHIz69P1De=S+=8Q1Uq zAM%8ESDu&0Idz9viYsbAJcN!m!o90WJ!~QsvpRzinFo<-Zzc6I#|b zK>+U+7!h_wruMNnDGQ$i)U9v{Yfx}Kk{@ulW*~Z=(+H9tXtc1s*|;J} zhc(@zk-fiJb|U=9TUmGibmw0^UYQ1nJ9=O`1e$3k^pA1Fx*I{3%~QTA38%nq=0`=u zjKgBCPPXAJMkgjl!+E)D6EGM-hyV!j4QK}=-OJts@-Xq~D+5hkBF_z9@%OlA*%DcP zzBH#7|Lla${QqQQqI^pQ4XZ+g^>zFK8ub zGCWD{QCg5b?J5!kZ1R#LBm_62vagppO2!HSZtij1{Jv9y6{y+RZmbq?{GB`MvkrDp z33~i=A%#kvp{Yt%EEY~%$NJB0S9Z%PgT;wrZmOm)5z6j*(3 z{~%m#XZVdFe(_5v#pwCaS+lTc%3xE}Q=KuX>M+wqe|eisl(*>Z<~!HMji=5WQh^|1 zZfryi`P-Ku{^J@g?mqR9+=09R!_~&>;0r;mxOAmO`22k#kOW7U@Y`}gxwYvO5PnCF zChsgV@oV-r^;)p%?Vk(H%I>TR{0Sb#Y+SznbLCx|C78&)|NP?)W3&uewHhCHn_MC+ zQN--ledH(;O$DPQMH68426GUq>N-T*6Zg9emUn1r_+79c=z3Me8 zeLX-_=hV=a3H&F-_XBLD*|LuAN-gpwM)bEITI&&~f<4!#Z9KOv1;b)PoQ;+8sE;Tu z{ts{f{u#_wVjcx2i5Oz{42or@@HqF5y1w-r0e|A?3m$BO0Skq=$2XB_l;5tbY|S?@ zTcLv2i%cMjZ#mO%PasbJ5F>iR)sGQJciVzX;J0mAT9TY~qH^0ah>nT{;WH1|J2_9R z;2=6mdsFC3{&u|#8+@M#Dun)Xs!32Dd%5bVD5Kbu%w|;mqbiuLa2Mq`OsKA00|$W` z({PaTn|%fbYmS8gZu67$=9bhKjvcs73&SfE3&LXrvcw~!`<%Wcy61Wm3tHAsdSDZe z=ItKYX14>5L#j+3Wsp03ssCm4C|~}#=sfhI)~U1(s~_F#zYVd$S&^#{I*~_iGd*w3 z8e4cviMvOX*r1+XwDaMa-5ow zWQg+cG+_e*RR$Vq8m4gsvdea47JU0h3H~04do36eqCQW3;ML)>Z#)=ztg)l_$zqMX z?;UoGMc*_A#4|;DhU<6kfH&8zHv&W=wGNht7jYLK@M=yfy0iXE25VN_i4G$2s>kqR zP&KPoo&vdC^f(TIjwiAbIpXMkloo}C4NK@L0h4OAeyI0XRTOywK$#M5fvW##Vfak#>*Uf;+kA92pDFQx zNME@cE_Hz!@E`cqlL<&^BLHTh0J{%&Y=#VJ_RU&2v0R`F(x&cXD>{1-e~fBN2^VbO z%btz+$!$0sOnb`;T$E5JsrQ!=aSn&ME6|onUl83568Khw#ba@b& ze6MTle2dW7)#Lgr0`FS+tE||LM0AXNe6+pKdgW`a_O#}fvVikp<^;#m322ahzwCT_ z0O{8sD-&8OBbsB4!32P)6|zn-7Y~`4IKRBX2|$2V7(T;5bWKOc%Tg*dGi+kbP}B5{ zHpI0662TMKA|KQd^h5f-+RQtX=jRHkC@6+NV_)@5$36(b_WKC{z5^$-J&VaiG!`L$ z18`ZS=10Fz;S+`8@)Ub{IBT}rR9DqbQ`yF@s;}39N`>&*nV8Mx^j%p54^{ObyFZlG z>Va`n<-^}(3XZR7fPbu8P+sM2-B;GlwI<7wB0q)u3TyC{R%}ek5?T*FsGhYHx)!D- zs9FI)>781rgZ7UT9Cz*>`yfk(zHbcAHarH?YicO*;@_@*?CLk~juv14pfAN_= zZOF5VUkc{gCk~4eLBwcc zOov`dE~&FRRsygvEgM)qvqiCwV8*(eSBqI|GJIlY>_!3-Tiwu~Bku!l#&(b7XAkERq3&~Z2HlMa#6$W3h+DM_^7FynuB;~ls{w|M9SZJ(AIik zHrtil{!PK3{mUb@#Sy(`+rsV@Isp@5YkIESaq@5XRM%r7vw0bNyF><4TDFchAn+cB zXXiJoP$+>f&z{j?QjdjLkFM5Dje868UtkU>D_`T3al>J29i?{501~SOVCB63R*ZV2 zKQRcI6ow<<#bE)rk?rg?qYfJ7Dz2uivhtIbH4-0Jvi%09@9KGz%vUd_ng(pc!toI$Ic#z!TH^c=3dzfeyOnW? zw=sb*yAjl)_){Hd7>r1qWkWnfRpr$mu80dMylN#^4t@CUiG}y9)${wMt%})#ux^$P zYinUpY#fONwDzFiJdC;T`TukM@H0*gl${sXzao%_ps#(W)v|C-Q{h!BOZt6NtS}BE z-)*-!6+Araf3CN!u2^^80Kp`~%Nmho^5GjYPgD||qsW{yc0uCU=y-Z1Rv93p?p8-_ zN5bbX22&}$IfBX3ZWC69R2?EoXwioCPuDghh8d*>j_zygS ze@tRuEE%&-I+l-hqLcnLjIGRHO7zP!cGHP?ckKq=P7e&=`*o=offcPok+3UMV4F=} zK0M#yY(`Lz;Af|I#FTU=h_@EURD8rCWO0B5O;?^2?X9~s@_|fCt=Kh}ASM0Faa23d zJOyy>rJN9{FYEhTudrCF*8K&B4OBYBiS59`zOzdKz`{SL!4s|LdLd3+)cGLAQ|Y^Z z9Tx8NpEVQDO`Kx**w=AW|iA&)i+`%20B zweMv|dE)CGS%_Y%R225Lis5jSSFXC!*cL&)n2!z=?&a5=9Nt?=#8zMYJk8S0 zmjcM*XlCgYZm6`CBxP`X-m=qj&pxEATb+S2a<9NC&2)0H7tO1jBRILV#uxYFLP-;j z=tY?ZarT(uHIdEPxm3P?~YRTmcoI-%L4m5uzj_Np9^(J zbmm;|GIb~85WQBY9$n|O;z4XQ^S@9iISu-|U$89bVTCjqLA`D>kHuqI`~;wM!&oqZ za9ZawVE7T`aqn44Ng0hnkrsFS@2{0QSm+@NzYo}f&o76$>u>wxYf$&4L3gQ@soSGO zV(j=<)Un&MGp0n^XWld+7iMD3(1Ov+6M(ccW4n2(!Rm`zm1`+np0+pO?4U`T?p8h< z`#>o*&95W(3ah9Iv#IDv=Tw7t=bnh{{hi@d7_07LKRd@Z)9@WHu(+u#>48}*GSsVDVG_WT#u5cslF-|l*JilEp~C$!qn+FpK!&OD>NNBQY4IDN@(gu zW4&EorsD{w1Sm{9g=2%ZCNuCH&55jHzxoX`?DykD*bP3jLh}t)?nTd~Q6tm$sUKTb zj&H^&EiNGEvc8}|acvH8XhV-bXCovHm~IrNwEY0cFipBO1#Ns(%;kI zU1jmy25~3WF_7ij?TgX3bv#UCHOO!GkE~crP7U6&!Z8vAoKaZwU%Tw-y;Zr~!@A7x z2u|y&8En=s+kMX%c$5mn{(H8;7BEOf_xHZT!mk59n;_Gx1}Zr|GUUKxmj~`H{;mqC z#v2$SDBw6~OM@YLzgn@R+mlc;Pv_KMbn|4FK{-+z#%k4?RJ;u!+i)v=Vvvyr!c$-C z;Fl65FBx};ArM?~EefJv0dim;a;gRiJ~LS}uo8|)?C$TeA_oKB!g_=9xBpF&3S{2N zKG-8o=KQpgrdKHWTGdAPLE$ftcafwGXrW8!Vzf7eK4-2YzS}9T2gPCV(gK!>C(Q$b zh{*oGXqdG78B`|e$4L(OVM{0Ukz}Q%9J27h9_ba9e)*wWN4whZ>ZYfbhwG%v#0;pF zq@+U33B(iXa(t0nnM2{k9&(A(WU5sdC?H`B%@gSC^5$+_t4vWwC*fU%Gchg;+6I0A zMNMQ*SpB_ItE-wu;^-x=QfKMdz5JO^zaFMsvmXAASq43_3%Ao<3K`H8guW*cU;X4K zs6Q$^!?jv)<$Y|aIs5{vAOet8rIdxZdpgThpduBo%NIA0UP}kFwHAZ-|b>PNq z_uv+O@0^^9ENfCOL>DycP|zlN$$QuBh}>togAMIxX=(xuW%1272{d#|%H);c{u?=n z^tVB*K$Kb@+SRRiqKc|NVe6>~MAS$663$~{)O}kbRli}QiAVx&O?^!NAT&v6PYdw; zrD4wst}ezG#RVHEV%g^kH-mIe!aeVs7~A@$W><*HtBC-`)YhJU9lEe6N?svqiJpO? zWa`OSAax^5u`px`4S1a9TXJ>4Nvwub-|y;%cb#pFcLFLH<+z~~V^BI;;~TkNU6&j{ z*oB84$2K`BEBX~l3(xYH`Gj6^X*G&cIef8cfao2mbCBcCYg>>(iDp9^K)R{$5)Ewe zjGbZSy`L4tB1W#2r_=@!D%vO-IXBNTDRTP5B(QN) zlYN|k0WX3@Zvd`>sRU+`mNA~wwa&i}hr)Kdoh2R?@}zCw3a^EctiK@y45`A~e%bDR{X-eJVF8ldt3xE1h(PPO*OO!$fryx}NhTc4vJ@nD_2z*w zG6J(riJ@M022C%?7uldj5oF5s@Eg?r__PWF4zghuB`(zlSlGxLM9bK@x0e53T{S08 zKI8jB$KXe#aORM0FUy#hzi`jnMKw-6$~jNdOk}yHDhjqj%4@!+R!W*vH#3q{?g*d{ zyZSD@5fRNSnjOG*c6DglaB36p=@Xb*_6%yrfM}0WlN@=qez;woCg!%>vx?TWX+}>S zss+loFox@FEBG<^*4RN5)G6CQ#XXA2$cFnT&h}tTmRxg}E3l1;1hbRbM2259Zs*Cq zo1X^~)29gkE9JcxxM1Ij`B%=f8pti>nXYt3AMvt6zwdX znlNVe@h-KT*2cffE8xI>ujs=^k!Y38<^G#5EQx37*;Q60G1@N@vzna@=IQV$Q#a+}0j zu6~qq*>M&7cQjG)DhSiS1XwV1W4jd04m*Cm`#xX!uJ5bTpYcTBT&<~5R=AKw9a3SE z0Q~YLv|m~u0t<*{-o`dC1{du+5ANPNcxU6x%4Wle4+Ik>P#qS;Bv4kU>1aer;RA4E z1!7D%bSR44F0hiS+!BsAFOB<0T$gm^V_G9^Y6t5YkX$wi6?r5b@B24p)i0p?ol+Os zkrhA{-jrC-@RmRKDHfpcl-Pa}3q;5veA%R3T~!KNnavAn&&^kpuiA1Z8e_Tpc|Pdx zMCe7OF060vhmr*%`?cG`X%jT^`@4S~XGwKGT@Au$8IC$n;QMO50+Z=`+(e?o8$ek3 z%`!Tidh*ldwxXV9i_JerK2LwsH`fhO7v^%-C>U)w?1I^YMZf&Nw;}2gQ}>(mZCqF# z5%Y5Gf+D2QZAfy@;0E)k_iq6cRPay@!a8{wEAi5i#L+i)q=pT=P;WH;!?Zo%DE`Cv zQUs^t@`VY4EXQTxqUE^B<5}C z27CLClAdxB1{Or=-X4b-yh#%~7~~8ln38%o1|3Iw^8D~I0y+LmH*UZ8Ol1S6eLn4$ zP5-uk)6fzyj(_dyB7qF|Rr`v6Rqy3ieJ&la2{-0!+Nw`fu@n+GcIxGW8uc`)Kb8!8 ziEG@P%3f5%)VoYep>eI0e8nN3>b|6e`5!nl6o0Q9_a90N781)cVpTzXIK;2m%>Sr; z#BLmP4Pl=ZK($gXw~1D*%7G6cn!ijMS8>}KbdeucU?nz^xsAFzecQKZdp3&sdMyGa zx>DLQUo*Kb%z@_@O>Gl?e*Q-;m2x9OJo+^f7ymMZ&+7Z0i!iL1{b|B!2@p(u zwrU^ip;TNY!edbFCX1-G7UTtR?xma#Q@CGlZNa6EU?!(zg$MvY7-9W?QG}|RQAu<6 zhcIVGKU{fw{e+CNuKK7~veK^sTu5rSO6hlro^#RyrB~B@>L6M|7>1sTYH5%YrUx^@ z?bdJsi~q?v#c6`-(7Vt$;+P=J@+vpd%?oocR?C)q?+v0m5>AQ{rAZ>n=UnFW?Q0{W}g6vSOo}(^E z-CUvpVpbb``W&MM)3On+^y*q!2h`G9p<*~p!I!}V=9C2^aUMH)b7{lHpWg@L-tcca z&qJ(&0E0~=dUpIrC-d)fHM3&ZV@RwwBFXaLMkeEBI6miNn~U;c0V;)zE9!J@L>#^j zmI=GLe1|Dc`jToR#Q2rqL(qjmBh-4^ap`C%Hno|owYPl$x0eKzPlJ}-WGp>{{Od#g zDJxp)^;Xj8g1z_G`*r~$XBQccjT3Jm-g!6XMJn1nO7Fy{i@e^T@77jp=P3Hkp}<0q zvj?fWVWxpC2E=HZDl(-id=lhxam5hPJbPE}kd9l1)CwGA4cL|9!6kvLTs#?HILEfm zC)GO#rm4m`J6j#47P~@QZfF><)+<19#Az<4`!TsVp*@>jwJic><~Pe#?$&b%z`d2F z=1wTbxPn}5-p?kB%1DBC(_O@rGcIDW%edCF4yb8oGv{^mCF(Ohjk)4fDtnnwQK=~~ z$cI&!mi&2urhCAnXR!@)5QXe<6abOy~2ptK(45=}cV)7YDa2K;b>aXi1(obDKm23+9SU*F5oBN6M~gi`*mnM>$G8ioJ3j& zz|;vpU#$a1{sXTJ*4IH)!Dl`$4yNgKOn~_CWx^Y_cL5ydsjP?lGD@zfRF~_Q@wLM`V*E zsQ5(j%6K^rt|nvodmT=qh^)rC{cp?6;6jTn(`o{vKzklU?46QF@`p>Uorgy(KQfyw z?i7(fOXX`-8{G%!dlgGJ>;#bs$CNx&M<^TA+!y!w>B+{pdqpm;0q^N|{1XrXfbqL2 z<~S_q7g9G7(9_Jtuwew2*OX_yI1`K_Y@P&&Ixy(ewKoy~jvS;Vi zp{+#heNLF!6_qCt*^zb@#0ZyGUQIw?L8Bzp@ex?>PEuUFES14NNqh1t*dGW zVE&yOVz`d2()G7vz#}-w7#wmt$-GrA8EH?IyML~>bbTa65Q?id1Yqc=b+9RD*QgtkM`*`MszKIQ#aL=x4hjKr7G`p&-(j!1Lqb!(1Q1O^X8 zu_#6^g)d?#Eg%XH@*V9Xf^8Qwa=ce7vMp)AjMzgppS1^8{8CtG; z{v^$Yg*(psK3zIv-!^HpwQvu}-+Hq1?*U2fe7)Qm44g?VgqejefsqGn@Tj0Iwx4x} z9~{={2!p+(ESM+TiyA&m{ySE^AU$_`!>9=_p&@W=LBRsx4FMD6#ua45d~D}2ca8~i zUNgqy3Xg1QB-blQ4x`e+kMft$V8O->1@>?^b!C_*;>%OAudDlwjo}OtW=w4=oBmp!A~Kaga;g@RImT2T_dR8Q1;iQIt!_@p)!8T1rUhXbd6z` z8TMDiQx&=}Yg{=TwwvDb+5rJl^B~U2Pji+c;+N`YgQQ)XyOR<~V0XjUeGlw_^g{fq z+vn0z0JS{%O`le!RP4g>KUE~_=NhZfvb%3r&@`=&C~w-_Gjtv_!9-qWzW%OOaa=FK zv25S+FNsa)UG!Q+W4x5tP1K&W(6`HOd9$M|HaixhkCrOXt(Ap9R9!P zdx#Kh_W6d*hv~L06E2(BACralXTqS9F7o_`un52I;}iMv;(fUv!1%y5c*KYwV@W4y zb3b3sbe)65;QNQG#fSpJ^e2cYkBvo;>Fa&xLs{vB+iq8L`8NiC_biTHp-i1Unq``O zcd6rxrLarDVaeBu4FxSd&KY00_z+F4t$*}2^q6*MC+`gf!r@IvdFBD4^hfRetW!EpaMYmT1J;XC?aJkR#b?z)O!Q0*3wp^5;S)d&X@auJad%6`AM zB6DV`=%DuvXsC{9jyD7n19%D-93|d|Aw?jVC2m(IVzdv3*y{5IR~^l@Em!th=2w!0 zYS1J{u?Oyt!uMOC-`>HOV;HbWZ`PI!vI?R91q}VzYc>B!*~mLyM7lOoL&WBcAc;c! zI*y>$2o}lNr!=;&;EF(B7l1d17hd=Z82l13ONcE?irrM&*A|THUZt%YU*I!{U5EZE zs|95C4TGR%Tb7x#IL!Tk61EcJkea*?xshjwCP^8?pSji5wNsWptxh9($Q<$2KW=AG z-G*}#u#$|RzXZu?M1){(WWWNMpI9JBZVyo=nCY;-sYwstQw zMkl-l1GgBRE}C2$JAljBuI|XKI3~YL^7r`Hi<|N(3@Y~!@3fv3ZoAie;6R0G3~63E zaY_F>>K^sf*G3ebayhFdNZyixB%SFB=vLG+}!RHf-Z=AbGT(sjhvTUlz57y;Wm_?Ikz?u zH~aYB;vi>-#rQuOhI5MKdps6r`P_|fryNOq`B0UJ4FLtwot!uQ@hT25-nV}e9k{{b$)*LgQFNhp8HxhV__(uxbv(YwzIw1XM3TK9m~vtH&}+dYQV$PXqDF+{>yNZ_N`r-< ziD&%(^TjR%2g<$sY5(H-$2gRpa)Zio>qx +:::tip +Tools like Lightroom, Darktable, digiKam and other applications can also be configured to write changes to `.xmp` files, in order to avoid modifying the original file. +::: -XMP sidecars are external XML files that contain metadata related to media files. Many applications read and write these files either exclusively or in addition to the metadata written to image files. They can be a powerful tool for editing and storing metadata of a media file without modifying the media file itself. When Immich receives or detects an XMP sidecar for a media file, it will attempt to extract the metadata from both the sidecar as well as the media file. It will prioritize the metadata for fields in the sidecar but will fall back and use the metadata in the media file if necessary. +## Metadata Fields -When importing files via the CLI bulk uploader or parsing photo metadata for external libraries, Immich will automatically detect XMP sidecar files as files that exist next to the original media file. Immich will look files that have the same name as the photo, but with the `.xmp` file extension. The same name can either include the photo's file extension or without the photo's file extension. For example, for a photo named `PXL_20230401_203352928.MP.jpg`, Immich will look for an XMP file named either `PXL_20230401_203352928.MP.jpg.xmp` or `PXL_20230401_203352928.MP.xmp`. If both `PXL_20230401_203352928.MP.jpg.xmp` and `PXL_20230401_203352928.MP.xmp` are present, Immich will prefer `PXL_20230401_203352928.MP.jpg.xmp`. +Immich does not support _all_ metadata fields. Below is a table showing what fields Immich can _read_ and _write_. It's important to note that writes do not replace the entire file contents, but are merged together with any existing fields. -There are 2 administrator jobs associated with sidecar files: `SYNC` and `DISCOVER`. The sync job will re-scan all media with existing sidecar files and queue them for a metadata refresh. This is a great use case when third-party applications are used to modify the metadata of media. The discover job will attempt to scan the filesystem for new sidecar files for all media that does not currently have a sidecar file associated with it. +:::info +Immich automatically queues a Sidecar Write job after editing the description, rating, or updating tags. +::: - +| Metadata | Immich writes to XMP | Immich reads from XMP | +| --------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Description** | `dc:description`, `tiff:ImageDescription` | `dc:description`, `tiff:ImageDescription` | +| **Rating** | `xmp:Rating` | `xmp:Rating` | +| **DateTime** | `exif:DateTimeOriginal`, `photoshop:DateCreated` | In prioritized order:
`exif:SubSecDateTimeOriginal`
`exif:DateTimeOriginal`
`xmp:SubSecCreateDate`
`xmp:CreateDate`
`xmp:CreationDate`
`xmp:MediaCreateDate`
`xmp:SubSecMediaCreateDate`
`xmp:DateTimeCreated` | +| **Location** | `exif:GPSLatitude`, `exif:GPSLongitude` | `exif:GPSLatitude`, `exif:GPSLongitude` | +| **Tags** | `digiKam:TagsList` | In prioritized order:
`digiKam:TagsList`
`lr:HierarchicalSubject`
`IPTC:Keywords` | + +:::note +All other fields (e.g. `Creator`, `Source`, IPTC, Lightroom edits) remain in the `.xmp` file and are **not searchable** in Immich. +::: + +## File Naming Rules + +A sidecar must share the base name of the media file: + +- ✅ `IMG_0001.jpg.xmp` ← preferred +- ✅ `IMG_0001.xmp` ← fallback +- ❌ `myphoto_meta.xmp` ← not recognized + +If both `.jpg.xmp` and `.xmp` are present, Immich uses the **`.jpg.xmp`** file. + +## CLI Support + +1. **Detect** – Immich looks for a `.xmp` file placed next to each media file during upload. +2. **Copy** – Both the media and the sidecar file are copied into Immich’s internal library folder. + The sidecar is renamed to match the internal filename template, e.g.: + `upload/library//YYYY/YYYY-MM-DD/IMG_0001.jpg` + `upload/library//YYYY/YYYY-MM-DD/IMG_0001.jpg.xmp` +3. **Extract** – Selected metadata (title, description, date, rating, tags) is parsed from the sidecar and saved to the database. +4. **Write-back** – If you later update tags, rating, or description in the web UI, Immich will update **both** the database _and_ the copied `.xmp` file to stay in sync. + +## External Library (Mounted Folder) Support + +1. **Detect** – The `DISCOVER` job automatically associates `.xmp` files that sit next to existing media files in your mounted folder. No files are moved or renamed. +2. **Extract** – Immich reads and saves the same metadata fields from the sidecar to the database. +3. **Write-back** – If Immich has **write access** to the mount, any future metadata edits (e.g., rating or tags) are also written back to the original `.xmp` file on disk. + +:::danger +If the mount is **read-only**, Immich cannot update either the sidecar **or** the database — **metadata edits will silently fail** with no warning see issue [#10538](https://github.com/immich-app/immich/issues/10538) for more details. +::: + +## Admin Jobs + +Immich provides two admin jobs for managing sidecars: + +| Job | What it does | +| ---------- | ------------------------------------------------------------------------------------------------- | +| `DISCOVER` | Finds new `.xmp` files next to media that don’t already have one linked | +| `SYNC` | Re-reads existing `.xmp` files and refreshes metadata in the database (e.g. after external edits) | + +![Sidecar Admin Jobs](./img/sidecar-jobs.webp) From 0700e61d20f2d837fbfb9547c389bb602c8714f1 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 11 Sep 2025 16:07:36 +0200 Subject: [PATCH 08/30] fix: trigger for weblate checks (#21816) --- .github/workflows/weblate-lock.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index e0634b6af2..0d41a70956 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -1,7 +1,8 @@ name: Weblate checks on: - pull_request_review: + pull_request: + branches: [main] permissions: {} From ef6e4f4699bda3b5485de6ca7a1b47077948196b Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 11 Sep 2025 10:09:18 -0400 Subject: [PATCH 09/30] docs: update tag details (#21815) --- docs/docs/features/tags.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/features/tags.md b/docs/docs/features/tags.md index ca663e9edd..a5b6752c81 100644 --- a/docs/docs/features/tags.md +++ b/docs/docs/features/tags.md @@ -1,6 +1,6 @@ # Tags -Immich supports hierarchical tags, with the ability to read existing tags from the `TagList` and `Keywords` EXIF properties. Any changes to tags made through Immich are also written back to a [sidecar](/docs/features/xmp-sidecars) file. You can re-run the metadata extraction jobs for all assets to import your existing tags. +Immich supports hierarchical tags, with the ability to read existing tags from the XMP `TagsList` field and IPTC `Keywords` field. Any changes to tags made through Immich are also written back to a [sidecar](/docs/features/xmp-sidecars) file. You can re-run the metadata extraction jobs for all assets to import your existing tags. ## Enable tags feature From 39c1ebf69803f01ba479219f956cfe86f30130e8 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 11 Sep 2025 16:31:02 +0200 Subject: [PATCH 10/30] fix: proper triggers for weblate checks (#21818) --- .github/workflows/weblate-lock.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index 0d41a70956..d765db6c1a 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -3,6 +3,12 @@ name: Weblate checks on: pull_request: branches: [main] + types: + - opened + - synchronize + - ready_for_review + - auto_merge_enabled + - auto_merge_disabled permissions: {} From 722a464e236e71e90eb7455db8ecb644cc8ec659 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:31:15 +0530 Subject: [PATCH 11/30] fix: android background backups (#21795) * upload using dart client * add connectivity api * respect backup network setting * comment as to why we need to wait for setForegroundAsync call * log assets skipped due to network constraint * dynamic spawning -> false --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex --- .../app/alextran/immich/MainActivity.kt | 3 + .../immich/background/BackgroundWorker.g.kt | 20 +++ .../immich/background/BackgroundWorker.kt | 82 +++++++++++ .../immich/connectivity/Connectivity.g.kt | 116 ++++++++++++++++ .../connectivity/ConnectivityApiImpl.kt | 39 ++++++ mobile/ios/Runner.xcodeproj/project.pbxproj | 28 +++- .../Background/BackgroundWorker.g.swift | 17 +++ .../Runner/Background/BackgroundWorker.swift | 4 + .../Runner/Connectivity/Connectivity.g.swift | 129 ++++++++++++++++++ .../Connectivity/ConnectivityApiImpl.swift | 6 + .../services/background_worker.service.dart | 46 +++++-- .../network_capability_extensions.dart | 8 ++ mobile/lib/main.dart | 2 +- .../lib/platform/background_worker_api.g.dart | 23 ++++ mobile/lib/platform/connectivity_api.g.dart | 87 ++++++++++++ .../infrastructure/platform.provider.dart | 3 + .../lib/repositories/upload.repository.dart | 65 ++++++++- mobile/lib/services/server_info.service.dart | 9 ++ mobile/lib/services/upload.service.dart | 71 ++++++++-- mobile/makefile | 2 + mobile/pigeon/background_worker_api.dart | 2 + mobile/pigeon/connectivity_api.dart | 20 +++ 22 files changed, 755 insertions(+), 27 deletions(-) create mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt create mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/ConnectivityApiImpl.kt create mode 100644 mobile/ios/Runner/Connectivity/Connectivity.g.swift create mode 100644 mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift create mode 100644 mobile/lib/extensions/network_capability_extensions.dart create mode 100644 mobile/lib/platform/connectivity_api.g.dart create mode 100644 mobile/pigeon/connectivity_api.dart diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt index a87feddd1a..d395cc2243 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt @@ -5,6 +5,8 @@ import android.os.Build import android.os.ext.SdkExtensions import app.alextran.immich.background.BackgroundWorkerApiImpl import app.alextran.immich.background.BackgroundWorkerFgHostApi +import app.alextran.immich.connectivity.ConnectivityApi +import app.alextran.immich.connectivity.ConnectivityApiImpl import app.alextran.immich.images.ThumbnailApi import app.alextran.immich.images.ThumbnailsImpl import app.alextran.immich.sync.NativeSyncApi @@ -34,6 +36,7 @@ class MainActivity : FlutterFragmentActivity() { NativeSyncApi.setUp(messenger, nativeSyncApiImpl) ThumbnailApi.setUp(messenger, ThumbnailsImpl(ctx)) BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx)) + ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx)) } } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt index b9826f80e9..4d9e2c0caf 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt @@ -111,6 +111,7 @@ interface BackgroundWorkerFgHostApi { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface BackgroundWorkerBgHostApi { fun onInitialized() + fun showNotification(title: String, content: String) fun close() companion object { @@ -138,6 +139,25 @@ interface BackgroundWorkerBgHostApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val titleArg = args[0] as String + val contentArg = args[1] as String + val wrapped: List = try { + api.showNotification(titleArg, contentArg) + listOf(null) + } catch (exception: Throwable) { + BackgroundWorkerPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$separatedMessageChannelSuffix", codec) if (api != null) { diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt index 43124a957e..b69730018b 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt @@ -1,18 +1,27 @@ package app.alextran.immich.background +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.Context +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC +import android.os.Build import android.os.Handler import android.os.Looper +import android.os.PowerManager import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.work.ForegroundInfo import androidx.work.ListenableWorker import androidx.work.WorkerParameters import app.alextran.immich.MainActivity +import app.alextran.immich.R import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import io.flutter.FlutterInjector import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.embedding.engine.loader.FlutterLoader +import java.util.concurrent.TimeUnit private const val TAG = "BackgroundWorker" @@ -40,15 +49,32 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : /// Flag to track whether the background task has completed to prevent duplicate completions private var isComplete = false + private val notificationManager = + ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + private var foregroundFuture: ListenableFuture? = null + init { if (!loader.initialized()) { loader.startInitialization(ctx) } } + companion object { + private const val NOTIFICATION_CHANNEL_ID = "immich::background_worker::notif" + private const val NOTIFICATION_ID = 100 + } + override fun startWork(): ListenableFuture { Log.i(TAG, "Starting background upload worker") + val notificationChannel = NotificationChannel( + NOTIFICATION_CHANNEL_ID, + NOTIFICATION_CHANNEL_ID, + NotificationManager.IMPORTANCE_LOW + ) + notificationManager.createNotificationChannel(notificationChannel) + loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { engine = FlutterEngine(ctx) @@ -82,6 +108,34 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : flutterApi?.onAndroidUpload { handleHostResult(it) } } + // TODO: Move this to a separate NotificationManager class + override fun showNotification(title: String, content: String) { + val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID) + .setSmallIcon(R.drawable.notification_icon) + .setOnlyAlertOnce(true) + .setOngoing(true) + .setTicker(title) + .setContentTitle(title) + .setContentText(content) + .build() + + if (isIgnoringBatteryOptimizations()) { + foregroundFuture = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + setForegroundAsync( + ForegroundInfo( + NOTIFICATION_ID, + notification, + FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + ) + } else { + setForegroundAsync(ForegroundInfo(NOTIFICATION_ID, notification)) + } + } else { + notificationManager.notify(NOTIFICATION_ID, notification) + } + } + override fun close() { if (isComplete) { return @@ -95,6 +149,8 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : } } + waitForForegroundPromotion() + Handler(Looper.getMainLooper()).postDelayed({ complete(Result.failure()) }, 5000) @@ -135,6 +191,32 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : engine?.destroy() engine = null flutterApi = null + notificationManager.cancel(NOTIFICATION_ID) + waitForForegroundPromotion() completionHandler.set(success) } + + /** + * Returns `true` if the app is ignoring battery optimizations + */ + private fun isIgnoringBatteryOptimizations(): Boolean { + val powerManager = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager + return powerManager.isIgnoringBatteryOptimizations(ctx.packageName) + } + + /** + * Calls to setForegroundAsync() that do not complete before completion of a ListenableWorker will signal an IllegalStateException + * https://android-review.googlesource.com/c/platform/frameworks/support/+/1262743 + * Wait for a short period of time for the foreground promotion to complete before completing the worker + */ + private fun waitForForegroundPromotion() { + val foregroundFuture = this.foregroundFuture + if (foregroundFuture != null && !foregroundFuture.isCancelled && !foregroundFuture.isDone) { + try { + foregroundFuture.get(500, TimeUnit.MILLISECONDS) + } catch (e: Exception) { + // ignored, there is nothing to be done + } + } + } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt new file mode 100644 index 0000000000..434ba47ca1 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt @@ -0,0 +1,116 @@ +// Autogenerated from Pigeon (v26.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package app.alextran.immich.connectivity + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +private object ConnectivityPigeonUtils { + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } + } +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +enum class NetworkCapability(val raw: Int) { + CELLULAR(0), + WIFI(1), + VPN(2), + UNMETERED(3); + + companion object { + fun ofRaw(raw: Int): NetworkCapability? { + return values().firstOrNull { it.raw == raw } + } + } +} +private open class ConnectivityPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as Long?)?.let { + NetworkCapability.ofRaw(it.toInt()) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is NetworkCapability -> { + stream.write(129) + writeValue(stream, value.raw) + } + else -> super.writeValue(stream, value) + } + } +} + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface ConnectivityApi { + fun getCapabilities(): List + + companion object { + /** The codec used by ConnectivityApi. */ + val codec: MessageCodec by lazy { + ConnectivityPigeonCodec() + } + /** Sets up an instance of `ConnectivityApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: ConnectivityApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val taskQueue = binaryMessenger.makeBackgroundTaskQueue() + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getCapabilities()) + } catch (exception: Throwable) { + ConnectivityPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/ConnectivityApiImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/ConnectivityApiImpl.kt new file mode 100644 index 0000000000..e8554dd63a --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/ConnectivityApiImpl.kt @@ -0,0 +1,39 @@ +package app.alextran.immich.connectivity + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.net.wifi.WifiManager + +class ConnectivityApiImpl(context: Context) : ConnectivityApi { + private val connectivityManager = + context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + private val wifiManager = + context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + + override fun getCapabilities(): List { + val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + ?: return emptyList() + + val hasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE) + val hasCellular = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) + val hasVpn = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) + val isUnmetered = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + + return buildList { + if (hasWifi) add(NetworkCapability.WIFI) + if (hasCellular) add(NetworkCapability.CELLULAR) + if (hasVpn) { + add(NetworkCapability.VPN) + if (!hasWifi && !hasCellular) { + if (wifiManager.isWifiEnabled) add(NetworkCapability.WIFI) + // If VPN is active, but neither WIFI nor CELLULAR is reported as active, + // assume CELLULAR if WIFI is not enabled + else add(NetworkCapability.CELLULAR) + } + } + if (isUnmetered) add(NetworkCapability.UNMETERED) + } + } +} diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 4e68390113..524778d89d 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ @@ -18,6 +18,8 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */; }; B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; }; + B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */; }; + B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */; }; B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; }; D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; }; F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -97,6 +99,8 @@ B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; }; B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = ""; }; B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = ""; }; + B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = ""; }; + B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityApiImpl.swift; sourceTree = ""; }; B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = ""; }; E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -129,8 +133,6 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); path = Sync; sourceTree = ""; }; @@ -243,6 +245,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + B25D37792E72CA15008B6CA7 /* Connectivity */, B21E34A62E5AF9760031FDB9 /* Background */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */, FA9973382CF6DF4B000EF859 /* Runner.entitlements */, @@ -271,6 +274,15 @@ path = Background; sourceTree = ""; }; + B25D37792E72CA15008B6CA7 /* Connectivity */ = { + isa = PBXGroup; + children = ( + B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */, + B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */, + ); + path = Connectivity; + sourceTree = ""; + }; FAC6F8B62D287F120078CB2F /* ShareExtension */ = { isa = PBXGroup; children = ( @@ -507,10 +519,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -539,10 +555,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -558,7 +578,9 @@ 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */, + B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */, FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */, + B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */, FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */, B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */, FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */, diff --git a/mobile/ios/Runner/Background/BackgroundWorker.g.swift b/mobile/ios/Runner/Background/BackgroundWorker.g.swift index bfc0b26d9b..45a6402fe8 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.g.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.g.swift @@ -114,6 +114,7 @@ class BackgroundWorkerFgHostApiSetup { /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol BackgroundWorkerBgHostApi { func onInitialized() throws + func showNotification(title: String, content: String) throws func close() throws } @@ -136,6 +137,22 @@ class BackgroundWorkerBgHostApiSetup { } else { onInitializedChannel.setMessageHandler(nil) } + let showNotificationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + showNotificationChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let titleArg = args[0] as! String + let contentArg = args[1] as! String + do { + try api.showNotification(title: titleArg, content: contentArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + showNotificationChannel.setMessageHandler(nil) + } let closeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { closeChannel.setMessageHandler { _, reply in diff --git a/mobile/ios/Runner/Background/BackgroundWorker.swift b/mobile/ios/Runner/Background/BackgroundWorker.swift index 835632a5d0..9a965bd360 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.swift @@ -119,6 +119,10 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { }) } + func showNotification(title: String, content: String) throws { + // No-op on iOS for the time being + } + /** * Cancels the currently running background task, either due to timeout or external request. * Sends a cancel signal to the Flutter side and sets up a fallback timer to ensure diff --git a/mobile/ios/Runner/Connectivity/Connectivity.g.swift b/mobile/ios/Runner/Connectivity/Connectivity.g.swift new file mode 100644 index 0000000000..45333f03d8 --- /dev/null +++ b/mobile/ios/Runner/Connectivity/Connectivity.g.swift @@ -0,0 +1,129 @@ +// Autogenerated from Pigeon (v26.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + + +enum NetworkCapability: Int { + case cellular = 0 + case wifi = 1 + case vpn = 2 + case unmetered = 3 +} + +private class ConnectivityPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return NetworkCapability(rawValue: enumResultAsInt) + } + return nil + default: + return super.readValue(ofType: type) + } + } +} + +private class ConnectivityPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? NetworkCapability { + super.writeByte(129) + super.writeValue(value.rawValue) + } else { + super.writeValue(value) + } + } +} + +private class ConnectivityPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return ConnectivityPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return ConnectivityPigeonCodecWriter(data: data) + } +} + +class ConnectivityPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = ConnectivityPigeonCodec(readerWriter: ConnectivityPigeonCodecReaderWriter()) +} + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol ConnectivityApi { + func getCapabilities() throws -> [NetworkCapability] +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class ConnectivityApiSetup { + static var codec: FlutterStandardMessageCodec { ConnectivityPigeonCodec.shared } + /// Sets up an instance of `ConnectivityApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ConnectivityApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + #if os(iOS) + let taskQueue = binaryMessenger.makeBackgroundTaskQueue?() + #else + let taskQueue: FlutterTaskQueue? = nil + #endif + let getCapabilitiesChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getCapabilitiesChannel.setMessageHandler { _, reply in + do { + let result = try api.getCapabilities() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getCapabilitiesChannel.setMessageHandler(nil) + } + } +} diff --git a/mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift b/mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift new file mode 100644 index 0000000000..0261cb26fb --- /dev/null +++ b/mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift @@ -0,0 +1,6 @@ + +class ConnectivityApiImpl: ConnectivityApi { + func getCapabilities() throws -> [NetworkCapability] { + [] + } +} diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 18f07dc021..3bb0d7980f 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -1,10 +1,15 @@ import 'dart:async'; +import 'dart:io'; import 'dart:ui'; import 'package:background_downloader/background_downloader.dart'; +import 'package:cancellation_token_http/http.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/extensions/network_capability_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/intl_keys.g.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/platform/background_worker_api.g.dart'; @@ -13,11 +18,13 @@ import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; import 'package:immich_mobile/services/localization.service.dart'; +import 'package:immich_mobile/services/server_info.service.dart'; import 'package:immich_mobile/services/upload.service.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart'; @@ -42,6 +49,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { final Drift _drift; final DriftLogger _driftLogger; final BackgroundWorkerBgHostApi _backgroundHostApi; + final CancellationToken _cancellationToken = CancellationToken(); final Logger _logger = Logger('BackgroundWorkerBgService'); bool _isCleanedUp = false; @@ -87,6 +95,13 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { configureFileDownloaderNotifications(); + if (Platform.isAndroid) { + await _backgroundHostApi.showNotification( + IntlKeys.uploading_media.t(), + IntlKeys.backup_background_service_in_progress_notification.t(), + ); + } + // Notify the host that the background worker service has been initialized and is ready to use _backgroundHostApi.onInitialized(); } catch (error, stack) { @@ -102,7 +117,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { final sw = Stopwatch()..start(); await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6)); - await _handleBackup(processBulk: false); + await _handleBackup(); sw.stop(); _logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s"); @@ -155,9 +170,13 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { try { _isCleanedUp = true; + _cancellationToken.cancel(); _logger.info("Cleaning up background worker"); final cleanupFutures = [ - workerManager.dispose(), + workerManager.dispose().catchError((_) async { + // Discard any errors on the dispose call + return; + }), _drift.close(), _driftLogger.close(), _ref.read(backgroundSyncProvider).cancel(), @@ -175,7 +194,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { } } - Future _handleBackup({bool processBulk = true}) async { + Future _handleBackup() async { if (!_isBackupEnabled || _isCleanedUp) { _logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine"); return; @@ -189,19 +208,22 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { return; } - if (processBulk) { - _logger.info("[_handleBackup 4] Resume backup from background"); + _logger.info("[_handleBackup 4] Resume backup from background"); + if (Platform.isIOS) { return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id); } - final activeTask = await _ref.read(uploadServiceProvider).getActiveTasks(currentUser.id); - if (activeTask.isNotEmpty) { - _logger.info("[_handleBackup 5] Resuming backup for active tasks from background"); - await _ref.read(uploadServiceProvider).resumeBackup(); - } else { - _logger.info("[_handleBackup 6] Starting serial backup for new tasks from background"); - await _ref.read(uploadServiceProvider).startBackupSerial(currentUser.id); + final canPing = await _ref.read(serverInfoServiceProvider).ping(); + if (!canPing) { + _logger.warning("[_handleBackup 5] Server is not reachable. Skipping backup from background"); + return; } + + final networkCapabilities = await _ref.read(connectivityApiProvider).getCapabilities(); + + return _ref + .read(uploadServiceProvider) + .startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken); } Future _syncAssets({Duration? hashTimeout}) async { diff --git a/mobile/lib/extensions/network_capability_extensions.dart b/mobile/lib/extensions/network_capability_extensions.dart new file mode 100644 index 0000000000..aeefc11e39 --- /dev/null +++ b/mobile/lib/extensions/network_capability_extensions.dart @@ -0,0 +1,8 @@ +import 'package:immich_mobile/platform/connectivity_api.g.dart'; + +extension NetworkCapabilitiesGetters on List { + bool get hasCellular => contains(NetworkCapability.cellular); + bool get hasWifi => contains(NetworkCapability.wifi); + bool get hasVpn => contains(NetworkCapability.vpn); + bool get isUnmetered => contains(NetworkCapability.unmetered); +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 4f74c30e3b..7e3d6152c9 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -46,7 +46,7 @@ void main() async { await Bootstrap.initDomain(isar, drift, logDb); await initApp(); // Warm-up isolate pool for worker manager - await workerManager.init(dynamicSpawning: true); + await workerManager.init(dynamicSpawning: false); await migrateDatabaseIfNeeded(isar, drift); HttpSSLOptions.apply(); diff --git a/mobile/lib/platform/background_worker_api.g.dart b/mobile/lib/platform/background_worker_api.g.dart index 4b5689f4df..9398b0a15b 100644 --- a/mobile/lib/platform/background_worker_api.g.dart +++ b/mobile/lib/platform/background_worker_api.g.dart @@ -142,6 +142,29 @@ class BackgroundWorkerBgHostApi { } } + Future showNotification(String title, String content) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([title, content]); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + Future close() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$pigeonVar_messageChannelSuffix'; diff --git a/mobile/lib/platform/connectivity_api.g.dart b/mobile/lib/platform/connectivity_api.g.dart new file mode 100644 index 0000000000..c348356f81 --- /dev/null +++ b/mobile/lib/platform/connectivity_api.g.dart @@ -0,0 +1,87 @@ +// Autogenerated from Pigeon (v26.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +enum NetworkCapability { cellular, wifi, vpn, unmetered } + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is NetworkCapability) { + buffer.putUint8(129); + writeValue(buffer, value.index); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + final int? value = readValue(buffer) as int?; + return value == null ? null : NetworkCapability.values[value]; + default: + return super.readValueOfType(type, buffer); + } + } +} + +class ConnectivityApi { + /// Constructor for [ConnectivityApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + ConnectivityApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future> getCapabilities() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } +} diff --git a/mobile/lib/providers/infrastructure/platform.provider.dart b/mobile/lib/providers/infrastructure/platform.provider.dart index 05901a4fec..dec5c6905e 100644 --- a/mobile/lib/providers/infrastructure/platform.provider.dart +++ b/mobile/lib/providers/infrastructure/platform.provider.dart @@ -1,6 +1,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/background_worker.service.dart'; import 'package:immich_mobile/platform/background_worker_api.g.dart'; +import 'package:immich_mobile/platform/connectivity_api.g.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/platform/thumbnail_api.g.dart'; @@ -8,4 +9,6 @@ final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgServ final nativeSyncApiProvider = Provider((_) => NativeSyncApi()); +final connectivityApiProvider = Provider((_) => ConnectivityApi()); + final thumbnailApi = ThumbnailApi(); diff --git a/mobile/lib/repositories/upload.repository.dart b/mobile/lib/repositories/upload.repository.dart index c8b06ae102..2d99631d51 100644 --- a/mobile/lib/repositories/upload.repository.dart +++ b/mobile/lib/repositories/upload.repository.dart @@ -1,7 +1,21 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:background_downloader/background_downloader.dart'; +import 'package:cancellation_token_http/http.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:logging/logging.dart'; + +class UploadTaskWithFile { + final File file; + final UploadTask task; + + const UploadTaskWithFile({required this.file, required this.task}); +} final uploadRepositoryProvider = Provider((ref) => UploadRepository()); @@ -31,7 +45,7 @@ class UploadRepository { return FileDownloader().enqueue(task); } - Future enqueueBackgroundAll(List tasks) { + Future> enqueueBackgroundAll(List tasks) { return FileDownloader().enqueueAll(tasks); } @@ -74,4 +88,53 @@ class UploadRepository { Paused: ${pausedTasks.length} """); } + + Future backupWithDartClient(Iterable tasks, CancellationToken cancelToken) async { + final httpClient = Client(); + final String savedEndpoint = Store.get(StoreKey.serverEndpoint); + + Logger logger = Logger('UploadRepository'); + for (final candidate in tasks) { + if (cancelToken.isCancelled) { + logger.warning("Backup was cancelled by the user"); + break; + } + + try { + final fileStream = candidate.file.openRead(); + final assetRawUploadData = MultipartFile( + "assetData", + fileStream, + candidate.file.lengthSync(), + filename: candidate.task.filename, + ); + + final baseRequest = MultipartRequest('POST', Uri.parse('$savedEndpoint/assets')); + + baseRequest.headers.addAll(candidate.task.headers); + baseRequest.fields.addAll(candidate.task.fields); + baseRequest.files.add(assetRawUploadData); + + final response = await httpClient.send(baseRequest, cancellationToken: cancelToken); + + final responseBody = jsonDecode(await response.stream.bytesToString()); + + if (![200, 201].contains(response.statusCode)) { + final error = responseBody; + + logger.warning( + "Error(${error['statusCode']}) uploading ${candidate.task.filename} | Created on ${candidate.task.fields["fileCreatedAt"]} | ${error['error']}", + ); + + continue; + } + } on CancelledException { + logger.warning("Backup was cancelled by the user"); + break; + } catch (error, stackTrace) { + logger.warning("Error backup asset: ${error.toString()}: $stackTrace"); + continue; + } + } + } } diff --git a/mobile/lib/services/server_info.service.dart b/mobile/lib/services/server_info.service.dart index 4319d9dbae..e1238999d7 100644 --- a/mobile/lib/services/server_info.service.dart +++ b/mobile/lib/services/server_info.service.dart @@ -14,6 +14,15 @@ class ServerInfoService { const ServerInfoService(this._apiService); + Future ping() async { + try { + await _apiService.serverInfoApi.pingServer().timeout(const Duration(seconds: 5)); + return true; + } catch (e) { + return false; + } + } + Future getDiskInfo() async { try { final dto = await _apiService.serverInfoApi.getStorage(); diff --git a/mobile/lib/services/upload.service.dart b/mobile/lib/services/upload.service.dart index a711608e7f..e41d730ff8 100644 --- a/mobile/lib/services/upload.service.dart +++ b/mobile/lib/services/upload.service.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; +import 'package:cancellation_token_http/http.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; @@ -19,6 +20,7 @@ import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; final uploadServiceProvider = Provider((ref) { @@ -51,6 +53,7 @@ class UploadService { final StorageRepository _storageRepository; final DriftLocalAssetRepository _localAssetRepository; final AppSettingsService _appSettingsService; + final Logger _logger = Logger('UploadService'); final StreamController _taskStatusController = StreamController.broadcast(); final StreamController _taskProgressController = StreamController.broadcast(); @@ -78,7 +81,7 @@ class UploadService { _taskProgressController.close(); } - Future enqueueTasks(List tasks) { + Future> enqueueTasks(List tasks) { return _uploadRepository.enqueueBackgroundAll(tasks); } @@ -138,7 +141,6 @@ class UploadService { } final batch = candidates.skip(i).take(batchSize).toList(); - List tasks = []; for (final asset in batch) { final task = await _getUploadTask(asset); @@ -156,9 +158,7 @@ class UploadService { } } - // Enqueue All does not work from the background on Android yet. This method is a temporary workaround - // that enqueues tasks one by one. - Future startBackupSerial(String userId) async { + Future startBackupWithHttpClient(String userId, bool hasWifi, CancellationToken token) async { await _storageRepository.clearCache(); shouldAbortQueuingTasks = false; @@ -168,14 +168,29 @@ class UploadService { return; } - for (final asset in candidates) { - if (shouldAbortQueuingTasks) { + const batchSize = 100; + for (int i = 0; i < candidates.length; i += batchSize) { + if (shouldAbortQueuingTasks || token.isCancelled) { break; } - final task = await _getUploadTask(asset); - if (task != null) { - await _uploadRepository.enqueueBackground(task); + final batch = candidates.skip(i).take(batchSize).toList(); + List tasks = []; + for (final asset in batch) { + final requireWifi = _shouldRequireWiFi(asset); + if (requireWifi && !hasWifi) { + _logger.warning('Skipping upload for ${asset.id} because it requires WiFi'); + continue; + } + + final task = await _getUploadTaskWithFile(asset); + if (task != null) { + tasks.add(task); + } + } + + if (tasks.isNotEmpty && !shouldAbortQueuingTasks) { + await _uploadRepository.backupWithDartClient(tasks, token); } } } @@ -242,6 +257,42 @@ class UploadService { } } + Future _getUploadTaskWithFile(LocalAsset asset) async { + final entity = await _storageRepository.getAssetEntityForAsset(asset); + if (entity == null) { + return null; + } + + final file = await _storageRepository.getFileForAsset(asset.id); + if (file == null) { + return null; + } + + final originalFileName = entity.isLivePhoto ? p.setExtension(asset.name, p.extension(file.path)) : asset.name; + + String metadata = UploadTaskMetadata( + localAssetId: asset.id, + isLivePhotos: entity.isLivePhoto, + livePhotoVideoId: '', + ).toJson(); + + return UploadTaskWithFile( + file: file, + task: await buildUploadTask( + file, + createdAt: asset.createdAt, + modifiedAt: asset.updatedAt, + originalFileName: originalFileName, + deviceAssetId: asset.id, + metadata: metadata, + group: "group", + priority: 0, + isFavorite: asset.isFavorite, + requiresWiFi: false, + ), + ); + } + Future _getUploadTask(LocalAsset asset, {String group = kBackupGroup, int? priority}) async { final entity = await _storageRepository.getAssetEntityForAsset(asset); if (entity == null) { diff --git a/mobile/makefile b/mobile/makefile index 1a20e769ef..61854830d9 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -9,9 +9,11 @@ pigeon: dart run pigeon --input pigeon/native_sync_api.dart dart run pigeon --input pigeon/thumbnail_api.dart dart run pigeon --input pigeon/background_worker_api.dart + dart run pigeon --input pigeon/connectivity_api.dart dart format lib/platform/native_sync_api.g.dart dart format lib/platform/thumbnail_api.g.dart dart format lib/platform/background_worker_api.g.dart + dart format lib/platform/connectivity_api.g.dart watch: dart run build_runner watch --delete-conflicting-outputs diff --git a/mobile/pigeon/background_worker_api.dart b/mobile/pigeon/background_worker_api.dart index 193bbc5832..8aa0f5f5ee 100644 --- a/mobile/pigeon/background_worker_api.dart +++ b/mobile/pigeon/background_worker_api.dart @@ -24,6 +24,8 @@ abstract class BackgroundWorkerBgHostApi { // required platform channels to notify the native side to start the background upload void onInitialized(); + void showNotification(String title, String content); + // Called from the background flutter engine to request the native side to cleanup void close(); } diff --git a/mobile/pigeon/connectivity_api.dart b/mobile/pigeon/connectivity_api.dart new file mode 100644 index 0000000000..c5677ee20e --- /dev/null +++ b/mobile/pigeon/connectivity_api.dart @@ -0,0 +1,20 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/platform/connectivity_api.g.dart', + swiftOut: 'ios/Runner/Connectivity/Connectivity.g.swift', + swiftOptions: SwiftOptions(includeErrorClass: false), + kotlinOut: 'android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt', + kotlinOptions: KotlinOptions(package: 'app.alextran.immich.connectivity'), + dartOptions: DartOptions(), + dartPackageName: 'immich_mobile', + ), +) +enum NetworkCapability { cellular, wifi, vpn, unmetered } + +@HostApi() +abstract class ConnectivityApi { + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getCapabilities(); +} From 42a03f2556677e22ec3ec3de10c3e5d17127af4d Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 11 Sep 2025 14:02:03 -0500 Subject: [PATCH 12/30] fix: concurrency issue (#21830) --- .../services/background_worker.service.dart | 61 +++++++----- .../lib/domain/utils/sync_linked_album.dart | 5 +- mobile/lib/main.dart | 2 +- mobile/lib/utils/isolate.dart | 95 ++++++++++--------- 4 files changed, 90 insertions(+), 73 deletions(-) diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 3bb0d7980f..4df221d41b 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -169,7 +169,10 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { } try { + final backgroundSyncManager = _ref.read(backgroundSyncProvider); _isCleanedUp = true; + _ref.dispose(); + _cancellationToken.cancel(); _logger.info("Cleaning up background worker"); final cleanupFutures = [ @@ -179,14 +182,13 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { }), _drift.close(), _driftLogger.close(), - _ref.read(backgroundSyncProvider).cancel(), - _ref.read(backgroundSyncProvider).cancelLocal(), + backgroundSyncManager.cancel(), + backgroundSyncManager.cancelLocal(), ]; if (_isar.isOpen) { cleanupFutures.add(_isar.close()); } - _ref.dispose(); await Future.wait(cleanupFutures); _logger.info("Background worker resources cleaned up"); } catch (error, stack) { @@ -195,35 +197,42 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { } Future _handleBackup() async { - if (!_isBackupEnabled || _isCleanedUp) { - _logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine"); - return; - } + await runZonedGuarded( + () async { + if (!_isBackupEnabled || _isCleanedUp) { + _logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine"); + return; + } - _logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service"); + _logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service"); - final currentUser = _ref.read(currentUserProvider); - if (currentUser == null) { - _logger.warning("[_handleBackup 3] No current user found. Skipping backup from background"); - return; - } + final currentUser = _ref.read(currentUserProvider); + if (currentUser == null) { + _logger.warning("[_handleBackup 3] No current user found. Skipping backup from background"); + return; + } - _logger.info("[_handleBackup 4] Resume backup from background"); - if (Platform.isIOS) { - return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id); - } + _logger.info("[_handleBackup 4] Resume backup from background"); + if (Platform.isIOS) { + return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id); + } - final canPing = await _ref.read(serverInfoServiceProvider).ping(); - if (!canPing) { - _logger.warning("[_handleBackup 5] Server is not reachable. Skipping backup from background"); - return; - } + final canPing = await _ref.read(serverInfoServiceProvider).ping(); + if (!canPing) { + _logger.warning("[_handleBackup 5] Server is not reachable. Skipping backup from background"); + return; + } - final networkCapabilities = await _ref.read(connectivityApiProvider).getCapabilities(); + final networkCapabilities = await _ref.read(connectivityApiProvider).getCapabilities(); - return _ref - .read(uploadServiceProvider) - .startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken); + return _ref + .read(uploadServiceProvider) + .startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken); + }, + (error, stack) { + debugPrint("Error in backup zone $error, $stack"); + }, + ); } Future _syncAssets({Duration? hashTimeout}) async { diff --git a/mobile/lib/domain/utils/sync_linked_album.dart b/mobile/lib/domain/utils/sync_linked_album.dart index 9df69799ae..d4094f74cc 100644 --- a/mobile/lib/domain/utils/sync_linked_album.dart +++ b/mobile/lib/domain/utils/sync_linked_album.dart @@ -1,9 +1,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; Future syncLinkedAlbumsIsolated(ProviderContainer ref) { - final user = ref.read(currentUserProvider); + final user = Store.tryGet(StoreKey.currentUser); if (user == null) { return Future.value(); } diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 7e3d6152c9..4f74c30e3b 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -46,7 +46,7 @@ void main() async { await Bootstrap.initDomain(isar, drift, logDb); await initApp(); // Warm-up isolate pool for worker manager - await workerManager.init(dynamicSpawning: false); + await workerManager.init(dynamicSpawning: true); await migrateDatabaseIfNeeded(isar, drift); HttpSSLOptions.apply(); diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart index cca1498e0f..75e51d4360 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -31,55 +31,62 @@ Cancelable runInIsolateGentle({ } return workerManager.executeGentle((cancelledChecker) async { - BackgroundIsolateBinaryMessenger.ensureInitialized(token); - DartPluginRegistrant.ensureInitialized(); + await runZonedGuarded( + () async { + BackgroundIsolateBinaryMessenger.ensureInitialized(token); + DartPluginRegistrant.ensureInitialized(); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false); - final ref = ProviderContainer( - overrides: [ - // TODO: Remove once isar is removed - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - cancellationProvider.overrideWithValue(cancelledChecker), - driftProvider.overrideWith(driftOverride(drift)), - ], - ); + final (isar, drift, logDb) = await Bootstrap.initDB(); + await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false); + final ref = ProviderContainer( + overrides: [ + // TODO: Remove once isar is removed + dbProvider.overrideWithValue(isar), + isarProvider.overrideWithValue(isar), + cancellationProvider.overrideWithValue(cancelledChecker), + driftProvider.overrideWith(driftOverride(drift)), + ], + ); - Logger log = Logger("IsolateLogger"); + Logger log = Logger("IsolateLogger"); - try { - HttpSSLOptions.apply(applyNative: false); - return await computation(ref); - } on CanceledError { - log.warning("Computation cancelled ${debugLabel == null ? '' : ' for $debugLabel'}"); - } catch (error, stack) { - log.severe("Error in runInIsolateGentle ${debugLabel == null ? '' : ' for $debugLabel'}", error, stack); - } finally { - try { - await LogService.I.dispose(); - await logDb.close(); - await ref.read(driftProvider).close(); - - // Close Isar safely try { - final isar = ref.read(isarProvider); - if (isar.isOpen) { - await isar.close(); - } - } catch (e) { - debugPrint("Error closing Isar: $e"); - } + HttpSSLOptions.apply(applyNative: false); + return await computation(ref); + } on CanceledError { + log.warning("Computation cancelled ${debugLabel == null ? '' : ' for $debugLabel'}"); + } catch (error, stack) { + log.severe("Error in runInIsolateGentle ${debugLabel == null ? '' : ' for $debugLabel'}", error, stack); + } finally { + try { + ref.dispose(); - ref.dispose(); - } catch (error, stack) { - debugPrint("Error closing resources in isolate: $error, $stack"); - } finally { - ref.dispose(); - // Delay to ensure all resources are released - await Future.delayed(const Duration(seconds: 2)); - } - } + await LogService.I.dispose(); + await logDb.close(); + await drift.close(); + + // Close Isar safely + try { + if (isar.isOpen) { + await isar.close(); + } + } catch (e) { + debugPrint("Error closing Isar: $e"); + } + } catch (error, stack) { + debugPrint("Error closing resources in isolate: $error, $stack"); + } finally { + ref.dispose(); + // Delay to ensure all resources are released + await Future.delayed(const Duration(seconds: 2)); + } + } + return null; + }, + (error, stack) { + debugPrint("Error in isolate zone: $error, $stack"); + }, + ); return null; }); } From 7893ac25fbae2f102a589696a4f5a71fb5905077 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Fri, 12 Sep 2025 00:50:39 +0530 Subject: [PATCH 13/30] fix: always use en locale for parsing timeline datetime (#21796) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/infrastructure/repositories/timeline.repository.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 61428ede92..97c9b6f493 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -595,7 +595,7 @@ extension on String { GroupAssetsBy.none => throw ArgumentError("GroupAssetsBy.none is not supported for date formatting"), }; try { - return DateFormat(format).parse(this); + return DateFormat(format, 'en').parse(this); } catch (e) { throw FormatException("Invalid date format: $this", e); } From ae827e1406bfdd5ea1c9e6c716b42e512dc91177 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 11 Sep 2025 21:29:58 +0200 Subject: [PATCH 14/30] fix: define call secrets in merge-translations (#21831) --- .github/workflows/merge-translations.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/merge-translations.yml b/.github/workflows/merge-translations.yml index c2392a5b78..a0329c8f73 100644 --- a/.github/workflows/merge-translations.yml +++ b/.github/workflows/merge-translations.yml @@ -1,8 +1,15 @@ name: Merge translations on: - workflow_call: workflow_dispatch: + workflow_call: + secrets: + PUSH_O_MATIC_APP_ID: + required: true + PUSH_O_MATIC_APP_KEY: + required: true + WEBLATE_TOKEN: + required: true permissions: {} From 03af60e8ebeac0aed24a9ff266561d8ec6691c6e Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 11 Sep 2025 21:34:40 +0200 Subject: [PATCH 15/30] chore(web): update translations (#21814) Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/ Translation: Immich/immich Co-authored-by: DevServs Co-authored-by: Taiki M --- i18n/ja.json | 8 ++++---- i18n/ru.json | 42 +++++++++++++++++++++--------------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/i18n/ja.json b/i18n/ja.json index bed981bb83..bd78f4cf87 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -1473,9 +1473,9 @@ "permission_onboarding_permission_limited": "写真へのアクセスが制限されています。Immichが写真のバックアップと管理を行うには、システム設定から写真と動画のアクセス権限を変更してください。", "permission_onboarding_request": "Immichは写真へのアクセス許可が必要です", "person": "人物", - "person_age_months": "{months, plural, one {# ヶ月} other {# ヶ月}} 前", - "person_age_year_months": "1 年, {months, plural, one {# ヶ月} other {# ヶ月}} 前", - "person_age_years": "{years, plural, other {# 年}}前", + "person_age_months": "生後 {months, plural, one {# ヶ月} other {# ヶ月}}", + "person_age_year_months": "1 歳と, {months, plural, one {# ヶ月} other {# ヶ月}}", + "person_age_years": "{years, plural, other {# 歳}}", "person_birthdate": "{date}生まれ", "person_hidden": "{name}{hidden, select, true { (非表示)} other {}}", "photo_shared_all_users": "写真をすべてのユーザーと共有したか、共有するユーザーがいないようです。", @@ -2024,7 +2024,7 @@ "upload_success": "アップロード成功、新しくアップロードされたアセットを見るにはページを更新してください。", "upload_to_immich": "Immichにアップロード ({count})", "uploading": "アップロード中", - "uploading_media": "メディアをアップロード", + "uploading_media": "メディアをアップロード中", "url": "URL", "usage": "使用容量", "use_biometric": "生体認証をご利用ください", diff --git a/i18n/ru.json b/i18n/ru.json index cfab2ca06c..cddd6263fa 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -35,7 +35,7 @@ "add_url": "Добавить URL", "added_to_archive": "Добавлено в архив", "added_to_favorites": "Добавлено в избранное", - "added_to_favorites_count": "Добавлено{count, number} в избранное", + "added_to_favorites_count": "{count, plural, one {# объект добавлен} many {# объектов добавлено} other {# объекта добавлено}} в избранное", "admin": { "add_exclusion_pattern_description": "Добавьте шаблоны исключений. Поддерживаются символы подстановки *, ** и ?. Чтобы игнорировать все файлы в любом каталоге с именем \"Raw\", укажите \"**/Raw/**\". Чтобы игнорировать все файлы, заканчивающиеся на \".tif\", используйте \"**/*.tif\". Чтобы игнорировать путь целиком, укажите \"/path/to/ignore/**\".", "admin_user": "Администратор", @@ -448,7 +448,7 @@ "all_albums": "Все альбомы", "all_people": "Все люди", "all_videos": "Все видео", - "allow_dark_mode": "Разрешить темный режим", + "allow_dark_mode": "Разрешить тёмный режим", "allow_edits": "Разрешить редактирование", "allow_public_user_to_download": "Разрешить скачивание", "allow_public_user_to_upload": "Разрешить добавление файлов", @@ -1112,14 +1112,14 @@ "home_page_add_to_album_conflicts": "Добавлено {added} медиа в альбом {album}. {failed} медиа уже в альбоме.", "home_page_add_to_album_err_local": "Пока нельзя добавлять локальные объекты в альбомы, пропуск", "home_page_add_to_album_success": "Добавлено {added} медиа в альбом {album}.", - "home_page_album_err_partner": "Пока нельзя добавить медиа партнера в альбом, пропуск", + "home_page_album_err_partner": "Невозможно добавить объекты партнёра в альбом, пропуск", "home_page_archive_err_local": "Пока нельзя добавить локальные файлы в архив, пропуск", - "home_page_archive_err_partner": "Невозможно архивировать медиа партнера, пропуск", + "home_page_archive_err_partner": "Невозможно добавить объекты партнёра в архив, пропуск", "home_page_building_timeline": "Построение хронологии", - "home_page_delete_err_partner": "Невозможно удалить медиа партнера, пропуск", + "home_page_delete_err_partner": "Невозможно удалить объекты партнёра, пропуск", "home_page_delete_remote_err_local": "Невозможно удалить локальные файлы с сервера, пропуск", "home_page_favorite_err_local": "Пока нельзя добавить в избранное локальные файлы, пропуск", - "home_page_favorite_err_partner": "Пока нельзя добавить в избранное медиа партнера, пропуск", + "home_page_favorite_err_partner": "Невозможно добавить объекты партнёра в избранное, пропуск", "home_page_first_time_notice": "Перед началом использования приложения выберите альбом с объектами для резервного копирования, чтобы они отобразились на временной шкале", "home_page_locked_error_local": "Невозможно переместить локальные объекты в личную папку, пропуск", "home_page_locked_error_partner": "Невозможно переместить объекты партнёра в личную папку, пропуск", @@ -1154,8 +1154,8 @@ "in_albums": "В {count, plural, one {# альбоме} other {# альбомах}}", "in_archive": "В архиве", "include_archived": "Отображать архив", - "include_shared_albums": "Включать общие альбомы", - "include_shared_partner_assets": "Включать общие ресурсы партнера", + "include_shared_albums": "Включать объекты общих альбомов", + "include_shared_partner_assets": "Включать объекты партнёров", "individual_share": "Индивидуальная подборка", "individual_shares": "Подборки", "info": "Информация", @@ -1367,7 +1367,7 @@ "no_duplicates_found": "Дубликатов не обнаружено.", "no_exif_info_available": "Нет доступной информации exif", "no_explore_results_message": "Загружайте больше фотографий, чтобы наслаждаться вашей коллекцией.", - "no_favorites_message": "Добавляйте в избранное, чтобы быстро найти свои лучшие фотографии и видео", + "no_favorites_message": "Добавляйте объекты в избранное, чтобы быстрее находить свои лучшие фото и видео", "no_libraries_message": "Создайте внешнюю библиотеку для просмотра в Immich сторонних фотографий и видео", "no_locked_photos_message": "Фото и видео, перемещенные в личную папку, скрыты и не отображаются при просмотре библиотеки.", "no_name": "Нет имени", @@ -1422,18 +1422,18 @@ "other_variables": "Другие переменные", "owned": "Мои", "owner": "Владелец", - "partner": "Партнер", - "partner_can_access": "{partner} имеет доступ", - "partner_can_access_assets": "Все ваши фотографии и видеозаписи, кроме тех, которые находятся в Архиве и Корзине", - "partner_can_access_location": "Местоположение, где были сделаны ваши фотографии", - "partner_list_user_photos": "Фотографии пользователя {user}", + "partner": "Партнёр", + "partner_can_access": "Пользователю {partner} доступны", + "partner_can_access_assets": "Все ваши фото и видео, кроме тех, что находятся в архиве и корзине", + "partner_can_access_location": "Места, где были сделаны ваши фото и видео", + "partner_list_user_photos": "Фото и видео пользователя {user}", "partner_list_view_all": "Посмотреть все", - "partner_page_empty_message": "У вашего партнёра еще нет доступа к вашим фото.", + "partner_page_empty_message": "Вы пока никому из партнёров не предоставили доступ к своим фото и видео.", "partner_page_no_more_users": "Выбраны все доступные пользователи", "partner_page_partner_add_failed": "Не удалось добавить партнёра", "partner_page_select_partner": "Выбрать партнёра", "partner_page_shared_to_title": "Поделиться с...", - "partner_page_stop_sharing_content": "Пользователь {partner} больше не сможет получить доступ к вашим фото.", + "partner_page_stop_sharing_content": "Пользователь {partner} больше не будет иметь доступ к вашим фото и видео.", "partner_sharing": "Совместное использование", "partners": "Партнёры", "password": "Пароль", @@ -1796,7 +1796,7 @@ "shared_by": "Поделился", "shared_by_user": "Владелец: {user}", "shared_by_you": "Вы поделились", - "shared_from_partner": "Фото и видео пользователя {partner}", + "shared_from_partner": "Пользователь {partner} предоставил вам доступ", "shared_intent_upload_button_progress_text": "{current} / {total} Загружено", "shared_link_app_bar_title": "Публичные ссылки", "shared_link_clipboard_copied_massage": "Скопировано в буфер обмена", @@ -1833,7 +1833,7 @@ "shared_links_description": "Делитесь фотографиями и видео по ссылке", "shared_photos_and_videos_count": "{assetCount, plural, other {# фото и видео.}}", "shared_with_me": "Доступные мне", - "shared_with_partner": "Совместно с {partner}", + "shared_with_partner": "Вы предоставили доступ пользователю {partner}", "sharing": "Общие", "sharing_enter_password": "Пожалуйста, введите пароль для просмотра этой страницы.", "sharing_page_album": "Общие альбомы", @@ -1851,7 +1851,7 @@ "show_gallery": "Показать галерею", "show_hidden_people": "Показать скрытых людей", "show_in_timeline": "Показать на временной шкале", - "show_in_timeline_setting_description": "Отображать фото и видео этого пользователя в своей ленте", + "show_in_timeline_setting_description": "Отображать фото и видео этого пользователя на своей временной шкале", "show_keyboard_shortcuts": "Показать сочетания клавиш", "show_metadata": "Показывать метаданные", "show_or_hide_info": "Показать или скрыть информацию", @@ -1897,9 +1897,9 @@ "status": "Состояние", "stop_casting": "Остановить трансляцию", "stop_motion_photo": "Покадровая анимация", - "stop_photo_sharing": "Закрыть доступ партнёра к вашим фото?", + "stop_photo_sharing": "Закрыть доступ партнёру?", "stop_photo_sharing_description": "Пользователь {partner} больше не имеет доступа к вашим фотографиям.", - "stop_sharing_photos_with_user": "Прекратить делиться своими фотографиями с этим пользователем", + "stop_sharing_photos_with_user": "Прекратить делиться своими фото и видео с этим пользователем", "storage": "Хранилище", "storage_label": "Метка хранилища", "storage_quota": "Квота хранилища", From f29230c8a69e04c30f8f449c58009e79da9136a1 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 11 Sep 2025 15:36:16 -0400 Subject: [PATCH 16/30] fix(web): handle buckets before year 1000 (#21832) --- web/src/lib/utils/timeline-util.spec.ts | 12 +++++++++++- web/src/lib/utils/timeline-util.ts | 7 +++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/web/src/lib/utils/timeline-util.spec.ts b/web/src/lib/utils/timeline-util.spec.ts index c77aefc0b4..bc05b78ad4 100644 --- a/web/src/lib/utils/timeline-util.spec.ts +++ b/web/src/lib/utils/timeline-util.spec.ts @@ -1,6 +1,6 @@ import { locale } from '$lib/stores/preferences.store'; import { parseUtcDate } from '$lib/utils/date-time'; -import { formatGroupTitle } from '$lib/utils/timeline-util'; +import { formatGroupTitle, toISOYearMonthUTC } from '$lib/utils/timeline-util'; import { DateTime } from 'luxon'; describe('formatGroupTitle', () => { @@ -77,3 +77,13 @@ describe('formatGroupTitle', () => { expect(formatGroupTitle(date)).toBe('Invalid DateTime'); }); }); + +describe('toISOYearMonthUTC', () => { + it('should prefix year with 0s', () => { + expect(toISOYearMonthUTC({ year: 28, month: 1 })).toBe('0028-01-01T00:00:00.000Z'); + }); + + it('should prefix month with 0s', () => { + expect(toISOYearMonthUTC({ year: 2025, month: 1 })).toBe('2025-01-01T00:00:00.000Z'); + }); +}); diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index 6a0f12c20e..9cf4428da6 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -94,8 +94,11 @@ export const fromTimelinePlainYearMonth = (timelineYearMonth: TimelineYearMonth) { zone: 'local', locale: get(locale) }, ) as DateTime; -export const toISOYearMonthUTC = ({ year, month }: TimelineYearMonth): string => - `${year}-${month.toString().padStart(2, '0')}-01T00:00:00.000Z`; +export const toISOYearMonthUTC = ({ year, month }: TimelineYearMonth): string => { + const yearFull = `${year}`.padStart(4, '0'); + const monthFull = `${month}`.padStart(2, '0'); + return `${yearFull}-${monthFull}-01T00:00:00.000Z`; +}; export function formatMonthGroupTitle(_date: DateTime): string { if (!_date.isValid) { From 4153848c68d416329deecf8b671e02bd754b4dd7 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 19:39:05 +0000 Subject: [PATCH 17/30] chore: version v1.142.0 --- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package.json | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package.json | 2 +- web/package.json | 2 +- 12 files changed, 16 insertions(+), 12 deletions(-) diff --git a/cli/package.json b/cli/package.json index 39536d88fa..6e25fb363c 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.88", + "version": "2.2.89", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 5b7d3a9507..031db08cb8 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.142.0", + "url": "https://v1.142.0.archive.immich.app" + }, { "label": "v1.141.1", "url": "https://v1.141.1.archive.immich.app" diff --git a/e2e/package.json b/e2e/package.json index d309067af8..517286f46e 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.141.1", + "version": "1.142.0", "description": "", "main": "index.js", "type": "module", diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 9375b599fa..0b78bad0dd 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 3013, - "android.injected.version.name" => "1.141.1", + "android.injected.version.code" => 3014, + "android.injected.version.name" => "1.142.0", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index d546a6493f..1d6c034ff9 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -22,7 +22,7 @@ platform :ios do path: "./Runner.xcodeproj", ) increment_version_number( - version_number: "1.141.1" + version_number: "1.142.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 339ae6ff5d..e77b969898 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.141.1 +- API version: 1.142.0 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 006de4f591..d3b7d6ac9a 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.141.1+3013 +version: 1.142.0+3014 environment: sdk: '>=3.8.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 7caf215042..da8093fb1e 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9858,7 +9858,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.141.1", + "version": "1.142.0", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index a44cd60d5b..9385f605bc 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.141.1", + "version": "1.142.0", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index bc38a69079..9d57167162 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.141.1 + * 1.142.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package.json b/server/package.json index c2a0c6c8cd..93b2ce8d2a 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.141.1", + "version": "1.142.0", "description": "", "author": "", "private": true, diff --git a/web/package.json b/web/package.json index 86574fde4f..4c4831c2e7 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.141.1", + "version": "1.142.0", "license": "GNU Affero General Public License version 3", "type": "module", "scripts": { From d84cc450f184d21bc9fcb5e825318387ab5a2d92 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 11 Sep 2025 15:15:10 -0500 Subject: [PATCH 18/30] chore: post release tasks (#21834) --- mobile/ios/Runner.xcodeproj/project.pbxproj | 30 +++++++++------------ mobile/ios/Runner/Info.plist | 4 +-- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 524778d89d..6ed0a16389 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 77; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -133,6 +133,8 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Sync; sourceTree = ""; }; @@ -519,14 +521,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -555,14 +553,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -711,7 +705,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 223; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -855,7 +849,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 223; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -885,7 +879,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 223; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -919,7 +913,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 223; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -962,7 +956,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 223; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1002,7 +996,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 223; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1041,7 +1035,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 223; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1085,7 +1079,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 223; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1126,7 +1120,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 219; + CURRENT_PROJECT_VERSION = 223; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 04e5e01392..c0ec917f21 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -80,7 +80,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.140.0 + 1.142.0 CFBundleSignature ???? CFBundleURLTypes @@ -107,7 +107,7 @@ CFBundleVersion - 219 + 223 FLTEnableImpeller ITSAppUsesNonExemptEncryption From 04c9531624f2f397475b4e9e5894748f864a9fc8 Mon Sep 17 00:00:00 2001 From: Stewart Rand Date: Fri, 12 Sep 2025 04:20:05 -0300 Subject: [PATCH 19/30] fix: format point count numbers on map view (#21848) Format numbers on map view --- web/src/lib/components/shared-components/map/map.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/shared-components/map/map.svelte b/web/src/lib/components/shared-components/map/map.svelte index 3c5bb5ece9..96116aa584 100644 --- a/web/src/lib/components/shared-components/map/map.svelte +++ b/web/src/lib/components/shared-components/map/map.svelte @@ -353,7 +353,7 @@
- {feature.properties?.point_count} + {feature.properties?.point_count?.toLocaleString()}
{/snippet} From a10a946d1af527776091ebe5e1953cff69ec6267 Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Fri, 12 Sep 2025 11:20:41 -0400 Subject: [PATCH 20/30] fix: let dev docker compose service runs as root (#21579) --- .devcontainer/server/container-compose-overrides.yml | 2 +- Makefile | 8 +++----- docker/docker-compose.dev.yml | 6 +++--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.devcontainer/server/container-compose-overrides.yml b/.devcontainer/server/container-compose-overrides.yml index abf34ad68c..0fa8f2ff48 100644 --- a/.devcontainer/server/container-compose-overrides.yml +++ b/.devcontainer/server/container-compose-overrides.yml @@ -26,7 +26,7 @@ services: env_file: !reset [] init: env_file: !reset [] - command: sh -c 'find /data -maxdepth 1 ! -path "/data/postgres" -type d -exec chown ${UID:-1000}:${GID:-1000} {} + 2>/dev/null || true; for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done' + command: sh -c 'find /data -maxdepth 1 ! -path "/data/postgres" -type d -exec chown ${UID:-0}:${GID:-0} {} + 2>/dev/null || true; for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-0}:${GID:-0} "$$path" || true; done' immich-machine-learning: env_file: !reset [] database: diff --git a/Makefile b/Makefile index 517e8d7523..8c5865448d 100644 --- a/Makefile +++ b/Makefile @@ -70,11 +70,9 @@ VOLUME_DIRS = \ # Helper function to chown, on error suggest remediation and exit define safe_chown - if chown $(2) $(or $(UID),1000):$(or $(GID),1000) "$(1)" 2>/dev/null; then \ - true; \ - else \ - STATUS=$$?; echo "Exit code: $$STATUS $(1)"; \ - echo "$$STATUS $(1)"; \ + CURRENT_OWNER=$$(stat -c '%u:%g' "$(1)" 2>/dev/null || echo "none"); \ + DESIRED_OWNER="$(or $(UID),0):$(or $(GID),0)"; \ + if [ "$$CURRENT_OWNER" != "$$DESIRED_OWNER" ] && ! chown -v $(2) $$DESIRED_OWNER "$(1)" 2>/dev/null; then \ echo "Permission denied when changing owner of volumes and upload location. Try running 'sudo make prepare-volumes' first."; \ exit 1; \ fi; diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 5c1a21c7ce..1bc1908d4e 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -21,7 +21,7 @@ services: # extends: # file: hwaccel.transcoding.yml # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding - user: '${UID:-1000}:${GID:-1000}' + user: '${UID:-0}:${GID:-0}' build: context: ../ dockerfile: server/Dockerfile @@ -82,7 +82,7 @@ services: image: immich-web-dev:latest # Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919 # user: 0:0 - user: '${UID:-1000}:${GID:-1000}' + user: '${UID:-0}:${GID:-0}' build: context: ../ dockerfile: server/Dockerfile @@ -189,7 +189,7 @@ services: env_file: - .env user: 0:0 - command: sh -c 'find /data -maxdepth 1 -type d -exec chown ${UID:-1000}:${GID:-1000} {} + 2>/dev/null || true; for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done' + command: sh -c 'find /data -maxdepth 1 -type d -exec chown ${UID:-0}:${GID:-0} {} + 2>/dev/null || true; for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-0}:${GID:-0} "$$path" || true; done' volumes: - pnpm-store:/usr/src/app/.pnpm-store - server-node_modules:/usr/src/app/server/node_modules From 23aa661324a11222b22e04cf134e39ac3b07ca3e Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Fri, 12 Sep 2025 21:46:39 +0200 Subject: [PATCH 21/30] fix: use mdq image with jq (#21860) --- .github/workflows/close-duplicates.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/close-duplicates.yml b/.github/workflows/close-duplicates.yml index d9ac98aa2d..0e831ff5d1 100644 --- a/.github/workflows/close-duplicates.yml +++ b/.github/workflows/close-duplicates.yml @@ -35,7 +35,7 @@ jobs: needs: [get_body, should_run] if: ${{ needs.should_run.outputs.should_run == 'true' }} container: - image: yshavit/mdq:0.9.0@sha256:4399483ca857fb1a7ed28a596f754c7373e358647de31ce14b79a27c91e1e35e + image: ghcr.io/immich-app/mdq:main@sha256:1669c75a5542333ff6b03c13d5fd259ea8d798188b84d5d99093d62e4542eb05 outputs: checked: ${{ steps.get_checkbox.outputs.checked }} steps: From 17bbcdf58418b3168f05b4b7aea5912129d86c54 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:56:00 -0400 Subject: [PATCH 22/30] chore(mobile): add `debugPrint` lint rule (#21872) * add lint rule * update usages * stragglers * use dcm * formatting * test ci * Revert "test ci" This reverts commit 8f864c4e4d3a7ec1a7e820b1afb3e801f2e82bc5. * revert whitespace change --- mobile/analysis_options.yaml | 7 ++++ .../services/background_worker.service.dart | 7 ++-- mobile/lib/domain/services/log.service.dart | 15 ++++----- .../lib/domain/services/partner.service.dart | 4 +-- .../services/sync_linked_album.service.dart | 4 +-- .../lib/extensions/translate_extensions.dart | 3 +- mobile/lib/main.dart | 17 +++++----- .../pages/drift_partner_detail.page.dart | 3 +- .../person_edit_birthday_modal.widget.dart | 3 +- .../people/person_edit_name_modal.widget.dart | 3 +- mobile/lib/providers/asset.provider.dart | 4 +-- mobile/lib/providers/auth.provider.dart | 7 ++-- .../lib/providers/backup/backup.provider.dart | 7 ++-- .../backup/drift_backup.provider.dart | 8 ++--- .../backup/manual_upload.provider.dart | 15 +++++---- mobile/lib/providers/theme.provider.dart | 7 ++-- .../upload_profile_image.provider.dart | 4 +-- mobile/lib/providers/websocket.provider.dart | 15 ++++----- .../lib/repositories/upload.repository.dart | 9 +++-- mobile/lib/routing/duplicate_guard.dart | 4 +-- mobile/lib/services/album.service.dart | 24 +++++++------- mobile/lib/services/api.service.dart | 4 +-- mobile/lib/services/asset.service.dart | 6 ++-- mobile/lib/services/background.service.dart | 22 ++++++------- mobile/lib/services/backup.service.dart | 19 ++++++----- .../services/local_notification.service.dart | 4 +-- mobile/lib/services/localization.service.dart | 4 +-- mobile/lib/services/search.service.dart | 4 +-- mobile/lib/services/server_info.service.dart | 10 +++--- mobile/lib/services/stack.service.dart | 10 +++--- mobile/lib/services/upload.service.dart | 4 +-- mobile/lib/theme/dynamic_theme.dart | 5 +-- mobile/lib/utils/debug_print.dart | 8 +++++ mobile/lib/utils/isolate.dart | 8 ++--- mobile/lib/utils/migration.dart | 33 +++++++------------ .../asset_viewer/detail_panel/exif_map.dart | 3 +- .../widgets/shared_link/shared_link_item.dart | 3 +- mobile/test/modules/utils/throttler_test.dart | 4 +-- 38 files changed, 168 insertions(+), 153 deletions(-) create mode 100644 mobile/lib/utils/debug_print.dart diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index bef051bff2..c26e1c6649 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -134,6 +134,13 @@ custom_lint: dart_code_metrics: rules: + - banned-usage: + entries: + - name: debugPrint + description: Use dPrint instead of debugPrint for proper tree-shaking in release builds. + exclude-paths: + - 'lib/utils/debug_print.dart' + severity: perf # All rules from "recommended" preset # Show potential errors # - avoid-cascade-after-if-null diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 4df221d41b..50f9f99191 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -31,6 +31,7 @@ import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:worker_manager/worker_manager.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class BackgroundWorkerFgService { final BackgroundWorkerFgHostApi _foregroundHostApi; @@ -159,7 +160,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { try { await _cleanup(); } catch (error, stack) { - debugPrint('Failed to cleanup background worker: $error with stack: $stack'); + dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack'); } } @@ -192,7 +193,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { await Future.wait(cleanupFutures); _logger.info("Background worker resources cleaned up"); } catch (error, stack) { - debugPrint('Failed to cleanup background worker: $error with stack: $stack'); + dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack'); } } @@ -230,7 +231,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { .startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken); }, (error, stack) { - debugPrint("Error in backup zone $error, $stack"); + dPrint(() => "Error in backup zone $error, $stack"); }, ); } diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart index d21cb7ab09..64010b9220 100644 --- a/mobile/lib/domain/services/log.service.dart +++ b/mobile/lib/domain/services/log.service.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; /// Service responsible for handling application logging. @@ -66,13 +66,12 @@ class LogService { } void _handleLogRecord(LogRecord r) { - if (kDebugMode) { - debugPrint( - '[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}' - '${r.error == null ? '' : '\nError: ${r.error}'}' - '${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}', - ); - } + dPrint( + () => + '[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}' + '${r.error == null ? '' : '\nError: ${r.error}'}' + '${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}', + ); final record = LogMessage( message: r.message, diff --git a/mobile/lib/domain/services/partner.service.dart b/mobile/lib/domain/services/partner.service.dart index 7733b5be6b..ce1bd9557b 100644 --- a/mobile/lib/domain/services/partner.service.dart +++ b/mobile/lib/domain/services/partner.service.dart @@ -1,7 +1,7 @@ -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class DriftPartnerService { final DriftPartnerRepository _driftPartnerRepository; @@ -30,7 +30,7 @@ class DriftPartnerService { Future toggleShowInTimeline(String partnerId, String userId) async { final partner = await _driftPartnerRepository.getPartner(partnerId, userId); if (partner == null) { - debugPrint("Partner not found: $partnerId for user: $userId"); + dPrint(() => "Partner not found: $partnerId for user: $userId"); return; } diff --git a/mobile/lib/domain/services/sync_linked_album.service.dart b/mobile/lib/domain/services/sync_linked_album.service.dart index 2c445f3aca..78750c9bd0 100644 --- a/mobile/lib/domain/services/sync_linked_album.service.dart +++ b/mobile/lib/domain/services/sync_linked_album.service.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; @@ -6,6 +5,7 @@ import 'package:immich_mobile/infrastructure/repositories/remote_album.repositor import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:logging/logging.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final syncLinkedAlbumServiceProvider = Provider( (ref) => SyncLinkedAlbumService( @@ -100,7 +100,7 @@ class SyncLinkedAlbumService { /// Creates a new remote album and links it to the local album Future _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async { - debugPrint("Creating new remote album for local album: ${localAlbum.name}"); + dPrint(() => "Creating new remote album for local album: ${localAlbum.name}"); final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, assetIds: []); await _remoteAlbumRepository.create(newRemoteAlbum, []); return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id); diff --git a/mobile/lib/extensions/translate_extensions.dart b/mobile/lib/extensions/translate_extensions.dart index cfd8c8cd1f..7677f3cbd8 100644 --- a/mobile/lib/extensions/translate_extensions.dart +++ b/mobile/lib/extensions/translate_extensions.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:intl/message_format.dart'; import 'package:flutter/material.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; extension StringTranslateExtension on String { String t({BuildContext? context, Map? args}) { @@ -39,7 +40,7 @@ String _translateHelper(BuildContext? context, String key, [Map? ? MessageFormat(translatedMessage, locale: Intl.defaultLocale ?? 'en').format(args) : translatedMessage; } catch (e) { - debugPrint('Translation failed for key "$key". Error: $e'); + dPrint(() => 'Translation failed for key "$key". Error: $e'); return key; } } diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 4f74c30e3b..2e8e9383a0 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -39,6 +39,7 @@ import 'package:intl/date_symbol_data_local.dart'; import 'package:logging/logging.dart'; import 'package:timezone/data/latest.dart'; import 'package:worker_manager/worker_manager.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; void main() async { ImmichWidgetsBinding(); @@ -69,9 +70,9 @@ Future initApp() async { if (kReleaseMode && Platform.isAndroid) { try { await FlutterDisplayMode.setHighRefreshRate(); - debugPrint("Enabled high refresh mode"); + dPrint(() => "Enabled high refresh mode"); } catch (e) { - debugPrint("Error setting high refresh rate: $e"); + dPrint(() => "Error setting high refresh rate: $e"); } } @@ -126,23 +127,23 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve void didChangeAppLifecycleState(AppLifecycleState state) { switch (state) { case AppLifecycleState.resumed: - debugPrint("[APP STATE] resumed"); + dPrint(() => "[APP STATE] resumed"); ref.read(appStateProvider.notifier).handleAppResume(); break; case AppLifecycleState.inactive: - debugPrint("[APP STATE] inactive"); + dPrint(() => "[APP STATE] inactive"); ref.read(appStateProvider.notifier).handleAppInactivity(); break; case AppLifecycleState.paused: - debugPrint("[APP STATE] paused"); + dPrint(() => "[APP STATE] paused"); ref.read(appStateProvider.notifier).handleAppPause(); break; case AppLifecycleState.detached: - debugPrint("[APP STATE] detached"); + dPrint(() => "[APP STATE] detached"); ref.read(appStateProvider.notifier).handleAppDetached(); break; case AppLifecycleState.hidden: - debugPrint("[APP STATE] hidden"); + dPrint(() => "[APP STATE] hidden"); ref.read(appStateProvider.notifier).handleAppHidden(); break; } @@ -200,7 +201,7 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve @override initState() { super.initState(); - initApp().then((_) => debugPrint("App Init Completed")); + initApp().then((_) => dPrint(() => "App Init Completed")); WidgetsBinding.instance.addPostFrameCallback((_) { // needs to be delayed so that EasyLocalization is working if (Store.isBetaTimelineEnabled) { diff --git a/mobile/lib/presentation/pages/drift_partner_detail.page.dart b/mobile/lib/presentation/pages/drift_partner_detail.page.dart index 95c5b008b3..f8a19b6b70 100644 --- a/mobile/lib/presentation/pages/drift_partner_detail.page.dart +++ b/mobile/lib/presentation/pages/drift_partner_detail.page.dart @@ -10,6 +10,7 @@ import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; @RoutePage() class DriftPartnerDetailPage extends StatelessWidget { @@ -68,7 +69,7 @@ class _InfoBoxState extends ConsumerState<_InfoBox> { _inTimeline = !_inTimeline; }); } catch (error, stack) { - debugPrint("Failed to toggle in timeline: $error $stack"); + dPrint(() => "Failed to toggle in timeline: $error $stack"); ImmichToast.show( context: context, toastType: ToastType.error, diff --git a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart index 8813224a5f..dd6390406b 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:scroll_date_picker/scroll_date_picker.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class DriftPersonBirthdayEditForm extends ConsumerStatefulWidget { final DriftPerson person; @@ -36,7 +37,7 @@ class _DriftPersonNameEditFormState extends ConsumerState(_selectedDate); } } catch (error) { - debugPrint('Error updating birthday: $error'); + dPrint(() => 'Error updating birthday: $error'); if (!context.mounted) { return; diff --git a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart index 46fd683b81..6de19000e0 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class DriftPersonNameEditForm extends ConsumerStatefulWidget { final DriftPerson person; @@ -34,7 +35,7 @@ class _DriftPersonNameEditFormState extends ConsumerState(newName); } } catch (error) { - debugPrint('Error updating name: $error'); + dPrint(() => 'Error updating name: $error'); if (!context.mounted) { return; diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart index 9c8b28e6cf..75635950ff 100644 --- a/mobile/lib/providers/asset.provider.dart +++ b/mobile/lib/providers/asset.provider.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; @@ -13,6 +12,7 @@ import 'package:immich_mobile/services/etag.service.dart'; import 'package:immich_mobile/services/exif.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:logging/logging.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final assetProvider = StateNotifierProvider((ref) { return AssetNotifier( @@ -68,7 +68,7 @@ class AssetNotifier extends StateNotifier { } final bool newRemote = await _assetService.refreshRemoteAssets(); final bool newLocal = await _albumService.refreshDeviceAlbums(); - debugPrint("changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal"); + dPrint(() => "changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal"); if (newRemote) { _ref.invalidate(memoryFutureProvider); } diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index 6c39eb0dec..9a15598998 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter_udid/flutter_udid.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; @@ -18,6 +17,7 @@ import 'package:immich_mobile/services/widget.service.dart'; import 'package:immich_mobile/utils/hash.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final authProvider = StateNotifierProvider((ref) { return AuthNotifier( @@ -150,10 +150,7 @@ class AuthNotifier extends StateNotifier { _log.severe("Error getting user information from the server [API EXCEPTION]", stackTrace); } catch (error, stackTrace) { _log.severe("Error getting user information from the server [CATCH ALL]", error, stackTrace); - - if (kDebugMode) { - debugPrint("Error getting user information from the server [CATCH ALL] $error $stackTrace"); - } + dPrint(() => "Error getting user information from the server [CATCH ALL] $error $stackTrace"); } // If the user is null, the login was not successful diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 76cb383465..03666466ff 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -2,8 +2,6 @@ import 'dart:io'; import 'package:cancellation_token_http/http.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -33,6 +31,7 @@ import 'package:immich_mobile/utils/diff.dart'; import 'package:logging/logging.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; +import 'package:immich_mobile/utils/debug_print.dart'; final backupProvider = StateNotifierProvider((ref) { return BackupNotifier( @@ -286,7 +285,7 @@ class BackupNotifier extends StateNotifier { state = state.copyWith(selectedBackupAlbums: selectedAlbums, excludedBackupAlbums: excludedAlbums); log.info("_getBackupAlbumsInfo: Found ${availableAlbums.length} available albums"); - debugPrint("_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms"); + dPrint(() => "_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms"); } /// @@ -428,7 +427,7 @@ class BackupNotifier extends StateNotifier { /// Invoke backup process Future startBackupProcess() async { - debugPrint("Start backup process"); + dPrint(() => "Start backup process"); assert(state.backupProgress == BackUpProgressEnum.idle); state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress); diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart index 21bee38004..40ec7b1077 100644 --- a/mobile/lib/providers/backup/drift_backup.provider.dart +++ b/mobile/lib/providers/backup/drift_backup.provider.dart @@ -4,7 +4,6 @@ import 'dart:convert'; import 'package:background_downloader/background_downloader.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; @@ -14,6 +13,7 @@ import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/upload.service.dart'; import 'package:logging/logging.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class EnqueueStatus { final int enqueueCount; @@ -329,16 +329,16 @@ class DriftBackupNotifier extends StateNotifier { } Future cancel() async { - debugPrint("Canceling backup tasks..."); + dPrint(() => "Canceling backup tasks..."); state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true); final activeTaskCount = await _uploadService.cancelBackup(); if (activeTaskCount > 0) { - debugPrint("$activeTaskCount tasks left, continuing to cancel..."); + dPrint(() => "$activeTaskCount tasks left, continuing to cancel..."); await cancel(); } else { - debugPrint("All tasks canceled successfully."); + dPrint(() => "All tasks canceled successfully."); // Clear all upload items when cancellation is complete state = state.copyWith(isCanceling: false, uploadItems: {}); } diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart index 1aea7f64cb..bfc079bfa3 100644 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ b/mobile/lib/providers/backup/manual_upload.provider.dart @@ -30,6 +30,7 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:logging/logging.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; +import 'package:immich_mobile/utils/debug_print.dart'; final manualUploadProvider = StateNotifierProvider((ref) { return ManualUploadNotifier( @@ -216,7 +217,7 @@ class ManualUploadNotifier extends StateNotifier { ); if (uploadAssets.isEmpty) { - debugPrint("[_startUpload] No Assets to upload - Abort Process"); + dPrint(() => "[_startUpload] No Assets to upload - Abort Process"); _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); return false; } @@ -294,10 +295,10 @@ class ManualUploadNotifier extends StateNotifier { } } else { openAppSettings(); - debugPrint("[_startUpload] Do not have permission to the gallery"); + dPrint(() => "[_startUpload] Do not have permission to the gallery"); } } catch (e) { - debugPrint("ERROR _startUpload: ${e.toString()}"); + dPrint(() => "ERROR _startUpload: ${e.toString()}"); hasErrors = true; } finally { _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); @@ -340,7 +341,7 @@ class ManualUploadNotifier extends StateNotifier { // waits until it has stopped to start the backup. final bool hasLock = await ref.read(backgroundServiceProvider).acquireLock(); if (!hasLock) { - debugPrint("[uploadAssets] could not acquire lock, exiting"); + dPrint(() => "[uploadAssets] could not acquire lock, exiting"); ImmichToast.show( context: context, msg: "failed".tr(), @@ -355,18 +356,18 @@ class ManualUploadNotifier extends StateNotifier { // check if backup is already in process - then return if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) { - debugPrint("[uploadAssets] Manual upload is already running - abort"); + dPrint(() => "[uploadAssets] Manual upload is already running - abort"); showInProgress = true; } if (_backupProvider.backupProgress == BackUpProgressEnum.inProgress) { - debugPrint("[uploadAssets] Auto Backup is already in progress - abort"); + dPrint(() => "[uploadAssets] Auto Backup is already in progress - abort"); showInProgress = true; return false; } if (_backupProvider.backupProgress == BackUpProgressEnum.inBackground) { - debugPrint("[uploadAssets] Background backup is running - abort"); + dPrint(() => "[uploadAssets] Background backup is running - abort"); showInProgress = true; } diff --git a/mobile/lib/providers/theme.provider.dart b/mobile/lib/providers/theme.provider.dart index 5f32e07578..1d5511f1ff 100644 --- a/mobile/lib/providers/theme.provider.dart +++ b/mobile/lib/providers/theme.provider.dart @@ -7,11 +7,12 @@ import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final immichThemeModeProvider = StateProvider((ref) { final themeMode = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.themeMode); - debugPrint("Current themeMode $themeMode"); + dPrint(() => "Current themeMode $themeMode"); if (themeMode == ThemeMode.light.name) { return ThemeMode.light; @@ -26,12 +27,12 @@ final immichThemePresetProvider = StateProvider((ref) { final appSettingsProvider = ref.watch(appSettingsServiceProvider); final primaryColorPreset = appSettingsProvider.getSetting(AppSettingsEnum.primaryColor); - debugPrint("Current theme preset $primaryColorPreset"); + dPrint(() => "Current theme preset $primaryColorPreset"); try { return ImmichColorPreset.values.firstWhere((e) => e.name == primaryColorPreset); } catch (e) { - debugPrint("Theme preset $primaryColorPreset not found. Applying default preset."); + dPrint(() => "Theme preset $primaryColorPreset not found. Applying default preset."); appSettingsProvider.setSetting(AppSettingsEnum.primaryColor, defaultColorPresetName); return defaultColorPreset; } diff --git a/mobile/lib/providers/upload_profile_image.provider.dart b/mobile/lib/providers/upload_profile_image.provider.dart index e9e467346b..5aa924ed1c 100644 --- a/mobile/lib/providers/upload_profile_image.provider.dart +++ b/mobile/lib/providers/upload_profile_image.provider.dart @@ -1,10 +1,10 @@ import 'dart:convert'; -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; enum UploadProfileStatus { idle, loading, success, failure } @@ -67,7 +67,7 @@ class UploadProfileImageNotifier extends StateNotifier var profileImagePath = await _userService.createProfileImage(file.name, await file.readAsBytes()); if (profileImagePath != null) { - debugPrint("Successfully upload profile image"); + dPrint(() => "Successfully upload profile image"); state = state.copyWith(status: UploadProfileStatus.success, profileImagePath: profileImagePath); return true; } diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index 3b0d5daab8..05427d011e 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -2,8 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; @@ -20,6 +18,7 @@ import 'package:immich_mobile/utils/debounce.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:socket_io_client/socket_io_client.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; enum PendingAction { assetDelete, assetUploaded, assetHidden, assetTrash } @@ -105,7 +104,7 @@ class WebsocketNotifier extends StateNotifier { headers["Authorization"] = "Basic ${base64.encode(utf8.encode(endpoint.userInfo))}"; } - debugPrint("Attempting to connect to websocket"); + dPrint(() => "Attempting to connect to websocket"); // Configure socket transports must be specified Socket socket = io( endpoint.origin, @@ -121,12 +120,12 @@ class WebsocketNotifier extends StateNotifier { ); socket.onConnect((_) { - debugPrint("Established Websocket Connection"); + dPrint(() => "Established Websocket Connection"); state = WebsocketState(isConnected: true, socket: socket, pendingChanges: state.pendingChanges); }); socket.onDisconnect((_) { - debugPrint("Disconnect to Websocket Connection"); + dPrint(() => "Disconnect to Websocket Connection"); state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges); }); @@ -150,13 +149,13 @@ class WebsocketNotifier extends StateNotifier { socket.on('on_config_update', _handleOnConfigUpdate); socket.on('on_new_release', _handleReleaseUpdates); } catch (e) { - debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}"); + dPrint(() => "[WEBSOCKET] Catch Websocket Error - ${e.toString()}"); } } } void disconnect() { - debugPrint("Attempting to disconnect from websocket"); + dPrint(() => "Attempting to disconnect from websocket"); _batchedAssetUploadReady.clear(); @@ -200,7 +199,7 @@ class WebsocketNotifier extends StateNotifier { } void listenUploadEvent() { - debugPrint("Start listening to event on_upload_success"); + dPrint(() => "Start listening to event on_upload_success"); state.socket?.on('on_upload_success', _handleOnUploadSuccess); } diff --git a/mobile/lib/repositories/upload.repository.dart b/mobile/lib/repositories/upload.repository.dart index 2d99631d51..38f2c22cf2 100644 --- a/mobile/lib/repositories/upload.repository.dart +++ b/mobile/lib/repositories/upload.repository.dart @@ -3,12 +3,12 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:cancellation_token_http/http.dart'; -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:logging/logging.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class UploadTaskWithFile { final File file; @@ -79,14 +79,17 @@ class UploadRepository { FileDownloader().database.allRecordsWithStatus(TaskStatus.paused, group: kBackupGroup), ]); - debugPrint(""" + dPrint( + () => + """ Upload Info: Enqueued: ${enqueuedTasks.length} Running: ${runningTasks.length} Canceled: ${canceledTasks.length} Waiting: ${waitingTasks.length} Paused: ${pausedTasks.length} - """); + """, + ); } Future backupWithDartClient(Iterable tasks, CancellationToken cancelToken) async { diff --git a/mobile/lib/routing/duplicate_guard.dart b/mobile/lib/routing/duplicate_guard.dart index 6f83d5297e..c55c7318d0 100644 --- a/mobile/lib/routing/duplicate_guard.dart +++ b/mobile/lib/routing/duplicate_guard.dart @@ -1,5 +1,5 @@ import 'package:auto_route/auto_route.dart'; -import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; /// Guards against duplicate navigation to this route class DuplicateGuard extends AutoRouteGuard { @@ -8,7 +8,7 @@ class DuplicateGuard extends AutoRouteGuard { void onNavigation(NavigationResolver resolver, StackRouter router) async { // Duplicate navigation if (resolver.route.name == router.current.name) { - debugPrint('DuplicateGuard: Preventing duplicate route navigation for ${resolver.route.name}'); + dPrint(() => 'DuplicateGuard: Preventing duplicate route navigation for ${resolver.route.name}'); resolver.next(false); } else { resolver.next(true); diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index 454f652035..a9eee0528e 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -3,7 +3,6 @@ import 'dart:collection'; import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; @@ -24,6 +23,7 @@ import 'package:immich_mobile/services/entity.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/utils/hash.dart'; import 'package:logging/logging.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final albumServiceProvider = Provider( (ref) => AlbumService( @@ -124,7 +124,7 @@ class AlbumService { } finally { _localCompleter.complete(changes); } - debugPrint("refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms"); + dPrint(() => "refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms"); return changes; } @@ -172,7 +172,7 @@ class AlbumService { } finally { _remoteCompleter.complete(changes); } - debugPrint("refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms"); + dPrint(() => "refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms"); return changes; } @@ -220,7 +220,7 @@ class AlbumService { return AlbumAddAssetsResponse(alreadyInAlbum: result.duplicates, successfullyAdded: addedAssets.length); } catch (e) { - debugPrint("Error addAssets ${e.toString()}"); + dPrint(() => "Error addAssets ${e.toString()}"); } return null; } @@ -242,7 +242,7 @@ class AlbumService { await _albumRepository.update(album); return true; } catch (e) { - debugPrint("Error setActivityEnabled ${e.toString()}"); + dPrint(() => "Error setActivityEnabled ${e.toString()}"); } return false; } @@ -271,7 +271,7 @@ class AlbumService { } return true; } catch (e) { - debugPrint("Error deleteAlbum ${e.toString()}"); + dPrint(() => "Error deleteAlbum ${e.toString()}"); } return false; } @@ -281,7 +281,7 @@ class AlbumService { await _albumApiRepository.removeUser(album.remoteId!, userId: "me"); return true; } catch (e) { - debugPrint("Error leaveAlbum ${e.toString()}"); + dPrint(() => "Error leaveAlbum ${e.toString()}"); return false; } } @@ -293,7 +293,7 @@ class AlbumService { await _updateAssets(album.id, remove: toRemove.toList()); return true; } catch (e) { - debugPrint("Error removeAssetFromAlbum ${e.toString()}"); + dPrint(() => "Error removeAssetFromAlbum ${e.toString()}"); } return false; } @@ -310,7 +310,7 @@ class AlbumService { return true; } catch (error) { - debugPrint("Error removeUser ${error.toString()}"); + dPrint(() => "Error removeUser ${error.toString()}"); return false; } } @@ -327,7 +327,7 @@ class AlbumService { return true; } catch (error) { - debugPrint("Error addUsers ${error.toString()}"); + dPrint(() => "Error addUsers ${error.toString()}"); } return false; } @@ -340,7 +340,7 @@ class AlbumService { await _albumRepository.update(album); return true; } catch (e) { - debugPrint("Error changeTitleAlbum ${e.toString()}"); + dPrint(() => "Error changeTitleAlbum ${e.toString()}"); return false; } } @@ -353,7 +353,7 @@ class AlbumService { await _albumRepository.update(album); return true; } catch (e) { - debugPrint("Error changeDescriptionAlbum ${e.toString()}"); + dPrint(() => "Error changeDescriptionAlbum ${e.toString()}"); return false; } } diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index fca9080c86..4033ffb184 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/material.dart'; import 'package:http/http.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; @@ -11,6 +10,7 @@ import 'package:immich_mobile/utils/url_helper.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:immich_mobile/utils/user_agent.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class ApiService implements Authentication { late ApiClient _apiClient; @@ -155,7 +155,7 @@ class ApiService implements Authentication { return endpoint; } } catch (e) { - debugPrint("Could not locate /.well-known/immich at $baseUrl"); + dPrint(() => "Could not locate /.well-known/immich at $baseUrl"); } return ""; diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index ee61929c81..b9fab35442 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; @@ -26,6 +25,7 @@ import 'package:immich_mobile/services/sync.service.dart'; import 'package:logging/logging.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:openapi/api.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final assetServiceProvider = Provider( (ref) => AssetService( @@ -87,7 +87,7 @@ class AssetService { getChangedAssets: _getRemoteAssetChanges, loadAssets: _getRemoteAssets, ); - debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms"); + dPrint(() => "refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms"); return changes; } @@ -156,7 +156,7 @@ class AssetService { if (a.isInDb) { await _assetRepository.transaction(() => _assetRepository.update(a)); } else { - debugPrint("[loadExif] parameter Asset is not from DB!"); + dPrint(() => "[loadExif] parameter Asset is not from DB!"); } } } diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index e6436df244..39620c17fb 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -7,7 +7,6 @@ import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities; import 'package:cancellation_token_http/http.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -33,6 +32,7 @@ import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:path_provider_foundation/path_provider_foundation.dart'; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; +import 'package:immich_mobile/utils/debug_print.dart'; final backgroundServiceProvider = Provider((ref) => BackgroundService()); @@ -165,7 +165,7 @@ class BackgroundService { ]); } } catch (error) { - debugPrint("[_updateNotification] failed to communicate with plugin"); + dPrint(() => "[_updateNotification] failed to communicate with plugin"); } return false; } @@ -177,7 +177,7 @@ class BackgroundService { return await _backgroundChannel.invokeMethod('showError', [title, content, individualTag]); } } catch (error) { - debugPrint("[_showErrorNotification] failed to communicate with plugin"); + dPrint(() => "[_showErrorNotification] failed to communicate with plugin"); } return false; } @@ -188,7 +188,7 @@ class BackgroundService { return await _backgroundChannel.invokeMethod('clearErrorNotifications'); } } catch (error) { - debugPrint("[_clearErrorNotifications] failed to communicate with plugin"); + dPrint(() => "[_clearErrorNotifications] failed to communicate with plugin"); } return false; } @@ -196,7 +196,7 @@ class BackgroundService { /// await to ensure this thread (foreground or background) has exclusive access Future acquireLock() async { if (_hasLock) { - debugPrint("WARNING: [acquireLock] called more than once"); + dPrint(() => "WARNING: [acquireLock] called more than once"); return true; } final int lockTime = Timeline.now; @@ -302,19 +302,19 @@ class BackgroundService { final bool hasAccess = await waitForLock; if (!hasAccess) { - debugPrint("[_callHandler] could not acquire lock, exiting"); + dPrint(() => "[_callHandler] could not acquire lock, exiting"); return false; } final translationsOk = await loadTranslations(); if (!translationsOk) { - debugPrint("[_callHandler] could not load translations"); + dPrint(() => "[_callHandler] could not load translations"); } final bool ok = await _onAssetsChanged(); return ok; } catch (error) { - debugPrint(error.toString()); + dPrint(() => error.toString()); return false; } finally { releaseLock(); @@ -324,7 +324,7 @@ class BackgroundService { _cancellationToken?.cancel(); return true; default: - debugPrint("Unknown method ${call.method}"); + dPrint(() => "Unknown method ${call.method}"); return false; } } @@ -344,9 +344,7 @@ class BackgroundService { HttpSSLOptions.apply(); ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken)); await ref.read(authServiceProvider).setOpenApiServiceEndpoint(); - if (kDebugMode) { - debugPrint("[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}"); - } + dPrint(() => "[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}"); final selectedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.select); final excludedAlbums = await ref.read(backupAlbumRepositoryProvider).getAllBySelection(BackupSelection.exclude); diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 3e29222b4c..539fd1fbd9 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:cancellation_token_http/http.dart' as http; import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -29,6 +28,7 @@ import 'package:openapi/api.dart'; import 'package:path/path.dart' as p; import 'package:permission_handler/permission_handler.dart' as pm; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; +import 'package:immich_mobile/utils/debug_print.dart'; final backupServiceProvider = Provider( (ref) => BackupService( @@ -69,7 +69,7 @@ class BackupService { try { return await _apiService.assetsApi.getAllUserAssetsByDeviceId(deviceId); } catch (e) { - debugPrint('Error [getDeviceBackupAsset] ${e.toString()}'); + dPrint(() => 'Error [getDeviceBackupAsset] ${e.toString()}'); return null; } } @@ -356,8 +356,9 @@ class BackupService { final error = responseBody; final errorMessage = error['message'] ?? error['error']; - debugPrint( - "Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}", + dPrint( + () => + "Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}", ); onError( @@ -398,11 +399,11 @@ class BackupService { } } } on http.CancelledException { - debugPrint("Backup was cancelled by the user"); + dPrint(() => "Backup was cancelled by the user"); anyErrors = true; break; } catch (error, stackTrace) { - debugPrint("Error backup asset: ${error.toString()}: $stackTrace"); + dPrint(() => "Error backup asset: ${error.toString()}: $stackTrace"); anyErrors = true; continue; } finally { @@ -411,7 +412,7 @@ class BackupService { await file?.delete(); await livePhotoFile?.delete(); } catch (e) { - debugPrint("ERROR deleting file: ${e.toString()}"); + dPrint(() => "ERROR deleting file: ${e.toString()}"); } } } @@ -454,7 +455,9 @@ class BackupService { if (![200, 201].contains(response.statusCode)) { var error = responseBody; - debugPrint("Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}"); + dPrint( + () => "Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}", + ); } return responseBody.containsKey('id') ? responseBody['id'] : null; diff --git a/mobile/lib/services/local_notification.service.dart b/mobile/lib/services/local_notification.service.dart index e7fc3292e2..bf85f4a9a9 100644 --- a/mobile/lib/services/local_notification.service.dart +++ b/mobile/lib/services/local_notification.service.dart @@ -1,9 +1,9 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/notification_permission.provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final localNotificationService = Provider( (ref) => LocalNotificationService(ref.watch(notificationPermissionProvider), ref), @@ -110,7 +110,7 @@ class LocalNotificationService { switch (notificationResponse.actionId) { case cancelUploadActionID: { - debugPrint("User cancelled manual upload operation"); + dPrint(() => "User cancelled manual upload operation"); ref.read(manualUploadProvider.notifier).cancelBackup(); } } diff --git a/mobile/lib/services/localization.service.dart b/mobile/lib/services/localization.service.dart index 8bee710544..af63894249 100644 --- a/mobile/lib/services/localization.service.dart +++ b/mobile/lib/services/localization.service.dart @@ -2,9 +2,9 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; import 'package:easy_localization/src/localization.dart'; -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; /// Workaround to manually load translations in another Isolate Future loadTranslations() async { @@ -17,7 +17,7 @@ Future loadTranslations() async { assetLoader: const CodegenLoader(), path: translationsPath, useOnlyLangCode: false, - onLoadError: (e) => debugPrint(e.toString()), + onLoadError: (e) => dPrint(() => e.toString()), fallbackLocale: locales.values.first, ); diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index 250fb67d82..f33adf80f9 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart'; @@ -10,6 +9,7 @@ import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final searchServiceProvider = Provider( (ref) => SearchService( @@ -43,7 +43,7 @@ class SearchService { model: model, ); } catch (e) { - debugPrint("[ERROR] [getSearchSuggestions] ${e.toString()}"); + dPrint(() => "[ERROR] [getSearchSuggestions] ${e.toString()}"); return []; } } diff --git a/mobile/lib/services/server_info.service.dart b/mobile/lib/services/server_info.service.dart index e1238999d7..0bce9366d2 100644 --- a/mobile/lib/services/server_info.service.dart +++ b/mobile/lib/services/server_info.service.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/server_info/server_config.model.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; @@ -6,6 +5,7 @@ import 'package:immich_mobile/models/server_info/server_features.model.dart'; import 'package:immich_mobile/models/server_info/server_version.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; final serverInfoServiceProvider = Provider((ref) => ServerInfoService(ref.watch(apiServiceProvider))); @@ -30,7 +30,7 @@ class ServerInfoService { return ServerDiskInfo.fromDto(dto); } } catch (e) { - debugPrint("Error [getDiskInfo] ${e.toString()}"); + dPrint(() => "Error [getDiskInfo] ${e.toString()}"); } return null; } @@ -42,7 +42,7 @@ class ServerInfoService { return ServerVersion.fromDto(dto); } } catch (e) { - debugPrint("Error [getServerVersion] ${e.toString()}"); + dPrint(() => "Error [getServerVersion] ${e.toString()}"); } return null; } @@ -54,7 +54,7 @@ class ServerInfoService { return ServerFeatures.fromDto(dto); } } catch (e) { - debugPrint("Error [getServerFeatures] ${e.toString()}"); + dPrint(() => "Error [getServerFeatures] ${e.toString()}"); } return null; } @@ -66,7 +66,7 @@ class ServerInfoService { return ServerConfig.fromDto(dto); } } catch (e) { - debugPrint("Error [getServerConfig] ${e.toString()}"); + dPrint(() => "Error [getServerConfig] ${e.toString()}"); } return null; } diff --git a/mobile/lib/services/stack.service.dart b/mobile/lib/services/stack.service.dart index c24b9fb7f8..88189c6bcd 100644 --- a/mobile/lib/services/stack.service.dart +++ b/mobile/lib/services/stack.service.dart @@ -1,10 +1,10 @@ -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:openapi/api.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class StackService { const StackService(this._api, this._assetRepository); @@ -16,7 +16,7 @@ class StackService { try { return _api.stacksApi.getStack(stackId); } catch (error) { - debugPrint("Error while fetching stack: $error"); + dPrint(() => "Error while fetching stack: $error"); } return null; } @@ -25,7 +25,7 @@ class StackService { try { return _api.stacksApi.createStack(StackCreateDto(assetIds: assetIds)); } catch (error) { - debugPrint("Error while creating stack: $error"); + dPrint(() => "Error while creating stack: $error"); } return null; } @@ -34,7 +34,7 @@ class StackService { try { return await _api.stacksApi.updateStack(stackId, StackUpdateDto(primaryAssetId: primaryAssetId)); } catch (error) { - debugPrint("Error while updating stack children: $error"); + dPrint(() => "Error while updating stack children: $error"); } return null; } @@ -54,7 +54,7 @@ class StackService { } await _assetRepository.transaction(() => _assetRepository.updateAll(removeAssets)); } catch (error) { - debugPrint("Error while deleting stack: $error"); + dPrint(() => "Error while deleting stack: $error"); } } } diff --git a/mobile/lib/services/upload.service.dart b/mobile/lib/services/upload.service.dart index e41d730ff8..9e9c81076b 100644 --- a/mobile/lib/services/upload.service.dart +++ b/mobile/lib/services/upload.service.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:cancellation_token_http/http.dart'; -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -22,6 +21,7 @@ import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; +import 'package:immich_mobile/utils/debug_print.dart'; final uploadServiceProvider = Provider((ref) { final service = UploadService( @@ -253,7 +253,7 @@ class UploadService { enqueueTasks([uploadTask]); } catch (error, stackTrace) { - debugPrint("Error handling live photo upload task: $error $stackTrace"); + dPrint(() => "Error handling live photo upload task: $error $stackTrace"); } } diff --git a/mobile/lib/theme/dynamic_theme.dart b/mobile/lib/theme/dynamic_theme.dart index 99b949c9ac..d0cb8e646f 100644 --- a/mobile/lib/theme/dynamic_theme.dart +++ b/mobile/lib/theme/dynamic_theme.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:immich_mobile/theme/theme_data.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; abstract final class DynamicTheme { const DynamicTheme._(); @@ -13,7 +14,7 @@ abstract final class DynamicTheme { final corePalette = await DynamicColorPlugin.getCorePalette(); if (corePalette != null) { final primaryColor = corePalette.toColorScheme().primary; - debugPrint('dynamic_color: Core palette detected.'); + dPrint(() => 'dynamic_color: Core palette detected.'); // Some palettes do not generate surface container colors accurately, // so we regenerate all colors using the primary color @@ -23,7 +24,7 @@ abstract final class DynamicTheme { ); } } catch (error) { - debugPrint('dynamic_color: Failed to obtain core palette: $error'); + dPrint(() => 'dynamic_color: Failed to obtain core palette: $error'); } } diff --git a/mobile/lib/utils/debug_print.dart b/mobile/lib/utils/debug_print.dart new file mode 100644 index 0000000000..21f55fc6a5 --- /dev/null +++ b/mobile/lib/utils/debug_print.dart @@ -0,0 +1,8 @@ +import 'package:flutter/foundation.dart'; + +@pragma('vm:prefer-inline') +void dPrint(String Function() message) { + if (kDebugMode) { + debugPrint(message()); + } +} diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart index 75e51d4360..16e3c032ad 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:ui'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; @@ -12,6 +11,7 @@ import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:logging/logging.dart'; import 'package:worker_manager/worker_manager.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class InvalidIsolateUsageException implements Exception { const InvalidIsolateUsageException(); @@ -71,10 +71,10 @@ Cancelable runInIsolateGentle({ await isar.close(); } } catch (e) { - debugPrint("Error closing Isar: $e"); + dPrint(() => "Error closing Isar: $e"); } } catch (error, stack) { - debugPrint("Error closing resources in isolate: $error, $stack"); + dPrint(() => "Error closing resources in isolate: $error, $stack"); } finally { ref.dispose(); // Delay to ensure all resources are released @@ -84,7 +84,7 @@ Cancelable runInIsolateGentle({ return null; }, (error, stack) { - debugPrint("Error in isolate zone: $error, $stack"); + dPrint(() => "Error in isolate zone: $error, $stack"); }, ); return null; diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index e5182a5999..10d71527fe 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -26,6 +25,7 @@ import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; // ignore: import_rule_photo_manager import 'package:photo_manager/photo_manager.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; const int targetVersion = 15; @@ -117,7 +117,7 @@ Future _isNewInstallation(Isar db, Drift drift) async { return true; } catch (error) { - debugPrint("[MIGRATION] Error checking if new installation: $error"); + dPrint(() => "[MIGRATION] Error checking if new installation: $error"); return false; } } @@ -143,10 +143,7 @@ Future _migrateDeviceAsset(Isar db) async { final PermissionState ps = await PhotoManager.requestPermissionExtend(); if (!ps.hasAccess) { - if (kDebugMode) { - debugPrint("[MIGRATION] Photo library permission not granted. Skipping device asset migration."); - } - + dPrint(() => "[MIGRATION] Photo library permission not granted. Skipping device asset migration."); return; } @@ -166,8 +163,8 @@ Future _migrateDeviceAsset(Isar db) async { localAssets = allDeviceAssets.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)).toList(); } - debugPrint("[MIGRATION] Device Asset Ids length - ${ids.length}"); - debugPrint("[MIGRATION] Local Asset Ids length - ${localAssets.length}"); + dPrint(() => "[MIGRATION] Device Asset Ids length - ${ids.length}"); + dPrint(() => "[MIGRATION] Local Asset Ids length - ${localAssets.length}"); ids.sort((a, b) => a.assetId.compareTo(b.assetId)); localAssets.sort((a, b) => a.assetId.compareTo(b.assetId)); final List toAdd = []; @@ -182,20 +179,14 @@ Future _migrateDeviceAsset(Isar db) async { return false; }, onlyFirst: (deviceAsset) { - if (kDebugMode) { - debugPrint('[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}'); - } + dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}'); }, onlySecond: (asset) { - if (kDebugMode) { - debugPrint('[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}'); - } + dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}'); }, ); - if (kDebugMode) { - debugPrint("[MIGRATION] Total number of device assets migrated - ${toAdd.length}"); - } + dPrint(() => "[MIGRATION] Total number of device assets migrated - ${toAdd.length}"); await db.writeTxn(() async { await db.deviceAssetEntitys.putAll(toAdd); @@ -215,7 +206,7 @@ Future migrateDeviceAssetToSqlite(Isar db, Drift drift) async { } }); } catch (error) { - debugPrint("[MIGRATION] Error while migrating device assets to SQLite: $error"); + dPrint(() => "[MIGRATION] Error while migrating device assets to SQLite: $error"); } } @@ -263,7 +254,7 @@ Future migrateBackupAlbumsToSqlite(Isar db, Drift drift) async { } }); } catch (error) { - debugPrint("[MIGRATION] Error while migrating backup albums to SQLite: $error"); + dPrint(() => "[MIGRATION] Error while migrating backup albums to SQLite: $error"); } } @@ -281,7 +272,7 @@ Future migrateStoreToSqlite(Isar db, Drift drift) async { } }); } catch (error) { - debugPrint("[MIGRATION] Error while migrating store values to SQLite: $error"); + dPrint(() => "[MIGRATION] Error while migrating store values to SQLite: $error"); } } @@ -296,7 +287,7 @@ Future migrateStoreToIsar(Isar db, Drift drift) async { await db.storeValues.putAll(driftStoreValues); }); } catch (error) { - debugPrint("[MIGRATION] Error while migrating store values to Isar: $error"); + dPrint(() => "[MIGRATION] Error while migrating store values to Isar: $error"); } } diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart index 04d01194e9..0edafa88c5 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class ExifMap extends StatelessWidget { final ExifInfo exifInfo; @@ -66,7 +67,7 @@ class ExifMap extends StatelessWidget { return; } - debugPrint('Opening Map Uri: $uri'); + dPrint(() => 'Opening Map Uri: $uri'); launchUrl(uri); }, onCreated: onMapCreated, diff --git a/mobile/lib/widgets/shared_link/shared_link_item.dart b/mobile/lib/widgets/shared_link/shared_link_item.dart index 0eced33ce3..cbd6e1f077 100644 --- a/mobile/lib/widgets/shared_link/shared_link_item.dart +++ b/mobile/lib/widgets/shared_link/shared_link_item.dart @@ -16,6 +16,7 @@ import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/search/thumbnail_with_info.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class SharedLinkItem extends ConsumerWidget { final SharedLink sharedLink; @@ -36,7 +37,7 @@ class SharedLinkItem extends ConsumerWidget { return Text("expired", style: TextStyle(color: Colors.red[300])).tr(); } final difference = sharedLink.expiresAt!.difference(DateTime.now()); - debugPrint("Difference: $difference"); + dPrint(() => "Difference: $difference"); if (difference.inDays > 0) { var dayDifference = difference.inDays; if (difference.inHours % 24 > 12) { diff --git a/mobile/test/modules/utils/throttler_test.dart b/mobile/test/modules/utils/throttler_test.dart index ba5d542fe6..1757826daf 100644 --- a/mobile/test/modules/utils/throttler_test.dart +++ b/mobile/test/modules/utils/throttler_test.dart @@ -1,6 +1,6 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/utils/throttle.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; class _Counter { int _count = 0; @@ -8,7 +8,7 @@ class _Counter { int get count => _count; void increment() { - debugPrint("Counter inside increment: $count"); + dPrint(() => "Counter inside increment: $count"); _count = _count + 1; } } From 994a7709212821aa87faaddac6ef4256425829a8 Mon Sep 17 00:00:00 2001 From: Stewart Rand Date: Sat, 13 Sep 2025 00:31:52 -0300 Subject: [PATCH 23/30] chore: improve context button accessibility (#21876) Make context menu button filled on album list and faces page --- web/src/lib/components/album-page/album-card.svelte | 2 +- web/src/lib/components/faces-page/people-card.svelte | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte index b6b44cadfe..26eccc2fd7 100644 --- a/web/src/lib/components/album-page/album-card.svelte +++ b/web/src/lib/components/album-page/album-card.svelte @@ -48,7 +48,7 @@ aria-label={$t('show_album_options')} icon={mdiDotsVertical} shape="round" - variant="ghost" + variant="filled" size="medium" class="icon-white-drop-shadow" onclick={showAlbumContextMenu} diff --git a/web/src/lib/components/faces-page/people-card.svelte b/web/src/lib/components/faces-page/people-card.svelte index 51fd53b7d5..95d5db7a92 100644 --- a/web/src/lib/components/faces-page/people-card.svelte +++ b/web/src/lib/components/faces-page/people-card.svelte @@ -64,9 +64,10 @@ {#if showVerticalDots}
From 913b3789cc6a47a7c7f987ef2660e5371e301790 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 12 Sep 2025 22:32:15 -0500 Subject: [PATCH 24/30] chore: simplify timeline switcher toggle (#21864) chore: timeline switcher option simplify --- mobile/lib/pages/common/settings.page.dart | 38 +-- .../widgets/settings/advanced_settings.dart | 16 +- .../settings/beta_timeline_list_tile.dart | 247 ++++-------------- 3 files changed, 66 insertions(+), 235 deletions(-) diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index 014136ddb4..b23c420971 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -12,7 +12,6 @@ import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewe import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart'; -import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart'; import 'package:immich_mobile/widgets/settings/language_settings.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart'; import 'package:immich_mobile/widgets/settings/notification_setting.dart'; @@ -20,7 +19,6 @@ import 'package:immich_mobile/widgets/settings/preference_settings/preference_se import 'package:immich_mobile/widgets/settings/settings_card.dart'; enum SettingSection { - beta('sync_status', Icons.sync_outlined, "sync_status_subtitle"), advanced('advanced', Icons.build_outlined, "advanced_settings_tile_subtitle"), assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"), backup('backup', Icons.cloud_upload_outlined, "backup_settings_subtitle"), @@ -28,14 +26,14 @@ enum SettingSection { networking('networking_settings', Icons.wifi, "networking_subtitle"), notifications('notifications', Icons.notifications_none_rounded, "setting_notifications_subtitle"), preferences('preferences_settings_title', Icons.interests_outlined, "preferences_settings_subtitle"), - timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined, "asset_list_settings_subtitle"); + timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined, "asset_list_settings_subtitle"), + beta('sync_status', Icons.sync_outlined, "sync_status_subtitle"); final String title; final String subtitle; final IconData icon; Widget get widget => switch (this) { - SettingSection.beta => const _BetaLandscapeToggle(), SettingSection.advanced => const AdvancedSettings(), SettingSection.assetViewer => const AssetViewerSettings(), SettingSection.backup => @@ -45,6 +43,7 @@ enum SettingSection { SettingSection.notifications => const NotificationSetting(), SettingSection.preferences => const PreferenceSetting(), SettingSection.timeline => const AssetListSettings(), + SettingSection.beta => const SyncStatusAndActions(), }; const SettingSection(this.title, this.icon, this.subtitle); @@ -59,7 +58,7 @@ class SettingsPage extends StatelessWidget { context.locale; return Scaffold( appBar: AppBar(centerTitle: false, title: const Text('settings').tr()), - body: context.isMobile ? const _MobileLayout() : const _TabletLayout(), + body: context.isMobile ? const SafeArea(child: _MobileLayout()) : const SafeArea(child: _TabletLayout()), ); } } @@ -72,7 +71,6 @@ class _MobileLayout extends StatelessWidget { .expand( (setting) => setting == SettingSection.beta ? [ - const BetaTimelineListTile(), if (Store.isBetaTimelineEnabled) SettingsCard( icon: Icons.sync_outlined, @@ -93,7 +91,7 @@ class _MobileLayout extends StatelessWidget { .toList(); return ListView( physics: const ClampingScrollPhysics(), - padding: const EdgeInsets.only(top: 10.0, bottom: 56), + padding: const EdgeInsets.only(top: 10.0, bottom: 16), children: [...settings], ); } @@ -134,21 +132,6 @@ class _TabletLayout extends HookWidget { } } -class _BetaLandscapeToggle extends HookWidget { - const _BetaLandscapeToggle(); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(height: 100, child: BetaTimelineListTile()), - if (Store.isBetaTimelineEnabled) const Expanded(child: SyncStatusAndActions()), - ], - ); - } -} - @RoutePage() class SettingsSubPage extends StatelessWidget { const SettingsSubPage(this.section, {super.key}); @@ -158,9 +141,14 @@ class SettingsSubPage extends StatelessWidget { @override Widget build(BuildContext context) { context.locale; - return Scaffold( - appBar: AppBar(centerTitle: false, title: Text(section.title).tr()), - body: section.widget, + return SafeArea( + bottom: true, + top: false, + right: true, + child: Scaffold( + appBar: AppBar(centerTitle: false, title: Text(section.title).tr()), + body: section.widget, + ), ); } } diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index cd2fa93b85..7a107b47d8 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -14,6 +14,7 @@ import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart'; +import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart'; import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart'; import 'package:immich_mobile/widgets/settings/local_storage_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; @@ -91,7 +92,7 @@ class AdvancedSettings extends HookConsumerWidget { title: "advanced_settings_prefer_remote_title".tr(), subtitle: "advanced_settings_prefer_remote_subtitle".tr(), ), - const LocalStorageSettings(), + if (!Store.isBetaTimelineEnabled) const LocalStorageSettings(), SettingsSwitchListTile( enabled: !isLoggedIn, valueNotifier: allowSelfSignedSSLCert, @@ -101,12 +102,13 @@ class AdvancedSettings extends HookConsumerWidget { ), const CustomeProxyHeaderSettings(), SslClientCertSettings(isLoggedIn: ref.read(currentUserProvider) != null), - SettingsSwitchListTile( - valueNotifier: useAlternatePMFilter, - title: "advanced_settings_enable_alternate_media_filter_title".tr(), - subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(), - ), - // TODO: Remove this check when beta timeline goes stable + if (!Store.isBetaTimelineEnabled) + SettingsSwitchListTile( + valueNotifier: useAlternatePMFilter, + title: "advanced_settings_enable_alternate_media_filter_title".tr(), + subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(), + ), + const BetaTimelineListTile(), if (Store.isBetaTimelineEnabled) SettingsSwitchListTile( valueNotifier: readonlyModeEnabled, diff --git a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart index a498145275..8786fe2c5f 100644 --- a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart +++ b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart @@ -1,5 +1,3 @@ -import 'dart:math' as math; - import 'package:auto_route/auto_route.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -12,50 +10,11 @@ import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -class BetaTimelineListTile extends ConsumerStatefulWidget { +class BetaTimelineListTile extends ConsumerWidget { const BetaTimelineListTile({super.key}); @override - ConsumerState createState() => _BetaTimelineListTileState(); -} - -class _BetaTimelineListTileState extends ConsumerState with SingleTickerProviderStateMixin { - late AnimationController _animationController; - late Animation _rotationAnimation; - late Animation _pulseAnimation; - late Animation _gradientAnimation; - - @override - void initState() { - super.initState(); - _animationController = AnimationController(duration: const Duration(seconds: 3), vsync: this); - - _rotationAnimation = Tween( - begin: 0, - end: 2 * math.pi, - ).animate(CurvedAnimation(parent: _animationController, curve: Curves.linear)); - - _pulseAnimation = Tween( - begin: 1, - end: 1.1, - ).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut)); - - _gradientAnimation = Tween( - begin: 0, - end: 1, - ).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut)); - - _animationController.repeat(reverse: true); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final betaTimelineValue = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.betaTimeline); final serverInfo = ref.watch(serverInfoProvider); final auth = ref.watch(authProvider); @@ -64,168 +23,50 @@ class _BetaTimelineListTileState extends ConsumerState wit return const SizedBox.shrink(); } - return AnimatedBuilder( - animation: _animationController, - builder: (context, child) { - void onSwitchChanged(bool value) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: value ? const Text("Enable Beta Timeline") : const Text("Disable Beta Timeline"), - content: value - ? const Text("Are you sure you want to enable the beta timeline?") - : const Text("Are you sure you want to disable the beta timeline?"), - actions: [ - TextButton( - onPressed: () { - context.pop(); - }, - child: Text( - "cancel".t(context: context), - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: context.colorScheme.outline), - ), - ), - ElevatedButton( - onPressed: () async { - Navigator.of(context).pop(); - context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]); - }, - child: Text("ok".t(context: context)), - ), - ], - ); - }, - ); - } - - final gradientColors = [ - Color.lerp( - context.primaryColor.withValues(alpha: 0.5), - context.primaryColor.withValues(alpha: 0.3), - _gradientAnimation.value, - )!, - Color.lerp( - context.logoPink.withValues(alpha: 0.2), - context.logoPink.withValues(alpha: 0.4), - _gradientAnimation.value, - )!, - Color.lerp( - context.logoRed.withValues(alpha: 0.3), - context.logoRed.withValues(alpha: 0.5), - _gradientAnimation.value, - )!, - ]; - - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(12)), - gradient: LinearGradient( - colors: gradientColors, - stops: const [0.0, 0.5, 1.0], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - transform: GradientRotation(_rotationAnimation.value * 0.5), - ), - boxShadow: [ - BoxShadow(color: context.primaryColor.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, 2)), - ], - ), - child: Container( - margin: const EdgeInsets.all(2), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10.5)), - color: context.scaffoldBackgroundColor, - ), - child: Material( - borderRadius: const BorderRadius.all(Radius.circular(10.5)), - child: InkWell( - borderRadius: const BorderRadius.all(Radius.circular(10.5)), - onTap: () => onSwitchChanged(!betaTimelineValue), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - child: Row( - children: [ - Transform.scale( - scale: _pulseAnimation.value, - child: Transform.rotate( - angle: _rotationAnimation.value * 0.02, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient( - colors: [ - context.primaryColor.withValues(alpha: 0.2), - context.primaryColor.withValues(alpha: 0.1), - ], - ), - ), - child: Icon(Icons.auto_awesome, color: context.primaryColor, size: 20), - ), - ), - ), - const SizedBox(width: 28), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "advanced_settings_beta_timeline_title".t(context: context), - style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), - ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(8)), - gradient: LinearGradient( - colors: [ - context.primaryColor.withValues(alpha: 0.8), - context.primaryColor.withValues(alpha: 0.6), - ], - ), - ), - child: Text( - 'NEW', - style: context.textTheme.labelSmall?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 10, - height: 1.2, - ), - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - "advanced_settings_beta_timeline_subtitle".t(context: context), - style: context.textTheme.labelLarge?.copyWith( - color: context.textTheme.labelLarge?.color?.withValues(alpha: 0.9), - ), - maxLines: 2, - ), - ], - ), - ), - Switch.adaptive( - value: betaTimelineValue, - onChanged: onSwitchChanged, - activeColor: context.primaryColor, - ), - ], - ), + void onSwitchChanged(bool value) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: value ? const Text("Enable Beta Timeline") : const Text("Disable Beta Timeline"), + content: value + ? const Text("Are you sure you want to enable the beta timeline?") + : const Text("Are you sure you want to disable the beta timeline?"), + actions: [ + TextButton( + onPressed: () { + context.pop(); + }, + child: Text( + "cancel".t(context: context), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: context.colorScheme.outline), ), ), - ), - ), - ); - }, + ElevatedButton( + onPressed: () async { + Navigator.of(context).pop(); + context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]); + }, + child: Text("ok".t(context: context)), + ), + ], + ); + }, + ); + } + + return Padding( + padding: const EdgeInsets.only(left: 4.0), + child: ListTile( + title: Text("advanced_settings_beta_timeline_title".t(context: context)), + subtitle: Text("advanced_settings_beta_timeline_subtitle".t(context: context)), + trailing: Switch.adaptive( + value: betaTimelineValue, + onChanged: onSwitchChanged, + activeColor: context.primaryColor, + ), + onTap: () => onSwitchChanged(!betaTimelineValue), + ), ); } } From cdc26f2c7bfcc2097ee78b852f816ed53a6e2fbe Mon Sep 17 00:00:00 2001 From: Stewart Rand Date: Sat, 13 Sep 2025 00:32:50 -0300 Subject: [PATCH 25/30] fix: z-index of top bar on show/hide people view (#21847) Fix z-index of top bar on show/hide people view --- .../lib/components/faces-page/manage-people-visibility.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/faces-page/manage-people-visibility.svelte b/web/src/lib/components/faces-page/manage-people-visibility.svelte index b8154bc36e..5d03b7e558 100644 --- a/web/src/lib/components/faces-page/manage-people-visibility.svelte +++ b/web/src/lib/components/faces-page/manage-people-visibility.svelte @@ -110,7 +110,7 @@
Date: Sat, 13 Sep 2025 00:42:25 -0300 Subject: [PATCH 26/30] fix: keep adequate space around page title (#21881) Keep space around page title --- web/src/lib/components/layouts/user-page-layout.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/layouts/user-page-layout.svelte b/web/src/lib/components/layouts/user-page-layout.svelte index 19381e07c4..70e17792e0 100644 --- a/web/src/lib/components/layouts/user-page-layout.svelte +++ b/web/src/lib/components/layouts/user-page-layout.svelte @@ -68,7 +68,7 @@
{#if title} -
{title}
+
{title}
{/if} {#if description}

{description}

From 1823a28e59882a7e104d49e619b55a9e42285387 Mon Sep 17 00:00:00 2001 From: Stewart Rand Date: Sat, 13 Sep 2025 00:42:42 -0300 Subject: [PATCH 27/30] chore: improve date text slide-in transition (#21879) * Make date text slide-in transition smooth * fix: lint --------- Co-authored-by: Alex Tran --- .../lib/components/photos-page/asset-date-group.svelte | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index 9cf50d1354..c07cbb4635 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -16,7 +16,7 @@ import { fromTimelinePlainDate, getDateLocaleString } from '$lib/utils/timeline-util'; import type { Snippet } from 'svelte'; import { flip } from 'svelte/animate'; - import { fly, scale } from 'svelte/transition'; + import { scale } from 'svelte/transition'; let { isUploading } = uploadAssetsStore; @@ -169,10 +169,11 @@ class="flex pt-7 pb-5 max-md:pt-5 max-md:pb-3 h-6 place-items-center text-xs font-medium text-immich-fg dark:text-immich-dark-fg md:text-sm" style:width={dayGroup.width + 'px'} > - {#if !singleSelect && ((hoveredDayGroup === dayGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dayGroup.groupTitle))} + {#if !singleSelect}
handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))} onkeydown={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))} > From 4059638151708e3ce59ed0ef52cac07ea88635d8 Mon Sep 17 00:00:00 2001 From: Stewart Rand Date: Sat, 13 Sep 2025 00:43:22 -0300 Subject: [PATCH 28/30] fix: context menu jank (#21844) * Fix issue with context menu jank by only applying overflow styling when transition is complete * Remove comment Co-authored-by: Alex --------- Co-authored-by: Alex --- .../context-menu/context-menu.svelte | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/web/src/lib/components/shared-components/context-menu/context-menu.svelte b/web/src/lib/components/shared-components/context-menu/context-menu.svelte index e3e7c45c89..aad037148c 100644 --- a/web/src/lib/components/shared-components/context-menu/context-menu.svelte +++ b/web/src/lib/components/shared-components/context-menu/context-menu.svelte @@ -40,6 +40,8 @@ // of zero when starting the 'slide' animation. let height: number = $state(0); + let isTransitioned = $state(false); + $effect(() => { if (menuElement) { let layoutDirection = direction; @@ -64,6 +66,12 @@ style:top="{top}px" transition:slide={{ duration: 250, easing: quintOut }} use:clickOutside={{ onOutclick: onClose }} + onintroend={() => { + isTransitioned = true; + }} + onoutrostart={() => { + isTransitioned = false; + }} >