From 3e12b108667994394a7efb569243286d788049dd Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Mon, 16 Sep 2024 18:05:34 +0200 Subject: [PATCH 01/57] fix: remove bad examples of 'from' domain for emails (#12728) * fix: use example.com domain for from_address_description * fix: remove unnecessary screenshot from docs --- .../docs/administration/email-notification.mdx | 8 +++----- .../docs/administration/img/email-settings.png | Bin 222808 -> 0 bytes web/src/lib/i18n/ar.json | 2 +- web/src/lib/i18n/bg.json | 2 +- web/src/lib/i18n/ca.json | 2 +- web/src/lib/i18n/cs.json | 2 +- web/src/lib/i18n/da.json | 2 +- web/src/lib/i18n/de.json | 2 +- web/src/lib/i18n/en.json | 2 +- web/src/lib/i18n/es.json | 2 +- web/src/lib/i18n/et.json | 2 +- web/src/lib/i18n/fa.json | 2 +- web/src/lib/i18n/fi.json | 2 +- web/src/lib/i18n/he.json | 2 +- web/src/lib/i18n/hi.json | 2 +- web/src/lib/i18n/hr.json | 2 +- web/src/lib/i18n/hu.json | 2 +- web/src/lib/i18n/id.json | 2 +- web/src/lib/i18n/it.json | 2 +- web/src/lib/i18n/ja.json | 2 +- web/src/lib/i18n/ko.json | 2 +- web/src/lib/i18n/nb_NO.json | 2 +- web/src/lib/i18n/nl.json | 2 +- web/src/lib/i18n/pl.json | 2 +- web/src/lib/i18n/pt.json | 2 +- web/src/lib/i18n/pt_BR.json | 2 +- web/src/lib/i18n/ro.json | 2 +- web/src/lib/i18n/ru.json | 2 +- web/src/lib/i18n/sr_Cyrl.json | 2 +- web/src/lib/i18n/sr_Latn.json | 2 +- web/src/lib/i18n/sv.json | 2 +- web/src/lib/i18n/ta.json | 2 +- web/src/lib/i18n/th.json | 2 +- web/src/lib/i18n/tr.json | 2 +- web/src/lib/i18n/uk.json | 2 +- web/src/lib/i18n/vi.json | 2 +- web/src/lib/i18n/zh_Hant.json | 2 +- web/src/lib/i18n/zh_SIMPLIFIED.json | 2 +- 38 files changed, 39 insertions(+), 41 deletions(-) delete mode 100644 docs/docs/administration/img/email-settings.png diff --git a/docs/docs/administration/email-notification.mdx b/docs/docs/administration/email-notification.mdx index 4a2a0b5a837f7..93b1051053069 100644 --- a/docs/docs/administration/email-notification.mdx +++ b/docs/docs/administration/email-notification.mdx @@ -8,13 +8,11 @@ Immich supports the option to send notifications via Email for the following eve ## SMTP settings -You can access the settings panel from the web at `Administration -> Settings -> Notification settings` +You can access the settings panel from the web at `Administration -> Settings -> Notification settings`. -Under Email, enter the following details to connect with SMTP servers. +Under Email, enter the required details to connect with an SMTP server. -You can use the following [guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server. - - +You can use [this guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server. ## User's notifications settings diff --git a/docs/docs/administration/img/email-settings.png b/docs/docs/administration/img/email-settings.png deleted file mode 100644 index a0d71354267fba88f6194cadac1b5912d82fb493..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 222808 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%0DA8$(FY-(vXtEBy1H3YA~Ix#1WX?9 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\"", + "notification_email_from_address_description": "Електронна поща на изпращача, например: \"Immich Photo Server \"", "notification_email_host_description": "Хост на сървъра за електронна поща (например: smtp.immich.app)", "notification_email_ignore_certificate_errors": "Игорнорирайте сертификационни грешки", "notification_email_ignore_certificate_errors_description": "Игнорирай грешки свързани с валидация на TLS сертификат (не се препоръчва)", diff --git a/web/src/lib/i18n/ca.json b/web/src/lib/i18n/ca.json index a0fd6ff437f7f..ba33c9b15606b 100644 --- a/web/src/lib/i18n/ca.json +++ b/web/src/lib/i18n/ca.json @@ -147,7 +147,7 @@ "note_cannot_be_changed_later": "NOTA: Això és irreversible!", "note_unlimited_quota": "Nota: Intruduïu 0 per a quota il·limitada", "notification_email_from_address": "Des de l'adreça", - "notification_email_from_address_description": "Adreça de correu electrònic del remitent, per exemple: \"Immich Photo Server \"", + "notification_email_from_address_description": "Adreça de correu electrònic del remitent, per exemple: \"Immich Photo Server \"", "notification_email_host_description": "Amfitrió del servidor de correu electrònic (p.ex. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignora els errors de certificat", "notification_email_ignore_certificate_errors_description": "Ignora els errors de validació de certificat TLS (no recomanat)", diff --git a/web/src/lib/i18n/cs.json b/web/src/lib/i18n/cs.json index ec97fe01b21b9..e49d3700ee94f 100644 --- a/web/src/lib/i18n/cs.json +++ b/web/src/lib/i18n/cs.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "UPOZORNĚNÍ: Toto nelze později změnit!", "note_unlimited_quota": "Upozornění: Pro neomezenou kvótu zadejte 0", "notification_email_from_address": "Adresa Od", - "notification_email_from_address_description": "E-mailová adresa odesílatele, např.: \"Immich Photo Server \"", + "notification_email_from_address_description": "E-mailová adresa odesílatele, např.: \"Immich Photo Server \"", "notification_email_host_description": "Adresa e-mailového serveru (např. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorovat chyby certifikátů", "notification_email_ignore_certificate_errors_description": "Ignorovat chyby ověření certifikátu TLS (nedoporučuje se)", diff --git a/web/src/lib/i18n/da.json b/web/src/lib/i18n/da.json index eb9d99d074c10..ab1d57d48e347 100644 --- a/web/src/lib/i18n/da.json +++ b/web/src/lib/i18n/da.json @@ -148,7 +148,7 @@ "note_cannot_be_changed_later": "BEMÆRK: Dette kan ikke ændres senere!", "note_unlimited_quota": "Bemærk: Indsæt 0 for uendelig kvote", "notification_email_from_address": "Fra adressse", - "notification_email_from_address_description": "Afsenderemailadresse, for eksempel: \"Immich Billedserver \"", + "notification_email_from_address_description": "Afsenderemailadresse, for eksempel: \"Immich Billedserver \"", "notification_email_host_description": "Host af emailserver (fx smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorér certifikatfejl", "notification_email_ignore_certificate_errors_description": "Ignorér TLS-certifikatgodkendelsesfejl (ikke anbefalet)", diff --git a/web/src/lib/i18n/de.json b/web/src/lib/i18n/de.json index 77b420db00ebb..d519352862c2b 100644 --- a/web/src/lib/i18n/de.json +++ b/web/src/lib/i18n/de.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "HINWEIS: Dies kann später nicht mehr geändert werden!", "note_unlimited_quota": "Hinweis: 0 eingeben für unlimitiertes Kontingent", "notification_email_from_address": "Von", - "notification_email_from_address_description": "E-Mail-Adresse des Senders, zum Beispiel: \"Immich Photo Server \"", + "notification_email_from_address_description": "E-Mail-Adresse des Senders, zum Beispiel: \"Immich Photo Server \"", "notification_email_host_description": "Host des E-Mail-Servers (z.B. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignoriere Zertifikats-Fehler", "notification_email_ignore_certificate_errors_description": "TLS-Zertifikatsvalidierungsfehler ignorieren (nicht empfohlen)", diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index e27cc54d52156..f880dab34737a 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -150,7 +150,7 @@ "note_cannot_be_changed_later": "NOTE: This cannot be changed later!", "note_unlimited_quota": "Note: Enter 0 for unlimited quota", "notification_email_from_address": "From address", - "notification_email_from_address_description": "Sender email address, for example: \"Immich Photo Server \"", + "notification_email_from_address_description": "Sender email address, for example: \"Immich Photo Server \"", "notification_email_host_description": "Host of the email server (e.g. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignore certificate errors", "notification_email_ignore_certificate_errors_description": "Ignore TLS certificate validation errors (not recommended)", diff --git a/web/src/lib/i18n/es.json b/web/src/lib/i18n/es.json index f0c89ffb46adc..31c613dcbdb6e 100644 --- a/web/src/lib/i18n/es.json +++ b/web/src/lib/i18n/es.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "NOTA: No se puede cambiar posteriormente!", "note_unlimited_quota": "Nota: usa 0 para espacio sin límites", "notification_email_from_address": "Desde", - "notification_email_from_address_description": "Dirección de correo electrónico del remitente, por ejemplo: \"Immich Photo Server \"", + "notification_email_from_address_description": "Dirección de correo electrónico del remitente, por ejemplo: \"Immich Photo Server \"", "notification_email_host_description": "Host del servidor de correo electrónico (por ejemplo: smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorar errores de certificado", "notification_email_ignore_certificate_errors_description": "Ignorar los errores de validación del certificado TLS (no recomendado)", diff --git a/web/src/lib/i18n/et.json b/web/src/lib/i18n/et.json index 25cdba4cb41a3..49b60cd0529de 100644 --- a/web/src/lib/i18n/et.json +++ b/web/src/lib/i18n/et.json @@ -125,7 +125,7 @@ "migration_job_description": "Migreeri üksuste ja nägude pisipildid uusimale kaustastruktuurile", "note_cannot_be_changed_later": "MÄRKUS: Seda ei saa hiljem muuta!", "notification_email_from_address": "Saatja aadress", - "notification_email_from_address_description": "Saatja e-posti aadress, näiteks: \"Immich Photo Server \"", + "notification_email_from_address_description": "Saatja e-posti aadress, näiteks: \"Immich Photo Server \"", "notification_email_host_description": "E-posti serveri host (nt. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignoreeri sertifikaadi vigu", "notification_email_ignore_certificate_errors_description": "Ignoreeri TLS sertifikaadi valideerimise vigu (mittesoovituslik)", diff --git a/web/src/lib/i18n/fa.json b/web/src/lib/i18n/fa.json index 0eb6b7b014f19..6cd77c157fbcd 100644 --- a/web/src/lib/i18n/fa.json +++ b/web/src/lib/i18n/fa.json @@ -144,7 +144,7 @@ "note_cannot_be_changed_later": "توجه: این را نمی توان بعداً تغییر داد!", "note_unlimited_quota": "توجه: برای سهمیه نامحدود، عدد 0 را وارد کنید", "notification_email_from_address": "آدرس فرستنده", - "notification_email_from_address_description": "آدرس ایمیل فرستنده، به عنوان مثال:\"Immich سرور عکس \"", + "notification_email_from_address_description": "آدرس ایمیل فرستنده، به عنوان مثال:\"Immich سرور عکس \"", "notification_email_host_description": "میزبان سرور ایمیل (مثلاً smtp.immich.app)", "notification_email_ignore_certificate_errors": "خطاهای گواهی را نادیده بگیر", "notification_email_ignore_certificate_errors_description": "خطاهای اعتبارسنجی گواهی TLS را نادیده بگیر (توصیه نمی‌شود)", diff --git a/web/src/lib/i18n/fi.json b/web/src/lib/i18n/fi.json index da9a71379cfe5..6d951b93f9e89 100644 --- a/web/src/lib/i18n/fi.json +++ b/web/src/lib/i18n/fi.json @@ -148,7 +148,7 @@ "note_cannot_be_changed_later": "Huom: Tätä ei voi enää myöhemmin vaihtaa!", "note_unlimited_quota": "Huom: Määritä 0 rajoittamattomaksi kiintiöksi", "notification_email_from_address": "Lähettäjän osoite", - "notification_email_from_address_description": "Lähettäjän sähköpostiosoite. Esimerkiksi \"Immich Kuvapalvelin \"", + "notification_email_from_address_description": "Lähettäjän sähköpostiosoite. Esimerkiksi \"Immich Kuvapalvelin \"", "notification_email_host_description": "Sähköpostipalvelin (esim. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Älä huomioi sertifikaattivirheitä", "notification_email_ignore_certificate_errors_description": "Älä huomioi TLS sertifikaattien validointivirheitä (ei suositeltu)", diff --git a/web/src/lib/i18n/he.json b/web/src/lib/i18n/he.json index aefa831897de7..e2b836256801d 100644 --- a/web/src/lib/i18n/he.json +++ b/web/src/lib/i18n/he.json @@ -148,7 +148,7 @@ "note_cannot_be_changed_later": "הערה: אי אפשר לשנות זאת מאוחר יותר!", "note_unlimited_quota": "הערה: הזן 0 עבור מכסת אחסון בלתי מוגבלת", "notification_email_from_address": "מכתובת", - "notification_email_from_address_description": "כתובת דוא\"ל של השולח, לדוגמה: \"Immich שרת תמונות \"", + "notification_email_from_address_description": "כתובת דוא\"ל של השולח, לדוגמה: \"Immich שרת תמונות \"", "notification_email_host_description": "מארח שרת הדוא\"ל (למשל smtp.immich.app)", "notification_email_ignore_certificate_errors": "התעלם משגיאות תעודה", "notification_email_ignore_certificate_errors_description": "התעלם משגיאות אימות תעודת TLS (לא מומלץ)", diff --git a/web/src/lib/i18n/hi.json b/web/src/lib/i18n/hi.json index 2f2aabfb7eccb..d84c612224a4d 100644 --- a/web/src/lib/i18n/hi.json +++ b/web/src/lib/i18n/hi.json @@ -147,7 +147,7 @@ "note_cannot_be_changed_later": "नोट: इसे बाद में बदला नहीं जा सकता!", "note_unlimited_quota": "नोट: असीमित कोटा के लिए 0 दर्ज करें", "notification_email_from_address": "इस पते से", - "notification_email_from_address_description": "प्रेषक का ईमेल पता, उदाहरण के लिए: \"इमिच फोटो सर्वर \"", + "notification_email_from_address_description": "प्रेषक का ईमेल पता, उदाहरण के लिए: \"इमिच फोटो सर्वर \"", "notification_email_host_description": "ईमेल सर्वर का होस्ट (उदा. smtp.immitch.app)", "notification_email_ignore_certificate_errors": "प्रमाणपत्र त्रुटियों पर ध्यान न दें", "notification_email_ignore_certificate_errors_description": "टीएलएस प्रमाणपत्र सत्यापन त्रुटियों पर ध्यान न दें (अनुशंसित नहीं)", diff --git a/web/src/lib/i18n/hr.json b/web/src/lib/i18n/hr.json index 291abaa6908d5..16d08bbfca211 100644 --- a/web/src/lib/i18n/hr.json +++ b/web/src/lib/i18n/hr.json @@ -146,7 +146,7 @@ "note_cannot_be_changed_later": "NAPOMENA: Ovo se ne može promijeniti kasnije!", "note_unlimited_quota": "Napomena: Unesite 0 za neograničenu kvotu", "notification_email_from_address": "Od adrese", - "notification_email_from_address_description": "E-mail adresa pošiljatelja, na primjer: \"Immich Photo Server \"", + "notification_email_from_address_description": "E-mail adresa pošiljatelja, na primjer: \"Immich Photo Server \"", "notification_email_host_description": "Poslužitelja e-pošte (npr. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignoriraj pogreške certifikata", "notification_email_ignore_certificate_errors_description": "Ignoriraj pogreške provjere valjanosti TLS certifikata (nije preporučeno)", diff --git a/web/src/lib/i18n/hu.json b/web/src/lib/i18n/hu.json index 753839c384b19..249b663a773ef 100644 --- a/web/src/lib/i18n/hu.json +++ b/web/src/lib/i18n/hu.json @@ -149,7 +149,7 @@ "note_cannot_be_changed_later": "FIGYELEM: ezt később nem lehet megváltoztatni!", "note_unlimited_quota": "Megjegyzés: 0 - korlátlan kvóta", "notification_email_from_address": "Feladó cím", - "notification_email_from_address_description": "Küldő email címe, például: \"Immich Fotószerver \"", + "notification_email_from_address_description": "Küldő email címe, például: \"Immich Fotószerver \"", "notification_email_host_description": "Email szerver kiszolgálója (pl. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Tanúsítvány hibák figyelmen kívül hagyása", "notification_email_ignore_certificate_errors_description": "TLS tanúsítvány érvényességi hibák figyelmen kívül hagyása (nem ajánlott)", diff --git a/web/src/lib/i18n/id.json b/web/src/lib/i18n/id.json index ea5ad94b2719e..1321bd358bd1e 100644 --- a/web/src/lib/i18n/id.json +++ b/web/src/lib/i18n/id.json @@ -150,7 +150,7 @@ "note_cannot_be_changed_later": "CATATAN: Ini tidak akan dapat diubah lagi!", "note_unlimited_quota": "Catatan: Masukkan 0 untuk kuota tidak terbatas", "notification_email_from_address": "Dari alamat", - "notification_email_from_address_description": "Alamat surel pengirim, misalnya: \"Server Foto Immich \"", + "notification_email_from_address_description": "Alamat surel pengirim, misalnya: \"Server Foto Immich \"", "notification_email_host_description": "Hos server surel (mis. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Abaikan eror sertifikat", "notification_email_ignore_certificate_errors_description": "Abaikan eror validasi sertifikat TLS (tidak disarankan)", diff --git a/web/src/lib/i18n/it.json b/web/src/lib/i18n/it.json index 6782b8fbb9350..cbe3651927edb 100644 --- a/web/src/lib/i18n/it.json +++ b/web/src/lib/i18n/it.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "NOTA: Non potrà essere modificato in futuro!", "note_unlimited_quota": "Nota: Inserisci 0 per una quota illimitata", "notification_email_from_address": "Indirizzo mittente", - "notification_email_from_address_description": "Indirizzo email mittente, ad esempio: \"Server Foto Immich \"", + "notification_email_from_address_description": "Indirizzo email mittente, ad esempio: \"Server Foto Immich \"", "notification_email_host_description": "Host del server email (es. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignora errori di certificato", "notification_email_ignore_certificate_errors_description": "Ignora errori di validazione del certificato TLS (sconsigliato)", diff --git a/web/src/lib/i18n/ja.json b/web/src/lib/i18n/ja.json index 017d52fb30857..53dbde9a9e5f8 100644 --- a/web/src/lib/i18n/ja.json +++ b/web/src/lib/i18n/ja.json @@ -148,7 +148,7 @@ "note_cannot_be_changed_later": "注意: 後から変更できません!", "note_unlimited_quota": "注意: 無制限にする場合は0を入力してください", "notification_email_from_address": "送信メールアドレス", - "notification_email_from_address_description": "送信メールアドレスを設定します(例: \"Immich Photo Server \" )", + "notification_email_from_address_description": "送信メールアドレスを設定します(例: \"Immich Photo Server \" )", "notification_email_host_description": "送信メールサーバーを設定します(例:smtp.immich.app)", "notification_email_ignore_certificate_errors": "証明書エラーを無視", "notification_email_ignore_certificate_errors_description": "TLS証明書の検証エラーを無視します(非推奨)", diff --git a/web/src/lib/i18n/ko.json b/web/src/lib/i18n/ko.json index cd3db1310267c..df46923b5ef73 100644 --- a/web/src/lib/i18n/ko.json +++ b/web/src/lib/i18n/ko.json @@ -148,7 +148,7 @@ "note_cannot_be_changed_later": "주의: 추후 변경할 수 없습니다!", "note_unlimited_quota": "참고: 할당량을 설정하지 않으려면 0을 입력하세요.", "notification_email_from_address": "보낸 사람 이메일", - "notification_email_from_address_description": "보낸 사람의 이메일 주소, 예: \"Immich Photo Server \"", + "notification_email_from_address_description": "보낸 사람의 이메일 주소, 예: \"Immich Photo Server \"", "notification_email_host_description": "이메일 서버의 호스트 (예: smtp.immich.app)", "notification_email_ignore_certificate_errors": "인증서 오류 무시", "notification_email_ignore_certificate_errors_description": "TLS 인증서 유효성 검사 오류 무시 (권장되지 않음)", diff --git a/web/src/lib/i18n/nb_NO.json b/web/src/lib/i18n/nb_NO.json index df56d27a237bf..1c0e2f5eef116 100644 --- a/web/src/lib/i18n/nb_NO.json +++ b/web/src/lib/i18n/nb_NO.json @@ -146,7 +146,7 @@ "note_cannot_be_changed_later": "MERK: Dette kan ikke endres senere!", "note_unlimited_quota": "Merk: Skriv inn 0 for ubegrenset kvote", "notification_email_from_address": "Fra adresse", - "notification_email_from_address_description": "Avsenderens e-postadresse, for eksempel: \"Immich Photo Server \"", + "notification_email_from_address_description": "Avsenderens e-postadresse, for eksempel: \"Immich Photo Server \"", "notification_email_host_description": "Verten til e-posts serveren (f.eks. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorer sertifikatfeil", "notification_email_ignore_certificate_errors_description": "Ignorer valideringsfeil for TLS-sertifikat (ikke anbefalt)", diff --git a/web/src/lib/i18n/nl.json b/web/src/lib/i18n/nl.json index d6b3373152c63..786a1627febbb 100644 --- a/web/src/lib/i18n/nl.json +++ b/web/src/lib/i18n/nl.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "LET OP: Dit kan later niet meer worden gewijzigd!", "note_unlimited_quota": "Opmerking: voer 0 in voor onbeperkt", "notification_email_from_address": "Adres afzender", - "notification_email_from_address_description": "E-mailadres van de afzender, bijvoorbeeld: \"Immich Foto Server \"", + "notification_email_from_address_description": "E-mailadres van de afzender, bijvoorbeeld: \"Immich Foto Server \"", "notification_email_host_description": "Host van de e-mailserver (bijv. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Negeer certificaatfouten", "notification_email_ignore_certificate_errors_description": "Negeer TLS certificaat validatiefouten (niet aanbevolen)", diff --git a/web/src/lib/i18n/pl.json b/web/src/lib/i18n/pl.json index ff06e41233100..089a6550cd28f 100644 --- a/web/src/lib/i18n/pl.json +++ b/web/src/lib/i18n/pl.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "UWAŻAJ: Nie można tego później zmienić!", "note_unlimited_quota": "Wpisz by wyłączyć limit", "notification_email_from_address": "Z adresu", - "notification_email_from_address_description": "Adres e-mail nadawcy, na przykład: „Immich Photo Server ”", + "notification_email_from_address_description": "Adres e-mail nadawcy, na przykład: „Immich Photo Server ”", "notification_email_host_description": "Host serwera e-mail (np. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignoruj niepoprawny certyfikat", "notification_email_ignore_certificate_errors_description": "Ignoruj błąd walidacji certyfikatu TLS (nie zalecane)", diff --git a/web/src/lib/i18n/pt.json b/web/src/lib/i18n/pt.json index 24c13ef03d58d..ebe1e85729148 100644 --- a/web/src/lib/i18n/pt.json +++ b/web/src/lib/i18n/pt.json @@ -149,7 +149,7 @@ "note_cannot_be_changed_later": "NOTA: Isto não pode ser alterado posteriormente!", "note_unlimited_quota": "Observação: insira 0 para cota ilimitada", "notification_email_from_address": "A partir do endereço", - "notification_email_from_address_description": "Endereço de e-mail do remetente, por exemplo: \"Immich Photo Server \"", + "notification_email_from_address_description": "Endereço de e-mail do remetente, por exemplo: \"Immich Photo Server \"", "notification_email_host_description": "Host do servidor de e-mail (por exemplo, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorar erros de certificado", "notification_email_ignore_certificate_errors_description": "Ignorar erros de validação de certificado TLS (não recomendado)", diff --git a/web/src/lib/i18n/pt_BR.json b/web/src/lib/i18n/pt_BR.json index 1e0de69aeda59..05f7bee7b0c9d 100644 --- a/web/src/lib/i18n/pt_BR.json +++ b/web/src/lib/i18n/pt_BR.json @@ -148,7 +148,7 @@ "note_cannot_be_changed_later": "NOTA: Isto não pode ser alterado posteriormente!", "note_unlimited_quota": "Observação: insira 0 para cota ilimitada", "notification_email_from_address": "A partir do endereço", - "notification_email_from_address_description": "Endereço de e-mail do remetente, por exemplo: \"Immich Photo Server \"", + "notification_email_from_address_description": "Endereço de e-mail do remetente, por exemplo: \"Immich Photo Server \"", "notification_email_host_description": "Host do servidor de e-mail (por exemplo, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorar erros de certificado", "notification_email_ignore_certificate_errors_description": "Ignorar erros de validação de certificado TLS (não recomendado)", diff --git a/web/src/lib/i18n/ro.json b/web/src/lib/i18n/ro.json index 534794b6d312c..29acdd03ce225 100644 --- a/web/src/lib/i18n/ro.json +++ b/web/src/lib/i18n/ro.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "NOTĂ: Nu se va mai putea modifica ulterior!", "note_unlimited_quota": "Notă: Introduceți 0 pentru cotă nelimitată", "notification_email_from_address": "De la adresa", - "notification_email_from_address_description": "Adresa expeditorului, spre exemplu: „Immich Photo Server ”", + "notification_email_from_address_description": "Adresa expeditorului, spre exemplu: „Immich Photo Server ”", "notification_email_host_description": "Adresa serverului de email (e.g. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ingnoră erorile de certificat", "notification_email_ignore_certificate_errors_description": "Ignoră erorile de validare a certificatului TLS (nerecomandat)", diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index 660f7bc84c952..bd725a11cfa34 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "ПРИМЕЧАНИЕ: Это невозможно изменить позже!", "note_unlimited_quota": "Примечание: Введите 0 для неограниченной квоты или оставьте пустым", "notification_email_from_address": "Адрес отправителя", - "notification_email_from_address_description": "Адрес электронной почты отправителя, например: \"Immich Photo Server \"", + "notification_email_from_address_description": "Адрес электронной почты отправителя, например: \"Immich Photo Server \"", "notification_email_host_description": "Доменное имя почтового сервера (например, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Игнорировать ошибки сертификата", "notification_email_ignore_certificate_errors_description": "Игнорировать ошибки проверки сертификата TLS (не рекомендуется)", diff --git a/web/src/lib/i18n/sr_Cyrl.json b/web/src/lib/i18n/sr_Cyrl.json index 6618aeab1dbc3..7bcd1e3dd8634 100644 --- a/web/src/lib/i18n/sr_Cyrl.json +++ b/web/src/lib/i18n/sr_Cyrl.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "НАПОМЕНА: Ovo se kasnije ne može promeniti!", "note_unlimited_quota": "Напомена: Unesite 0 za neograničenu kvotu", "notification_email_from_address": "Са адресе", - "notification_email_from_address_description": "Адреса е-поште пошиљаоца, на пример: \"Immich foto server \"", + "notification_email_from_address_description": "Адреса е-поште пошиљаоца, на пример: \"Immich foto server \"", "notification_email_host_description": "Хост сервера е-поште (нпр. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Занемарите грешке сертификата", "notification_email_ignore_certificate_errors_description": "Игноришите грешке у валидацији ТЛС сертификата (не препоручује се)", diff --git a/web/src/lib/i18n/sr_Latn.json b/web/src/lib/i18n/sr_Latn.json index ea40525a81d6a..beb2009b4d4a8 100644 --- a/web/src/lib/i18n/sr_Latn.json +++ b/web/src/lib/i18n/sr_Latn.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "NAPOMENA: Ovo se kasnije ne može promeniti!", "note_unlimited_quota": "Napomena: Unesite 0 za neograničenu kvotu", "notification_email_from_address": "Sa adrese", - "notification_email_from_address_description": "Adresa e-pošte pošiljaoca, na primer: \"Immich foto server \"", + "notification_email_from_address_description": "Adresa e-pošte pošiljaoca, na primer: \"Immich foto server \"", "notification_email_host_description": "Host servera e-pošte (npr. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Zanemarite greške sertifikata", "notification_email_ignore_certificate_errors_description": "Ignorišite greške u validaciji TLS sertifikata (ne preporučuje se)", diff --git a/web/src/lib/i18n/sv.json b/web/src/lib/i18n/sv.json index cebb74377e4f2..b00d521b20fe6 100644 --- a/web/src/lib/i18n/sv.json +++ b/web/src/lib/i18n/sv.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "OBS: Detta kan inte ändras i efterhand!", "note_unlimited_quota": "OBS: Skriv 0 för obegränsad kvota", "notification_email_from_address": "Från adress", - "notification_email_from_address_description": "Avsändarens epost, t.ex.: \"Immich Fotoserver \"", + "notification_email_from_address_description": "Avsändarens epost, t.ex.: \"Immich Fotoserver \"", "notification_email_host_description": "Värd för epostservern (t.ex. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorera certifikatfel", "notification_email_ignore_certificate_errors_description": "Ignorera valideringsfel för TLS-certifikat (rekommenderas ej)", diff --git a/web/src/lib/i18n/ta.json b/web/src/lib/i18n/ta.json index ec3f27124bdc2..bb7e888b76133 100644 --- a/web/src/lib/i18n/ta.json +++ b/web/src/lib/i18n/ta.json @@ -143,7 +143,7 @@ "note_cannot_be_changed_later": "குறிப்பு: இதை பின்னர் மாற்ற முடியாது!", "note_unlimited_quota": "குறிப்பு: வரம்பற்ற ஒதுக்கீட்டிற்கு 0 ஐ உள்ளிடவும்", "notification_email_from_address": "முகவரியிலிருந்து", - "notification_email_from_address_description": "அனுப்புநரின் மின்னஞ்சல் முகவரி, எடுத்துக்காட்டாக: \"இம்மிச் புகைப்பட சேவையகம் \"", + "notification_email_from_address_description": "அனுப்புநரின் மின்னஞ்சல் முகவரி, எடுத்துக்காட்டாக: \"இம்மிச் புகைப்பட சேவையகம் \"", "notification_email_host_description": "மின்னஞ்சல் சேவையகத்தின் ஹோஸ்ட் (எடுத்துக்காட்டாக: smtp.immich.app)", "notification_email_ignore_certificate_errors": "சான்றிதழ் பிழைகளை புறக்கணிக்கவும்", "notification_email_ignore_certificate_errors_description": "TLS சான்றிதழ் சரிபார்ப்பு பிழைகளை புறக்கணிக்கவும் (பரிந்துரைக்கப்படவில்லை)", diff --git a/web/src/lib/i18n/th.json b/web/src/lib/i18n/th.json index 19496b423843f..32336bfb4e9ce 100644 --- a/web/src/lib/i18n/th.json +++ b/web/src/lib/i18n/th.json @@ -147,7 +147,7 @@ "note_cannot_be_changed_later": "หมายเหตุ: ไม่สามารถเปลี่ยนภายหลังได้!", "note_unlimited_quota": "หมายเหตุ: ใส่เลข 0 สําหรับโควต้าไม่จํากัด", "notification_email_from_address": "จากที่อยู่", - "notification_email_from_address_description": "อีเมลผู้ส่ง อย่างเช่น \"Immich Photo Server \"", + "notification_email_from_address_description": "อีเมลผู้ส่ง อย่างเช่น \"Immich Photo Server \"", "notification_email_host_description": "ที่อยู่เซิร์ฟเวอร์อีเมล (เช่น smtp.immich.app)", "notification_email_ignore_certificate_errors": "ไม่สนใจข้อผิดพลาดเกี่ยวกับใบรับรอง", "notification_email_ignore_certificate_errors_description": "ไม่สนใจการยืนยันใบรับรอง TLS ผิดพลาด (ไม่แนะนำ)", diff --git a/web/src/lib/i18n/tr.json b/web/src/lib/i18n/tr.json index 4fefbf2f2132a..3a8a0db8af481 100644 --- a/web/src/lib/i18n/tr.json +++ b/web/src/lib/i18n/tr.json @@ -151,7 +151,7 @@ "note_cannot_be_changed_later": "NOT: Bu daha sonra değiştirilemez!", "note_unlimited_quota": "NOT: Sınırsız kota için 0 yazın", "notification_email_from_address": "Şu adresten", - "notification_email_from_address_description": "Göndericinin email adresi, örnek: \"Immich Fotoğraf Sunucusu \"", + "notification_email_from_address_description": "Göndericinin email adresi, örnek: \"Immich Fotoğraf Sunucusu \"", "notification_email_host_description": "E-posta sunucusunun ana bilgisayarı (örneğin, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Sertifika hatalarını görmezden gel", "notification_email_ignore_certificate_errors_description": "TLS sertifika doğrulama ayarlarını görmezden gel (Önerilmez)", diff --git a/web/src/lib/i18n/uk.json b/web/src/lib/i18n/uk.json index 64411ef7586f4..ce72fde8b42ff 100644 --- a/web/src/lib/i18n/uk.json +++ b/web/src/lib/i18n/uk.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "ПРИМІТКА: Це не можна змінити пізніше!", "note_unlimited_quota": "Примітка: Введіть 0 для необмеженого обсягу квоти", "notification_email_from_address": "З адреси", - "notification_email_from_address_description": "Адреса електронної пошти відправника, наприклад: \"Immich Photo Server \"", + "notification_email_from_address_description": "Адреса електронної пошти відправника, наприклад: \"Immich Photo Server \"", "notification_email_host_description": "Хост поштового сервера (наприклад, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ігнорувати помилки сертифіката", "notification_email_ignore_certificate_errors_description": "Ігнорувати помилки перевірки сертифікатів TLS (не рекомендується)", diff --git a/web/src/lib/i18n/vi.json b/web/src/lib/i18n/vi.json index c4f23ec273520..e94eb7a46471f 100644 --- a/web/src/lib/i18n/vi.json +++ b/web/src/lib/i18n/vi.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "LƯU Ý: Cài đặt này không thể thay đổi được sau khi lưu!", "note_unlimited_quota": "Lưu ý: Nhập 0 để hạn mức không giới hạn", "notification_email_from_address": "Địa chỉ email người gửi", - "notification_email_from_address_description": "Địa chỉ email của người gửi, ví dụ: \"Immich Photo Server \"", + "notification_email_from_address_description": "Địa chỉ email của người gửi, ví dụ: \"Immich Photo Server \"", "notification_email_host_description": "Địa chỉ máy chủ email (ví dụ: smtp.immich.app)", "notification_email_ignore_certificate_errors": "Bỏ qua các lỗi chứng chỉ", "notification_email_ignore_certificate_errors_description": "Bỏ qua lỗi xác thực chứng chỉ TLS (không khuyến nghị)", diff --git a/web/src/lib/i18n/zh_Hant.json b/web/src/lib/i18n/zh_Hant.json index 30e32f60c9ed1..fb9a18a1f507f 100644 --- a/web/src/lib/i18n/zh_Hant.json +++ b/web/src/lib/i18n/zh_Hant.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "註:之後就無法更改嘍!", "note_unlimited_quota": "註:輸入 0 表示不限制配額", "notification_email_from_address": "寄件地址", - "notification_email_from_address_description": "寄件者電子郵件地址(例:Immich Photo Server )", + "notification_email_from_address_description": "寄件者電子郵件地址(例:Immich Photo Server )", "notification_email_host_description": "電子郵件伺服器主機(例:smtp.immich.app)", "notification_email_ignore_certificate_errors": "忽略憑證錯誤", "notification_email_ignore_certificate_errors_description": "忽略 TLS 憑證驗證錯誤(不建議)", diff --git a/web/src/lib/i18n/zh_SIMPLIFIED.json b/web/src/lib/i18n/zh_SIMPLIFIED.json index b56c2d29ebd94..e879365f410bb 100644 --- a/web/src/lib/i18n/zh_SIMPLIFIED.json +++ b/web/src/lib/i18n/zh_SIMPLIFIED.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "注意:此项一旦设定,以后无法更改!", "note_unlimited_quota": "提示:输入0表示无限制", "notification_email_from_address": "发件人地址", - "notification_email_from_address_description": "发件人邮箱地址,例如“Immich 服务器 ”", + "notification_email_from_address_description": "发件人邮箱地址,例如“Immich 服务器 ”", "notification_email_host_description": "邮件服务器主机(例如 smtp.immich.app)", "notification_email_ignore_certificate_errors": "忽略证书错误", "notification_email_ignore_certificate_errors_description": "忽略TLS证书验证错误(不建议)", From edb085691a67c84997f4f34ca2dede7f82af2bb2 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 16 Sep 2024 18:19:57 +0200 Subject: [PATCH 02/57] chore(web): update translations (#12590) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/fi/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/hr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/lv/ 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/ro/ 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/sr_Cyrl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/ Translation: Immich/immich Co-authored-by: Bezruchenko Simon Co-authored-by: Boris Garmev Co-authored-by: David Abner Ciuhan Co-authored-by: Dean Cvjetanović Co-authored-by: Denis Pacquier Co-authored-by: Eero Jääskeläinen Co-authored-by: Javier Montón Co-authored-by: Junghyuk Kwon Co-authored-by: Michal Micech Co-authored-by: Miki Mrvos Co-authored-by: Mārtiņš Bruņenieks Co-authored-by: Owen Higgins Co-authored-by: Pat Oakly Co-authored-by: Poramate Homprakob Co-authored-by: Riccardo Co-authored-by: RoanV Co-authored-by: Roger Veciana Rovira Co-authored-by: Rémi Saurel Co-authored-by: Sam Smith Co-authored-by: Vladimir Petrov (Vlado) Co-authored-by: Xo Co-authored-by: aarhor Co-authored-by: chapvic Co-authored-by: dvbthien Co-authored-by: kiwinho Co-authored-by: pyccl Co-authored-by: pyorot Co-authored-by: waclaw66 --- web/src/lib/i18n/bg.json | 80 +- web/src/lib/i18n/ca.json | 73 +- web/src/lib/i18n/cs.json | 3 + web/src/lib/i18n/da.json | 14 + web/src/lib/i18n/de.json | 2 +- web/src/lib/i18n/es.json | 65 +- web/src/lib/i18n/fi.json | 6 + web/src/lib/i18n/fr.json | 52 +- web/src/lib/i18n/he.json | 14 +- web/src/lib/i18n/hr.json | 1136 ++++++++++++++++----------- web/src/lib/i18n/it.json | 3 + web/src/lib/i18n/ko.json | 8 +- web/src/lib/i18n/lv.json | 120 +-- web/src/lib/i18n/nb_NO.json | 3 + web/src/lib/i18n/nl.json | 2 + web/src/lib/i18n/ro.json | 304 ++++--- web/src/lib/i18n/ru.json | 5 +- web/src/lib/i18n/sk.json | 4 +- web/src/lib/i18n/sr_Cyrl.json | 3 + web/src/lib/i18n/sr_Latn.json | 3 + web/src/lib/i18n/th.json | 17 +- web/src/lib/i18n/uk.json | 4 + web/src/lib/i18n/vi.json | 3 + web/src/lib/i18n/zh_SIMPLIFIED.json | 66 +- 24 files changed, 1237 insertions(+), 753 deletions(-) diff --git a/web/src/lib/i18n/bg.json b/web/src/lib/i18n/bg.json index ef739e745219c..29ac04eda8b1e 100644 --- a/web/src/lib/i18n/bg.json +++ b/web/src/lib/i18n/bg.json @@ -137,7 +137,7 @@ "map_settings_description": "Управление на настройките на картата", "map_style_description": "URL адрес към файл \"style.json\" за задаване на стил на картата", "metadata_extraction_job": "Извличане на метаданни", - "metadata_extraction_job_description": "Извличане на метаданни от всеки ресурс, като GPS и резолюция", + "metadata_extraction_job_description": "Извличане на метаданни от всеки от ресурсите, като GPS локация, лица и резолюция на файловете", "metadata_faces_import_setting": "Включи импорт на лице", "metadata_faces_import_setting_description": "Импортирай лица от EXIF данни и помощни файлове", "metadata_settings": "Опции за метаданни", @@ -176,7 +176,7 @@ "oauth_issuer_url": "URL на издателя", "oauth_mobile_redirect_uri": "URI за мобилно пренасочване", "oauth_mobile_redirect_uri_override": "URI пренасочване за мобилни устройства", - "oauth_mobile_redirect_uri_override_description": "Разреши когато 'app.immich:/' е невалиден пренасочвар адрес/URI.", + "oauth_mobile_redirect_uri_override_description": "Разреши когато доставчика за OAuth удостоверяване не позволява за мобилни URI идентификатори, като '{callback}'", "oauth_profile_signing_algorithm": "Алгоритъм за създаване на профили", "oauth_profile_signing_algorithm_description": "Алгоритъм излпозлван за вписване на потребителски профил.", "oauth_scope": "Област/обхват на приложение", @@ -244,7 +244,7 @@ "thumbnail_generation_job": "Генериране на миниатюри", "thumbnail_generation_job_description": "Генерирайте големи, малки и замъглени миниатюри за всеки актив, както и миниатюри за всеки човек", "transcoding_acceleration_api": "API за ускоряване", - "transcoding_acceleration_api_description": "API, който ще взаимодейства с вашето устройство, за да ускори транскодирането. Тази настройка е „best effort“: тя ще се върне към софтуерно транскодиране при повреда. VP9 може или не може да работи в зависимост от вашия хардуер.", + "transcoding_acceleration_api_description": "API интерфейсът, който ще взаимодейства с вашето устройство, за да ускори транскодирането. Тази настройка е „възможно най-доброто“: тя ще се върне към софтуерно транскодиране при повреда. VP9 може и да не работи в зависимост от вашия хардуер.", "transcoding_acceleration_nvenc": "NVENC (необходим NVIDIA GPU)", "transcoding_acceleration_qsv": "Quick Sync (необходим 7th поколение Intel CPU или по-ново)", "transcoding_acceleration_rkmpp": "RKMPP (само на Rockchip SOCs)", @@ -252,9 +252,9 @@ "transcoding_accepted_audio_codecs": "Допустими аудио кодеци", "transcoding_accepted_audio_codecs_description": "Изберете кои аудио кодеци не са нужни за разкодиране. Използва се само за определени правила за разкодиране.", "transcoding_accepted_containers": "Приети контейнери", - "transcoding_accepted_containers_description": "Изберете кои формати на контейнери не трябва да се пренасочват към MP4. Използва се само за определени правила за разкодиране.", + "transcoding_accepted_containers_description": "Изберете кои формати на контейнери не е нужно да бъдат преобразувани в MP4 формат. Използва се само за определени правила за разкодиране.", "transcoding_accepted_video_codecs": "Приети видео кодеци", - "transcoding_accepted_video_codecs_description": "Изберете кои видео кодеци не трябва да се разкодиране. Използва се само за определени правила за разкодиране.", + "transcoding_accepted_video_codecs_description": "Изберете кои видео кодеци не трябват за разкодиране. Използва се само за определени правила за разкодиране.", "transcoding_advanced_options_description": "Опции, които повечето потребители не трябва да променят", "transcoding_audio_codec": "Аудио кодек", "transcoding_audio_codec_description": "Opus е опцията с най-високо качество, но има по-ниска съвместимост със стари устройства или софтуер.", @@ -446,7 +446,7 @@ "copy_to_clipboard": "Копиране в клипборда", "country": "Държава", "cover": "", - "covers": "", + "covers": "Обложка", "create": "Създай", "create_album": "Създай албум", "create_library": "Създай библиотека", @@ -938,19 +938,22 @@ "search_city": "", "search_country": "", "search_for_existing_person": "", - "search_people": "", - "search_places": "", + "search_people": "Търсете на хора", + "search_places": "Търсене на места", "search_state": "", - "search_timezone": "", - "search_type": "", - "search_your_photos": "", + "search_tags": "Търсене на етикети...", + "search_timezone": "Търсене на часова зона...", + "search_type": "Тип на търсене", + "search_your_photos": "Търсете вашите снимки", "searching_locales": "", "second": "Секунда", - "select_album_cover": "", - "select_all": "", - "select_avatar_color": "", - "select_face": "", + "see_all_people": "Вижте всички хора", + "select_album_cover": "Изберете обложка на албум", + "select_all": "Изберете всички", + "select_avatar_color": "Изберете цвят на аватара", + "select_face": "Изберете лице", "select_featured_photo": "", + "select_from_computer": "Изберете от компютъра", "select_keep_all": "", "select_library_owner": "Изберете собственик на библиотека", "select_new_face": "Изберете ново лице", @@ -998,28 +1001,40 @@ "show_metadata": "Покажи метаданни", "show_or_hide_info": "Покажи или скрий информацията", "show_password": "Покажи паролата", - "show_person_options": "", - "show_progress_bar": "", - "show_search_options": "", + "show_person_options": "Показване на опции за лица", + "show_progress_bar": "Показване на прогрес бара", + "show_search_options": "Показване на опциите за търсене", + "show_supporter_badge": "Значка поддръжник", + "show_supporter_badge_description": "Покажи значка поддръжник", "shuffle": "Разбъркване", - "sign_out": "", - "sign_up": "", + "sidebar": "Странична лента", + "sidebar_display_description": "Показване на връзка към изгледа в страничната лента", + "sign_out": "Отписване", + "sign_up": "Запиши се", "size": "Размер", - "skip_to_content": "", + "skip_to_content": "Премини към съдържанието", + "skip_to_folders": "Премини към папките", + "skip_to_tags": "Премини към етикетите", "slideshow": "Слайдшоу", - "slideshow_settings": "", - "sort_albums_by": "", + "slideshow_settings": "Настройки за слайдшоу", + "sort_albums_by": "Сортиране на албуми по...", + "sort_created": "Дата на създаване", + "sort_items": "Брой елементи", + "sort_modified": "Дата на промяна", + "sort_oldest": "Най-старата снимка", + "sort_recent": "Най-новата снимка", "sort_title": "Заглавие", "source": "Източник", "stack": "", - "stack_selected_photos": "", + "stack_duplicates": "Подреждане на дубликати", + "stack_selected_photos": "Подреждане на избрани снимки", "stacktrace": "", "start": "Старт", - "start_date": "", + "start_date": "Начална дата", "state": "", "status": "Статус", "stop_motion_photo": "", - "stop_photo_sharing": "Да спрете ли споделянето на вашите снимки?", + "stop_photo_sharing": "Да спра ли споделянето на вашите снимки?", "stop_photo_sharing_description": "{partner} вече няма достъп до вашите снимки.", "stop_sharing_photos_with_user": "Прекратете споделянето на снимки с този потребител", "storage": "Пространство на хранилището", @@ -1030,6 +1045,12 @@ "sunrise_on_the_beach": "Изгрев на плажа", "swap_merge_direction": "Размяна посоката на сливане", "sync": "Синхронизиране", + "tag": "Таг", + "tag_created": "Създаден етикет: {tag}", + "tag_feature_description": "Разглеждане на снимки и видеоклипове, групирани по теми с логически тагове", + "tag_not_found_question": "Не можете да намерите етикет? Създайте такъв тук", + "tag_updated": "Актуализиран етикет: {tag}", + "tags": "Етикет", "template": "Шаблон", "theme": "Тема", "theme_selection": "Избор на тема", @@ -1048,7 +1069,7 @@ "total_usage": "Общо използвано", "trash": "кошче", "trash_all": "Изхвърли всички", - "trash_count": "", + "trash_count": "Кошче {count, number}", "trash_no_results_message": "Изтритите снимки и видеоклипове ще се показват тук.", "trashed_items_will_be_permanently_deleted_after": "Изхвърлените в кошчето елементи ще бъдат изтрити за постоянно след {days, plural, one {# day} other {# days}}.", "type": "Тип", @@ -1062,6 +1083,7 @@ "unlink_oauth": "", "unlinked_oauth_account": "", "unnamed_album": "Албум без име", + "unnamed_album_delete_confirmation": "Сигурни ли сте, че искате да изтриете този албум?", "unnamed_share": "Споделяне без име", "unsaved_change": "Незапазена промяна", "unselect_all": "Деселектирайте всички", @@ -1085,7 +1107,7 @@ "user_purchase_settings": "Покупка", "user_purchase_settings_description": "Управлявай покупката си", "user_role_set": "Задай {user} като {role}", - "user_usage_detail": "", + "user_usage_detail": "Подробности за използването на потребителя", "username": "Потребителско име", "users": "Потребители", "utilities": "Инструменти", @@ -1103,9 +1125,11 @@ "view_album": "Разгледай албума", "view_all": "Преглед на всички", "view_all_users": "Преглед на всички потребители", + "view_in_timeline": "Покажи във времева линия", "view_links": "Преглед на връзките", "view_next_asset": "Преглед на следващия файл", "view_previous_asset": "Преглед на предишния файл", + "view_stack": "Покажи в стек", "viewer": "", "waiting": "в изчакване", "warning": "Внимание", diff --git a/web/src/lib/i18n/ca.json b/web/src/lib/i18n/ca.json index ba33c9b15606b..e9c695f79a74e 100644 --- a/web/src/lib/i18n/ca.json +++ b/web/src/lib/i18n/ca.json @@ -129,16 +129,21 @@ "map_enable_description": "Habilita característiques del mapa", "map_gps_settings": "Configuració de mapa i GPS", "map_gps_settings_description": "Gestiona la configuració de mapa i GPS (Geocodificació inversa)", + "map_implications": "La funció mapa depèn del servei extern de tesel·les (tiles.immich.cloud)", "map_light_style": "Tema clar", "map_manage_reverse_geocoding_settings": "Gestiona els paràmetres de geocodificació inversa", "map_reverse_geocoding": "Geocodificació inversa", "map_reverse_geocoding_enable_description": "Habilita la geocodificació inversa", "map_reverse_geocoding_settings": "Configuració de Geocodificació Inversa", - "map_settings": "Configuració del mapa i GPS", + "map_settings": "Mapa", "map_settings_description": "Gestiona la configuració del mapa", "map_style_description": "URL a un tema del mapa style.json", "metadata_extraction_job": "Extreure metadades", "metadata_extraction_job_description": "Extreu la informació de metadades de cada element, com per exemple el GPS i la resolució", + "metadata_faces_import_setting": "Activar la importació de cares", + "metadata_faces_import_setting_description": "Importar cares des de les metadades EXIF de les imatges i arxius auxiliars", + "metadata_settings": "Configuració de les metadades", + "metadata_settings_description": "Administrar la configuració de les metadades", "migration_job": "Migració", "migration_job_description": "Migra les miniatures d'elements i cares cap a la nova estructura de carpetes", "no_paths_added": "Cap camí afegit", @@ -173,7 +178,7 @@ "oauth_issuer_url": "URL de l'emissor", "oauth_mobile_redirect_uri": "URI de redirecció mòbil", "oauth_mobile_redirect_uri_override": "Sobreescriu l'URI de redirecció mòbil", - "oauth_mobile_redirect_uri_override_description": "Habilita quan 'app.immich:/' és una URI de redirecció invàlida.", + "oauth_mobile_redirect_uri_override_description": "Habilita quan el proveïdor d'OAuth no permet una URI mòbil, com ara '{callback}'", "oauth_profile_signing_algorithm": "Algoritme de signatura del perfil", "oauth_profile_signing_algorithm_description": "Algoritme utilitzat per signar el perfil d’usuari.", "oauth_scope": "Abast", @@ -278,7 +283,7 @@ "transcoding_preferred_hardware_device": "Dispositiu de maquinari preferit", "transcoding_preferred_hardware_device_description": "S'aplica només a VAAPI i QSV. Estableix el node dri utilitzat per a la transcodificació de maquinari.", "transcoding_preset_preset": "Preestablert (-preset)", - "transcoding_preset_preset_description": "Velocitat de compressió. Els valors predefinits més lents produeixen fitxers més petits i augmenten la qualitat quan s'orienta a una taxa de bits determinada. VP9 ignora les velocitats superiors a \"més ràpides\".", + "transcoding_preset_preset_description": "Velocitat de compressió. Els valors predefinits més lents produeixen fitxers més petits i augmenten la qualitat quan s'orienta a una taxa de bits determinada. VP9 ignora les velocitats superiors a 'més ràpides'.", "transcoding_reference_frames": "Fotogrames de referència", "transcoding_reference_frames_description": "El nombre de fotogrames a fer referència en comprimir un fotograma determinat. Els valors més alts milloren l'eficiència de la compressió, però alenteixen la codificació. 0 estableix aquest valor automàticament.", "transcoding_required_description": "Només vídeos que no tenen un format acceptat", @@ -320,7 +325,8 @@ "user_settings": "Configuració d'usuaris", "user_settings_description": "Gestiona la configuració dels usuaris", "user_successfully_removed": "L'usuari {email} s'ha eliminat correctament.", - "version_check_enabled_description": "Activa sol·licituds periòdiques a GitHub per comprovar si hi ha versions noves", + "version_check_enabled_description": "Activa la comprovació de la versió", + "version_check_implications": "La funció de comprovació de versions depèn de comunicacions periòdiques amb github.com", "version_check_settings": "Comprovació de versió", "version_check_settings_description": "Activa/desactiva la notificació de nova versió", "video_conversion_job": "Transcodificació de vídeos", @@ -336,7 +342,8 @@ "album_added": "Àlbum afegit", "album_added_notification_setting_description": "Rep una notificació per correu quan siguis afegit a un àlbum compartit", "album_cover_updated": "Portada de l'àlbum actualitzada", - "album_delete_confirmation": "N'esteu segur que voleu suprimir l'àlbum {album}?\nSi aquest àlbum és compartit, altres usuaris no hi podran accedir més.", + "album_delete_confirmation": "Esteu segur que voleu suprimir l'àlbum {album}?", + "album_delete_confirmation_description": "Si aquest àlbum es comparteix, els altres usuaris ja no podran accedir-hi.", "album_info_updated": "Informació de l'àlbum actualitzada", "album_leave": "Sortir de l'àlbum?", "album_leave_confirmation": "N'esteu segur que voleu sortir de {album}?", @@ -360,6 +367,7 @@ "allow_edits": "Permet editar", "allow_public_user_to_download": "Permet que l'usuari públic pugui descarregar", "allow_public_user_to_upload": "Permet que l'usuari públic pugui carregar", + "anti_clockwise": "En sentit antihorari", "api_key": "Clau API", "api_key_description": "Aquest valor només es mostrarà una vegada. Assegureu-vos de copiar-lo abans de tancar la finestra.", "api_key_empty": "El nom de la clau de l'API no pot estar buit", @@ -383,6 +391,7 @@ "asset_offline": "Element fora de línia", "asset_offline_description": "Aquest element està fora de línia. L'Immich no pot accedir a la seva ubicació. Si us plau, assegureu-vos que l'actiu està disponible i després torneu la llibreria.", "asset_skipped": "Saltat", + "asset_skipped_in_trash": "A la paperera", "asset_uploaded": "Carregat", "asset_uploading": "S'està carregant...", "assets": "Elements", @@ -440,9 +449,11 @@ "clear_all_recent_searches": "Esborra totes les cerques recents", "clear_message": "Neteja el missatge", "clear_value": "Neteja el valor", + "clockwise": "En sentit horari", "close": "Tanca", "collapse": "Tanca", "collapse_all": "Redueix-ho tot", + "color": "Color", "color_theme": "Tema de color", "comment_deleted": "Comentari esborrat", "comment_options": "Opcions de comentari", @@ -476,6 +487,8 @@ "create_new_person": "Crea una nova persona", "create_new_person_hint": "Assigna els elements seleccionats a una persona nova", "create_new_user": "Crea un usuari nou", + "create_tag": "Crear etiqueta", + "create_tag_description": "Crear una nova etiqueta. Per les etiquetes aniuades, escriu la ruta comperta de l'etiqueta, incloses les barres diagonals.", "create_user": "Crea un usuari", "created": "Creat", "current_device": "Dispositiu actual", @@ -499,6 +512,8 @@ "delete_library": "Suprimeix la llibreria", "delete_link": "Esborra l'enllaç", "delete_shared_link": "Odstranit sdílený odkaz", + "delete_tag": "Eliminar etiqueta", + "delete_tag_confirmation_prompt": "Estàs segur que vols eliminar l'etiqueta {tagName}?", "delete_user": "Suprimeix l'usuari", "deleted_shared_link": "Suprimeix l'enllaç compartit", "description": "Descripció", @@ -516,6 +531,8 @@ "do_not_show_again": "No tornis a mostrar aquest missatge", "done": "Fet", "download": "Descarregar", + "download_include_embedded_motion_videos": "Vídeos incrustats", + "download_include_embedded_motion_videos_description": "Incloure vídeos incrustats en fotografies en moviment com un arxiu separat", "download_settings": "Descarregar", "download_settings_description": "Gestioneu la configuració relacionada amb la descàrrega de recursos", "downloading": "Baixant", @@ -545,10 +562,15 @@ "edit_location": "Edita ubicació", "edit_name": "Edita el nom", "edit_people": "Edita la gent", + "edit_tag": "Editar etiqueta", "edit_title": "Edita títol", "edit_user": "Edita l'usuari", "edited": "Editat", "editor": "Editor", + "editor_close_without_save_prompt": "No es desaran els canvis", + "editor_close_without_save_title": "Tancar l'editor?", + "editor_crop_tool_h2_aspect_ratios": "Relació d'aspecte", + "editor_crop_tool_h2_rotation": "Rotació", "email": "Correu electrònic", "empty": "", "empty_album": "", @@ -638,6 +660,7 @@ "unable_to_get_comments_number": "No es pot obtenir el nombre de comentaris", "unable_to_get_shared_link": "No s'ha pogut obtenir l'enllaç compartit", "unable_to_hide_person": "No es pot amagar la persona", + "unable_to_link_motion_video": "No es pot enllaçar el vídeo en moviment", "unable_to_link_oauth_account": "No es pot enllaçar el compte OAuth", "unable_to_load_album": "No es pot carregar l'àlbum", "unable_to_load_asset_activity": "No es pot carregar l'activitat dels recursos", @@ -678,6 +701,7 @@ "unable_to_submit_job": "No es pot enviar la tasca", "unable_to_trash_asset": "No es pot eliminar el recurs a la paperera", "unable_to_unlink_account": "No es pot desenllaçar el compte", + "unable_to_unlink_motion_video": "No es pot desvincular el vídeo en moviment", "unable_to_update_album_cover": "No es pot actualitzar la portada de l'àlbum", "unable_to_update_album_info": "No es pot actualitzar la informació de l'àlbum", "unable_to_update_library": "No es pot actualitzar la biblioteca", @@ -698,6 +722,7 @@ "expired": "Caducat", "expires_date": "Caduca el {date}", "explore": "Explorar", + "explorer": "Explorador", "export": "Exporta", "export_as_json": "Exportar com a JSON", "extension": "Extensió", @@ -711,6 +736,8 @@ "feature": "", "feature_photo_updated": "Foto destacada actualitzada", "featurecollection": "", + "features": "Característiques", + "features_setting_description": "Administrar les funcions de l'aplicació", "file_name": "Nom de l'arxiu", "file_name_or_extension": "Nom de l'arxiu o extensió", "filename": "Nom del fitxer", @@ -719,6 +746,8 @@ "filter_people": "Filtra persones", "find_them_fast": "Trobeu-los ràpidament pel nom amb la cerca", "fix_incorrect_match": "Corregiu la coincidència incorrecta", + "folders": "Carpetes", + "folders_feature_description": "Explorar la vista de carpetes per les fotos i vídeos del sistema d'arxius", "force_re-scan_library_files": "Força a tornar a escanejar tots els fitxers de la biblioteca", "forward": "Endavant", "general": "General", @@ -803,6 +832,7 @@ "license_trial_info_3": "{accountAge, plural, one {# dia} other {# dies}}", "light": "Llum", "like_deleted": "M'agrada suprimit", + "link_motion_video": "Enllaçar vídeo en moviment", "link_options": "Opcions d'enllaç", "link_to_oauth": "Enllaç a OAuth", "linked_oauth_account": "Compte OAuth enllaçat", @@ -896,12 +926,14 @@ "ok": "D'acord", "oldest_first": "El més vell primer", "onboarding": "Onboarding", + "onboarding_privacy_description": "Les següents funcions (opcionals) depenen de serveis externs i poden desactivarse en qualsevol moment de dels ajustos.", "onboarding_theme_description": "Trieu un tema de color per a la vostra instància. Podeu canviar-ho més endavant a la vostra configuració.", "onboarding_welcome_description": "Configurem la vostra instància amb alguns paràmetres habituals.", "onboarding_welcome_user": "Benvingut, {user}", "online": "En línia", "only_favorites": "Només preferits", "only_refreshes_modified_files": "Només actualitza els fitxers modificats", + "open_in_map_view": "Obrir a la vista del mapa", "open_in_openstreetmap": "Obre a OpenStreetMap", "open_the_search_filters": "Obriu els filtres de cerca", "options": "Opcions", @@ -936,6 +968,7 @@ "pending": "Pendent", "people": "Persones", "people_edits_count": "{count, plural, one {# persona editada} other {# persones editades}}", + "people_feature_description": "Explorar fotos i vídeos agrupades per persona", "people_sidebar_description": "Mostrar un enllaç a Persones a la barra lateral", "perform_library_tasks": "", "permanent_deletion_warning": "Avís d'eliminació permanent", @@ -967,6 +1000,7 @@ "previous_memory": "Memòria anterior", "previous_or_next_photo": "Foto anterior o següent", "primary": "Primària", + "privacy": "Privacitat", "profile_image_of_user": "Imatge de perfil de {user}", "profile_picture_set": "Imatge de perfil configurada.", "public_album": "Àlbum públic", @@ -1004,6 +1038,10 @@ "purchase_server_title": "Servidor", "purchase_settings_server_activated": "La clau de producte del servidor la gestiona l'administrador", "range": "", + "rating": "Valoració", + "rating_clear": "Esborrar valoració", + "rating_count": "{count, plural, one {# estrella} other {# estrelles}}", + "rating_description": "Mostrar la valoració EXIF al panell d'informació", "raw": "", "reaction_options": "Opcions de reacció", "read_changelog": "Llegeix el registre de canvis", @@ -1036,6 +1074,7 @@ "removed_from_archive": "Eliminat de l'arxiu", "removed_from_favorites": "Eliminat dels preferits", "removed_from_favorites_count": "{count, plural, other {# eliminats}} dels preferits", + "removed_tagged_assets": "Etiqueta eliminada de {count, plural, one {# actiu} other {# actius}}", "rename": "Canviar nom", "repair": "Reparació", "repair_no_results_message": "Els fitxers sense seguiment i que falten es mostraran aquí", @@ -1082,9 +1121,11 @@ "search_for_existing_person": "Busca una persona existent", "search_no_people": "Cap persona", "search_no_people_named": "Cap persona anomenada \"{name}\"", + "search_options": "Opcions de cerca", "search_people": "Buscar persones", "search_places": "Buscar llocs", "search_state": "Buscar per regió...", + "search_tags": "Cercant etiquetes...", "search_timezone": "Buscar per fus horari...", "search_type": "Buscar per tipus", "search_your_photos": "Cerca les teves fotos", @@ -1126,6 +1167,7 @@ "shared_by_user": "Compartit per {user}", "shared_by_you": "Compartit per tu", "shared_from_partner": "Fotos de {partner}", + "shared_link_options": "Opcions d'enllaços compartits", "shared_links": "Enllaços compartits", "shared_photos_and_videos_count": "{assetCount, plural, other {# fotos i vídeos compartits.}}", "shared_with_partner": "Compartit amb {partner}", @@ -1134,6 +1176,7 @@ "sharing_sidebar_description": "Mostra un enllaç a Compartit a la barra lateral", "shift_to_permanent_delete": "premeu ⇧ per suprimir el recurs permanentment", "show_album_options": "Mostra les opcions d'àlbum", + "show_albums": "Mostrar àlbums", "show_all_people": "Veure totes les persones", "show_and_hide_people": "Mostra i amaga persones", "show_file_location": "Mostra l'ubicació del fitxer", @@ -1151,10 +1194,14 @@ "show_supporter_badge": "Insígnia de contribuent", "show_supporter_badge_description": "Mostra una insígnia de contributor", "shuffle": "Mescla", + "sidebar": "Barra lateral", + "sidebar_display_description": "Mostra un enllaç a la vista a la barra lateral", "sign_out": "Tanca sessió", "sign_up": "Registrar-se", "size": "Mida", "skip_to_content": "Salta al contingut", + "skip_to_folders": "Anar a carpetes", + "skip_to_tags": "Anar a etiquetes", "slideshow": "Diapositives", "slideshow_settings": "Configuració de diapositives", "sort_albums_by": "Ordena àlbums per...", @@ -1166,6 +1213,8 @@ "sort_title": "Títol", "source": "Font", "stack": "Apila", + "stack_duplicates": "Aplicar duplicats", + "stack_select_one_photo": "Selecciona una imatge principal per la pila", "stack_selected_photos": "Apila les fotos seleccionades", "stacked_assets_count": "Apilats {count, plural, one {# element} other {# elements}}", "stacktrace": "Traça de pila", @@ -1185,6 +1234,14 @@ "sunrise_on_the_beach": "Albada a la platja", "swap_merge_direction": "Canvia la direcció d'unió", "sync": "Sincronitza", + "tag": "Etiqueta", + "tag_assets": "Etiquetar actius", + "tag_created": "Etiqueta creada: {tag}", + "tag_feature_description": "Exploreu fotos i vídeos agrupats per temes d'etiquetes lògiques", + "tag_not_found_question": "No trobeu una etiqueta? Creeu-ne una aquí", + "tag_updated": "Etiqueta actualizada: {tag}", + "tagged_assets": "{count, plural, one {#Etiquetat} other {#Etiquetats}} {count, plural, one {# actiu} other {# actius}}", + "tags": "Etiquetes", "template": "Plantilla", "theme": "Tema", "theme_selection": "Selecció de tema", @@ -1196,9 +1253,10 @@ "to_change_password": "Canviar la contrasenya", "to_favorite": "Prefereix", "to_login": "Iniciar sessió", + "to_parent": "Anar als pares", "to_trash": "Paperera", "toggle_settings": "Canvia configuració", - "toggle_theme": "Canvia tema", + "toggle_theme": "Alternar tema", "toggle_visibility": "Canvia visibilitat", "total_usage": "Ús total", "trash": "Paperera", @@ -1217,9 +1275,11 @@ "unknown_album": "Àlbum desconegut", "unknown_year": "Any desconegut", "unlimited": "Il·limitat", + "unlink_motion_video": "Desvincular vídeo en moviment", "unlink_oauth": "Desvincula OAuth", "unlinked_oauth_account": "Compte Oauth desvinculat", "unnamed_album": "Àlbum sense nom", + "unnamed_album_delete_confirmation": "Segur que voleu esborrar aquest àlbum?", "unnamed_share": "Compartit sense nom", "unsaved_change": "Canvi no desat", "unselect_all": "Deselecciona-ho tot", @@ -1267,6 +1327,7 @@ "view_album": "Veure l'àlbum", "view_all": "Veure tot", "view_all_users": "Mostra tot els usuaris", + "view_in_timeline": "Mostrar a la línia de temps", "view_links": "Mostra enllaços", "view_next_asset": "Mostra el següent element", "view_previous_asset": "Mostra l'element anterior", diff --git a/web/src/lib/i18n/cs.json b/web/src/lib/i18n/cs.json index e49d3700ee94f..c2d7bce0e5664 100644 --- a/web/src/lib/i18n/cs.json +++ b/web/src/lib/i18n/cs.json @@ -661,6 +661,7 @@ "unable_to_get_comments_number": "Nelze načíst počet komentářů", "unable_to_get_shared_link": "Nepodařilo se získat sdílený odkaz", "unable_to_hide_person": "Nelze skrýt osobu", + "unable_to_link_motion_video": "Nelze připojit pohyblivé video", "unable_to_link_oauth_account": "Nelze propojit OAuth účet", "unable_to_load_album": "Nelze načíst album", "unable_to_load_asset_activity": "Nelze načíst aktivitu položky", @@ -701,6 +702,7 @@ "unable_to_submit_job": "Nelze odeslat úlohu", "unable_to_trash_asset": "Nelze vyhodit položku do koše", "unable_to_unlink_account": "Nelze zrušit propojení účtu", + "unable_to_unlink_motion_video": "Nelze odpojit pohyblivé video", "unable_to_update_album_cover": "Nelze aktualizovat obal alba", "unable_to_update_album_info": "Nelze aktualizovat informace o albu", "unable_to_update_library": "Nelze aktualizovat knihovnu", @@ -1292,6 +1294,7 @@ "unknown_album": "Neznámé album", "unknown_year": "Neznámý rok", "unlimited": "Neomezeně", + "unlink_motion_video": "Odpojit pohyblivé video", "unlink_oauth": "Zrušit OAuth propojení", "unlinked_oauth_account": "OAuth účet odpojen", "unnamed_album": "Nepojmenované album", diff --git a/web/src/lib/i18n/da.json b/web/src/lib/i18n/da.json index ab1d57d48e347..1e2c9a2b4ab54 100644 --- a/web/src/lib/i18n/da.json +++ b/web/src/lib/i18n/da.json @@ -140,6 +140,10 @@ "map_style_description": "URL til en style.json for et korttema", "metadata_extraction_job": "Udtræk metadata", "metadata_extraction_job_description": "Udtræk metadataoplysninger fra hvert Billede/Video, såsom GPS og opløsning", + "metadata_faces_import_setting": "Aktivér for at importere ansigter", + "metadata_faces_import_setting_description": "Importerer ansigter fra billed EXIF-data og forbandt filer", + "metadata_settings": "Metadatainstillinger", + "metadata_settings_description": "Håndtér metadataindstillinger", "migration_job": "Migrering", "migration_job_description": "Migrér miniaturebilleder for aktiver og ansigter til den seneste mappestruktur", "no_paths_added": "Ingen stier tilføjet", @@ -347,15 +351,25 @@ "album_options": "Albumindstillinger", "album_remove_user": "Fjern bruger?", "album_remove_user_confirmation": "Er du sikker på at du vil fjerne {user}?", + "album_share_no_users": "Det ser ud til at du har delt denne album med alle brugere, eller du har ikke nogen brugere til at dele med.", "album_updated": "Album opdateret", "album_updated_setting_description": "Modtag en emailnotifikation når et delt album får nye mediefiler", + "album_user_left": "Forlod {album}", + "album_user_removed": "Fjernede {user}", "albums": "Albummer", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albummer}}", "all": "Alt", + "all_albums": "Alle albummer", "all_people": "Alle personer", + "all_videos": "Alle videoer", "allow_dark_mode": "Tillad mørk tilstand", "allow_edits": "Tillad redigeringer", + "allow_public_user_to_download": "Tillad offentlige brugere til at hente", + "allow_public_user_to_upload": "Tillad offentlige brugere til at uploade", + "anti_clockwise": "Mod uret", "api_key": "API-nøgle", + "api_key_description": "Denne værdi vises kun én gang. Venligst kopiér den før du lukker vinduet.", + "api_key_empty": "Din API-nøgle-navn burde ikke være tom", "api_keys": "API-nøgler", "app_settings": "Appindstillinger", "appears_in": "Optræder i", diff --git a/web/src/lib/i18n/de.json b/web/src/lib/i18n/de.json index d519352862c2b..352006ef6eb88 100644 --- a/web/src/lib/i18n/de.json +++ b/web/src/lib/i18n/de.json @@ -1171,7 +1171,7 @@ "server_stats": "Server-Statistiken", "server_version": "Server-Version", "set": "Speichern", - "set_as_album_cover": "Als Albumcover gesetzt", + "set_as_album_cover": "Als Albumcover festlegen", "set_as_profile_picture": "Als Profilbild festlegen", "set_date_of_birth": "Geburtsdatum festlegen", "set_profile_picture": "Profilbild einstellen", diff --git a/web/src/lib/i18n/es.json b/web/src/lib/i18n/es.json index 31c613dcbdb6e..013631919286a 100644 --- a/web/src/lib/i18n/es.json +++ b/web/src/lib/i18n/es.json @@ -312,7 +312,7 @@ "trash_settings_description": "Administrar la configuración de la papelera", "untracked_files": "Archivos sin seguimiento", "untracked_files_description": "La aplicación no rastrea estos archivos. Puede ser el resultado de movimientos fallidos, cargas interrumpidas o sin procesar debido a un error", - "user_delete_delay": "La cuenta {user} y los archivos se programarán para su eliminación permanente en {delay, plural, one {# day} other {# days}}.", + "user_delete_delay": "La cuenta {user} y los archivos se programarán para su eliminación permanente en {delay, plural, one {# día} other {# días}}.", "user_delete_delay_settings": "Eliminar retardo", "user_delete_delay_settings_description": "Número de días después de la eliminación para eliminar permanentemente la cuenta y los activos de un usuario. El trabajo de eliminación de usuarios se ejecuta a medianoche para comprobar si hay usuarios que estén listos para su eliminación. Los cambios a esta configuración se evaluarán en la próxima ejecución.", "user_delete_immediately": "La cuenta {user} y los archivos se pondrán en cola para su eliminación permanente inmediatamente.", @@ -336,8 +336,8 @@ "admin_password": "Contraseña del Administrador", "administration": "Administración", "advanced": "Avanzada", - "age_months": "Tiempo {months, plural, one {# month} other {# months}}", - "age_year_months": "1 año, {months, plural, one {# month} other {# months}}", + "age_months": "Tiempo {months, plural, one {# mes} other {# meses}}", + "age_year_months": "1 año, {months, plural, one {# mes} other {# meses}}", "age_years": "Edad {years, plural, one {# año} other {# años}}", "album_added": "Álbum añadido", "album_added_notification_setting_description": "Reciba una notificación por correo electrónico cuando lo agreguen a un álbum compartido", @@ -400,12 +400,12 @@ "assets_added_to_name_count": "Añadido {count, plural, one {# asset} other {# assets}} a {hasName, select, true {{name}} other {new album}}", "assets_count": "{count, plural, one {# activo} other {# activos}}", "assets_moved_to_trash": "Se movió {count, plural, one {# activo} other {# activos}} a la papelera", - "assets_moved_to_trash_count": "Movido {count, plural, one {# asset} other {# assets}} a la papelera", - "assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# asset} other {# assets}}", - "assets_removed_count": "Eliminado {count, plural, one {# asset} other {# assets}}", + "assets_moved_to_trash_count": "Movido {count, plural, one {# elemento} other {# elementos}} a la papelera", + "assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# elemento} other {# elementos}}", + "assets_removed_count": "Eliminado {count, plural, one {# elemento} other {# elementos}}", "assets_restore_confirmation": "¿Está seguro de que desea restaurar todos sus archivos eliminados? ¡No puedes deshacer esta acción!", - "assets_restored_count": "Restaurado {count, plural, one {# asset} other {# assets}}", - "assets_trashed_count": "Borrado {count, plural, one {# asset} other {# assets}}", + "assets_restored_count": "Restaurado {count, plural, one {# elemento} other {# elementos}}", + "assets_trashed_count": "Borrado {count, plural, one {# elemento} other {# elementos}}", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} ya forma parte del álbum", "authorized_devices": "Dispositivos Autorizados", "back": "Atrás", @@ -416,7 +416,7 @@ "blurred_background": "Fondo borroso", "build": "Compilación", "build_image": "Construir Imagen", - "bulk_delete_duplicates_confirmation": "¿Estás seguro de que deseas eliminar de forma masiva {count, plural, one {# duplicate asset} other {# duplicate assets}}? Esto mantendrá el activo más grande de cada grupo y eliminará permanentemente todos los demás duplicados. ¡Esta acción no se puede deshacer!", + "bulk_delete_duplicates_confirmation": "¿Estás seguro de que deseas eliminar de forma masiva {count, plural, one {# elemento duplicado} other {# elementos duplicados}}? Esto mantendrá el activo más grande de cada grupo y eliminará permanentemente todos los demás duplicados. ¡Esta acción no se puede deshacer!", "bulk_keep_duplicates_confirmation": "¿Estas seguro de que desea mantener {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto resolverá todos los grupos duplicados sin borrar nada.", "bulk_trash_duplicates_confirmation": "¿Estas seguro de que desea eliminar masivamente {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto mantendrá el archivo más grande de cada grupo y eliminará todos los demás duplicados.", "buy": "Comprar Immich", @@ -589,7 +589,7 @@ "cant_apply_changes": "No se pueden aplicar los cambios", "cant_change_activity": "No se puede realizar la actividad {enabled, select, true {disable} other {enable}}", "cant_change_asset_favorite": "No se puede cambiar favorito para este archivo", - "cant_change_metadata_assets_count": "No se pueden cambiar los metadatos de {count, plural, one {# asset} other {# assets}}", + "cant_change_metadata_assets_count": "No se pueden cambiar los metadatos de {count, plural, one {# elemento} other {# elementos}}", "cant_get_faces": "No se encuentran caras", "cant_get_number_of_comments": "No se puede obtener la cantidad de comentarios", "cant_search_people": "No se puede buscar a personas", @@ -616,7 +616,7 @@ "failed_to_unstack_assets": "Error al desagrupar los archivos", "import_path_already_exists": "Esta ruta de importación ya existe.", "incorrect_email_or_password": "Contraseña o email incorrecto", - "paths_validation_failed": "Falló la validación en {paths, plural, one {# carpetas} other {# carpetas}}", + "paths_validation_failed": "Falló la validación en {paths, plural, one {# carpeta} other {# carpetas}}", "profile_picture_transparent_pixels": "Las imágenes de perfil no pueden tener píxeles transparentes. Por favor amplíe y/o mueva la imagen.", "quota_higher_than_disk_size": "Se ha establecido una cuota superior al tamaño del disco", "repair_unable_to_check_items": "No se puede verificar {count, select, one {elemento} other {elementos}}", @@ -634,7 +634,7 @@ "unable_to_change_favorite": "Imposible cambiar el archivo favorito", "unable_to_change_location": "No se puede cambiar de ubicación", "unable_to_change_password": "No se puede cambiar la contraseña", - "unable_to_change_visibility": "No se puede cambiar la visibilidad de {count, plural, one {# person} other {# people}}", + "unable_to_change_visibility": "No se puede cambiar la visibilidad de {count, plural, one {# persona} other {# personas}}", "unable_to_check_item": "", "unable_to_check_items": "", "unable_to_complete_oauth_login": "No se puede completar el inicio de sesión de OAuth", @@ -661,6 +661,7 @@ "unable_to_get_comments_number": "No se puede obtener el número de comentarios", "unable_to_get_shared_link": "Error al obtener el enlace compartido", "unable_to_hide_person": "No se puede ocultar a la persona", + "unable_to_link_motion_video": "No se puede enlazar el vídeo en movimiento", "unable_to_link_oauth_account": "No se puede vincular la cuenta OAuth", "unable_to_load_album": "No se puede cargar el álbum", "unable_to_load_asset_activity": "No se puede cargar la actividad de los archivos", @@ -701,6 +702,7 @@ "unable_to_submit_job": "No se puede enviar el trabajo", "unable_to_trash_asset": "No se puede eliminar el archivo", "unable_to_unlink_account": "No se puede desvincular la cuenta", + "unable_to_unlink_motion_video": "No se puede desvincular el vídeo en movimiento", "unable_to_update_album_cover": "No se puede actualizar la portada del álbum", "unable_to_update_album_info": "No se puede actualizar la información del álbum", "unable_to_update_library": "No se puede actualizar la biblioteca", @@ -846,6 +848,7 @@ "license_trial_info_4": "Por favor, considera la compra de una licencia para apoyar el desarrollo continuo del servicio", "light": "Claro", "like_deleted": "Me gusta eliminado", + "link_motion_video": "Enlazar vídeo en movimiento", "link_options": "Opciones de enlace", "link_to_oauth": "Enlace a OAuth", "linked_oauth_account": "Cuenta OAuth vinculada", @@ -888,7 +891,7 @@ "merge_people_limit": "Solo puedes fusionar hasta 5 caras a la vez", "merge_people_prompt": "¿Quieres fusionar a estas personas? Esta acción es irreversible.", "merge_people_successfully": "Personas fusionadas correctamente", - "merged_people_count": "Fusionar {count, plural, one {# person} other {# people}}", + "merged_people_count": "Fusionada {count, plural, one {# persona} other {# personas}}", "minimize": "Minimizar", "minute": "Minuto", "missing": "Perdido", @@ -969,9 +972,9 @@ "password_required": "Contraseña requerida", "password_reset_success": "Restablecimiento de contraseña exitoso", "past_durations": { - "days": "Pasados {days, plural, one {day} other {# days}}", - "hours": "Pasadas {hours, plural, one {hour} other {# hours}}", - "years": "Pasado(s) {years, plural, one {year} other {# years}}" + "days": "Pasados {days, plural, one {día} other {# días}}", + "hours": "Pasadas {hours, plural, one {hora} other {# horas}}", + "years": "Pasado(s) {years, plural, one {año} other {# años}}" }, "path": "Ruta", "pattern": "Patrón", @@ -980,18 +983,18 @@ "paused": "Detenido", "pending": "Pendiente", "people": "Personas", - "people_edits_count": "Editado {count, plural, one {# person} other {# people}}", + "people_edits_count": "Editada {count, plural, one {# persona} other {# personas}}", "people_feature_description": "Explorar fotos y vídeos agrupados por personas", "people_sidebar_description": "Mostrar un enlace a Personas en la barra lateral", "perform_library_tasks": "", "permanent_deletion_warning": "Advertencia de eliminación permanente", "permanent_deletion_warning_setting_description": "Mostrar una advertencia al eliminar archivos permanentemente", "permanently_delete": "Borrar permanentemente", - "permanently_delete_assets_count": "Eliminar permanentemente {count, plural, one {asset} other {assets}}", - "permanently_delete_assets_prompt": "¿Está seguro de que desea eliminar permanentemente {count, plural, one {¿este activo?} other {¿estos # activos?}} Esto también eliminará {count, plural, one {de tu} other {de tus}} álbum(es).", + "permanently_delete_assets_count": "Eliminar permanentemente {count, plural, one {elemento} other {elementos}}", + "permanently_delete_assets_prompt": "¿Está seguro de que desea eliminar permanentemente {count, plural, one {este activo?} other {estos # activos?}} Esto también eliminará {count, plural, one {de tu} other {de tus}} álbum(es).", "permanently_deleted_asset": "Archivo eliminado permanentemente", "permanently_deleted_assets": "Eliminado permanentemente {count, plural, one {# activo} other {# activos}}", - "permanently_deleted_assets_count": "Eliminado permanentemente {count, plural, one {# asset} other {# assets}}", + "permanently_deleted_assets_count": "Eliminado permanentemente {count, plural, one {# elemento} other {# elementos}}", "person": "Persona", "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}", "photo_shared_all_users": "Parece que compartiste tus fotos con todos los usuarios o no tienes ningún usuario con quien compartirlas.", @@ -1060,8 +1063,8 @@ "reaction_options": "Opciones de reacción", "read_changelog": "Leer registro de cambios", "reassign": "Reasignar", - "reassigned_assets_to_existing_person": "Reasignado {count, plural, one {# asset} other {# assets}} to {name, select, null {an existing person} other {{name}}}", - "reassigned_assets_to_new_person": "Reasignado {count, plural, one {# asset} other {# assets}} a un nuevo usuario", + "reassigned_assets_to_existing_person": "Reasignado {count, plural, one {# elemento} other {# elementos}} a {name, select, null {una persona existente} other {{name}}}", + "reassigned_assets_to_new_person": "Reasignado {count, plural, one {# elemento} other {# elementos}} a un nuevo usuario", "reassing_hint": "Asignar archivos seleccionados a una persona existente", "recent": "Reciente", "recent_searches": "Búsquedas recientes", @@ -1075,8 +1078,8 @@ "refreshing_metadata": "Recargando metadatos", "regenerating_thumbnails": "Recargando miniaturas", "remove": "Eliminar", - "remove_assets_album_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# asset} other {# assets}} del álbum?", - "remove_assets_shared_link_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# asset} other {# assets}} del enlace compartido?", + "remove_assets_album_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# elemento} other {# elementos}} del álbum?", + "remove_assets_shared_link_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# elemento} other {# elementos}} del enlace compartido?", "remove_assets_title": "¿Eliminar activos?", "remove_custom_date_range": "Eliminar intervalo de fechas personalizado", "remove_from_album": "Eliminar del álbum", @@ -1087,7 +1090,7 @@ "removed_api_key": "Clave API eliminada: {name}", "removed_from_archive": "Eliminado del archivo", "removed_from_favorites": "Eliminado de favoritos", - "removed_from_favorites_count": "{count, plural, other {Removed #}} de favoritos", + "removed_from_favorites_count": "{count, plural, other {Eliminados #}} de favoritos", "removed_tagged_assets": "Etiqueta eliminada de {count, plural, one {# activo} other {# activos}}", "rename": "Renombrar", "repair": "Reparar", @@ -1135,6 +1138,7 @@ "search_for_existing_person": "Buscar persona existente", "search_no_people": "Ninguna persona", "search_no_people_named": "Ninguna persona llamada \"{name}\"", + "search_options": "Opciones de búsqueda", "search_people": "Buscar personas", "search_places": "Buscar lugar", "search_state": "Buscar región/estado...", @@ -1229,7 +1233,7 @@ "stack_duplicates": "Apilar duplicados", "stack_select_one_photo": "Selecciona una imagen principal para la pila", "stack_selected_photos": "Apilar fotos seleccionadas", - "stacked_assets_count": "Apilados {count, plural, one {# asset} other {# assets}}", + "stacked_assets_count": "Apilado(s) {count, plural, one {# activo} other {# activos}}", "stacktrace": "Stacktrace", "start": "Inicio", "start_date": "Fecha de inicio", @@ -1289,6 +1293,7 @@ "unknown_album": "Álbum desconocido", "unknown_year": "Año desconocido", "unlimited": "Ilimitado", + "unlink_motion_video": "Desvincular vídeo en movimiento", "unlink_oauth": "Desvincular OAuth", "unlinked_oauth_account": "Cuenta OAuth desconectada", "unnamed_album": "Album sin nombre", @@ -1298,14 +1303,14 @@ "unselect_all": "Limpiar selección", "unselect_all_duplicates": "Deseleccionar todos los duplicados", "unstack": "Desapilar", - "unstacked_assets_count": "Sin apilar {count, plural, one {# asset} other {# assets}}", + "unstacked_assets_count": "Desapilado(s) {count, plural, one {# elemento} other {# elementos}}", "untracked_files": "Archivos no monitorizados", "untracked_files_decription": "Estos archivos no están siendo monitorizados por la aplicación. Es posible que sean resultado de errores al moverlos, cargas interrumpidas o por un fallo de la aplicación", "up_next": "A continuación", "updated_password": "Contraseña actualizada", "upload": "Subir", "upload_concurrency": "Cargas simultáneas", - "upload_errors": "Carga completada con {count, plural, one {# error} other {# errors}}, actualice la página para ver los nuevos recursos de carga.", + "upload_errors": "Carga completada con {count, plural, one {# error} other {# errores}}, actualice la página para ver los nuevos recursos de carga.", "upload_progress": "Restante {remaining, number} - Procesado {processed, number}/{total, number}", "upload_skipped_duplicates": "Saltado {count, plural, one {# duplicate asset} other {# duplicate assets}}", "upload_status_duplicates": "Duplicados", @@ -1347,14 +1352,14 @@ "view_previous_asset": "Mostrar elemento anterior", "view_stack": "Ver Pila", "viewer": "Visualizador", - "visibility_changed": "Visibilidad cambiada para {count, plural, one {# person} other {# people}}", + "visibility_changed": "Visibilidad cambiada para {count, plural, one {# persona} other {# personas}}", "waiting": "Esperando", "warning": "Advertencia", "week": "Semana", "welcome": "Bienvenido", "welcome_to_immich": "Bienvenido a immich", "year": "Año", - "years_ago": "Hace {years, plural, one {# year} other {# years}}", + "years_ago": "Hace {years, plural, one {# año} other {# años}}", "yes": "Sí", "you_dont_have_any_shared_links": "No tienes ningún enlace compartido", "zoom_image": "Acercar Imagen" diff --git a/web/src/lib/i18n/fi.json b/web/src/lib/i18n/fi.json index 6d951b93f9e89..15a3dc0a265cb 100644 --- a/web/src/lib/i18n/fi.json +++ b/web/src/lib/i18n/fi.json @@ -140,6 +140,10 @@ "map_style_description": "style.json -karttateeman URL", "metadata_extraction_job": "Kerää metadata", "metadata_extraction_job_description": "Poimi metatiedot aineistoista, kuten GPS ja resoluutio", + "metadata_faces_import_setting": "Ota käyttöön kasvojen tuonti", + "metadata_faces_import_setting_description": "Tuo kasvot kuvan EXIF -tiedoista ja kylkiäistiedostoista", + "metadata_settings": "Metatietoasetukset", + "metadata_settings_description": "Hallitse metatietoja", "migration_job": "Migrointi", "migration_job_description": "Migroi aineiston pikkukuvat ja kasvot uusimpaan kansiorakenteeseen", "no_paths_added": "Polkuja ei asetettu", @@ -963,6 +967,7 @@ "send_message": "Lähetä viesti", "send_welcome_email": "Lähetä tervetuloviesti", "server": "Palvelin", + "server_online": "Palvelin on linjalla", "server_stats": "Palvelimen tilastot", "server_version": "Palvelimen versio", "set": "Aseta", @@ -1113,6 +1118,7 @@ "view_album": "Näytä albumi", "view_all": "Näytä kaikki", "view_all_users": "Näytä kaikki käyttäjät", + "view_in_timeline": "Näytä aikajanalla", "view_links": "Näytä linkit", "view_next_asset": "Näytä seuraava", "view_previous_asset": "Näytä edellinen", diff --git a/web/src/lib/i18n/fr.json b/web/src/lib/i18n/fr.json index 9edcb1fdd2807..9628573b0d30b 100644 --- a/web/src/lib/i18n/fr.json +++ b/web/src/lib/i18n/fr.json @@ -148,7 +148,7 @@ "migration_job_description": "Migration des miniatures pour les médias et les visages vers la dernière structure de dossiers", "no_paths_added": "Aucun chemin n'a été ajouté", "no_pattern_added": "Aucun schéma d'exclusion n'a été ajouté", - "note_apply_storage_label_previous_assets": "Remarque : pour appliquer l'étiquette de stockage à des médias précédemment téléversés, exécutez la commande", + "note_apply_storage_label_previous_assets": "Remarque : pour appliquer l'étiquette de stockage à des médias précédemment envoyés, exécutez la commande", "note_cannot_be_changed_later": "REMARQUE : Il n'est pas possible de modifier ce paramètre ultérieurement !", "note_unlimited_quota": "Note : saisir 0 pour un quota illimité", "notification_email_from_address": "Depuis l'adresse", @@ -228,14 +228,14 @@ "storage_template_hash_verification_enabled": "Vérification du hachage activée", "storage_template_hash_verification_enabled_description": "Active la vérification du hachage, ne désactivez pas cette option à moins d'être sûr de ce que vous faites", "storage_template_migration": "Migration du modèle de stockage", - "storage_template_migration_description": "Appliquer le modèle courant {template} aux médias précédemment téléchargés", - "storage_template_migration_info": "Les changements de modèle ne s'appliqueront qu'aux nouveaux médias. Pour appliquer rétroactivement le modèle aux médias précédemment téléchargés, exécutez la tâche {job}.", + "storage_template_migration_description": "Appliquer le modèle courant {template} aux médias précédemment envoyés", + "storage_template_migration_info": "Les changements de modèle ne s'appliqueront qu'aux nouveaux médias. Pour appliquer rétroactivement le modèle aux médias précédemment envoyés, exécutez la tâche {job}.", "storage_template_migration_job": "Tâche de migration du modèle de stockage", "storage_template_more_details": "Pour plus de détails sur cette fonctionnalité, reportez-vous au Modèle de stockage et à ses implications", "storage_template_onboarding_description": "Lorsqu'elle est activée, cette fonctionnalité réorganise les fichiers basés sur un modèle défini par l'utilisateur. En raison de problèmes de stabilité, la fonction a été désactivée par défaut. Pour plus d'informations, veuillez consulter la documentation.", "storage_template_path_length": "Limite approximative de la longueur du chemin : {length, number}/{limit, number}", "storage_template_settings": "Modèle de stockage", - "storage_template_settings_description": "Gérer la structure des dossiers et le nom des fichiers du média téléversé", + "storage_template_settings_description": "Gérer la structure des dossiers et le nom des fichiers du média envoyé", "storage_template_user_label": "{label} est l'étiquette de stockage de l'utilisateur", "system_settings": "Paramètres du système", "theme_custom_css_settings": "CSS personnalisé", @@ -311,7 +311,7 @@ "trash_settings": "Corbeille", "trash_settings_description": "Gérer les paramètres de la corbeille", "untracked_files": "Fichiers non suivis", - "untracked_files_description": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat d'erreurs de déplacement, téléchargements interrompus, ou abandons en raison d'un bug", + "untracked_files_description": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat d'erreurs de déplacement, d'envois interrompus, ou d'abandons en raison d'un bug", "user_delete_delay": "La suppression définitive du compte et des médias de {user} sera programmée dans {delay, plural, one {# jour} other {# jours}}.", "user_delete_delay_settings": "Délai de suppression", "user_delete_delay_settings_description": "Nombre de jours après la validation pour supprimer définitivement le compte et les médias d'un utilisateur. La suppression des utilisateurs se lance à minuit. Les modifications apportées à ce paramètre seront pris en compte lors de la prochaine exécution.", @@ -366,7 +366,7 @@ "allow_dark_mode": "Autoriser le mode sombre", "allow_edits": "Autoriser les modifications", "allow_public_user_to_download": "Permettre aux utilisateurs non connectés de télécharger", - "allow_public_user_to_upload": "Permettre aux utilisateurs non connectés de téléverser", + "allow_public_user_to_upload": "Permettre l'envoi aux utilisateurs non connectés", "anti_clockwise": "Sens anti-horaire", "api_key": "Clé API", "api_key_description": "Cette valeur ne sera affichée qu'une seule fois. Assurez-vous de la copier avant de fermer la fenêtre.", @@ -391,8 +391,9 @@ "asset_offline": "Média hors ligne", "asset_offline_description": "Ce média est hors ligne. Immich ne peut pas accéder à son emplacement physique. Veuillez vous assurez que le média est disponible, puis relancez l'analyse de la bibliothèque.", "asset_skipped": "Sauté", - "asset_uploaded": "Téléversé", - "asset_uploading": "Chargement...", + "asset_skipped_in_trash": "À la corbeille", + "asset_uploaded": "Envoyé", + "asset_uploading": "Envoi...", "assets": "Médias", "assets_added_count": "{count, plural, one {# média ajouté} other {# médias ajoutés}}", "assets_added_to_album_count": "{count, plural, one {# média ajouté} other {# médias ajoutés}} à l'album", @@ -537,7 +538,7 @@ "download_settings_description": "Gérer les paramètres de téléchargement des médias", "downloading": "Téléchargement", "downloading_asset_filename": "Téléchargement du média {filename}", - "drop_files_to_upload": "Déposer des fichiers n'importe où pour téléverser", + "drop_files_to_upload": "Déposez les fichiers n'importe où pour envoyer", "duplicates": "Doublons", "duplicates_description": "Examiner chaque groupe et indiquer s'il y a des doublons", "duration": "Durée", @@ -613,7 +614,7 @@ "failed_to_remove_product_key": "Échec de suppression de la clé du produit", "failed_to_stack_assets": "Impossible d'empiler les médias", "failed_to_unstack_assets": "Impossible de dépiler les médias", - "import_path_already_exists": "Ce chemin d'import existe déjà.", + "import_path_already_exists": "Ce chemin d'importation existe déjà.", "incorrect_email_or_password": "Courriel ou mot de passe incorrect", "paths_validation_failed": "Validation échouée pour {paths, plural, one {# un chemin} other {# plusieurs chemins}}", "profile_picture_transparent_pixels": "Les images de profil ne peuvent pas avoir de pixels transparents. Veuillez agrandir et/ou déplacer l'image.", @@ -623,7 +624,7 @@ "unable_to_add_assets_to_shared_link": "Impossible d'ajouter des médias au lien partagé", "unable_to_add_comment": "Impossible d'ajouter un commentaire", "unable_to_add_exclusion_pattern": "Impossible d'ajouter un schéma d'exclusion", - "unable_to_add_import_path": "Impossible d'ajouter un chemin d'import", + "unable_to_add_import_path": "Impossible d'ajouter le chemin d'importation", "unable_to_add_partners": "Impossible d'ajouter des partenaires", "unable_to_add_remove_archive": "Impossible {archived, select, true {de supprimer des médias de} other {d'ajouter des médias à}} l'archive", "unable_to_add_remove_favorites": "Impossible {favorite, select, true {d'ajouter des médias aux} other {de supprimer des médias des}} favoris", @@ -648,18 +649,19 @@ "unable_to_delete_asset": "Suppression du média impossible", "unable_to_delete_assets": "Erreur lors de la suppression des médias", "unable_to_delete_exclusion_pattern": "Suppression du modèle d'exclusion impossible", - "unable_to_delete_import_path": "Suppression du chemin d'import impossible", + "unable_to_delete_import_path": "Suppression du chemin d'importation impossible", "unable_to_delete_shared_link": "Suppression du lien de partage impossible", "unable_to_delete_user": "Suppression de l'utilisateur impossible", "unable_to_download_files": "Impossible de télécharger les fichiers", "unable_to_edit_exclusion_pattern": "Modification du modèle d'exclusion impossible", - "unable_to_edit_import_path": "Modification du chemin d'import impossible", + "unable_to_edit_import_path": "Modification du chemin d'importation impossible", "unable_to_empty_trash": "Impossible de vider la corbeille", "unable_to_enter_fullscreen": "Mode plein écran indisponible", "unable_to_exit_fullscreen": "Sortie du mode plein écran impossible", "unable_to_get_comments_number": "Impossible d'obtenir le nombre de commentaires", "unable_to_get_shared_link": "Échec de la récupération du lien partagé", "unable_to_hide_person": "Impossible de cacher la personne", + "unable_to_link_motion_video": "Impossible de lier la photo animée", "unable_to_link_oauth_account": "Impossible de lier le compte OAuth", "unable_to_load_album": "Impossible de charger l'album", "unable_to_load_asset_activity": "Impossible de charger l'activité du média", @@ -700,6 +702,7 @@ "unable_to_submit_job": "Impossible d'exécuter la tâche", "unable_to_trash_asset": "Impossible de mettre le média à la corbeille", "unable_to_unlink_account": "Impossible de détacher le compte", + "unable_to_unlink_motion_video": "Impossible de détacher la photo animée", "unable_to_update_album_cover": "Impossible de mettre à jour la couverture de l'album", "unable_to_update_album_info": "Impossible de mettre à jour les informations de l'album", "unable_to_update_library": "Impossible de mettre à jour la bibliothèque", @@ -707,7 +710,7 @@ "unable_to_update_settings": "Impossible de mettre à jour les paramètres", "unable_to_update_timeline_display_status": "Impossible de mettre à jour le statut d'affichage de la timeline", "unable_to_update_user": "Impossible de mettre à jour l'utilisateur", - "unable_to_upload_file": "Impossible de téléverser le fichier" + "unable_to_upload_file": "Impossible d'envoyer le fichier" }, "every_day_at_onepm": "", "every_night_at_midnight": "", @@ -845,6 +848,7 @@ "license_trial_info_4": "Pensez à acheter une licence pour soutenir le développement du service", "light": "Clair", "like_deleted": "Réaction « j'aime » supprimée", + "link_motion_video": "Lier la photo animée", "link_options": "Options de lien", "link_to_oauth": "Lien au service OAuth", "linked_oauth_account": "Compte OAuth rattaché", @@ -913,10 +917,10 @@ "no_albums_with_name_yet": "Il semble que vous n'ayez pas encore d'albums avec ce nom.", "no_albums_yet": "Il semble que vous n'ayez pas encore d'album.", "no_archived_assets_message": "Archiver des photos et vidéos pour les masquer dans votre bibliothèque", - "no_assets_message": "CLIQUER ICI POUR IMPORTER VOTRE PREMIÈRE PHOTO", + "no_assets_message": "CLIQUER ICI POUR ENVOYER VOTRE PREMIÈRE PHOTO", "no_duplicates_found": "Aucun doublon n'a été trouvé.", "no_exif_info_available": "Aucune information exif disponible", - "no_explore_results_message": "Importer plus de photos pour explorer votre collection.", + "no_explore_results_message": "Envoyez plus de photos pour explorer votre collection.", "no_favorites_message": "Ajouter des photos et vidéos à vos favoris pour les retrouver plus rapidement", "no_libraries_message": "Créer une bibliothèque externe pour voir vos photos et vidéos dans un autre espace de stockage", "no_name": "Pas de nom", @@ -925,7 +929,7 @@ "no_results_description": "Essayez un synonyme ou un mot-clé plus général", "no_shared_albums_message": "Créer un album pour partager vos photos et vidéos avec les personnes de votre réseau", "not_in_any_album": "Dans aucun album", - "note_apply_storage_label_to_previously_uploaded assets": "Note : Pour appliquer l'étiquette de stockage aux médias déjà importés, lancer la", + "note_apply_storage_label_to_previously_uploaded assets": "Note : Pour appliquer l'étiquette de stockage aux médias déjà envoyés, lancer la", "note_unlimited_quota": "Note : Saisir 0 pour définir un quota illimité", "notes": "Notes", "notification_toggle_setting_description": "Activer les notifications par courriel", @@ -1134,6 +1138,7 @@ "search_for_existing_person": "Rechercher une personne existante", "search_no_people": "Aucune personne", "search_no_people_named": "Aucune personne nommée « {name} »", + "search_options": "Rechercher une option", "search_people": "Rechercher une personne", "search_places": "Rechercher un lieu", "search_state": "Rechercher par état/région...", @@ -1288,6 +1293,7 @@ "unknown_album": "", "unknown_year": "Année inconnue", "unlimited": "Illimité", + "unlink_motion_video": "Détacher la photo animée", "unlink_oauth": "Déconnecter OAuth", "unlinked_oauth_account": "Compte OAuth non connecté", "unnamed_album": "Album sans nom", @@ -1299,18 +1305,18 @@ "unstack": "Désempiler", "unstacked_assets_count": "{count, plural, one {# média dépilé} other {# médias dépilés}}", "untracked_files": "Fichiers non suivis", - "untracked_files_decription": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat de déplacements échoués, de téléchargements interrompus ou laissés pour compte à cause d'un bug", + "untracked_files_decription": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat de déplacements échoués, d'envois interrompus ou laissés pour compte à cause d'un bug", "up_next": "Suite", "updated_password": "Mot de passe mis à jour", - "upload": "Téléverser", - "upload_concurrency": "Envoi simultané", - "upload_errors": "Le téléversement s'est achevé avec {count, plural, one {# erreur} other {# erreurs}}. Rafraîchir la page pour voir les nouveaux médias téléversés.", + "upload": "Envoyer", + "upload_concurrency": "Envois simultanés", + "upload_errors": "L'envoi s'est achevé avec {count, plural, one {# erreur} other {# erreurs}}. Rafraîchir la page pour voir les nouveaux médias envoyés.", "upload_progress": "{remaining, number} restant(s) - {processed, number} traité(s)/{total, number}", "upload_skipped_duplicates": "{count, plural, one {# doublon ignoré} other {# doublons ignorés}}", "upload_status_duplicates": "Doublons", "upload_status_errors": "Erreurs", - "upload_status_uploaded": "Téléversé", - "upload_success": "Téléversement réussi. Rafraîchir la page pour voir les nouveaux médias téléversés.", + "upload_status_uploaded": "Envoyé", + "upload_success": "Envoi réussi. Rafraîchir la page pour voir les nouveaux médias envoyés.", "url": "URL", "usage": "Utilisation", "use_custom_date_range": "Utilisez une plage de date personnalisée à la place", diff --git a/web/src/lib/i18n/he.json b/web/src/lib/i18n/he.json index e2b836256801d..05eab7a804519 100644 --- a/web/src/lib/i18n/he.json +++ b/web/src/lib/i18n/he.json @@ -139,7 +139,11 @@ "map_settings_description": "נהל הגדרות מפה", "map_style_description": "כתובת אתר לערכת נושא של מפה style.json", "metadata_extraction_job": "חלץ מטא-נתונים", - "metadata_extraction_job_description": "חלץ מידע מטא-נתונים מכל נכס, כגון GPS ורזולוציה", + "metadata_extraction_job_description": "חלץ מידע מטא-נתונים מכל נכס, כגון GPS, פנים ורזולוציה", + "metadata_faces_import_setting": "אפשר יבוא פנים", + "metadata_faces_import_setting_description": "יבא פנים מנתוני EXIF של תמונה ומקבצים נלווים", + "metadata_settings": "הגדרות מטא-נתונים", + "metadata_settings_description": "נהל הגדרות מטא-נתונים", "migration_job": "העברה", "migration_job_description": "העבר תמונות ממוזערות של נכסים ופנים למבנה התיקיות העדכני ביותר", "no_paths_added": "לא נוספו נתיבים", @@ -387,6 +391,7 @@ "asset_offline": "נכס לא מקוון", "asset_offline_description": "הנכס הזה אינו מקוון. Immich לא יכול לגשת למיקום הקובץ שלו. נא לוודא שהנכס זמין ואז סרוק מחדש את הספרייה.", "asset_skipped": "דילג", + "asset_skipped_in_trash": "באשפה", "asset_uploaded": "הועלה", "asset_uploading": "מעלה...", "assets": "נכסים", @@ -656,6 +661,7 @@ "unable_to_get_comments_number": "לא ניתן להשיג את מספר התגובות", "unable_to_get_shared_link": "קבלת קישור משותף נכשלה", "unable_to_hide_person": "לא ניתן להסתיר אדם", + "unable_to_link_motion_video": "לא ניתן לקשר סרטון תנועה", "unable_to_link_oauth_account": "לא ניתן לקשר חשבון OAuth", "unable_to_load_album": "לא ניתן לטעון אלבום", "unable_to_load_asset_activity": "לא ניתן לטעון את פעילות הנכס", @@ -696,6 +702,7 @@ "unable_to_submit_job": "לא ניתן לשלוח משימה", "unable_to_trash_asset": "לא ניתן להעביר נכס לאשפה", "unable_to_unlink_account": "לא ניתן לבטל קישור חשבון", + "unable_to_unlink_motion_video": "לא ניתן לבטל קישור סרטון תנועה", "unable_to_update_album_cover": "לא ניתן לעדכן עטיפת אלבום", "unable_to_update_album_info": "לא ניתן לעדכן פרטי אלבום", "unable_to_update_library": "לא ניתן לעדכן ספרייה", @@ -841,6 +848,7 @@ "license_trial_info_4": "אנא שקול לרכוש רישיון כדי לתמוך בפיתוח המתמשך של השירות", "light": "בהיר", "like_deleted": "לייק נמחק", + "link_motion_video": "קשר סרטון תנועה", "link_options": "אפשרויות קישור", "link_to_oauth": "קישור ל-OAuth", "linked_oauth_account": "חשבון OAuth מקושר", @@ -1130,6 +1138,7 @@ "search_for_existing_person": "חפש אדם קיים", "search_no_people": "אין אנשים", "search_no_people_named": "אין אנשים בשם \"{name}\"", + "search_options": "אפשרויות חיפוש", "search_people": "חפש אנשים", "search_places": "חפש מקומות", "search_state": "חפש מדינה...", @@ -1208,6 +1217,8 @@ "sign_up": "הרשמה", "size": "גודל", "skip_to_content": "דלג לתוכן", + "skip_to_folders": "דלג לתיקיות", + "skip_to_tags": "דלג לתגים", "slideshow": "מצגת שקופיות", "slideshow_settings": "הגדרות מצגת שקופיות", "sort_albums_by": "מיין אלבומים לפי...", @@ -1282,6 +1293,7 @@ "unknown_album": "אלבום לא ידוע", "unknown_year": "שנה לא ידועה", "unlimited": "בלתי מוגבל", + "unlink_motion_video": "בטל קישור סרטון תנועה", "unlink_oauth": "בטל קישור OAuth", "unlinked_oauth_account": "בוטל קישור חשבון OAuth", "unnamed_album": "אלבום ללא שם", diff --git a/web/src/lib/i18n/hr.json b/web/src/lib/i18n/hr.json index 16d08bbfca211..954eeff202d6a 100644 --- a/web/src/lib/i18n/hr.json +++ b/web/src/lib/i18n/hr.json @@ -27,10 +27,11 @@ "added_to_favorites": "Dodano u omiljeno", "added_to_favorites_count": "Dodano {count, number} u omiljeno", "admin": { - "add_exclusion_pattern_description": "", + "add_exclusion_pattern_description": "Dodajte uzorke izuzimanja. Globiranje pomoću *, ** i ? je podržano. Za ignoriranje svih datoteka u bilo kojem direktoriju pod nazivom \"Raw\", koristite \"**/Raw/**\". Da biste zanemarili sve datoteke koje završavaju na \".tif\", koristite \"**/*.tif\". Da biste zanemarili apsolutni put, koristite \"/path/to/ignore/**\".", "authentication_settings": "Postavke autentikacije", "authentication_settings_description": "Uredi lozinku, OAuth, i druge postavke autentikacije", "authentication_settings_disable_all": "Jeste li sigurni da želite onemogućenit sve načine prijave? Prijava će biti potpuno onemogućena.", + "authentication_settings_reenable": "Za ponovno uključivanje upotrijebite naredbu poslužitelja.", "background_task_job": "Pozadinski zadaci", "check_all": "Provjeri sve", "cleared_jobs": "Izbrisani poslovi za: {job}", @@ -72,8 +73,8 @@ "job_settings": "Postavke posla", "job_settings_description": "Upravljajte istovremenošću poslova", "job_status": "Status posla", - "jobs_delayed": "", - "jobs_failed": "", + "jobs_delayed": "{jobCount, plural, other {# delayed}}", + "jobs_failed": "{jobCount, plural, other {# failed}}", "library_created": "Stvorena biblioteka: {library}", "library_cron_expression": "Cron izraz", "library_cron_expression_description": "Postavite interval skeniranja koristeći cron format. Za više informacija pogledajte npr. Crontab Guru", @@ -96,8 +97,8 @@ "machine_learning_clip_model_description": "Naziv CLIP modela navedenog ovdje. Imajte na umu da morate ponovno pokrenuti posao 'Pametno Pretraživanje' za sve slike nakon promjene modela.", "machine_learning_duplicate_detection": "Detekcija Duplikata", "machine_learning_duplicate_detection_enabled": "Omogući detekciju duplikata", - "machine_learning_duplicate_detection_enabled_description": "", - "machine_learning_duplicate_detection_setting_description": "", + "machine_learning_duplicate_detection_enabled_description": "Ako je onemogućeno, potpuno identična sredstva i dalje će biti deduplicirana.", + "machine_learning_duplicate_detection_setting_description": "Upotrijebite CLIP ugradnje da biste pronašli vjerojatne duplikate", "machine_learning_enabled": "Uključi strojsko učenje", "machine_learning_enabled_description": "Ukoliko je ovo isključeno, sve funkcije strojnoga učenja biti će isključene bez obzira na postavke ispod.", "machine_learning_facial_recognition": "Detekcija lica", @@ -138,6 +139,10 @@ "map_style_description": "URL na style.json temu karte", "metadata_extraction_job": "Izdvoj metapodatke", "metadata_extraction_job_description": "Izdvojite podatke o metapodacima iz svakog sredstva, kao što su GPS i rezolucija", + "metadata_faces_import_setting": "Omogući uvoz lica", + "metadata_faces_import_setting_description": "Uvezite lica iz EXIF podataka slike i sidecar datoteka", + "metadata_settings": "Postavke Metapodataka", + "metadata_settings_description": "Upravljanje postavkama metapodataka", "migration_job": "Migracija", "migration_job_description": "Premjestite minijature za sredstva i lica u najnoviju strukturu mapa", "no_paths_added": "Nema dodanih putanja", @@ -171,18 +176,20 @@ "oauth_enable_description": "Prijavite se putem OAutha", "oauth_issuer_url": "URL Izdavatelja", "oauth_mobile_redirect_uri": "Mobilnog Preusmjeravanja URI", - "oauth_mobile_redirect_uri_override": "", - "oauth_mobile_redirect_uri_override_description": "", - "oauth_scope": "", + "oauth_mobile_redirect_uri_override": "Nadjačavanje URI-preusmjeravanja za mobilne uređaje", + "oauth_mobile_redirect_uri_override_description": "Omogući kada pružatelj OAuth ne dopušta mobilni URI, poput '{callback}'", + "oauth_profile_signing_algorithm": "Algoritam za potpisivanje profila", + "oauth_profile_signing_algorithm_description": "Algoritam koji se koristi za potpisivanje korisničkog profila.", + "oauth_scope": "Opseg", "oauth_settings": "OAuth", "oauth_settings_description": "Upravljanje postavkama za prijavu kroz OAuth", "oauth_settings_more_details": "Za više pojedinosti o ovoj značajci pogledajte uputstva.", - "oauth_signing_algorithm": "", - "oauth_storage_label_claim": "", - "oauth_storage_label_claim_description": "", - "oauth_storage_quota_claim": "", - "oauth_storage_quota_claim_description": "", - "oauth_storage_quota_default": "", + "oauth_signing_algorithm": "Algoritam potpisivanja", + "oauth_storage_label_claim": "Potraživanje oznake za pohranu", + "oauth_storage_label_claim_description": "Automatski postavite korisničku oznaku za pohranu na vrijednost ovog zahtjeva.", + "oauth_storage_quota_claim": "Zahtjev za kvotom pohrane", + "oauth_storage_quota_claim_description": "Automatski postavite korisničku kvotu pohrane na vrijednost ovog zahtjeva.", + "oauth_storage_quota_default": "Zadana kvota pohrane (GiB)", "oauth_storage_quota_default_description": "Kvota u GiB koja će se koristiti kada nema zahtjeva (unesite 0 za neograničenu kvotu).", "offline_paths": "Izvanmrežne putanje", "offline_paths_description": "Ovi rezultati mogu biti posljedica ručnog brisanja datoteka koje nisu dio vanjske biblioteke.", @@ -196,8 +203,8 @@ "registration_description": "Budući da ste prvi korisnik na sustavu, bit ćete dodijeljeni administratorsku ulogu i odgovorni ste za administrativne poslove, a dodatne korisnike kreirat ćete sami.", "removing_offline_files": "Uklanjanje izvanmrežnih datoteka", "repair_all": "Popravi sve", - "repair_matched_items": "", - "repaired_items": "", + "repair_matched_items": "Podudaranje {count, plural, one {# item} other {# items}}", + "repaired_items": "Popravljeno {count, plural, one {# item} other {# items}}", "require_password_change_on_login": "Zahtijevajte od korisnika promjenu lozinke pri prvoj prijavi", "reset_settings_to_default": "Vrati postavke na zadane", "reset_settings_to_recent_saved": "Resetirajte postavke na nedavno spremljene postavke", @@ -210,25 +217,33 @@ "server_settings_description": "Upravljanje postavkama servera", "server_welcome_message": "Poruka dobrodošlice", "server_welcome_message_description": "Poruka koja je prikazana na prijavi.", - "sidecar_job": "", - "sidecar_job_description": "", - "slideshow_duration_description": "", - "smart_search_job_description": "", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration": "", - "storage_template_migration_job": "", - "storage_template_settings": "", - "storage_template_settings_description": "", - "system_settings": "", + "sidecar_job": "Sidecar metapodaci", + "sidecar_job_description": "Otkrijte ili sinkronizirajte sidecar metapodatke iz datotečnog sustava", + "slideshow_duration_description": "Broj sekundi za prikaz svake slike", + "smart_search_job_description": "Pokrenite strojno učenje na sredstvima za podršku pametnog pretraživanja", + "storage_template_date_time_description": "Vremenska oznaka stvaranja sredstva koristi se za informacije o datumu i vremenu", + "storage_template_date_time_sample": "Vrijeme uzorka {date}", + "storage_template_enable_description": "Omogući mehanizam predloška za pohranu", + "storage_template_hash_verification_enabled": "Omogućena hash provjera", + "storage_template_hash_verification_enabled_description": "Omogućuje hash provjeru, nemojte je onemogućiti osim ako niste sigurni u implikacije", + "storage_template_migration": "Migracija predloška za pohranu", + "storage_template_migration_description": "Primijenite trenutni {template} na prethodno prenesena sredstva", + "storage_template_migration_info": "Promjene predloška primjenjivat će se samo na nova sredstva. Za retroaktivnu primjenu predloška na prethodno prenesena sredstva, pokrenite {job}.", + "storage_template_migration_job": "Posao Migracije Predloška Pohrane", + "storage_template_more_details": "Za više pojedinosti o ovoj značajci pogledajte Predložak pohrane i njegove implikacije", + "storage_template_onboarding_description": "Kada je omogućena, ova će značajka automatski organizirati datoteke na temelju korisnički definiranog predloška. Zbog problema sa stabilnošću značajka je isključena prema zadanim postavkama. Za više informacija pogledajte dokumentaciju.", + "storage_template_path_length": "Približno ograničenje duljine putanje: {length, number}/{limit, number}", + "storage_template_settings": "Predložak pohrane", + "storage_template_settings_description": "Upravljajte strukturom mape i nazivom datoteke učitanog sredstva", + "storage_template_user_label": "{label} je korisnička oznaka za pohranu", + "system_settings": "Postavke Sustava", "theme_custom_css_settings": "Prilagođeni CSS", "theme_custom_css_settings_description": "Kaskadni listovi stilova (CSS) omogućuju prilagođavanje dizajna Immicha.", "theme_settings": "Postavke tema", "theme_settings_description": "Upravljajte prilagodbom Immich web sučelja", - "these_files_matched_by_checksum": "", + "these_files_matched_by_checksum": "Ove datoteke se podudaraju prema njihovim kontrolnim zbrojevima", "thumbnail_generation_job": "Generirajte sličice", - "thumbnail_generation_job_description": "", + "thumbnail_generation_job_description": "Generirajte velike, male i zamućene sličice za svaki materijal, kao i sličice za svaku osobu", "transcoding_acceleration_api": "API ubrzanja", "transcoding_acceleration_api_description": "API koji će komunicirati s vašim uređajem radi ubrzanja transkodiranja. Ova postavka je 'najveći trud': vratit će se na softversko transkodiranje u slučaju kvara. VP9 može ili ne mora raditi ovisno o vašem hardveru.", "transcoding_acceleration_nvenc": "NVENC (zahtjeva NVIDIA GPU)", @@ -240,201 +255,290 @@ "transcoding_accepted_containers": "Prihvaćeni kontenjeri", "transcoding_accepted_containers_description": "Odaberite koji formati spremnika ne moraju biti remulksirani u MP4. Koristi se samo za određena pravila transkodiranja.", "transcoding_accepted_video_codecs": "Prihvaćeni video kodeci", - "transcoding_accepted_video_codecs_description": "", + "transcoding_accepted_video_codecs_description": "Odaberite koje video kodeke nije potrebno transkodirati. Koristi se samo za određena pravila transkodiranja.", "transcoding_advanced_options_description": "Postavke većina korisnika ne treba mjenjati", "transcoding_audio_codec": "Audio kodek", "transcoding_audio_codec_description": "Opus je opcija s najvećom kvalitetom, no ima manju podršku s starim uređajima i softverima.", "transcoding_bitrate_description": "Videozapisi veći od maksimalne brzine prijenosa ili nisu u prihvatljivom formatu", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", - "transcoding_hardware_acceleration": "", - "transcoding_hardware_acceleration_description": "", - "transcoding_hardware_decoding": "", - "transcoding_hardware_decoding_setting_description": "", + "transcoding_codecs_learn_more": "Da biste saznali više o terminologiji koja se ovdje koristi, pogledajte FFmpeg dokumentaciju za H.264 kodek, HEVC kodek i VP9 kodek.", + "transcoding_constant_quality_mode": "Način stalne kvalitete", + "transcoding_constant_quality_mode_description": "ICQ je bolji od CQP-a, ali neki uređaji za hardversko ubrzanje ne podržavaju ovaj način rada. Postavljanje ove opcije daje prednost navedenom načinu rada kada se koristi kodiranje temeljeno na kvaliteti. NVENC je zanemaren jer ne podržava ICQ.", + "transcoding_constant_rate_factor": "Faktor konstantne stope (-crf)", + "transcoding_constant_rate_factor_description": "Razina kvalitete videa. Uobičajene vrijednosti su 23 za H.264, 28 za HEVC, 31 za VP9 i 35 za AV1. Niže je bolje, ali stvara veće datoteke.", + "transcoding_disabled_description": "Nemojte transkodirati nijedan videozapis, može prekinuti reprodukciju na nekim klijentima", + "transcoding_hardware_acceleration": "Hardversko Ubrzanje", + "transcoding_hardware_acceleration_description": "Eksperimentalno; puno brže, ali će imati nižu kvalitetu pri istoj bitrate postavci", + "transcoding_hardware_decoding": "Hardversko dekodiranje", + "transcoding_hardware_decoding_setting_description": "Odnosi se samo na NVENC, QSV i RKMPP. Omogućuje ubrzanje s kraja na kraj umjesto samo ubrzavanja kodiranja. Možda neće raditi na svim videozapisima.", "transcoding_hevc_codec": "HEVC kodek", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", + "transcoding_max_b_frames": "Maksimalni B-frameovi", + "transcoding_max_b_frames_description": "Više vrijednosti poboljšavaju učinkovitost kompresije, ali usporavaju kodiranje. Možda nije kompatibilan s hardverskim ubrzanjem na starijim uređajima. 0 onemogućuje B-frameove, dok -1 automatski postavlja ovu vrijednost.", "transcoding_max_bitrate": "Maksimalne brzina prijenosa (bitrate)", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_tone_mapping_npl": "", - "transcoding_tone_mapping_npl_description": "", - "transcoding_transcode_policy": "", - "transcoding_transcode_policy_description": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", - "transcoding_video_codec": "", - "transcoding_video_codec_description": "", - "trash_enabled_description": "", - "trash_number_of_days": "", - "trash_number_of_days_description": "", - "trash_settings": "", - "trash_settings_description": "", - "untracked_files": "", - "untracked_files_description": "", - "user_delete_delay_settings": "", - "user_delete_delay_settings_description": "", - "user_management": "", - "user_password_has_been_reset": "", - "user_password_reset_description": "", - "user_settings": "", - "user_settings_description": "", - "user_successfully_removed": "", - "version_check_enabled_description": "", - "version_check_settings": "", - "version_check_settings_description": "", - "video_conversion_job": "", - "video_conversion_job_description": "" + "transcoding_max_bitrate_description": "Postavljanje maksimalne brzine prijenosa može učiniti veličine datoteka predvidljivijima uz manji trošak za kvalitetu. Pri 720p, tipične vrijednosti su 2600k za VP9 ili HEVC ili 4500k za H.264. Onemogućeno ako je postavljeno na 0.", + "transcoding_max_keyframe_interval": "Maksimalni interval ključnih sličica", + "transcoding_max_keyframe_interval_description": "Postavlja maksimalnu udaljenost slika između ključnih kadrova. Niže vrijednosti pogoršavaju učinkovitost kompresije, ali poboljšavaju vrijeme traženja i mogu poboljšati kvalitetu u scenama s brzim kretanjem. 0 automatski postavlja ovu vrijednost.", + "transcoding_optimal_description": "Videozapisi koji su veći od ciljne rezolucije ili nisu u prihvatljivom formatu", + "transcoding_preferred_hardware_device": "Preferirani hardverski uređaj", + "transcoding_preferred_hardware_device_description": "Odnosi se samo na VAAPI i QSV. Postavlja dri node koji se koristi za hardversko transkodiranje.", + "transcoding_preset_preset": "Preset (-preset)", + "transcoding_preset_preset_description": "Brzina kompresije. Sporije postavke proizvode manje datoteke i povećavaju kvalitetu pri ciljanju određene postavke bitratea. VP9 zanemaruje brzine iznad 'brže'.", + "transcoding_reference_frames": "Referentne slike", + "transcoding_reference_frames_description": "Broj slika za referencu prilikom komprimiranja određene slike. Više vrijednosti poboljšavaju učinkovitost kompresije, ali usporavaju kodiranje. 0 automatski postavlja ovu vrijednost.", + "transcoding_required_description": "Samo videozapisi koji nisu u prihvaćenom formatu", + "transcoding_settings": "Postavke Video Transkodiranja", + "transcoding_settings_description": "Upravljajte informacijama o razlučivosti i kodiranju video datoteka", + "transcoding_target_resolution": "Ciljana rezolucija", + "transcoding_target_resolution_description": "Veće razlučivosti mogu sačuvati više detalja, ali trebaju dulje za kodiranje, imaju veće veličine datoteka i mogu smanjiti odziv aplikacije.", + "transcoding_temporal_aq": "Vremenski AQ", + "transcoding_temporal_aq_description": "Odnosi se samo na NVENC. Povećava kvalitetu scena s puno detalja i malo pokreta. Možda nije kompatibilan sa starijim uređajima.", + "transcoding_threads": "Sljedovi (Threads)", + "transcoding_threads_description": "Više vrijednosti dovode do bržeg kodiranja, ali ostavljaju manje prostora poslužitelju za obradu drugih zadataka dok je aktivan. Ova vrijednost ne smije biti veća od broja CPU jezgri. Maksimalno povećava iskorištenje ako je postavljeno na 0.", + "transcoding_tone_mapping": "Tonsko preslikavanje", + "transcoding_tone_mapping_description": "Pokušava sačuvati izgled HDR videozapisa kada se pretvori u SDR. Svaki algoritam čini različite kompromise za boju, detalje i svjetlinu. Hable čuva detalje, Mobius čuva boju, a Reinhard svjetlinu.", + "transcoding_tone_mapping_npl": "Tone-mapping NPL", + "transcoding_tone_mapping_npl_description": "Boje će se prilagoditi tako da izgledaju normalno za zaslon ove svjetline. Suprotno intuiciji, niže vrijednosti povećavaju svjetlinu videa i obrnuto budući da kompenziraju svjetlinu zaslona. 0 automatski postavlja ovu vrijednost.", + "transcoding_transcode_policy": "Pravila transkodiranja", + "transcoding_transcode_policy_description": "Pravila o tome kada se video treba transkodirati. HDR videozapisi uvijek će biti transkodirani (osim ako je transkodiranje onemogućeno).", + "transcoding_two_pass_encoding": "Kodiranje u dva prolaza", + "transcoding_two_pass_encoding_setting_description": "Transkodiranje u dva prolaza za proizvodnju bolje kodiranih videozapisa. Kada je omogućena maksimalna brzina prijenosa (potrebna za rad s H.264 i HEVC), ovaj način rada koristi raspon brzine prijenosa na temelju maksimalne brzine prijenosa i zanemaruje CRF. Za VP9, CRF se može koristiti ako je maksimalna brzina prijenosa onemogućena.", + "transcoding_video_codec": "Video Kodek", + "transcoding_video_codec_description": "VP9 ima visoku učinkovitost i web-kompatibilnost, ali treba dulje za transkodiranje. HEVC ima sličnu izvedbu, ali ima slabiju web kompatibilnost. H.264 široko je kompatibilan i brzo se transkodira, ali proizvodi mnogo veće datoteke. AV1 je najučinkovitiji kodek, ali nema podršku na starijim uređajima.", + "trash_enabled_description": "Omogućite značajke Smeća", + "trash_number_of_days": "Broj dana", + "trash_number_of_days_description": "Broj dana za držanje sredstava u smeću prije njihovog trajnog uklanjanja", + "trash_settings": "Postavke Smeća", + "trash_settings_description": "Upravljanje postavkama smeća", + "untracked_files": "Nepraćene datoteke", + "untracked_files_description": "Aplikacija ne prati ove datoteke. Mogu biti rezultat neuspjelih premještanja, prekinutih prijenosa ili izostale zbog pogreške", + "user_delete_delay": "Račun i sredstva korisnika {user} bit će zakazani za trajno brisanje za {delay, plural, one {# day} other {# days}}.", + "user_delete_delay_settings": "Brisanje odgode", + "user_delete_delay_settings_description": "Broj dana nakon uklanjanja za trajno brisanje korisničkog računa i imovine. Posao brisanja korisnika pokreće se u ponoć kako bi se provjerili korisnici koji su spremni za brisanje. Promjene ove postavke bit će procijenjene pri sljedećem izvršavanju.", + "user_delete_immediately": "Račun i sredstva korisnika {user} bit će stavljeni u red čekanja za trajno brisanje odmah.", + "user_delete_immediately_checkbox": "Stavite korisnika i imovinu u red za trenutačno brisanje", + "user_management": "Upravljanje Korisnicima", + "user_password_has_been_reset": "Korisnička lozinka je poništena:", + "user_password_reset_description": "Molimo dostavite privremenu lozinku korisniku i obavijestite ga da će morati promijeniti lozinku pri sljedećoj prijavi.", + "user_restore_description": "Račun korisnika {user} bit će vraćen.", + "user_restore_scheduled_removal": "Vrati korisnika - zakazano uklanjanje {date, date, long}", + "user_settings": "Korisničke Postavke", + "user_settings_description": "Upravljanje korisničkim postavkama", + "user_successfully_removed": "Korisnik {email} je uspješno uklonjen.", + "version_check_enabled_description": "Omogući provjeru verzije", + "version_check_implications": "Značajka provjere verzije oslanja se na periodičnu komunikaciju s github.com", + "version_check_settings": "Provjera Verzije", + "version_check_settings_description": "Omogućite/onemogućite obavijest o novoj verziji", + "video_conversion_job": "Transkodiranje videozapisa", + "video_conversion_job_description": "Transkodiranje videozapisa za veću kompatibilnost s preglednicima i uređajima" }, - "admin_email": "", - "admin_password": "", - "administration": "", - "advanced": "", - "album_added": "", - "album_added_notification_setting_description": "", - "album_cover_updated": "", - "album_info_updated": "", - "album_name": "", - "album_options": "", - "album_updated": "", - "album_updated_setting_description": "", - "albums": "", - "albums_count": "", - "all": "", - "all_people": "", - "allow_dark_mode": "", - "allow_edits": "", - "api_key": "", - "api_keys": "", - "app_settings": "", - "appears_in": "", - "archive": "", - "archive_or_unarchive_photo": "", + "admin_email": "E-pošta administratora", + "admin_password": "Admin Lozinka", + "administration": "Administracija", + "advanced": "Napredno", + "age_months": "Dob {months, plural, one {# month} other {# months}}", + "age_year_months": "Dob 1 godina, {months, plural, one {# month} other {# months}}", + "age_years": "{years, plural, other {Age #}}", + "album_added": "Album dodan", + "album_added_notification_setting_description": "Primite obavijest e-poštom kada ste dodani u dijeljeni album", + "album_cover_updated": "Naslovnica albuma ažurirana", + "album_delete_confirmation": "Jeste li sigurni da želite izbrisati album {album}?", + "album_delete_confirmation_description": "Ako se ovaj album dijeli, drugi korisnici mu više neće moći pristupiti.", + "album_info_updated": "Podaci o albumu ažurirani", + "album_leave": "Napustiti album?", + "album_leave_confirmation": "Jeste li sigurni da želite napustiti {album}?", + "album_name": "Naziv Albuma", + "album_options": "Opcije albuma", + "album_remove_user": "Ukloni korisnika?", + "album_remove_user_confirmation": "Jeste li sigurni da želite ukloniti {user}?", + "album_share_no_users": "Čini se da ste podijelili ovaj album sa svim korisnicima ili nemate nijednog korisnika s kojim biste ga dijelili.", + "album_updated": "Album ažuriran", + "album_updated_setting_description": "Primite obavijest e-poštom kada dijeljeni album ima nova sredstva", + "album_user_left": "Napušten {album}", + "album_user_removed": "Uklonjen {user}", + "album_with_link_access": "Dopusti svima s poveznicom pristup fotografijama i osobama u ovom albumu.", + "albums": "Albumi", + "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albumi}}", + "all": "Sve", + "all_albums": "Svi albumi", + "all_people": "Svi ljudi", + "all_videos": "Svi videi", + "allow_dark_mode": "Dozvoli tamni način", + "allow_edits": "Dozvoli izmjene", + "allow_public_user_to_download": "Dopusti javnom korisniku preuzimanje", + "allow_public_user_to_upload": "Dopusti javnom korisniku učitavanje", + "anti_clockwise": "Suprotno smjeru kazaljke na satu", + "api_key": "API Ključ", + "api_key_description": "Ova će vrijednost biti prikazana samo jednom. Obavezno ju kopirajte prije zatvaranja prozora.", + "api_key_empty": "Naziv vašeg API ključa ne smije biti prazan", + "api_keys": "API Ključevi", + "app_settings": "Postavke Aplikacije", + "appears_in": "Pojavljuje se u", + "archive": "Arhiva", + "archive_or_unarchive_photo": "Arhivirajte ili dearhivirajte fotografiju", + "archive_size": "Veličina arhive", + "archive_size_description": "Konfigurirajte veličinu arhive za preuzimanja (u GiB)", "archived": "", - "asset_offline": "", - "assets": "", - "authorized_devices": "", - "back": "", - "backward": "", - "blurred_background": "", - "camera": "", - "camera_brand": "", - "camera_model": "", - "cancel": "", - "cancel_search": "", - "cannot_merge_people": "", - "cannot_update_the_description": "", + "archived_count": "{count, plural, other {Archived #}}", + "are_these_the_same_person": "Je li ovo ista osoba?", + "are_you_sure_to_do_this": "Jeste li sigurni da to želite učiniti?", + "asset_added_to_album": "Dodano u album", + "asset_adding_to_album": "Dodavanje u album...", + "asset_description_updated": "Opis imovine je ažuriran", + "asset_filename_is_offline": "Sredstvo {filename} je izvan mreže", + "asset_has_unassigned_faces": "Materijal ima nedodijeljena lica", + "asset_hashing": "Hashiranje...", + "asset_offline": "Sredstvo izvan mreže", + "asset_offline_description": "Ovaj materijal je izvan mreže. Immich ne može pristupiti lokaciji datoteke. Provjerite je li sredstvo dostupno, a zatim ponovno skenirajte biblioteku.", + "asset_skipped": "Preskočeno", + "asset_skipped_in_trash": "U smeću", + "asset_uploaded": "Učitano", + "asset_uploading": "Učitavanje...", + "assets": "Sredstva", + "assets_added_count": "Dodano {count, plural, one {# asset} other {# assets}}", + "assets_added_to_album_count": "Dodano {count, plural, one {# asset} other {# assets}} u album", + "assets_added_to_name_count": "Dodano {count, plural, one {# asset} other {# assets}} u {hasName, select, true {{name}} other {new album}}", + "assets_count": "{count, plural, one {# asset} other {# assets}}", + "assets_moved_to_trash_count": "{count, plural, one {# asset} other {# asset}} premješteno u smeće", + "assets_permanently_deleted_count": "Trajno izbrisano {count, plural, one {# asset} other {# assets}}", + "assets_removed_count": "Uklonjeno {count, plural, one {# asset} other {# assets}}", + "assets_restore_confirmation": "Jeste li sigurni da želite vratiti sve svoje resurse bačene u otpad? Ne možete poništiti ovu radnju!", + "assets_restored_count": "Vraćeno {count, plural, one {# asset} other {# assets}}", + "assets_trashed_count": "Bačeno u smeće {count, plural, one {# asset} other {# assets}}", + "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} već dio albuma", + "authorized_devices": "Ovlašteni Uređaji", + "back": "Nazad", + "back_close_deselect": "Natrag, zatvorite ili poništite odabir", + "backward": "Unazad", + "birthdate_saved": "Datum rođenja uspješno spremljen", + "birthdate_set_description": "Datum rođenja se koristi za izračunavanje godina ove osobe u trenutku fotografije.", + "blurred_background": "Zamućena pozadina", + "build": "Sagradi (Build)", + "build_image": "Sagradi (Build) Image", + "bulk_delete_duplicates_confirmation": "Jeste li sigurni da želite skupno izbrisati {count, plural, one {# duplicate asset} other {# duplicate asset}}? Ovo će zadržati najveće sredstvo svake grupe i trajno izbrisati sve druge duplikate. Ne možete poništiti ovu radnju!", + "bulk_keep_duplicates_confirmation": "Jeste li sigurni da želite zadržati {count, plural, one {# duplicate asset} other {# duplicate asset}}? Ovo će riješiti sve duplicirane grupe bez brisanja ičega.", + "bulk_trash_duplicates_confirmation": "Jeste li sigurni da želite na veliko baciti u smeće {count, plural, one {# duplicate asset} other {# duplicate asset}}? Ovo će zadržati najveće sredstvo svake grupe i baciti sve ostale duplikate u smeće.", + "buy": "Kupi Immich", + "camera": "Kamera", + "camera_brand": "Marka kamere", + "camera_model": "Model kamere", + "cancel": "Otkaži", + "cancel_search": "Otkaži pretragu", + "cannot_merge_people": "Nije moguće spojiti osobe", + "cannot_undo_this_action": "Ne možete poništiti ovu radnju!", + "cannot_update_the_description": "Nije moguće ažurirati opis", "cant_apply_changes": "", "cant_get_faces": "", "cant_search_people": "", "cant_search_places": "", - "change_date": "", - "change_expiration_time": "", - "change_location": "", - "change_name": "", - "change_name_successfully": "", - "change_password": "", - "change_your_password": "", - "changed_visibility_successfully": "", - "check_all": "", - "check_logs": "", - "choose_matching_people_to_merge": "", - "city": "", - "clear": "", - "clear_all": "", - "clear_message": "", - "clear_value": "", - "close": "", - "collapse_all": "", - "color_theme": "", - "comment_options": "", - "comments_are_disabled": "", - "confirm": "", - "confirm_admin_password": "", - "confirm_delete_shared_link": "", - "confirm_password": "", - "contain": "", - "context": "", - "continue": "", - "copied_image_to_clipboard": "", - "copied_to_clipboard": "", - "copy_error": "", - "copy_file_path": "", - "copy_image": "", - "copy_link": "", - "copy_link_to_clipboard": "", - "copy_password": "", - "copy_to_clipboard": "", - "country": "", - "cover": "", - "covers": "", - "create": "", - "create_album": "", - "create_library": "", - "create_link": "", - "create_link_to_share": "", - "create_new_person": "", - "create_new_user": "", - "create_user": "", - "created": "", - "current_device": "", - "custom_locale": "", - "custom_locale_description": "", - "dark": "", - "date_after": "", - "date_and_time": "", - "date_before": "", - "date_range": "", - "day": "", - "default_locale": "", - "default_locale_description": "", - "delete": "", - "delete_album": "", - "delete_api_key_prompt": "", - "delete_key": "", - "delete_library": "", - "delete_link": "", - "delete_shared_link": "", - "delete_user": "", - "deleted_shared_link": "", - "description": "", - "details": "", - "direction": "", - "disabled": "", - "disallow_edits": "", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", - "display_options": "", - "display_order": "", - "display_original_photos": "", - "display_original_photos_setting_description": "", - "done": "", - "download": "", - "downloading": "", - "duration": "", + "change_date": "Promjena datuma", + "change_expiration_time": "Promjena vremena isteka", + "change_location": "Promjena lokacije", + "change_name": "Promjena imena", + "change_name_successfully": "Promijena imena uspješna", + "change_password": "Promjena Lozinke", + "change_password_description": "Ovo je ili prvi put da se prijavljujete u sustav ili je poslan zahtjev za promjenom lozinke. Unesite novu lozinku ispod.", + "change_your_password": "Promijenite lozinku", + "changed_visibility_successfully": "Vidljivost je uspješno promijenjena", + "check_all": "Provjeri Sve", + "check_logs": "Provjera Zapisa", + "choose_matching_people_to_merge": "Odaberite odgovarajuće osobe za spajanje", + "city": "Grad", + "clear": "Očisti", + "clear_all": "Očisti sve", + "clear_all_recent_searches": "Izbriši sva nedavna pretraživanja", + "clear_message": "Jasna poruka", + "clear_value": "Očisti vrijednost", + "clockwise": "U smjeru kazaljke na satu", + "close": "Zatvori", + "collapse": "Sažimanje", + "collapse_all": "Sažmi sve", + "color": "Boja", + "color_theme": "Tema boja", + "comment_deleted": "Komentar izbrisan", + "comment_options": "Opcije komentara", + "comments_and_likes": "Komentari i lajkovi", + "comments_are_disabled": "Komentari onemogućeni", + "confirm": "Potvrdi", + "confirm_admin_password": "Potvrdite lozinku administratora", + "confirm_delete_shared_link": "Jeste li sigurni da želite izbrisati ovu zajedničku vezu?", + "confirm_password": "Potvrdite lozinku", + "contain": "Sadrži", + "context": "Kontekst", + "continue": "Nastavi", + "copied_image_to_clipboard": "Slika je kopirana u međuspremnik.", + "copied_to_clipboard": "Kopirano u međuspremnik!", + "copy_error": "Greška kopiranja", + "copy_file_path": "Kopiraj put datoteke", + "copy_image": "Kopiraj Sliku", + "copy_link": "Kopiraj poveznicu", + "copy_link_to_clipboard": "Kopiraj poveznicu u međuspremnik", + "copy_password": "Kopiraj lozinku", + "copy_to_clipboard": "Kopiraj u međuspremnik", + "country": "Država", + "cover": "Naslovnica", + "covers": "Naslovnice", + "create": "Kreiraj", + "create_album": "Kreiraj album", + "create_library": "Kreiraj Biblioteku", + "create_link": "Kreiraj poveznicu", + "create_link_to_share": "Izradite vezu za dijeljenje", + "create_link_to_share_description": "Dopusti svakome s vezom da vidi odabrane fotografije", + "create_new_person": "Stvorite novu osobu", + "create_new_person_hint": "Dodijelite odabrana sredstva novoj osobi", + "create_new_user": "Kreiraj novog korisnika", + "create_tag": "Stvori oznaku", + "create_tag_description": "Napravite novu oznaku. Za ugniježđene oznake unesite punu putanju oznake uključujući kose crte.", + "create_user": "Stvori korisnika", + "created": "Stvoreno", + "current_device": "Trenutačni uređaj", + "custom_locale": "Prilagođena Lokalizacija", + "custom_locale_description": "Formatiranje datuma i brojeva na temelju jezika i regije", + "dark": "Tamno", + "date_after": "Datum nakon", + "date_and_time": "Datum i Vrijeme", + "date_before": "Datum prije", + "date_of_birth_saved": "Datum rođenja uspješno spremljen", + "date_range": "Razdoblje", + "day": "Dan", + "deduplicate_all": "Dedupliciraj Sve", + "default_locale": "Zadana lokalizacija", + "default_locale_description": "Oblikujte datume i brojeve na temelju jezika preglednika", + "delete": "Izbriši", + "delete_album": "Izbriši album", + "delete_api_key_prompt": "Jeste li sigurni da želite izbrisati ovaj API ključ?", + "delete_duplicates_confirmation": "Jeste li sigurni da želite trajno izbrisati ove duplikate?", + "delete_key": "Ključ za brisanje", + "delete_library": "Izbriši knjižnicu", + "delete_link": "Izbriši poveznicu", + "delete_shared_link": "Izbriši dijeljenu poveznicu", + "delete_tag": "Izbriši oznaku", + "delete_tag_confirmation_prompt": "Jeste li sigurni da želite izbrisati oznaku {tagName}?", + "delete_user": "Izbriši korisnika", + "deleted_shared_link": "Izbrisana dijeljena poveznica", + "description": "Opis", + "details": "Detalji", + "direction": "Smjer", + "disabled": "Onemogućeno", + "disallow_edits": "Zabrani izmjene", + "discover": "Otkrij", + "dismiss_all_errors": "Odbaci sve pogreške", + "dismiss_error": "Odbaci pogrešku", + "display_options": "Mogućnosti prikaza", + "display_order": "Redoslijed prikaza", + "display_original_photos": "Prikaz originalnih fotografija", + "display_original_photos_setting_description": "Radije prikažite izvornu fotografiju kada gledate materijal umjesto sličica kada je izvorni materijal kompatibilan s webom. To može rezultirati sporijim brzinama prikaza fotografija.", + "do_not_show_again": "Ne prikazuj više ovu poruku", + "done": "Gotovo", + "download": "Preuzmi", + "download_include_embedded_motion_videos": "Ugrađeni videozapisi", + "download_include_embedded_motion_videos_description": "Uključite videozapise ugrađene u fotografije s pokretom kao zasebnu datoteku", + "download_settings": "Preuzmi", + "download_settings_description": "Upravljajte postavkama koje se odnose na preuzimanje sredstava", + "downloading": "Preuzimanje", + "downloading_asset_filename": "Preuzimanje materijala {filename}", + "drop_files_to_upload": "Ispustite datoteke bilo gdje za prijenos", + "duplicates": "Duplikati", + "duplicates_description": "Razriješite svaku grupu tako da naznačite koji su duplikati, ako ih ima", + "duration": "Trajanje", "durations": { "days": "", "hours": "", @@ -442,254 +546,378 @@ "months": "", "years": "" }, - "edit_album": "", - "edit_avatar": "", - "edit_date": "", - "edit_date_and_time": "", - "edit_exclusion_pattern": "", - "edit_faces": "", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", - "edit_link": "", - "edit_location": "", - "edit_name": "", - "edit_people": "", - "edit_title": "", - "edit_user": "", - "edited": "", - "editor": "", - "email": "", + "edit": "Izmjena", + "edit_album": "Uredi album", + "edit_avatar": "Uredi avatar", + "edit_date": "Uredi datum", + "edit_date_and_time": "Uredite datum i vrijeme", + "edit_exclusion_pattern": "Uredi uzorak izuzimanja", + "edit_faces": "Uređivanje lica", + "edit_import_path": "Uredi put uvoza", + "edit_import_paths": "Uredi Uvozne Putanje", + "edit_key": "Ključ za uređivanje", + "edit_link": "Uredi poveznicu", + "edit_location": "Uredi lokaciju", + "edit_name": "Uredi ime", + "edit_people": "Uredi ljude", + "edit_tag": "Uredi oznaku", + "edit_title": "Uredi Naslov", + "edit_user": "Uredi korisnika", + "edited": "Uređeno", + "editor": "Urednik", + "editor_close_without_save_prompt": "Promjene neće biti spremljene", + "editor_close_without_save_title": "Zatvoriti uređivač?", + "editor_crop_tool_h2_aspect_ratios": "Omjeri stranica", + "editor_crop_tool_h2_rotation": "Rotacija", + "email": "E-pošta", "empty_album": "", - "empty_trash": "", - "enable": "", - "enabled": "", - "end_date": "", - "error": "", - "error_loading_image": "", + "empty_trash": "Isprazni smeće", + "empty_trash_confirmation": "Jeste li sigurni da želite isprazniti smeće? Time će se iz Immicha trajno ukloniti sva sredstva u otpadu.\nNe možete poništiti ovu radnju!", + "enable": "Omogući", + "enabled": "Omogućeno", + "end_date": "Datum završetka", + "error": "Greška", + "error_loading_image": "Pogreška pri učitavanju slike", + "error_title": "Greška - Nešto je pošlo krivo", "errors": { - "cleared_jobs": "", - "exclusion_pattern_already_exists": "", - "failed_job_command": "", - "import_path_already_exists": "", - "paths_validation_failed": "", - "quota_higher_than_disk_size": "", - "repair_unable_to_check_items": "", - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_exclusion_pattern": "", - "unable_to_add_import_path": "", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", - "unable_to_change_password": "", - "unable_to_copy_to_clipboard": "", - "unable_to_create_api_key": "", - "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", - "unable_to_delete_exclusion_pattern": "", - "unable_to_delete_import_path": "", - "unable_to_delete_shared_link": "", - "unable_to_delete_user": "", - "unable_to_edit_exclusion_pattern": "", - "unable_to_edit_import_path": "", - "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", - "unable_to_link_oauth_account": "", - "unable_to_load_album": "", - "unable_to_load_asset_activity": "", - "unable_to_load_items": "", - "unable_to_load_liked_status": "", - "unable_to_play_video": "", - "unable_to_refresh_user": "", - "unable_to_remove_album_users": "", - "unable_to_remove_api_key": "", - "unable_to_remove_library": "", - "unable_to_remove_offline_files": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_api_key": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", - "unable_to_submit_job": "", - "unable_to_trash_asset": "", - "unable_to_unlink_account": "", - "unable_to_update_library": "", - "unable_to_update_location": "", - "unable_to_update_settings": "", - "unable_to_update_timeline_display_status": "", - "unable_to_update_user": "" + "cannot_navigate_next_asset": "Nije moguće prijeći na sljedeći materijal", + "cannot_navigate_previous_asset": "Nije moguće prijeći na prethodni materijal", + "cant_apply_changes": "Nije moguće primijeniti promjene", + "cant_change_activity": "Ne mogu {enabled, select, true {disable} druge {enable}} aktivnosti", + "cant_change_asset_favorite": "Nije moguće promijeniti favorita za sredstvo", + "cant_change_metadata_assets_count": "Nije moguće promijeniti metapodatke {count, plural, one {# asset} other {# assets}}", + "cant_get_faces": "Ne mogu dobiti lica", + "cant_get_number_of_comments": "Ne mogu dobiti broj komentara", + "cant_search_people": "Ne mogu pretraživati ljude", + "cant_search_places": "Ne mogu pretraživati mjesta", + "cleared_jobs": "Izbrisani poslovi za: {job}", + "error_adding_assets_to_album": "Pogreška pri dodavanju materijala u album", + "error_adding_users_to_album": "Pogreška pri dodavanju korisnika u album", + "error_deleting_shared_user": "Pogreška pri brisanju dijeljenog korisnika", + "error_downloading": "Pogreška pri preuzimanju {filename}", + "error_hiding_buy_button": "Pogreška pri skrivanju gumba za kupnju", + "error_removing_assets_from_album": "Pogreška prilikom uklanjanja materijala iz albuma, provjerite konzolu za više pojedinosti", + "error_selecting_all_assets": "Pogreška pri odabiru svih sredstava", + "exclusion_pattern_already_exists": "Ovaj uzorak izuzimanja već postoji.", + "failed_job_command": "Naredba {command} nije uspjela za posao: {job}", + "failed_to_create_album": "Izrada albuma nije uspjela", + "failed_to_create_shared_link": "Stvaranje dijeljene veze nije uspjelo", + "failed_to_edit_shared_link": "Nije uspjelo uređivanje dijeljene poveznice", + "failed_to_get_people": "Dohvaćanje ljudi nije uspjelo", + "failed_to_load_asset": "Učitavanje sredstva nije uspjelo", + "failed_to_load_assets": "Učitavanje sredstava nije uspjelo", + "failed_to_load_people": "Učitavanje ljudi nije uspjelo", + "failed_to_remove_product_key": "Uklanjanje ključa proizvoda nije uspjelo", + "failed_to_stack_assets": "Slaganje sredstava nije uspjelo", + "failed_to_unstack_assets": "Nije uspjelo uklanjanje snopa sredstava", + "import_path_already_exists": "Ovaj uvozni put već postoji.", + "incorrect_email_or_password": "Netočna adresa e-pošte ili lozinka", + "paths_validation_failed": "{paths, plural, one {# putanja nije prošla} other {# putanje nisu prošle}} provjeru valjanosti", + "profile_picture_transparent_pixels": "Profilne slike ne smiju imati prozirne piksele. Povećajte i/ili pomaknite sliku.", + "quota_higher_than_disk_size": "Postavili ste kvotu veću od veličine diska", + "repair_unable_to_check_items": "Nije moguće provjeriti {count, select, one {item} other {items}}", + "unable_to_add_album_users": "Nije moguće dodati korisnike u album", + "unable_to_add_assets_to_shared_link": "Nije moguće dodati sredstva na dijeljenu poveznicu", + "unable_to_add_comment": "Nije moguće dodati komentar", + "unable_to_add_exclusion_pattern": "Nije moguće dodati uzorak izuzimanja", + "unable_to_add_import_path": "Nije moguće dodati putanju uvoza", + "unable_to_add_partners": "Nije moguće dodati partnere", + "unable_to_add_remove_archive": "Nije moguće {arhivirano, odabrati, istinito {ukloniti sredstvo iz} druge {dodati sredstvo u}} arhivu", + "unable_to_add_remove_favorites": "Nije moguće {favorite, select, true {add asset to} other {remove asset from}} favorite", + "unable_to_archive_unarchive": "Nije moguće {arhivirati, odabrati, istinito {arhivirati} ostalo {dearhivirati}}", + "unable_to_change_album_user_role": "Nije moguće promijeniti ulogu korisnika albuma", + "unable_to_change_date": "Nije moguće promijeniti datum", + "unable_to_change_favorite": "Nije moguće promijeniti favorita za sredstvo", + "unable_to_change_location": "Nije moguće promijeniti lokaciju", + "unable_to_change_password": "Nije moguće promijeniti lozinku", + "unable_to_change_visibility": "Nije moguće promijeniti vidljivost za {count, plural, one {# osobu} other {# osobe}}", + "unable_to_complete_oauth_login": "Nije moguće dovršiti OAuth prijavu", + "unable_to_connect": "Povezivanje nije moguće", + "unable_to_connect_to_server": "Nije moguće spojiti se na poslužitelj", + "unable_to_copy_to_clipboard": "Nije moguće kopirati u međuspremnik, provjerite pristupate li stranici putem https-a", + "unable_to_create_admin_account": "Nije moguće stvoriti administratorski račun", + "unable_to_create_api_key": "Nije moguće izraditi novi API ključ", + "unable_to_create_library": "Nije moguće stvoriti biblioteku", + "unable_to_create_user": "Nije moguće stvoriti korisnika", + "unable_to_delete_album": "Nije moguće izbrisati album", + "unable_to_delete_asset": "Nije moguće izbrisati sredstvo", + "unable_to_delete_assets": "Pogreška pri brisanju sredstava", + "unable_to_delete_exclusion_pattern": "Nije moguće izbrisati uzorak izuzimanja", + "unable_to_delete_import_path": "Nije moguće izbrisati put uvoza", + "unable_to_delete_shared_link": "Nije moguće izbrisati dijeljenu poveznicu", + "unable_to_delete_user": "Nije moguće izbrisati korisnika", + "unable_to_download_files": "Nije moguće preuzeti datoteke", + "unable_to_edit_exclusion_pattern": "Nije moguće urediti uzorak izuzimanja", + "unable_to_edit_import_path": "Nije moguće urediti put uvoza", + "unable_to_empty_trash": "Nije moguće isprazniti otpad", + "unable_to_enter_fullscreen": "Nije moguće otvoriti cijeli zaslon", + "unable_to_exit_fullscreen": "Nije moguće izaći iz cijelog zaslona", + "unable_to_get_comments_number": "Nije moguće dobiti broj komentara", + "unable_to_get_shared_link": "Dohvaćanje dijeljene veze nije uspjelo", + "unable_to_hide_person": "Nije moguće sakriti osobu", + "unable_to_link_motion_video": "Nije moguće povezati videozapis pokreta", + "unable_to_link_oauth_account": "Nije moguće povezati OAuth račun", + "unable_to_load_album": "Nije moguće učitati album", + "unable_to_load_asset_activity": "Nije moguće učitati aktivnost sredstva", + "unable_to_load_items": "Nije moguće učitati stavke", + "unable_to_load_liked_status": "Nije moguće učitati status sviđanja", + "unable_to_log_out_all_devices": "Nije moguće odjaviti sve uređaje", + "unable_to_log_out_device": "Nije moguće odjaviti uređaj", + "unable_to_login_with_oauth": "Nije moguće prijaviti se pomoću OAutha", + "unable_to_play_video": "Nije moguće reproducirati video", + "unable_to_reassign_assets_existing_person": "Nije moguće ponovno dodijeliti imovinu na {name, select, null {postojeću osobu} other {{name}}}", + "unable_to_reassign_assets_new_person": "Nije moguće ponovno dodijeliti imovinu novoj osobi", + "unable_to_refresh_user": "Nije moguće osvježiti korisnika", + "unable_to_remove_album_users": "Nije moguće ukloniti korisnike iz albuma", + "unable_to_remove_api_key": "Nije moguće ukloniti API ključ", + "unable_to_remove_assets_from_shared_link": "Nije moguće ukloniti sredstva iz dijeljene poveznice", + "unable_to_remove_library": "Nije moguće ukloniti biblioteku", + "unable_to_remove_offline_files": "Nije moguće ukloniti izvanmrežne datoteke", + "unable_to_remove_partner": "Nije moguće ukloniti partnera", + "unable_to_remove_reaction": "Nije moguće ukloniti reakciju", + "unable_to_repair_items": "Nije moguće popraviti stavke", + "unable_to_reset_password": "Nije moguće ponovno postaviti lozinku", + "unable_to_resolve_duplicate": "Nije moguće razriješiti duplikat", + "unable_to_restore_assets": "Nije moguće vratiti imovinu", + "unable_to_restore_trash": "Nije moguće vratiti otpad", + "unable_to_restore_user": "Nije moguće vratiti korisnika", + "unable_to_save_album": "Nije moguće spremiti album", + "unable_to_save_api_key": "Nije moguće spremiti API ključ", + "unable_to_save_date_of_birth": "Nije moguće spremiti datum rođenja", + "unable_to_save_name": "Nije moguće spremiti ime", + "unable_to_save_profile": "Nije moguće spremiti profil", + "unable_to_save_settings": "Nije moguće spremiti postavke", + "unable_to_scan_libraries": "Nije moguće skenirati knjižnice", + "unable_to_scan_library": "Nije moguće skenirati knjižnicu", + "unable_to_set_feature_photo": "Nije moguće postaviti istaknutu fotografiju", + "unable_to_set_profile_picture": "Nije moguće postaviti profilnu sliku", + "unable_to_submit_job": "Nije moguće poslati posao", + "unable_to_trash_asset": "Nije moguće baciti sredstvo u smeće", + "unable_to_unlink_account": "Nije moguće prekinuti vezu računa", + "unable_to_unlink_motion_video": "Nije moguće prekinuti vezu videozapisa pokreta", + "unable_to_update_album_cover": "Nije moguće ažurirati omot albuma", + "unable_to_update_album_info": "Nije moguće ažurirati informacije o albumu", + "unable_to_update_library": "Nije moguće ažurirati biblioteku", + "unable_to_update_location": "Nije moguće ažurirati lokaciju", + "unable_to_update_settings": "Nije moguće ažurirati postavke", + "unable_to_update_timeline_display_status": "Nije moguće ažurirati status prikaza vremenske trake", + "unable_to_update_user": "Nije moguće ažurirati korisnika", + "unable_to_upload_file": "Nije moguće učitati datoteku" }, - "exit_slideshow": "", - "expand_all": "", - "expire_after": "", - "expired": "", - "explore": "", - "export": "", - "export_as_json": "", - "extension": "", - "external": "", - "external_libraries": "", + "exif": "Exif", + "exit_slideshow": "Izađi iz projekcije slideova", + "expand_all": "Proširi sve", + "expire_after": "Istječe nakon", + "expired": "Isteklo", + "expires_date": "Ističe {date}", + "explore": "Istraži", + "explorer": "Pretraživač (Explorer)", + "export": "Izvoz", + "export_as_json": "Izvezi kao JSON", + "extension": "Proširenje (Extension)", + "external": "Vanjski", + "external_libraries": "Vanjske Biblioteke", + "face_unassigned": "Nedodijeljeno", "failed_to_get_people": "", - "favorite": "", - "favorite_or_unfavorite_photo": "", - "favorites": "", - "feature_photo_updated": "", - "file_name": "", - "file_name_or_extension": "", - "filename": "", - "filetype": "", - "filter_people": "", - "find_them_fast": "", - "fix_incorrect_match": "", - "force_re-scan_library_files": "", - "forward": "", - "general": "", - "get_help": "", - "getting_started": "", - "go_back": "", - "go_to_search": "", + "favorite": "Omiljeno", + "favorite_or_unfavorite_photo": "Omiljena ili neomiljena fotografija", + "favorites": "Omiljene", + "feature_photo_updated": "Istaknuta fotografija ažurirana", + "features": "Značajke (Features)", + "features_setting_description": "Upravljajte značajkama aplikacije", + "file_name": "Naziv datoteke", + "file_name_or_extension": "Naziv ili ekstenzija datoteke", + "filename": "Naziv datoteke", + "filetype": "Vrsta datoteke", + "filter_people": "Filtrirajte ljude", + "find_them_fast": "Pronađite ih brzo po imenu pomoću pretraživanja", + "fix_incorrect_match": "Ispravite netočno podudaranje", + "folders": "Mape", + "folders_feature_description": "Pregledavanje prikaza mape za fotografije i videozapise u sustavu datoteka", + "force_re-scan_library_files": "Prisilno ponovno skeniraj sve datoteke biblioteke", + "forward": "Naprijed", + "general": "Općenito", + "get_help": "Potražite pomoć", + "getting_started": "Početak Rada", + "go_back": "Idi natrag", + "go_to_search": "Idi na pretragu", "go_to_share_page": "", - "group_albums_by": "", - "has_quota": "", - "hide_gallery": "", - "hide_password": "", - "hide_person": "", - "host": "", - "hour": "", - "image": "", - "immich_logo": "", - "import_from_json": "", - "import_path": "", - "in_archive": "", - "include_archived": "", - "include_shared_albums": "", - "include_shared_partner_assets": "", - "individual_share": "", - "info": "", + "group_albums_by": "Grupiraj albume po...", + "group_no": "Nema grupiranja", + "group_owner": "Grupiraj po vlasniku", + "group_year": "Grupiraj po godini", + "has_quota": "Ima kvotu", + "hi_user": "Bok {name} ({email})", + "hide_all_people": "Sakrij sve ljude", + "hide_gallery": "Sakrij galeriju", + "hide_named_person": "Sakrij osobu {name}", + "hide_password": "Sakrij lozinku", + "hide_person": "Sakrij osobu", + "hide_unnamed_people": "Sakrij neimenovane osobe", + "host": "Domaćin", + "hour": "Sat", + "image": "Slika", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} snimljeno {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} snimljeno s {person1} {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} snimljeno s {person1} i {person2} {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} snimljeno s {person1}, {person2} i {person3} {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} snimljeno s {person1}, {person2} i {additionalCount, number} drugih {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1} {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1} i {person2} {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1}, {person2} i {person3} {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1}, {person2} i {additionalCount, number} drugih {date}", + "immich_logo": "Immich Logo", + "immich_web_interface": "Immich Web Sučelje", + "import_from_json": "Uvoz iz JSON-a", + "import_path": "Putanja uvoza", + "in_albums": "U {count, plural, one {# album} other {# albuma}}", + "in_archive": "U arhivi", + "include_archived": "Uključi arhivirano", + "include_shared_albums": "Uključi dijeljene albume", + "include_shared_partner_assets": "Uključite zajedničku imovinu partnera", + "individual_share": "Pojedinačni udio", + "info": "Informacije", "interval": { - "day_at_onepm": "", - "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" + "day_at_onepm": "Svaki dan u 13 sati", + "hours": "{hours, plural, one {Svaki sat} few {Svakih {hours, number} sata} other {Svakih {hours, number} sati}}", + "night_at_midnight": "Svaku večer u ponoć", + "night_at_twoam": "Svake noći u 2 ujutro" }, - "invite_people": "", - "invite_to_album": "", - "jobs": "", - "keep": "", - "keyboard_shortcuts": "", - "language": "", - "language_setting_description": "", - "last_seen": "", - "leave": "", - "let_others_respond": "", - "level": "", - "library": "", - "library_options": "", - "light": "", - "link_options": "", - "link_to_oauth": "", - "linked_oauth_account": "", - "list": "", - "loading": "", - "loading_search_results_failed": "", - "log_out": "", - "log_out_all_devices": "", - "login_has_been_disabled": "", - "look": "", - "loop_videos": "", - "loop_videos_description": "", - "make": "", - "manage_shared_links": "", - "manage_sharing_with_partners": "", - "manage_the_app_settings": "", - "manage_your_account": "", - "manage_your_api_keys": "", - "manage_your_devices": "", - "manage_your_oauth_connection": "", - "map": "", - "map_marker_with_image": "", - "map_settings": "", - "matches": "", - "media_type": "", - "memories": "", - "memories_setting_description": "", - "menu": "", - "merge": "", - "merge_people": "", - "merge_people_successfully": "", - "minimize": "", - "minute": "", - "missing": "", - "model": "", - "month": "", - "more": "", - "moved_to_trash": "", - "my_albums": "", - "name": "", - "name_or_nickname": "", - "never": "", - "new_api_key": "", - "new_password": "", - "new_person": "", - "new_user_created": "", - "newest_first": "", - "next": "", - "next_memory": "", - "no": "", - "no_albums_message": "", - "no_archived_assets_message": "", - "no_assets_message": "", - "no_duplicates_found": "", - "no_exif_info_available": "", - "no_explore_results_message": "", - "no_favorites_message": "", - "no_libraries_message": "", - "no_name": "", - "no_places": "", - "no_results": "", - "no_shared_albums_message": "", - "not_in_any_album": "", - "note_apply_storage_label_to_previously_uploaded assets": "", - "note_unlimited_quota": "", - "notes": "", - "notification_toggle_setting_description": "", - "notifications": "", - "notifications_setting_description": "", - "oauth": "", - "offline": "", - "offline_paths": "", - "offline_paths_description": "", - "ok": "", - "oldest_first": "", - "online": "", - "only_favorites": "", - "only_refreshes_modified_files": "", - "open_the_search_filters": "", - "options": "", - "organize_your_library": "", - "other": "", - "other_devices": "", - "other_variables": "", - "owned": "", - "owner": "", - "partner_can_access": "", + "invite_people": "Pozovite ljude", + "invite_to_album": "Pozovi u album", + "items_count": "{count, plural, one {# datoteka} other {# datoteke}}", + "jobs": "Poslovi", + "keep": "Zadrži", + "keep_all": "Zadrži Sve", + "keyboard_shortcuts": "Prečaci tipkovnice", + "language": "Jezik", + "language_setting_description": "Odaberite željeni jezik", + "last_seen": "Zadnji put viđen", + "latest_version": "Najnovija verzija", + "latitude": "Zemljopisna širina", + "leave": "Izađi", + "let_others_respond": "Dozvoli da drugi odgovore", + "level": "Razina", + "library": "Biblioteka", + "library_options": "Mogućnosti biblioteke", + "light": "Svjetlo", + "like_deleted": "Like izbrisan", + "link_motion_video": "Povežite videozapis pokreta", + "link_options": "Opcije veze", + "link_to_oauth": "Veza na OAuth", + "linked_oauth_account": "Povezani OAuth račun", + "list": "Popis", + "loading": "Učitavanje", + "loading_search_results_failed": "Učitavanje rezultata pretraživanja nije uspjelo", + "log_out": "Odjavi se", + "log_out_all_devices": "Odjava sa svih uređaja", + "logged_out_all_devices": "Odjavljeni su svi uređaji", + "logged_out_device": "Odjavljen uređaj", + "login": "Prijava", + "login_has_been_disabled": "Prijava je onemogućena.", + "logout_all_device_confirmation": "Jeste li sigurni da želite odjaviti sve uređaje?", + "logout_this_device_confirmation": "Jeste li sigurni da se želite odjaviti s ovog uređaja?", + "longitude": "Zemljopisna dužina", + "look": "Izgled", + "loop_videos": "Ponavljajte videozapise", + "loop_videos_description": "Omogućite automatsko ponavljanje videozapisa u pregledniku detalja.", + "make": "Proizvođač", + "manage_shared_links": "Upravljanje dijeljenim vezama", + "manage_sharing_with_partners": "Upravljajte dijeljenjem s partnerima", + "manage_the_app_settings": "Upravljajte postavkama aplikacije", + "manage_your_account": "Upravljajte svojim računom", + "manage_your_api_keys": "Upravljajte svojim API ključevima", + "manage_your_devices": "Upravljajte uređajima na kojima ste prijavljeni", + "manage_your_oauth_connection": "Upravljajte svojom OAuth vezom", + "map": "Karta", + "map_marker_for_images": "Oznaka karte za slike snimljene u {city}, {country}", + "map_marker_with_image": "Oznaka karte sa slikom", + "map_settings": "Postavke karte", + "matches": "Podudaranja", + "media_type": "Vrsta medija", + "memories": "Sjećanja", + "memories_setting_description": "Upravljajte onim što vidite u svojim sjećanjima", + "memory": "Memorija", + "memory_lane_title": "Traka sjećanja {title}", + "menu": "Izbornik", + "merge": "Spoji", + "merge_people": "Spajanje ljudi", + "merge_people_limit": "Možete spojiti najviše 5 lica odjednom", + "merge_people_prompt": "Želite li spojiti ove ljude? Ova radnja je nepovratna.", + "merge_people_successfully": "Uspješno spajanje ljudi", + "merged_people_count": "{count, plural, one {# Spojena osoba} other {# Spojene osobe}}", + "minimize": "Minimiziraj", + "minute": "Minuta", + "missing": "Nedostaje", + "model": "Model", + "month": "Mjesec", + "more": "Više", + "moved_to_trash": "Premješteno u smeće", + "my_albums": "Moji albumi", + "name": "Ime", + "name_or_nickname": "Ime ili nadimak", + "never": "Nikada", + "new_album": "Novi Album", + "new_api_key": "Novi API ključ", + "new_password": "Nova lozinka", + "new_person": "Nova osoba", + "new_user_created": "Stvoren novi korisnik", + "new_version_available": "DOSTUPNA NOVA VERZIJA", + "newest_first": "Prvo najnovije", + "next": "Sljedeće", + "next_memory": "Sljedeće sjećanje", + "no": "Ne", + "no_albums_message": "Izradite album za organiziranje svojih fotografija i videozapisa", + "no_albums_with_name_yet": "Čini se da još nemate nijedan album s ovim imenom.", + "no_albums_yet": "Čini se da još nemate nijedan album.", + "no_archived_assets_message": "Arhivirajte fotografije i videozapise kako biste ih sakrili iz prikaza fotografija", + "no_assets_message": "KLIKNITE DA PRENESETE SVOJU PRVU FOTOGRAFIJU", + "no_duplicates_found": "Nisu pronađeni duplikati.", + "no_exif_info_available": "Nema dostupnih exif podataka", + "no_explore_results_message": "Prenesite više fotografija da istražite svoju zbirku.", + "no_favorites_message": "Dodajte favorite kako biste brzo pronašli svoje najbolje slike i videozapise", + "no_libraries_message": "Stvorite vanjsku biblioteku za pregled svojih fotografija i videozapisa", + "no_name": "Bez imena", + "no_places": "Nema mjesta", + "no_results": "Nema rezultata", + "no_results_description": "Pokušajte sa sinonimom ili općenitijom ključnom riječi", + "no_shared_albums_message": "Stvorite album za dijeljenje fotografija i videozapisa s osobama u svojoj mreži", + "not_in_any_album": "Ni u jednom albumu", + "note_apply_storage_label_to_previously_uploaded assets": "Napomena: Da biste primijenili Oznaku za skladištenje na prethodno prenesena sredstva, pokrenite", + "note_unlimited_quota": "napomena: Unesite 0 za neograni%C4%8Denu kvotu", + "notes": "Bilješke", + "notification_toggle_setting_description": "Omogući obavijesti putem e-pošte", + "notifications": "Obavijesti", + "notifications_setting_description": "Upravljanje obavijestima", + "oauth": "OAuth", + "offline": "Izvan mreže", + "offline_paths": "Izvanmrežne putanje", + "offline_paths_description": "Ovi rezultati mogu biti posljedica ručnog brisanja datoteka koje nisu dio vanjske biblioteke.", + "ok": "Ok", + "oldest_first": "Prvo najstarije", + "onboarding": "Uključivanje (Onboarding)", + "onboarding_privacy_description": "Sljedeće (neobavezne) značajke oslanjaju se na vanjske usluge i mogu se onemogućiti u bilo kojem trenutku u postavkama administracije.", + "onboarding_theme_description": "Odaberite temu boja za svoj primjer. To možete kasnije promijeniti u postavkama.", + "onboarding_welcome_description": "Postavimo vašu instancu s nekim uobičajenim postavkama.", + "onboarding_welcome_user": "Dobro došli, {user}", + "online": "Dostupan (Online)", + "only_favorites": "Samo omiljeno", + "only_refreshes_modified_files": "Osvježava samo izmijenjene datoteke", + "open_in_map_view": "Otvori u prikazu karte", + "open_in_openstreetmap": "Otvori u OpenStreetMap", + "open_the_search_filters": "Otvorite filtre pretraživanja", + "options": "Opcije", + "or": "ili", + "organize_your_library": "Organizirajte svoju knjižnicu", + "original": "original", + "other": "Ostalo", + "other_devices": "Ostali uređaji", + "other_variables": "Ostale varijable", + "owned": "Vlasništvo", + "owner": "Vlasnik", + "partner": "Partner", + "partner_can_access": "{partner} može pristupiti", "partner_can_access_assets": "", "partner_can_access_location": "", "partner_sharing": "", diff --git a/web/src/lib/i18n/it.json b/web/src/lib/i18n/it.json index cbe3651927edb..daee687003cf1 100644 --- a/web/src/lib/i18n/it.json +++ b/web/src/lib/i18n/it.json @@ -661,6 +661,7 @@ "unable_to_get_comments_number": "Impossibile ottenere il numero di commenti", "unable_to_get_shared_link": "Impossibile ottenere il link condiviso", "unable_to_hide_person": "Impossibile nascondere persona", + "unable_to_link_motion_video": "Impossibile collegare video in movimento", "unable_to_link_oauth_account": "Impossibile collegare l'account OAuth", "unable_to_load_album": "Impossibile caricare l'album", "unable_to_load_asset_activity": "Impossibile caricare l'attività dell'asset", @@ -701,6 +702,7 @@ "unable_to_submit_job": "Impossibile eseguire l'attività", "unable_to_trash_asset": "Impossibile cestinare l'asset", "unable_to_unlink_account": "Impossibile scollegare l'account", + "unable_to_unlink_motion_video": "Impossibile scollegare video in movimento", "unable_to_update_album_cover": "Errore durante l'aggiornamento della copertina dell'album", "unable_to_update_album_info": "Impossibile aggiornare le informazioni sull'album", "unable_to_update_library": "Impossibile aggiornare la libreria", @@ -1290,6 +1292,7 @@ "unknown_album": "Album sconosciuto", "unknown_year": "Anno sconosciuto", "unlimited": "Illimitato", + "unlink_motion_video": "Scollega video in movimento", "unlink_oauth": "Scollega OAuth", "unlinked_oauth_account": "Scollega account OAuth", "unnamed_album": "Album senza nome", diff --git a/web/src/lib/i18n/ko.json b/web/src/lib/i18n/ko.json index df46923b5ef73..8adf988f5dd6d 100644 --- a/web/src/lib/i18n/ko.json +++ b/web/src/lib/i18n/ko.json @@ -139,7 +139,11 @@ "map_settings_description": "지도 설정 관리", "map_style_description": "지도 테마 style.json URL", "metadata_extraction_job": "메타데이터 추출", - "metadata_extraction_job_description": "각 항목에서 GPS, 해상도 등의 메타데이터 정보 추출", + "metadata_extraction_job_description": "각 항목에서 GPS, 인물 및 해상도 등의 메타데이터 정보 추출", + "metadata_faces_import_setting": "얼굴 가져오기 활성화", + "metadata_faces_import_setting_description": "사이드카 파일의 이미지 EXIF 데이터에서 얼굴 가져오기", + "metadata_settings": "메타데이터 설정", + "metadata_settings_description": "메타데이터 설정 관리", "migration_job": "마이그레이션", "migration_job_description": "각 항목의 섬네일 및 인물의 얼굴을 최신 폴더 구조로 마이그레이션", "no_paths_added": "추가된 경로 없음", @@ -1114,6 +1118,7 @@ "search_for_existing_person": "존재하는 인물 검색", "search_no_people": "인물이 없습니다.", "search_no_people_named": "\"{name}\" 인물을 찾을 수 없음", + "search_options": "검색 옵션", "search_people": "인물 검색", "search_places": "장소 검색", "search_state": "지역 검색...", @@ -1243,6 +1248,7 @@ "to_change_password": "비밀번호 변경", "to_favorite": "즐겨찾기", "to_login": "로그인", + "to_parent": "상위 항목으로", "to_root": "루트", "to_trash": "삭제", "toggle_settings": "설정 변경", diff --git a/web/src/lib/i18n/lv.json b/web/src/lib/i18n/lv.json index bf17ccb8135a4..2701cda4e8428 100644 --- a/web/src/lib/i18n/lv.json +++ b/web/src/lib/i18n/lv.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Pievienot koplietotam albumam", "added_to_archive": "Pievienots arhīvam", "added_to_favorites": "Pievienots izlasei", - "added_to_favorites_count": "Pievienots {count} izlasei", + "added_to_favorites_count": "Pievienots {count, number} izlasei", "admin": { "add_exclusion_pattern_description": "Pievienojiet izlaišanas shēmas. Aizstājējzīmju izmantoša *, **, un ? tiek atbalstīta. Lai ignorētu visus failus jebkurā direktorijā ar nosaukumu “RAW”, izmantojiet “**/RAW/**”. Lai ignorētu visus failus, kas beidzas ar “. tif”, izmantojiet “**/*. tif”. Lai ignorētu absolūto ceļu, izmantojiet “/path/to/ignore/**”.", "authentication_settings": "Autentifikācijas iestatījumi", @@ -44,6 +44,9 @@ "disable_login": "Atspējot pieteikšanos", "disabled": "", "duplicate_detection_job_description": "Palaidiet mašīnmācīšanos uz līdzekļiem, lai noteiktu līdzīgus attēlus. Paļaujas uz Viedo Meklēšanu", + "external_library_created_at": "Ārēja bibliotēka (izveidota {date})", + "external_library_management": "Ārējo bibliotēku pārvaldība", + "face_detection": "Seju noteikšana", "image_format_description": "", "image_prefer_embedded_preview": "", "image_prefer_embedded_preview_setting_description": "", @@ -82,7 +85,7 @@ "machine_learning_enabled_description": "", "machine_learning_facial_recognition": "", "machine_learning_facial_recognition_description": "", - "machine_learning_facial_recognition_model": "", + "machine_learning_facial_recognition_model": "Seju atpazīšanas modelis", "machine_learning_facial_recognition_model_description": "", "machine_learning_facial_recognition_setting_description": "", "machine_learning_max_detection_distance": "", @@ -102,11 +105,13 @@ "manage_log_settings": "", "map_dark_style": "", "map_enable_description": "", + "map_gps_settings": "Kartes un GPS iestatījumi", + "map_gps_settings_description": "Pārvaldīt karšu un GPS (apgrieztās ģeokodēšanas) iestatījumus", "map_light_style": "", "map_reverse_geocoding": "", "map_reverse_geocoding_enable_description": "", "map_reverse_geocoding_settings": "", - "map_settings": "", + "map_settings": "Karte", "map_settings_description": "", "map_style_description": "", "metadata_extraction_job_description": "", @@ -151,10 +156,12 @@ "password_enable_description": "", "password_settings": "", "password_settings_description": "", + "quota_size_gib": "Kvotas izmērs (GiB)", + "require_password_change_on_login": "Pieprasīt lietotājam mainīt paroli pēc pirmās pieteikšanās", "server_external_domain_settings": "", "server_external_domain_settings_description": "", - "server_settings": "", - "server_settings_description": "", + "server_settings": "Servera iestatījumi", + "server_settings_description": "Pārvaldīt servera iestatījumus", "server_welcome_message": "", "server_welcome_message_description": "", "sidecar_job_description": "", @@ -234,38 +241,46 @@ "trash_settings_description": "", "user_delete_delay_settings": "", "user_delete_delay_settings_description": "", + "user_management": "Lietotāju pārvaldība", "user_settings": "", "user_settings_description": "", - "version_check_enabled_description": "", + "version_check_enabled_description": "Ieslēgt versijas pārbaudi", + "version_check_implications": "Versiju pārbaudes funkcija ir atkarīga no periodiskas saziņas ar github.com", "version_check_settings": "", "version_check_settings_description": "", "video_conversion_job_description": "" }, - "admin_email": "", - "admin_password": "", - "administration": "", + "admin_email": "Administratora e-pasts", + "admin_password": "Administratora parole", + "administration": "Administrēšana", "advanced": "Papildu", - "album_added": "", + "album_added": "Albums pievienots", "album_added_notification_setting_description": "", - "album_cover_updated": "", - "album_info_updated": "", - "album_name": "", + "album_cover_updated": "Albuma attēls atjaunināts", + "album_info_updated": "Albuma informācija atjaunināta", + "album_leave": "Pamest albumu?", + "album_name": "Albuma nosaukums", "album_options": "", - "album_updated": "", + "album_remove_user": "Noņemt lietotāju?", + "album_updated": "Albums atjaunināts", "album_updated_setting_description": "", - "albums": "", + "albums": "Albumi", "all": "Viss", - "all_people": "", + "all_albums": "Visi albumi", + "all_people": "Visi cilvēki", + "all_videos": "Visi video", "allow_dark_mode": "", "allow_edits": "", - "api_key": "", - "api_keys": "", + "api_key": "API atslēga", + "api_keys": "API atslēgas", "app_settings": "", "appears_in": "", "archive": "Arhīvs", "archive_or_unarchive_photo": "", + "archive_size": "Arhīva izmērs", "archived": "", "asset_offline": "", + "asset_uploading": "Augšupielādē...", "assets": "aktīvi", "authorized_devices": "", "back": "Atpakaļ", @@ -303,7 +318,7 @@ "comments_are_disabled": "", "confirm": "Apstiprināt", "confirm_admin_password": "", - "confirm_password": "Apstiprināt Paroli", + "confirm_password": "Apstiprināt paroli", "contain": "", "context": "", "continue": "", @@ -324,8 +339,8 @@ "create_link": "Izveidot saiti", "create_link_to_share": "Izveidot kopīgošanas saiti", "create_new_person": "", - "create_new_user": "", - "create_user": "", + "create_new_user": "Izveidot jaunu lietotāju", + "create_user": "Izveidot lietotāju", "created": "", "current_device": "", "custom_locale": "", @@ -344,7 +359,7 @@ "delete_library": "", "delete_link": "", "delete_shared_link": "Dzēst Kopīgošanas saiti", - "delete_user": "", + "delete_user": "Dzēst lietotāju", "deleted_shared_link": "", "description": "Apraksts", "details": "INFORMĀCIJA", @@ -360,6 +375,7 @@ "done": "Gatavs", "download": "Lejupielādēt", "downloading": "", + "duplicates": "Dublikāti", "duration": "", "durations": { "days": "", @@ -382,7 +398,7 @@ "edit_name": "Rediģēt vārdu", "edit_people": "", "edit_title": "", - "edit_user": "", + "edit_user": "Labot lietotāju", "edited": "", "editor": "", "email": "E-pasts", @@ -395,6 +411,7 @@ "error": "", "error_loading_image": "", "errors": { + "failed_to_create_album": "Neizdevās izveidot albumu", "unable_to_add_album_users": "", "unable_to_add_comment": "", "unable_to_add_partners": "", @@ -405,10 +422,10 @@ "unable_to_check_items": "", "unable_to_create_admin_account": "", "unable_to_create_library": "", - "unable_to_create_user": "", + "unable_to_create_user": "Neizdevās izveidot lietotāju", "unable_to_delete_album": "", "unable_to_delete_asset": "", - "unable_to_delete_user": "", + "unable_to_delete_user": "Neizdevās dzēst lietotāju", "unable_to_empty_trash": "", "unable_to_enter_fullscreen": "", "unable_to_exit_fullscreen": "", @@ -450,11 +467,11 @@ "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", - "exit_slideshow": "", + "exit_slideshow": "Iziet no slīdrādes", "expand_all": "", "expire_after": "Derīguma termiņš beidzas pēc", "expired": "Derīguma termiņš beidzās", - "explore": "", + "explore": "Izpētīt", "extension": "", "external_libraries": "", "failed_to_get_people": "", @@ -471,6 +488,7 @@ "filetype": "", "filter_people": "", "fix_incorrect_match": "", + "folders": "Mapes", "force_re-scan_library_files": "", "forward": "", "general": "", @@ -480,7 +498,7 @@ "go_to_search": "", "go_to_share_page": "", "group_albums_by": "", - "has_quota": "", + "has_quota": "Ir kvota", "hide_gallery": "", "hide_password": "", "hide_person": "", @@ -491,7 +509,7 @@ "immich_logo": "", "import_path": "", "in_archive": "", - "include_archived": "Iekļaut Arhivētos", + "include_archived": "Iekļaut arhivētos", "include_shared_albums": "", "include_shared_partner_assets": "", "individual_share": "", @@ -537,8 +555,9 @@ "manage_your_api_keys": "", "manage_your_devices": "", "manage_your_oauth_connection": "", - "map": "", - "map_marker_with_image": "", + "map": "Karte", + "map_marker_for_images": "Kartes marķieris attēliem, kas uzņemti {city}, {country}", + "map_marker_with_image": "Kartes marķieris ar attēlu", "map_settings": "Kartes Iestatījumi", "media_type": "", "memories": "", @@ -559,9 +578,9 @@ "name_or_nickname": "", "never": "nekad", "new_api_key": "", - "new_password": "Jauna Parole", + "new_password": "Jaunā parole", "new_person": "", - "new_user_created": "", + "new_user_created": "Izveidots jauns lietotājs", "newest_first": "", "next": "Nākošais", "next_memory": "", @@ -569,6 +588,7 @@ "no_albums_message": "", "no_archived_assets_message": "", "no_assets_message": "", + "no_duplicates_found": "Dublikāti netika atrasti.", "no_exif_info_available": "", "no_explore_results_message": "", "no_favorites_message": "", @@ -587,8 +607,9 @@ "ok": "", "oldest_first": "", "online": "", - "only_favorites": "", + "only_favorites": "Tikai izlase", "only_refreshes_modified_files": "", + "open_in_openstreetmap": "Atvērt OpenStreetMap", "open_the_search_filters": "", "options": "Iestatījumi", "organize_your_library": "", @@ -658,14 +679,17 @@ "repair_no_results_message": "", "replace_with_upload": "", "require_password": "", + "require_user_to_change_password_on_first_login": "Pieprasīt lietotājam mainīt paroli pēc pirmās pieteikšanās", "reset": "", "reset_password": "", "reset_people_visibility": "", "reset_settings_to_default": "", + "resolve_duplicates": "Atrisināt dublēšanās gadījumus", + "resolved_all_duplicates": "Visi dublikāti ir atrisināti", "restore": "Atjaunot", "restore_user": "", "retry_upload": "", - "review_duplicates": "", + "review_duplicates": "Pārskatīt dublikātus", "role": "", "save": "Saglabāt", "saved_profile": "", @@ -691,8 +715,9 @@ "search_your_photos": "Meklēt Jūsu fotoattēlus", "searching_locales": "", "second": "", - "select_album_cover": "", + "select_album_cover": "Izvēlieties albuma vāciņu", "select_all": "", + "select_all_duplicates": "Atlasīt visus dublikātus", "select_avatar_color": "", "select_face": "", "select_featured_photo": "", @@ -702,7 +727,8 @@ "selected": "", "send_message": "", "server": "", - "server_stats": "", + "server_online": "Serveris tiešsaistē", + "server_stats": "Servera statistika", "set": "", "set_as_album_cover": "", "set_as_profile_picture": "", @@ -715,7 +741,7 @@ "shared": "Kopīgots", "shared_by": "", "shared_by_you": "", - "shared_links": "Kopīgotas Saites", + "shared_links": "Kopīgotās saites", "sharing": "Kopīgošana", "sharing_sidebar_description": "", "show_album_options": "", @@ -735,8 +761,8 @@ "sign_up": "", "size": "", "skip_to_content": "", - "slideshow": "", - "slideshow_settings": "", + "slideshow": "Slīdrāde", + "slideshow_settings": "Slīdrādes iestatījumi", "sort_albums_by": "", "stack": "Steks", "stack_selected_photos": "", @@ -746,7 +772,7 @@ "status": "", "stop_motion_photo": "", "stop_photo_sharing": "Beigt kopīgot jūsu fotogrāfijas?", - "storage": "", + "storage": "Uzglabāšanas vieta", "storage_label": "", "submit": "", "suggestions": "Ieteikumi", @@ -762,7 +788,7 @@ "toggle_settings": "", "toggle_theme": "", "toggle_visibility": "", - "total_usage": "", + "total_usage": "Kopējais lietojums", "trash": "Atkritne", "trash_all": "", "trash_no_results_message": "", @@ -774,6 +800,7 @@ "unknown": "", "unknown_album": "", "unknown_year": "", + "unlimited": "Neierobežots", "unlink_oauth": "", "unlinked_oauth_account": "", "unselect_all": "", @@ -782,16 +809,17 @@ "updated_password": "", "upload": "Augšupielādēt", "upload_concurrency": "", + "upload_status_duplicates": "Dublikāti", "upload_status_errors": "Kļūdas", "upload_status_uploaded": "Augšupielādēts", "url": "", - "usage": "", + "usage": "Lietojums", "user": "Lietotājs", "user_id": "Lietotāja ID", - "user_usage_detail": "", + "user_usage_detail": "Informācija par lietotāju lietojumu", "username": "", "users": "Lietotāji", - "utilities": "", + "utilities": "Rīki", "validate": "", "variables": "", "version": "Versija", @@ -804,7 +832,7 @@ "view_next_asset": "", "view_previous_asset": "", "viewer": "", - "waiting": "", + "waiting": "Gaida", "week": "", "welcome_to_immich": "", "year": "", diff --git a/web/src/lib/i18n/nb_NO.json b/web/src/lib/i18n/nb_NO.json index 1c0e2f5eef116..be2ae2638ea0c 100644 --- a/web/src/lib/i18n/nb_NO.json +++ b/web/src/lib/i18n/nb_NO.json @@ -138,6 +138,8 @@ "map_style_description": "URL til et style.json-karttema", "metadata_extraction_job": "Hent metadata", "metadata_extraction_job_description": "Hent metadatainformasjon fra hver fil, for eksempel GPS-posisjon og oppløsning", + "metadata_settings": "Metadatainnstillinger", + "metadata_settings_description": "Administrer metadatainnstillinger", "migration_job": "Migrering", "migration_job_description": "Migrer miniatyrbilder for filer og ansikter til den nyeste mappestrukturen", "no_paths_added": "Ingen filbaner lagt til", @@ -384,6 +386,7 @@ "asset_offline": "Fil utilgjengelig", "asset_offline_description": "Dette elementet er offline. Immich kan ikke aksessere dets lokasjon. Vennlist påse at elementet er tilgijengelig og skann så biblioteket på nytt.", "asset_skipped": "Hoppet over", + "asset_skipped_in_trash": "I søppelbøtten", "asset_uploaded": "Lastet opp", "asset_uploading": "Laster opp...", "assets": "Filer", diff --git a/web/src/lib/i18n/nl.json b/web/src/lib/i18n/nl.json index 786a1627febbb..dc9f003978033 100644 --- a/web/src/lib/i18n/nl.json +++ b/web/src/lib/i18n/nl.json @@ -391,6 +391,7 @@ "asset_offline": "Asset offline", "asset_offline_description": "Deze asset is offline. Immich kan de bestandslocatie niet openen. Controleer of de asset beschikbaar is en scan de bibliotheek opnieuw.", "asset_skipped": "Overgeslagen", + "asset_skipped_in_trash": "In prullenbak", "asset_uploaded": "Geüpload", "asset_uploading": "Uploaden...", "assets": "Assets", @@ -1135,6 +1136,7 @@ "search_for_existing_person": "Zoek naar bestaande persoon", "search_no_people": "Geen mensen", "search_no_people_named": "Geen mensen genaamd \"{name}\"", + "search_options": "Zoekopties", "search_people": "Zoek mensen", "search_places": "Zoek plaatsen", "search_state": "Zoek staat...", diff --git a/web/src/lib/i18n/ro.json b/web/src/lib/i18n/ro.json index 29acdd03ce225..02022569cd12a 100644 --- a/web/src/lib/i18n/ro.json +++ b/web/src/lib/i18n/ro.json @@ -235,95 +235,127 @@ "storage_template_onboarding_description": "Atunci când este activată, această caracteristică va organiza automat fișierele pe baza unui șablon definit de utilizator. Din cauza unor probleme de stabilitate, aceasta caracteristică este dezactivată implicit. Pentru mai multe informații, te rog sa consulți documentația.", "storage_template_path_length": "Limita de lungime pentru calea aproximativă: {length, number}/{limit, number}", "storage_template_settings": "Șablon stocare", - "storage_template_settings_description": "", + "storage_template_settings_description": "Gestionează structura folderelor și numele fișierelor pentru activele încărcate", + "storage_template_user_label": "{label} este eticheta de stocare a utilizatorului", "system_settings": "Setǎri de sistem", "theme_custom_css_settings": "CSS personalizat", - "theme_custom_css_settings_description": "", - "theme_settings": "", - "theme_settings_description": "", - "thumbnail_generation_job_description": "", + "theme_custom_css_settings_description": "Foile de stil în cascadă (CSS) permit personalizarea designului Immich.", + "theme_settings": "Setări temă", + "theme_settings_description": "Gestionează personalizarea interfeței web Immich", + "these_files_matched_by_checksum": "Aceste fișiere sunt comparate folosind sumele de control", + "thumbnail_generation_job": "Gerează miniaturi", + "thumbnail_generation_job_description": "Generează miniaturi mari, mici și estompate pentru fiecare resursă, precum și miniaturi pentru fiecare persoană", "transcode_policy_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", + "transcoding_acceleration_api": "API de accelerare", + "transcoding_acceleration_api_description": "API-ul care va interacționa cu dispozitivul tău pentru a accelera transcodarea. Această setare este 'best effort': va reveni la transcodarea software în caz de eșec. VP9 poate funcționa sau nu, în funcție de hardware-ul tău.", "transcoding_acceleration_nvenc": "NVENC (necesitǎ GPU NVIDIA)", "transcoding_acceleration_qsv": "Quick Sync (necesitǎ CPU Intel de generația a 7-a sau mai mare)", "transcoding_acceleration_rkmpp": "RKMPP (doar pe SOC-uri Rockchip)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Codec-uri audio acceptate", - "transcoding_accepted_audio_codecs_description": "", + "transcoding_accepted_audio_codecs_description": "Selectează care codec-uri audio nu trebuie să fie transcodificate. Se utilizează doar pentru anumite politici de transcodare.", + "transcoding_accepted_containers": "Containere acceptate", + "transcoding_accepted_containers_description": "Selectează formatele de containere care nu trebuie să fie remuxate în MP4. Se utilizează doar pentru anumite politici de transcodare.", "transcoding_accepted_video_codecs": "Codec-uri video acceptate", - "transcoding_accepted_video_codecs_description": "", - "transcoding_advanced_options_description": "", + "transcoding_accepted_video_codecs_description": "Selectează codec-urile video care nu trebuie să fie transcodificate. Se utilizează doar pentru anumite politici de transcodare.", + "transcoding_advanced_options_description": "Opțiuni pe care majoritatea utilizatorilor nu ar trebui să fie necesar să le schimbe", "transcoding_audio_codec": "Codec audio", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", - "transcoding_hardware_acceleration": "", - "transcoding_hardware_acceleration_description": "", - "transcoding_hardware_decoding": "", - "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", + "transcoding_audio_codec_description": "Opus este opțiunea cu cea mai bună calitate, dar are o compatibilitate mai scăzută cu dispozitivele sau software-ul mai vechi.", + "transcoding_bitrate_description": "Videoclipuri cu un bitrate mai mare decât maximul acceptat sau care nu sunt într-un format acceptat", + "transcoding_codecs_learn_more": "Pentru a afla mai multe despre terminologia folosită aici, consultă documentația FFmpeg pentru codec-ul H.264, codec-ul HEVC și codec-ul VP9.", + "transcoding_constant_quality_mode": "Mod de calitate constantă", + "transcoding_constant_quality_mode_description": "ICQ este mai bun decât CQP, dar unele dispozitive de accelerare hardware nu suportă acest mod. Setarea acestei opțiuni va prefera modul specificat atunci când folosești codificarea bazată pe calitate. Ignorat de NVENC deoarece nu suportă ICQ.", + "transcoding_constant_rate_factor": "Factor de rată constantă (-crf)", + "transcoding_constant_rate_factor_description": "Nivelul de calitate al videoclipului. Valorile tipice sunt 23 pentru H.264, 28 pentru HEVC, 31 pentru VP9 și 35 pentru AV1. Cu cât valoarea este mai mică, cu atât calitatea este mai bună, dar se generează fișiere mai mari.", + "transcoding_disabled_description": "Nu transcodifică niciun videoclip; acest lucru poate afecta redarea pe anumite dispozitive", + "transcoding_hardware_acceleration": "Accelerare Hardware", + "transcoding_hardware_acceleration_description": "Experimental; mult mai rapid, dar va avea o calitate mai scăzută la același bitrate", + "transcoding_hardware_decoding": "Decodare hardware", + "transcoding_hardware_decoding_setting_description": "Se aplică doar pentru NVENC, QSV și RKMPP. Activează accelerarea completă în loc de doar accelerarea codificării. S-ar putea să nu funcționeze pentru toate videoclipurile.", + "transcoding_hevc_codec": "codec HEVC", "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_tone_mapping_npl": "", - "transcoding_tone_mapping_npl_description": "", - "transcoding_transcode_policy": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", + "transcoding_max_b_frames_description": "Valorile mai mari îmbunătățesc eficiența compresiei, dar încetinesc codarea. Este posibil să nu fie compatibile cu accelerarea hardware pe dispozitivele mai vechi. 0 dezactivează cadrele B, în timp ce -1 setează această valoare automat.", + "transcoding_max_bitrate": "Bitrate maxim", + "transcoding_max_bitrate_description": "Setarea unei rate maxime de biți poate face dimensiunile fișierelor mai previzibile, cu un cost minor asupra calității. La 720p, valorile tipice sunt 2600k pentru VP9 sau HEVC, sau 4500k pentru H.264. Dezactivat dacă este setat la 0.", + "transcoding_max_keyframe_interval": "Interval maxim între cadre cheie", + "transcoding_max_keyframe_interval_description": "Setează distanța maximă între cadrele cheie. Valorile mai mici reduc eficiența compresiei, dar îmbunătățesc timpii de căutare și pot îmbunătăți calitatea în scenele cu mișcare rapidă. 0 setează această valoare automat.", + "transcoding_optimal_description": "Videoclipuri cu rezoluție mai mare decât cea țintă sau care nu sunt într-un format acceptat", + "transcoding_preferred_hardware_device": "Dispozitiv hardware preferat", + "transcoding_preferred_hardware_device_description": "Se aplică doar la VAAPI și QSV. Setează nodul DRI utilizat pentru transcodarea hardware.", + "transcoding_preset_preset": "Presetare (-preset)", + "transcoding_preset_preset_description": "Viteza de compresie. Presetările mai lente produc fișiere mai mici și îmbunătățesc calitatea atunci când vizezi o anumită rată de biți. VP9 ignoră vitezele de compresie mai mari decât 'mai rapid'.", + "transcoding_reference_frames": "Cadre de referință", + "transcoding_reference_frames_description": "Numărul de cadre de referință atunci când se comprimă un cadru dat. Valorile mai mari îmbunătățesc eficiența compresiei, dar încetinesc codarea. 0 setează această valoare automat.", + "transcoding_required_description": "Numai videoclipuri care nu sunt într-un format acceptat", + "transcoding_settings": "Setări de transcodare video", + "transcoding_settings_description": "Gestionează rezoluția și informațiile de codare ale fișierelor video", + "transcoding_target_resolution": "Rezoluția țintă", + "transcoding_target_resolution_description": "Rezoluțiile mai mari pot păstra mai multe detalii, dar necesită mai mult timp pentru codare, au dimensiuni mai mari ale fișierelor și pot reduce răspunsul aplicației.", + "transcoding_temporal_aq": "AQ temporal", + "transcoding_temporal_aq_description": "Se aplică doar la NVENC. Îmbunătățește calitatea scenelor cu detalii mari și mișcare redusă. Poate să nu fie compatibil cu dispozitivele mai vechi.", + "transcoding_threads": "Fire", + "transcoding_threads_description": "Valorile mai mari conduc la o codare mai rapidă, dar lasă mai puțin spațiu serverului pentru a procesa alte sarcini în timp ce este activ. Această valoare nu ar trebui să fie mai mare decât numărul de nuclee CPU. Maximizați utilizarea dacă este setat la 0.", + "transcoding_tone_mapping": "Mapare tonuri", + "transcoding_tone_mapping_description": "Încearcă să păstreze aspectul videoclipurilor HDR atunci când sunt convertite în SDR. Fiecare algoritm face compromisuri diferite pentru culoare, detalii și strălucire. Hable păstrează detaliile, Mobius păstrează culoarea, iar Reinhard păstrează strălucirea.", + "transcoding_tone_mapping_npl": "Mapare tonuri NPL", + "transcoding_tone_mapping_npl_description": "Culorile vor fi ajustate pentru a arăta normal pe un ecran cu această strălucire. În mod contraintuitiv, valorile mai mici cresc strălucirea videoclipului și invers, deoarece compensează pentru strălucirea ecranului. 0 setează această valoare automat.", + "transcoding_transcode_policy": "Politica de transcodare", + "transcoding_transcode_policy_description": "Politica pentru când un videoclip ar trebui să fie transcodificat. Videoclipurile HDR vor fi întotdeauna transcodificate (cu excepția cazului în care transcodarea este dezactivată).", + "transcoding_two_pass_encoding": "Codare în două treceri", + "transcoding_two_pass_encoding_setting_description": "Transcodificare în două treceri pentru a produce videoclipuri codificate mai bine. Când rata maximă de biți este activată (necesară pentru a funcționa cu H.264 și HEVC), acest mod utilizează un interval de rată de biți bazat pe rata maximă de biți și ignoră CRF. Pentru VP9, CRF poate fi utilizat dacă rata maximă de biți este dezactivată.", "transcoding_video_codec": "Codec video", "transcoding_video_codec_description": "VP9 are eficiențǎ mare și compatibilitate web, însǎ transcodarea este de duratǎ mai mare. HEVC se comportǎ asemǎnǎtor, însǎ are compatibilitate web mai micǎ. H.264 este foarte compatibil și rapid în transcodare, însǎ genereazǎ fișiere mult mai mari. AV1 este cel mai eficient codec dar nu este compatibil cu dispozitivele mai vechi.", - "trash_enabled_description": "", + "trash_enabled_description": "Activează funcțiile Coș de gunoi", "trash_number_of_days": "Numǎr de zile", "trash_number_of_days_description": "Numǎr de zile pentru pǎstrarea fișierelor în coșul de gunoi pânǎ la ștergerea permanentǎ", "trash_settings": "Setǎri coș de gunoi", "trash_settings_description": "Gestioneazǎ setǎrile coșului de gunoi", - "user_delete_delay_settings": "", - "user_delete_delay_settings_description": "", + "untracked_files": "Fișiere neurmărite", + "untracked_files_description": "Aceste fișiere nu sunt urmărite de aplicație. Ele pot fi rezultatul unor mutări eșuate, încărcări întrerupte sau pot rămâne în urmă din cauza unei erori", + "user_delete_delay": "Contul și resursele utilizatorului {user} vor fi programate pentru ștergere permanentă în {delay, plural, one {# zi} other {# zile}}.", + "user_delete_delay_settings": "Întârziere la ștergere", + "user_delete_delay_settings_description": "Numărul de zile după eliminare până la ștergerea permanentă a contului și a resurselor unui utilizator. Procesul de ștergere a utilizatorului rulează la miezul nopții pentru a verifica utilizatorii care sunt pregătiți pentru ștergere. Modificările aduse acestei setări vor fi evaluate la următoarea execuție.", + "user_delete_immediately": "Contul și resursele utilizatorului {user} vor fi puse în coadă pentru ștergere permanentă imediat.", + "user_delete_immediately_checkbox": "Pune utilizatorul și resursele în coadă pentru ștergere imediată", + "user_management": "Gestionarea Utilizatorilor", + "user_password_has_been_reset": "Parola utilizatorului a fost resetată:", + "user_password_reset_description": "Vă rugăm să furnizați utilizatorului parola temporară și să îi informați că va trebui să o schimbe la următoarea autentificare.", + "user_restore_description": "Contul utilizatorului {user} va fi restaurat.", + "user_restore_scheduled_removal": "Restaurare utilizator - ștergere programată pe {date, date, long}", "user_settings": "Setǎri utilizator", "user_settings_description": "Gestioneazǎ setǎrile utilizatorului", - "version_check_enabled_description": "Activeazǎ verificarea periodicǎ pe GitHub pentru versiuni noi", + "user_successfully_removed": "Utilizatorul {email} a fost eliminat cu succes.", + "version_check_enabled_description": "Activează verificarea versiunii", + "version_check_implications": "Funcția de verificare a versiunii se bazează pe comunicarea periodică cu github.com", "version_check_settings": "Verificare versiune", "version_check_settings_description": "Activeazǎ/dezactiveazǎ notificarea unei noi versiuni", - "video_conversion_job_description": "Transcodeazǎ videoclipurile pentru compatibilitate cu browsere și dispozitive" + "video_conversion_job": "Transcodați videoclipuri", + "video_conversion_job_description": "Transcodați videoclipurile pentru o compatibilitate mai mare cu browserele și dispozitivele" }, "admin_email": "E-mailul administratorului", - "admin_password": "Parola administratorului", + "admin_password": "Parolă administrator", "administration": "Administrare", "advanced": "Avansat", + "age_months": "Vârstă {months, plural, one {# lună} other {# luni}}", + "age_year_months": "Vârstă de 1 an, {months, plural, one {# lună} other {# luni}}", "album_added": "Album adăugat", "album_added_notification_setting_description": "Primiți o notificare prin e-mail când sunteți adăugat la un album partajat", "album_cover_updated": "Coperta albumului a fost actualizată", - "album_info_updated": "Informațiile albumului au fost actualizate", - "album_name": "Nume de album", - "album_options": "Opțiuni de album", + "album_delete_confirmation": "Ești sigur că vrei să ștergi albumul {album}?", + "album_delete_confirmation_description": "Dacă acest album este partajat, alți utilizatori nu vor mai putea accesa.", + "album_info_updated": "Informații album actualizate", + "album_leave": "Lăsați albumul?", + "album_leave_confirmation": "Ești sigur că dorești să părăsești {album}?", + "album_name": "Nume album", + "album_options": "Opțiuni album", + "album_remove_user": "Eliminare utilizator?", + "album_remove_user_confirmation": "Ești sigur că dorești eliminarea {user}?", + "album_share_no_users": "Se pare că ai partajat acest album cu toți utilizatorii sau nu ai niciun utilizator cu care să-l partajezi.", "album_updated": "Album actualizat", "album_updated_setting_description": "Primiți o notificare prin e-mail când un album partajat are elemente noi", + "album_user_left": "A părăsit {album}", + "album_user_removed": "{user} eliminat", + "album_with_link_access": "Permite oricui cu link-ul să vadă fotografiile și persoanele din acest album.", "albums": "Albume", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albume}}", "all": "Toate", @@ -334,40 +366,58 @@ "allow_edits": "Permite editări", "allow_public_user_to_download": "Permite utilizatorului public să descarce", "allow_public_user_to_upload": "Permite utilizatorului public să încarce", + "anti_clockwise": "În sens invers acelor de ceasornic", "api_key": "Cheie API", "api_key_description": "Această valoare va fi afișată o singură dată. Vă rugăm să vă asigurați că o copiați înainte de a închide fereastra.", "api_key_empty": "Numele cheii API nu trebuie să fie gol", "api_keys": "Chei API", - "app_settings": "Setări în aplicație", + "app_settings": "Setări Aplicație", "appears_in": "Apare în", "archive": "Arhivă", "archive_or_unarchive_photo": "Arhiveazǎ sau dezarhiveazǎ fotografia", + "archive_size": "Mărime arhivă", + "archive_size_description": "Configurează dimensiunea arhivei pentru descărcări (în GiB)", "archived": "", - "archived_count": "{count, plural, one {S-a arhivat #}, other {S-au arhivat #}}", + "archived_count": "{count, plural, other {Arhivat/e#}}", + "are_these_the_same_person": "Sunt aceștia aceeași persoană?", "are_you_sure_to_do_this": "Sunteți sigur că doriți să faceți acest lucru?", "asset_added_to_album": "Adăugat la album", - "asset_adding_to_album": "Se adauga la album...", + "asset_adding_to_album": "Se adaugă la album...", "asset_description_updated": "Descrierea activelor a fost actualizată", - "asset_filename_is_offline": "Activul {filename} este offline", - "asset_has_unassigned_faces": "Activul are fețe neatribuite", - "asset_hashing": "Hasurare...", + "asset_filename_is_offline": "Resursa {filename} este offline", + "asset_has_unassigned_faces": "Resursa are fețe neatribuite", + "asset_hashing": "Hașurare...", "asset_offline": "Resursă offline", - "asset_offline_description": "Acest activ este offline. Immich nu poate accesa locația fișierului său. Vă rugăm să vă asigurați că activul este disponibil și apoi să efectuați o nouă scanare a bibliotecii.", + "asset_offline_description": "Această resursă este offline. Immich nu poate accesa locația fișierului său. Vă rugăm să vă asigurați că resursa este disponibilă și apoi să efectuați o nouă scanare a bibliotecii.", "asset_skipped": "Sărit", + "asset_skipped_in_trash": "În gunoi", "asset_uploaded": "Încărcat", - "asset_uploading": "Se incărca...", + "asset_uploading": "Se incarcă...", "assets": "Resurse", + "assets_added_count": "Adăugat {count, plural, one {# resursă} other {# resurse}}", + "assets_added_to_album_count": "Am adăugat {count, plural, one {# resursă} other {# resurse}} în album", + "assets_added_to_name_count": "Am adăugat {count, plural, one {# resursă} other {# resurse}} în {hasName, select, true {{name}} other {albumul nou}}", + "assets_count": "{count, plural, one {# resursă} other {# resurse}}", + "assets_moved_to_trash_count": "Am mutat {count, plural, one {# resursă} other {# resurse}} în coșul de gunoi", + "assets_permanently_deleted_count": "Șters permanent {count, plural, one {# resursă} other {# resurse}}", + "assets_removed_count": "Eliminat {count, plural, one {# resursă} other {# resurse}}", + "assets_restore_confirmation": "Ești sigur că vrei să restaurezi toate resursele tale din coșul de gunoi? Nu poți anula această acțiune!", + "assets_restored_count": "Restaurat {count, plural, one {# resursă} other {# resurse}}", + "assets_trashed_count": "Mutat în coșul de gunoi {count, plural, one {# resursă} other {# resurse}}", + "assets_were_part_of_album_count": "{count, plural, one {Resursa era} other {Resursele erau}} deja parte din album", "authorized_devices": "Dispozitive autorizate", "back": "Înapoi", "back_close_deselect": "Înapoi, închidere sau deselectare", - "backward": "Invers", + "backward": "În sens invers", "birthdate_saved": "Data nașterii salvată cu succes", "birthdate_set_description": "Data nașterii este utilizată pentru a calcula vârsta acestei persoane la momentul realizării fotografiei.", "blurred_background": "Fundal neclar", "build": "Construiți", - "build_image": "Construiți o imagine", - "bulk_delete_duplicates_confirmation": "Sunteți sigur că doriți să ștergeți în masă {count, plural, one {# duplicate asset} other {# duplicate assets}}? Acest lucru va păstra cel mai mare activ din fiecare grup și va șterge definitiv toate celelalte duplicate. Nu puteți anula această acțiune!", - "buy": "Cumpără Immich", + "build_image": "Construiți imagine", + "bulk_delete_duplicates_confirmation": "Ești sigur că vrei să ștergi în masă {count, plural, one {# resursă duplicată} other {# resurse duplicate}}? Aceasta va păstra cea mai mare resursă din fiecare grup și va șterge permanent toate celelalte duplicate. Nu poți anula această acțiune!", + "bulk_keep_duplicates_confirmation": "Ești sigur că vrei să păstrezi {count, plural, one {# resursă duplicată} other {# resurse duplicate}}? Aceasta va rezolva toate grupurile duplicate fără a șterge nimic.", + "bulk_trash_duplicates_confirmation": "Ești sigur că vrei să muți în coșul de gunoi {count, plural, one {# resursă duplicată} other {# resurse duplicate}}? Aceasta va păstra cea mai mare resursă din fiecare grup și va muta în coșul de gunoi toate celelalte duplicate.", + "buy": "Achiziționează Immich", "camera": "Camerǎ", "camera_brand": "Marcǎ cameră", "camera_model": "Model cameră", @@ -381,37 +431,41 @@ "cant_search_people": "", "cant_search_places": "", "change_date": "Schimbă dată", - "change_expiration_time": "Shimbă data expirării", + "change_expiration_time": "Shimbă dată expirare", "change_location": "Schimbă locația", - "change_name": "Schimbă numele", - "change_name_successfully": "Schimbă numele cu succes", - "change_password": "Schimbă parola", - "change_password_description": "Aceasta este fie prima dată când vă conectați la sistem, fie vi s-a solicitat să vă schimbați parola. Vă rugăm să introduceți noua parolă mai jos.", + "change_name": "Schimbă nume", + "change_name_successfully": "Schimbare nume cu succes", + "change_password": "Schimbă Parolă", + "change_password_description": "Aceasta este fie prima dată când te conectezi în sistem, fie s-a făcut o solicitare pentru a schimba parola ta. Te rog să introduci noua parolă mai jos.", "change_your_password": "Schimbă-ți parola", - "changed_visibility_successfully": "Schimbă visibilitate cu succes", - "check_logs": "Verificarea logurilor", - "choose_matching_people_to_merge": "Alegeți persoanele potrivite pentru fuzionare", + "changed_visibility_successfully": "Schimbare vizibilitate cu succes", + "check_all": "Selectează Tot", + "check_logs": "Verifică Jurnale", + "choose_matching_people_to_merge": "Alegeți persoanele care se potrivesc pentru a le fuziona", "city": "Oraș", - "clear": "ȘTERGE", - "clear_all": "Șterge tot", + "clear": "Curăță", + "clear_all": "Curăță tot", + "clear_all_recent_searches": "Curăță toate căutările recente", "clear_message": "Șterge mesajul", - "clear_value": "Valoare clară", + "clear_value": "Șterge valoare", + "clockwise": "În sensul acelor de ceas", "close": "Închide", - "collapse": "Colaps", - "collapse_all": "Închideți pe toate", + "collapse": "Restrânge", + "collapse_all": "Restrânge pe toate", + "color": "Culoare", "color_theme": "Tema de culoare", "comment_deleted": "Comentariu șters", - "comment_options": "Opțiuni de comentariu", - "comments_and_likes": "Comentarii și aprecieri", + "comment_options": "Opțiuni comentariu", + "comments_and_likes": "Comentarii & aprecieri", "comments_are_disabled": "Comentariile sunt dezactivate", "confirm": "Confirmați", "confirm_admin_password": "Confirmați parola de administrator", "confirm_delete_shared_link": "Sunteți sigur că doriți să ștergeți acest link partajat?", "confirm_password": "Confirmați parola", - "contain": "Conține", + "contain": "Încadrează", "context": "Context", "continue": "Continuați", - "copied_image_to_clipboard": "Copiat imaginea în clipboard.", + "copied_image_to_clipboard": "Imaginea copiată în clipboard.", "copied_to_clipboard": "Copiat în clipboard!", "copy_error": "Eroare de copiere", "copy_file_path": "Copiați calea fișierului", @@ -421,64 +475,70 @@ "copy_password": "Copiați parola", "copy_to_clipboard": "Copiere în Clipboard", "country": "Țara", - "cover": "Acoperire", - "covers": "Acoperiri", + "cover": "Umple fereastra", + "covers": "Acoperă", "create": "Creează", "create_album": "Creează album", - "create_library": "Crearea bibliotecii", + "create_library": "Creează bibliotecă", "create_link": "Creează link", "create_link_to_share": "Creează link pentru a distribui", "create_link_to_share_description": "Permiteți oricui are link-ul să vadă fotografia (fotografiile) selectată(e)", "create_new_person": "Creați o persoană nouă", - "create_new_person_hint": "Atribuiți activele selectate unei persoane noi", - "create_new_user": "Crearea unui nou utilizator", + "create_new_person_hint": "Atribuiți resursele selectate unei persoane noi", + "create_new_user": "Creează utilizator nou", + "create_tag": "Creează etichetă", + "create_tag_description": "Creează o etichetă nouă. Pentru etichete imbricate, te rog să introduci calea completă a etichetei, inclusiv bare oblice (/).", "create_user": "Creează utilizator", "created": "Creat", "current_device": "Dispozitiv curent", - "custom_locale": "Local personalizat", + "custom_locale": "Setare regională personalizată", "custom_locale_description": "Formatați datele și numerele în funcție de limbă și regiune", - "dark": "Întuneric", - "date_after": "Data după", + "dark": "Întunecat", + "date_after": "Dată după", "date_and_time": "Dată și Oră", - "date_before": "Data anterioară", + "date_before": "Dată anterioară", "date_of_birth_saved": "Data nașterii salvată cu succes", "date_range": "Interval de date", - "day": "Ziua", + "day": "Zi", "deduplicate_all": "Deduplicați toate", - "default_locale": "Local implicit", - "default_locale_description": "Formatați datele și numerele în funcție de locația browserului dvs.", + "default_locale": "Setare regionlă implicită", + "default_locale_description": "Formatați datele și numerele în funcție de regiunea browserului dvs", "delete": "Șterge", "delete_album": "Șterge album", "delete_api_key_prompt": "Sunteți sigur că doriți să ștergeți această cheie API?", "delete_duplicates_confirmation": "Sunteți sigur că doriți să ștergeți permanent aceste duplicate?", - "delete_key": "Tasta de ștergere", - "delete_library": "Ștergeți biblioteca", - "delete_link": "Ștergeți linkul", - "delete_shared_link": "Ștergeți link-ul partajat", - "delete_user": "Ștergeți utilizatorul", + "delete_key": "Șterge cheie", + "delete_library": "Șterge biblioteca", + "delete_link": "Șterge linkul", + "delete_shared_link": "Șterge link-ul partajat", + "delete_tag": "Șterge etichetă", + "delete_tag_confirmation_prompt": "Ești sigur că vrei să ștergi eticheta {tagName} ?", + "delete_user": "Șterge utilizator", "deleted_shared_link": "Link partajat șters", "description": "Descriere", - "details": "DETALII", + "details": "Detalii", "direction": "Direcție", "disabled": "Dezactivat", - "disallow_edits": "Interziceți editările", + "disallow_edits": "Interzice modificările", "discover": "Descoperiți", - "dismiss_all_errors": "Eliminați toate erorile", - "dismiss_error": "Anulați eroarea", + "dismiss_all_errors": "Ignoră toate erorile", + "dismiss_error": "Ignorați eroarea", "display_options": "Opțiuni de afișare", "display_order": "Ordine de afișare", "display_original_photos": "Afișați fotografiile originale", - "display_original_photos_setting_description": "Preferați să afișați fotografia originală atunci când vizualizați un bun în loc de miniaturi atunci când bunul original este compatibil cu web. Acest lucru poate duce la o viteză mai mică de afișare a fotografiilor.", - "do_not_show_again": "Nu mai afișați acest mesaj", + "display_original_photos_setting_description": "Preferă să afișezi fotografia originală atunci când vizualizezi o resursă, în loc de miniaturi, atunci când resursa originală este compatibilă cu web-ul. Aceasta poate duce la viteze mai lente de afișare a fotografiilor.", + "do_not_show_again": "Nu mai afișa acest mesaj", "done": "Gata", "download": "Descarcă", + "download_include_embedded_motion_videos": "Videoclipuri încorporate", + "download_include_embedded_motion_videos_description": "Include videoclipurile încorporate în fotografiile în mișcare ca fișier separat", "download_settings": "Descarcă", - "download_settings_description": "Gestionați setările legate de descărcarea activelor", - "downloading": "Descărcare", - "downloading_asset_filename": "Descărcarea activului {filename}", - "drop_files_to_upload": "Aruncați fișiere oriunde pentru a le încărca", + "download_settings_description": "Gestionați setările legate de descărcarea resurselor", + "downloading": "Se descarcă", + "downloading_asset_filename": "Se descarcă resursa {filename}", + "drop_files_to_upload": "Trage fișierele aici pentru a le încărca", "duplicates": "Duplicate", - "duplicates_description": "Rezolvați fiecare grup indicând care, dacă există, sunt duplicate", + "duplicates_description": "Rezolvați fiecare grup indicând care sunt duplicate, dacă există", "duration": "Durată", "durations": { "days": "", @@ -487,13 +547,13 @@ "months": "", "years": "" }, - "edit": "Editare", - "edit_album": "Editare album", - "edit_avatar": "Editare avatar", - "edit_date": "Editează data", - "edit_date_and_time": "Editarea datei și orei", + "edit": "Modifică", + "edit_album": "Modificare album", + "edit_avatar": "Modificare avatar", + "edit_date": "Modifică data", + "edit_date_and_time": "Modifică data și ora", "edit_exclusion_pattern": "Editarea modelului de excludere", - "edit_faces": "Editează fețele", + "edit_faces": "Modifică fețele", "edit_import_path": "Editarea căii de import", "edit_import_paths": "Editarea căilor de import", "edit_key": "Tastă de editare", diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index bd725a11cfa34..44b9e48f954f5 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -1,7 +1,7 @@ { "about": "О продукте", "account": "Учётная запись", - "account_settings": "Настройки учётной записи", + "account_settings": "Настройки аккаунта", "acknowledge": "Подтвердить", "action": "Действие", "actions": "Действия", @@ -661,6 +661,7 @@ "unable_to_get_comments_number": "Не удалось получить количество комментариев", "unable_to_get_shared_link": "Не удалось получить общую ссылку", "unable_to_hide_person": "Невозможно скрыть персону", + "unable_to_link_motion_video": "Не удается связать движущееся видео", "unable_to_link_oauth_account": "Не удается связать учетную запись OAuth", "unable_to_load_album": "Невозможно загрузить альбом", "unable_to_load_asset_activity": "Не удалось загрузить активность объекта", @@ -701,6 +702,7 @@ "unable_to_submit_job": "Невозможно отправить задание", "unable_to_trash_asset": "Невозможно удалить актив", "unable_to_unlink_account": "Не удалось отсоединить учетную запись", + "unable_to_unlink_motion_video": "Не удается отсоединить движущееся видео", "unable_to_update_album_cover": "Невозможно обновить обложку альбома", "unable_to_update_album_info": "Невозможно обновить информацию об альбоме", "unable_to_update_library": "Не удалось обновить библиотеку", @@ -1291,6 +1293,7 @@ "unknown_album": "Неизвестный альбом", "unknown_year": "Неизвестный Год", "unlimited": "Не ограничено", + "unlink_motion_video": "Отсоединить движущееся видео", "unlink_oauth": "Отключить OAuth", "unlinked_oauth_account": "Отключить аккаунт OAuth", "unnamed_album": "Альбом без названия", diff --git a/web/src/lib/i18n/sk.json b/web/src/lib/i18n/sk.json index d6c066a4cc840..cd164a0ccf40b 100644 --- a/web/src/lib/i18n/sk.json +++ b/web/src/lib/i18n/sk.json @@ -112,7 +112,7 @@ "map_reverse_geocoding": "", "map_reverse_geocoding_enable_description": "", "map_reverse_geocoding_settings": "", - "map_settings": "", + "map_settings": "Mapa", "map_settings_description": "", "map_style_description": "", "metadata_extraction_job_description": "", @@ -460,7 +460,7 @@ "expand_all": "", "expire_after": "Expiruje po", "expired": "Vypršalo", - "explore": "", + "explore": "Preskúmať", "extension": "", "external_libraries": "", "failed_to_get_people": "", diff --git a/web/src/lib/i18n/sr_Cyrl.json b/web/src/lib/i18n/sr_Cyrl.json index 7bcd1e3dd8634..1241ad72fe7b3 100644 --- a/web/src/lib/i18n/sr_Cyrl.json +++ b/web/src/lib/i18n/sr_Cyrl.json @@ -661,6 +661,7 @@ "unable_to_get_comments_number": "Није могуће добити број коментара", "unable_to_get_shared_link": "Преузимање дељене везе није успело", "unable_to_hide_person": "Није могуће сакрити особу", + "unable_to_link_motion_video": "Није могуће повезати (link) видео снимак", "unable_to_link_oauth_account": "Није могуће повезати OAuth налог", "unable_to_load_album": "Није могуће учитати албум", "unable_to_load_asset_activity": "Није могуће учитати активност средстава", @@ -701,6 +702,7 @@ "unable_to_submit_job": "Није могуће предати задатак", "unable_to_trash_asset": "Није могуће избацити материјал у отпад", "unable_to_unlink_account": "Није могуће раскинути профил", + "unable_to_unlink_motion_video": "Није могуће прекинути везу са видео снимком", "unable_to_update_album_cover": "Није могуће ажурирати насловницу албума", "unable_to_update_album_info": "Није могуће ажурирати информације о албуму", "unable_to_update_library": "Није могуће ажурирати библиотеку", @@ -1291,6 +1293,7 @@ "unknown_album": "Nepoznat Album", "unknown_year": "Непозната Година", "unlimited": "Неограничено", + "unlink_motion_video": "Прекините везу са видео снимком", "unlink_oauth": "Прекини везу са Oauth-om", "unlinked_oauth_account": "Опозвана веза OAuth налога", "unnamed_album": "Неименовани албум", diff --git a/web/src/lib/i18n/sr_Latn.json b/web/src/lib/i18n/sr_Latn.json index beb2009b4d4a8..26f5483c69ac6 100644 --- a/web/src/lib/i18n/sr_Latn.json +++ b/web/src/lib/i18n/sr_Latn.json @@ -661,6 +661,7 @@ "unable_to_get_comments_number": "Nije moguće dobiti broj komentara", "unable_to_get_shared_link": "Preuzimanje deljene veze nije uspelo", "unable_to_hide_person": "Nije moguće sakriti osobu", + "unable_to_link_motion_video": "Nije moguće povezati video sa slikom", "unable_to_link_oauth_account": "Nije moguće povezati OAuth nalog", "unable_to_load_album": "Nije moguće učitati album", "unable_to_load_asset_activity": "Nije moguće učitati aktivnost sredstava", @@ -701,6 +702,7 @@ "unable_to_submit_job": "Nije moguće predati zadatak", "unable_to_trash_asset": "Nije moguće izbaciti materijal u otpad", "unable_to_unlink_account": "Nije moguće raskinuti profil", + "unable_to_unlink_motion_video": "Nije moguće odvezati video sa slikom", "unable_to_update_album_cover": "Nije moguće ažurirati naslovnicu albuma", "unable_to_update_album_info": "Nije moguće ažurirati informacije o albumu", "unable_to_update_library": "Nije moguće ažurirati biblioteku", @@ -1291,6 +1293,7 @@ "unknown_album": "Nepoznat Album", "unknown_year": "Nepoznata Godina", "unlimited": "Neograničeno", + "unlink_motion_video": "Odveži video od slike", "unlink_oauth": "Prekini vezu sa Oauth-om", "unlinked_oauth_account": "Opozvana veza OAuth naloga", "unnamed_album": "Neimenovani album", diff --git a/web/src/lib/i18n/th.json b/web/src/lib/i18n/th.json index 32336bfb4e9ce..f34fab2a1e6ca 100644 --- a/web/src/lib/i18n/th.json +++ b/web/src/lib/i18n/th.json @@ -25,7 +25,7 @@ "add_to_shared_album": "เพิ่มเข้าอัลบั้มที่แชร์", "added_to_archive": "เพิ่มเข้าที่เก็บถาวร", "added_to_favorites": "เพิ่มเข้ารายการโปรด", - "added_to_favorites_count": "{count} รูปถูกเพิ่มเข้ารายการโปรด", + "added_to_favorites_count": "{count, number} รูปถูกเพิ่มเข้ารายการโปรด", "admin": { "add_exclusion_pattern_description": "เพิ่มรูปแบบการยกเว้น การ Glob โดยใช้ *, ** และ ? ถูกรองรับ ถ้าต้องการละเว้นไฟล์ทั้งหมดในไดเร็กทอรีใดๆที่ชื่อว่า \"Raw\" ให้ใช้ \"**/Raw/**\" ถ้าต้องการละเว้นไฟล์ทั้งหมดที่ลงท้ายด้วย \".tif\" ให้ใช้ \"**/*.tif\" ถ้าต้องการละเว้นพาธที่เริ่มจากไดเรกทอรีบนสุดให้ใช้ \"/พาธ/ที่ต้องการ/ละเว้น/**\"", "authentication_settings": "ตั้งค่าการเข้าถึง", @@ -129,16 +129,21 @@ "map_enable_description": "เปิดใช้งานแผนที่", "map_gps_settings": "การตั้งค่าแผนที่และ GPS", "map_gps_settings_description": "จัดการการตั้งค่าแผนที่และ GPS (Reverse Geocoding)", + "map_implications": "ฟีเจอร์แผนที่ต้องการบริการแผ่นแผนที่จากภายนอก (tiles.immich.cloud)", "map_light_style": "แบบสว่าง", "map_manage_reverse_geocoding_settings": "จัดการการตั้งค่าแปลงพิกัดภูมิศาสตร์ ", "map_reverse_geocoding": "ประมวลผลชื่อทางภูมิศาสตร์", "map_reverse_geocoding_enable_description": "เปิดใช้งานประมวลผลชื่อทางภูมิศาสตร์", "map_reverse_geocoding_settings": "การตั้งค่าประมวลผลชื่อทางภูมิศาสตร์", - "map_settings": "การตั้งค่าแผนที่", + "map_settings": "แผนที่", "map_settings_description": "จัดการการตั้งค่าแผนที่", "map_style_description": "URL ไปยังธีมแผนที่ style.json", "metadata_extraction_job": "ดึงข้อมูล metadata", - "metadata_extraction_job_description": "ดึงข้อมูล metadata จากสื่อ เช่น GPS และความละเอียด", + "metadata_extraction_job_description": "ดึงข้อมูล metadata จากสื่อ เช่น GPS ใบหน้าและความละเอียด", + "metadata_faces_import_setting": "เปิดการนำเข้าข้อมูลใบหน้า", + "metadata_faces_import_setting_description": "นำเข้าข้อมูลใบหน้าจาก EXIF ของไฟล์ภาพและไฟล์ประกอบ", + "metadata_settings": "การตั้งค่า Metadata", + "metadata_settings_description": "จัดการการตั้งค่า Metadata", "migration_job": "การโยกย้าย", "migration_job_description": "ย้ายภาพตัวอย่างสื่อและใบหน้าไปยังโครงสร้างโฟลเดอร์ล่าสุด", "no_paths_added": "ไม่ได้เพิ่มพาธ", @@ -173,7 +178,9 @@ "oauth_issuer_url": "ผู้ออก URL", "oauth_mobile_redirect_uri": "URI เปลี่ยนเส้นทางบนโทรศัพท์", "oauth_mobile_redirect_uri_override": "แทนที่ URI เปลี่ยนเส้นทางบนโทรศัพท์", - "oauth_mobile_redirect_uri_override_description": "เปิดเมื่อ 'app.immich:/' เป็น URI เปลี่ยนเส้นทางที่ไม่ถูกต้อง", + "oauth_mobile_redirect_uri_override_description": "เปิดเมื่อ OAuth ไม่รองรับ URI บนอุปกรณ์ เช่น '{callback}'", + "oauth_profile_signing_algorithm": "อัลกอริทึมการรับรองบัญชีผู้ใช้", + "oauth_profile_signing_algorithm_description": "อัลกอริทึมใช้ในการรับรองบัญชีผู้ใช้", "oauth_scope": "ขอบเขต", "oauth_settings": "OAuth", "oauth_settings_description": "จัดการการตั้งค่าล็อกอินผ่าน OAuth", @@ -818,7 +825,7 @@ "status": "สถานะ", "stop_motion_photo": "", "stop_photo_sharing": "หยุดแชร์รูปภาพ?", - "storage": "ที่จัดเก็บ", + "storage": "พื้นที่จัดเก็บ", "storage_label": "", "submit": "ส่ง", "suggestions": "ข้อเสนอแนะ", diff --git a/web/src/lib/i18n/uk.json b/web/src/lib/i18n/uk.json index ce72fde8b42ff..3e24ccacc458d 100644 --- a/web/src/lib/i18n/uk.json +++ b/web/src/lib/i18n/uk.json @@ -660,6 +660,7 @@ "unable_to_get_comments_number": "Не вдалося отримати кількість коментарів", "unable_to_get_shared_link": "Не вдалося отримати спільне посилання", "unable_to_hide_person": "Неможливо приховати людину", + "unable_to_link_motion_video": "Не вдається зв'язати рухоме відео", "unable_to_link_oauth_account": "Не вдається прив'язати обліковий запис OAuth", "unable_to_load_album": "Неможливо завантажити альбом", "unable_to_load_asset_activity": "Неможливо завантажити активність активу", @@ -700,6 +701,7 @@ "unable_to_submit_job": "Не вдалося відправити завдання", "unable_to_trash_asset": "Неможливо вилучити актив", "unable_to_unlink_account": "Не вдається відв'язати обліковий запис", + "unable_to_unlink_motion_video": "Не вдається від'єднати рухоме відео", "unable_to_update_album_cover": "Неможливо оновити обкладинку альбому", "unable_to_update_album_info": "Неможливо оновити інформацію про альбом", "unable_to_update_library": "Не вдалося оновити бібліотеку", @@ -845,6 +847,7 @@ "license_trial_info_4": "Будь ласка, розгляньте можливість придбання ліцензії для підтримки подальшого розвитку сервісу", "light": "Світла", "like_deleted": "Лайк видалено", + "link_motion_video": "Посилання на рухоме відео", "link_options": "Налаштування посилання", "link_to_oauth": "Приєднання до OAuth", "linked_oauth_account": "Приєднаний акаунт OAuth", @@ -1288,6 +1291,7 @@ "unknown_album": "", "unknown_year": "Невідомий рік", "unlimited": "Без обмежень", + "unlink_motion_video": "Від'єднати рухоме відео", "unlink_oauth": "Від'єднайте OAuth", "unlinked_oauth_account": "Відключити акаунт OAuth", "unnamed_album": "Альбом без назви", diff --git a/web/src/lib/i18n/vi.json b/web/src/lib/i18n/vi.json index e94eb7a46471f..ec8c8d4e7f61a 100644 --- a/web/src/lib/i18n/vi.json +++ b/web/src/lib/i18n/vi.json @@ -660,6 +660,7 @@ "unable_to_get_comments_number": "Không thể lấy số lượng bình luận", "unable_to_get_shared_link": "Không thể lấy liên kết chia sẻ", "unable_to_hide_person": "Không thể ẩn người", + "unable_to_link_motion_video": "Không thể liên kết video chuyển động", "unable_to_link_oauth_account": "Không thể liên kết tài khoản OAuth", "unable_to_load_album": "Không thể tải album", "unable_to_load_asset_activity": "Không thể tải hoạt động của ảnh", @@ -700,6 +701,7 @@ "unable_to_submit_job": "Không thể gửi tác vụ", "unable_to_trash_asset": "Không thể chuyển ảnh vào thùng rác", "unable_to_unlink_account": "Không thể hủy liên kết tài khoản", + "unable_to_unlink_motion_video": "Không thể hủy liên kết video chuyển động", "unable_to_update_album_cover": "Không thể cập nhật ảnh bìa album", "unable_to_update_album_info": "Không thể cập nhật thông tin album", "unable_to_update_library": "Không thể cập nhật thư viện", @@ -1261,6 +1263,7 @@ "unknown_album": "", "unknown_year": "Năm không xác định", "unlimited": "Không giới hạn", + "unlink_motion_video": "Hủy liên kết video chuyển động", "unlink_oauth": "Huỷ liên kết OAuth", "unlinked_oauth_account": "Đã huỷ liên kết tài khoản OAuth", "unnamed_album": "Album chưa đặt tên", diff --git a/web/src/lib/i18n/zh_SIMPLIFIED.json b/web/src/lib/i18n/zh_SIMPLIFIED.json index e879365f410bb..08c236dcbf81c 100644 --- a/web/src/lib/i18n/zh_SIMPLIFIED.json +++ b/web/src/lib/i18n/zh_SIMPLIFIED.json @@ -27,7 +27,7 @@ "added_to_favorites": "添加到收藏", "added_to_favorites_count": "添加{count, number}项到收藏", "admin": { - "add_exclusion_pattern_description": "添加排除规则。支持使用 *、** 和 ? 通配符。比如要忽略名为 “Raw” 的任何目录中的所有文件,请使用 “**/Raw/**”;要忽略所有以 “.tif” 结尾的文件,请使用 “**/*.tif”;要忽略绝对路径,请使用 “/path/to/ignore/**”。", + "add_exclusion_pattern_description": "添加排除规则。支持使用 *、** 和 ? 通配符。比如要忽略任何名为 “Raw” 的文件夹中的所有文件,请使用 “**/Raw/**”;要忽略所有以 “.tif” 结尾的文件,请使用 “**/*.tif”;要忽略绝对路径,请使用 “/path/to/ignore/**”。", "authentication_settings": "认证设置", "authentication_settings_description": "管理密码、OAuth 和其它认证设置", "authentication_settings_disable_all": "确定要禁用所有的登录方式?此操作将完全禁止登录。", @@ -119,7 +119,7 @@ "machine_learning_settings": "机器学习设置", "machine_learning_settings_description": "管理机器学习功能和设置", "machine_learning_smart_search": "智能搜索", - "machine_learning_smart_search_description": "使用CLIP相似度进行图像语义搜索", + "machine_learning_smart_search_description": "使用CLIP以文搜图、智能搜图", "machine_learning_smart_search_enabled": "启用智能搜索", "machine_learning_smart_search_enabled_description": "如果禁用,则不会对图像编码以用于智能搜索。", "machine_learning_url_description": "机器学习服务器的URL", @@ -152,8 +152,8 @@ "note_cannot_be_changed_later": "注意:此项一旦设定,以后无法更改!", "note_unlimited_quota": "提示:输入0表示无限制", "notification_email_from_address": "发件人地址", - "notification_email_from_address_description": "发件人邮箱地址,例如“Immich 服务器 ”", - "notification_email_host_description": "邮件服务器主机(例如 smtp.immich.app)", + "notification_email_from_address_description": "发件人邮箱地址,例如“张三<12345@qq.com>”", + "notification_email_host_description": "服务器地址:(例如:smtp.qq.com)", "notification_email_ignore_certificate_errors": "忽略证书错误", "notification_email_ignore_certificate_errors_description": "忽略TLS证书验证错误(不建议)", "notification_email_password_description": "与邮件服务器进行身份验证时使用的密码", @@ -171,7 +171,7 @@ "oauth_auto_launch_description": "在登录页面自动启动OAuth登录", "oauth_auto_register": "自动注册", "oauth_auto_register_description": "使用OAuth登录后自动注册新用户", - "oauth_button_text": "按钮文本", + "oauth_button_text": "按钮名称", "oauth_client_id": "客户端ID", "oauth_client_secret": "客户端密钥", "oauth_enable_description": "使用OAuth登录", @@ -320,7 +320,7 @@ "user_management": "用户管理", "user_password_has_been_reset": "该用户的密码被重置:", "user_password_reset_description": "请向用户提供临时密码,并告知他们下次登录时需要更改密码。", - "user_restore_description": "{user}的账户将被恢复。", + "user_restore_description": "账户“{user}”将被恢复。", "user_restore_scheduled_removal": "恢复用户 - 计划于{date, date, long}删除", "user_settings": "用户设置", "user_settings_description": "管理用户设置", @@ -465,7 +465,7 @@ "confirm_delete_shared_link": "您确定要删除此共享链接吗?", "confirm_password": "确认密码", "contain": "包含", - "context": "图像语义搜索", + "context": "以文搜图", "continue": "继续", "copied_image_to_clipboard": "已复制图片至剪贴板。", "copied_to_clipboard": "已复制到剪切板!", @@ -496,12 +496,12 @@ "custom_locale": "自定义地区", "custom_locale_description": "日期和数字显示格式跟随语言和地区", "dark": "深色", - "date_after": "日期之后", + "date_after": "开始日期", "date_and_time": "日期与时间", - "date_before": "日期之前", + "date_before": "结束日期", "date_of_birth_saved": "出生日期保存成功", "date_range": "日期范围", - "day": "天", + "day": "日", "deduplicate_all": "删除所有重复项", "default_locale": "默认地区", "default_locale_description": "根据您的浏览器地区设置日期和数字显示格式", @@ -681,7 +681,7 @@ "unable_to_remove_library": "无法移除图库", "unable_to_remove_offline_files": "无法移除离线文件", "unable_to_remove_partner": "无法移除同伴", - "unable_to_remove_reaction": "无法移除反应", + "unable_to_remove_reaction": "无法移除回应", "unable_to_remove_user": "无法移除用户", "unable_to_repair_items": "无法修复项目", "unable_to_reset_password": "无法重置密码", @@ -761,7 +761,7 @@ "group_no": "未分组", "group_owner": "按所有者分组", "group_year": "按年分组", - "has_quota": "有限额", + "has_quota": "配额大小", "hi_user": "你好,{name}({email})", "hide_all_people": "隐藏所有人物", "hide_gallery": "隐藏相册", @@ -769,7 +769,7 @@ "hide_password": "隐藏密码", "hide_person": "隐藏人物", "hide_unnamed_people": "隐藏未命名的人物", - "host": "主机", + "host": "服务器", "hour": "时", "image": "图片", "image_alt_text_date": "在{date}拍摄的{isVideo, select, true {视频} other {照片}}", @@ -793,7 +793,7 @@ "in_albums": "在{count, plural, one {#个相册} other {#个相册}}中", "in_archive": "在归档中", "include_archived": "包括已归档", - "include_shared_albums": "包含共享相册", + "include_shared_albums": "包括共享相册", "include_shared_partner_assets": "包括同伴共享项目", "individual_share": "个人分享", "info": "信息", @@ -930,7 +930,7 @@ "no_shared_albums_message": "创建相册以共享照片和视频", "not_in_any_album": "不在任何相册中", "note_apply_storage_label_to_previously_uploaded assets": "提示:要将存储标签应用于之前上传的项目,运行以下命令", - "note_unlimited_quota": "注:输入 0 表示无限制配额", + "note_unlimited_quota": "注:输入 0 表示无限配额", "notes": "提示", "notification_toggle_setting_description": "启用邮件通知", "notifications": "通知", @@ -1060,7 +1060,7 @@ "rating_count": "{count, plural, one {#星} other {#星}}", "rating_description": "在信息面板中展示EXIF星级", "raw": "Raw", - "reaction_options": "反应选项", + "reaction_options": "回应选项", "read_changelog": "阅读更新日志", "reassign": "重新指派", "reassigned_assets_to_existing_person": "重新指派{count, plural, one {#个项目} other {#个项目}}到{name, select, null {已存在的人物} other {{name}}}", @@ -1081,7 +1081,7 @@ "remove_assets_album_confirmation": "确定要从项目中移除{count, plural, one {#个项目} other {#个项目}}?", "remove_assets_shared_link_confirmation": "确定要从共享链接中移除{count, plural, one {#个项目} other {#个项目}}?", "remove_assets_title": "移除项目?", - "remove_custom_date_range": "根据自定义日期范围移除", + "remove_custom_date_range": "取消自定义日期范围", "remove_from_album": "从相册中移除", "remove_from_favorites": "移出收藏", "remove_from_shared_link": "从共享链接中移除", @@ -1181,16 +1181,16 @@ "share": "共享", "shared": "共享", "shared_by": "共享自", - "shared_by_user": "由{user}共享", + "shared_by_user": "由“{user}”共享", "shared_by_you": "你的共享", - "shared_from_partner": "来自{partner}的照片", + "shared_from_partner": "来自“{partner}”的照片", "shared_link_options": "共享链接选项", "shared_links": "共享链接", "shared_photos_and_videos_count": "{assetCount, plural, other {#项已共享照片&视频。}}", - "shared_with_partner": "与{partner}共享", + "shared_with_partner": "与“{partner}”共享", "sharing": "共享", "sharing_enter_password": "请输入密码后查看此页面。", - "sharing_sidebar_description": "在侧边栏中显示共享链接", + "sharing_sidebar_description": "在侧边栏中显示“共享”链接", "shift_to_permanent_delete": "按住Shift键永久删除项目", "show_album_options": "显示相册选项", "show_albums": "显示相册", @@ -1216,9 +1216,9 @@ "sign_out": "注销", "sign_up": "注册", "size": "大小", - "skip_to_content": "跳到内容", - "skip_to_folders": "跳到文件夹", - "skip_to_tags": "跳到标签", + "skip_to_content": "跳转到内容", + "skip_to_folders": "跳转到文件夹", + "skip_to_tags": "跳转到标签", "slideshow": "幻灯片放映", "slideshow_settings": "放映设置", "sort_albums_by": "相册排序依据...", @@ -1241,15 +1241,15 @@ "status": "状态", "stop_motion_photo": "定格照片", "stop_photo_sharing": "停止共享照片?", - "stop_photo_sharing_description": "{partner}将不能访问你的照片。", + "stop_photo_sharing_description": "“{partner}”将不能访问你的照片。", "stop_sharing_photos_with_user": "停止与此用户共享照片", "storage": "存储空间", "storage_label": "存储标签", - "storage_usage": "总量:{available},已用{used}", + "storage_usage": "总量:{available}/已用:{used}", "submit": "提交", "suggestions": "建议", "sunrise_on_the_beach": "海滩上的日出", - "swap_merge_direction": "交换合并方向", + "swap_merge_direction": "互换合并方向", "sync": "同步", "tag": "标签", "tag_assets": "标记项目", @@ -1262,7 +1262,7 @@ "template": "模版", "theme": "主题", "theme_selection": "主题选项", - "theme_selection_description": "根据浏览器的系统首选项自动设置主题色", + "theme_selection_description": "跟随浏览器自动设置主题颜色", "they_will_be_merged_together": "项目将会合并到一起", "time_based_memories": "基于时间的回忆", "timezone": "时区", @@ -1319,15 +1319,15 @@ "upload_success": "上传成功,刷新页面查看新上传的项目。", "url": "URL", "usage": "用量", - "use_custom_date_range": "使用自定义日期范围", + "use_custom_date_range": "自定义日期范围", "user": "用户", "user_id": "用户ID", "user_license_settings": "授权", "user_license_settings_description": "管理你的授权", - "user_liked": "{user}点赞了{type, select, photo {该照片} video {该视频} asset {该项目} other {它}}", + "user_liked": "“{user}”点赞了{type, select, photo {该照片} video {该视频} asset {该项目} other {它}}", "user_purchase_settings": "购买", "user_purchase_settings_description": "管理购买订单", - "user_role_set": "设置{user}为{role}", + "user_role_set": "设置“{user}”为“{role}”", "user_usage_detail": "用户用量详情", "username": "用户名", "users": "用户", @@ -1336,7 +1336,7 @@ "variables": "变量", "version": "版本", "version_announcement_closing": "你的朋友,Alex", - "version_announcement_message": "嗨,伙计,当前应用出新版本了,请抽空阅读一下发行说明,并及时更新你的docker-compose.yml.env文件,避免存在错误配置,特别是当你是使用WatchTower或其它类似的自动升级工具时。", + "version_announcement_message": "嗨,朋友,当前应用出新版本了,请抽空阅读一下发行说明,并及时更新你的docker-compose.yml.env文件,避免存在配置错误,特别是当你是使用WatchTower或其它类似的自动升级工具时。", "video": "视频", "video_hover_setting": "鼠标悬停时播放视频缩略图", "video_hover_setting_description": "当鼠标悬停在项目上时播放视频缩略图。即使禁用了这个功能,也可以通过将鼠标悬停在播放图标上来开始播放。", @@ -1353,7 +1353,7 @@ "view_stack": "查看堆叠项目", "viewer": "预览", "visibility_changed": "{count, plural, one {#个人物} other {#个人物}}的可见性已修改", - "waiting": "队列中", + "waiting": "准备处理", "warning": "警告", "week": "周", "welcome": "欢迎", From 4a1ff6abce9a94e0f7d0921922edeae9879de5d7 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey <10599762+fyfrey@users.noreply.github.com> Date: Mon, 16 Sep 2024 22:26:14 +0200 Subject: [PATCH 03/57] refactor(mobile): repositories for album service (#12701) * refactor(mobile): repositories for album service * review feedback, first service unit test --- mobile/lib/entities/album.entity.dart | 3 +- mobile/lib/interfaces/album.interface.dart | 21 +++ mobile/lib/interfaces/asset.interface.dart | 8 ++ mobile/lib/interfaces/backup.interface.dart | 5 + mobile/lib/interfaces/user.interface.dart | 5 + mobile/lib/repositories/album.repository.dart | 85 ++++++++++++ mobile/lib/repositories/asset.repository.dart | 31 +++++ .../lib/repositories/backup.repository.dart | 20 +++ mobile/lib/repositories/user.repository.dart | 20 +++ mobile/lib/services/album.service.dart | 126 ++++++++---------- mobile/lib/services/background.service.dart | 19 ++- mobile/test/repository.mocks.dart | 13 ++ mobile/test/service.mocks.dart | 10 ++ mobile/test/services/album.service.test.dart | 52 ++++++++ 14 files changed, 347 insertions(+), 71 deletions(-) create mode 100644 mobile/lib/interfaces/album.interface.dart create mode 100644 mobile/lib/interfaces/asset.interface.dart create mode 100644 mobile/lib/interfaces/backup.interface.dart create mode 100644 mobile/lib/interfaces/user.interface.dart create mode 100644 mobile/lib/repositories/album.repository.dart create mode 100644 mobile/lib/repositories/asset.repository.dart create mode 100644 mobile/lib/repositories/backup.repository.dart create mode 100644 mobile/lib/repositories/user.repository.dart create mode 100644 mobile/test/repository.mocks.dart create mode 100644 mobile/test/service.mocks.dart create mode 100644 mobile/test/services/album.service.test.dart diff --git a/mobile/lib/entities/album.entity.dart b/mobile/lib/entities/album.entity.dart index c05b849dcd26d..b20cec97c33a5 100644 --- a/mobile/lib/entities/album.entity.dart +++ b/mobile/lib/entities/album.entity.dart @@ -164,12 +164,13 @@ class Album { } extension AssetsHelper on IsarCollection { - Future store(Album a) async { + Future store(Album a) async { await put(a); await a.owner.save(); await a.thumbnail.save(); await a.sharedUsers.save(); await a.assets.save(); + return a; } } diff --git a/mobile/lib/interfaces/album.interface.dart b/mobile/lib/interfaces/album.interface.dart new file mode 100644 index 0000000000000..c2ba650b6f407 --- /dev/null +++ b/mobile/lib/interfaces/album.interface.dart @@ -0,0 +1,21 @@ +import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; + +abstract interface class IAlbumRepository { + Future count({bool? local}); + Future create(Album album); + Future getById(int id); + Future getByName( + String name, { + bool? shared, + bool? remote, + }); + Future update(Album album); + Future delete(int albumId); + Future> getAll({bool? shared}); + Future removeUsers(Album album, List users); + Future addAssets(Album album, List assets); + Future removeAssets(Album album, List assets); + Future recalculateMetadata(Album album); +} diff --git a/mobile/lib/interfaces/asset.interface.dart b/mobile/lib/interfaces/asset.interface.dart new file mode 100644 index 0000000000000..46425ba617cda --- /dev/null +++ b/mobile/lib/interfaces/asset.interface.dart @@ -0,0 +1,8 @@ +import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; + +abstract interface class IAssetRepository { + Future> getByAlbum(Album album, {User? notOwnedBy}); + Future deleteById(List ids); +} diff --git a/mobile/lib/interfaces/backup.interface.dart b/mobile/lib/interfaces/backup.interface.dart new file mode 100644 index 0000000000000..e343a9d39019f --- /dev/null +++ b/mobile/lib/interfaces/backup.interface.dart @@ -0,0 +1,5 @@ +import 'package:immich_mobile/entities/backup_album.entity.dart'; + +abstract interface class IBackupRepository { + Future> getIdsBySelection(BackupSelection backup); +} diff --git a/mobile/lib/interfaces/user.interface.dart b/mobile/lib/interfaces/user.interface.dart new file mode 100644 index 0000000000000..d9841a1187595 --- /dev/null +++ b/mobile/lib/interfaces/user.interface.dart @@ -0,0 +1,5 @@ +import 'package:immich_mobile/entities/user.entity.dart'; + +abstract interface class IUserRepository { + Future> getByIds(List ids); +} diff --git a/mobile/lib/repositories/album.repository.dart b/mobile/lib/repositories/album.repository.dart new file mode 100644 index 0000000000000..08c939aa6ca87 --- /dev/null +++ b/mobile/lib/repositories/album.repository.dart @@ -0,0 +1,85 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/interfaces/album.interface.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:isar/isar.dart'; + +final albumRepositoryProvider = + Provider((ref) => AlbumRepository(ref.watch(dbProvider))); + +class AlbumRepository implements IAlbumRepository { + final Isar _db; + + AlbumRepository( + this._db, + ); + + @override + Future count({bool? local}) { + if (local == true) return _db.albums.where().localIdIsNotNull().count(); + if (local == false) return _db.albums.where().remoteIdIsNotNull().count(); + return _db.albums.count(); + } + + @override + Future create(Album album) => + _db.writeTxn(() => _db.albums.store(album)); + + @override + Future getByName(String name, {bool? shared, bool? remote}) { + var query = _db.albums.filter().nameEqualTo(name); + if (shared != null) { + query = query.sharedEqualTo(shared); + } + if (remote == true) { + query = query.localIdIsNull(); + } else if (remote == false) { + query = query.remoteIdIsNull(); + } + return query.findFirst(); + } + + @override + Future update(Album album) => + _db.writeTxn(() => _db.albums.store(album)); + + @override + Future delete(int albumId) => + _db.writeTxn(() => _db.albums.delete(albumId)); + + @override + Future> getAll({bool? shared}) { + final baseQuery = _db.albums.filter(); + QueryBuilder? query; + if (shared != null) { + query = baseQuery.sharedEqualTo(true); + } + return query?.findAll() ?? _db.albums.where().findAll(); + } + + @override + Future getById(int id) => _db.albums.get(id); + + @override + Future removeUsers(Album album, List users) => + _db.writeTxn(() => album.sharedUsers.update(unlink: users)); + + @override + Future addAssets(Album album, List assets) => + _db.writeTxn(() => album.assets.update(link: assets)); + + @override + Future removeAssets(Album album, List assets) => + _db.writeTxn(() => album.assets.update(unlink: assets)); + + @override + Future recalculateMetadata(Album album) async { + album.startDate = await album.assets.filter().fileCreatedAtProperty().min(); + album.endDate = await album.assets.filter().fileCreatedAtProperty().max(); + album.lastModifiedAssetTimestamp = + await album.assets.filter().updatedAtProperty().max(); + return album; + } +} diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart new file mode 100644 index 0000000000000..ea05feab38f68 --- /dev/null +++ b/mobile/lib/repositories/asset.repository.dart @@ -0,0 +1,31 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:isar/isar.dart'; + +final assetRepositoryProvider = + Provider((ref) => AssetRepository(ref.watch(dbProvider))); + +class AssetRepository implements IAssetRepository { + final Isar _db; + + AssetRepository( + this._db, + ); + + @override + Future> getByAlbum(Album album, {User? notOwnedBy}) { + var query = album.assets.filter(); + if (notOwnedBy != null) { + query = query.not().ownerIdEqualTo(notOwnedBy.isarId); + } + return query.findAll(); + } + + @override + Future deleteById(List ids) => + _db.writeTxn(() => _db.assets.deleteAll(ids)); +} diff --git a/mobile/lib/repositories/backup.repository.dart b/mobile/lib/repositories/backup.repository.dart new file mode 100644 index 0000000000000..c9d93f787769b --- /dev/null +++ b/mobile/lib/repositories/backup.repository.dart @@ -0,0 +1,20 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/backup_album.entity.dart'; +import 'package:immich_mobile/interfaces/backup.interface.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:isar/isar.dart'; + +final backupRepositoryProvider = + Provider((ref) => BackupRepository(ref.watch(dbProvider))); + +class BackupRepository implements IBackupRepository { + final Isar _db; + + BackupRepository( + this._db, + ); + + @override + Future> getIdsBySelection(BackupSelection backup) => + _db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll(); +} diff --git a/mobile/lib/repositories/user.repository.dart b/mobile/lib/repositories/user.repository.dart new file mode 100644 index 0000000000000..cd87eb17ecb24 --- /dev/null +++ b/mobile/lib/repositories/user.repository.dart @@ -0,0 +1,20 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/interfaces/user.interface.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:isar/isar.dart'; + +final userRepositoryProvider = + Provider((ref) => UserRepository(ref.watch(dbProvider))); + +class UserRepository implements IUserRepository { + final Isar _db; + + UserRepository( + this._db, + ); + + @override + Future> getByIds(List ids) async => + (await _db.users.getAllById(ids)).cast(); +} diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index ef56f9bf6c12a..92302a0d88f29 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -5,6 +5,10 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/album.interface.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; +import 'package:immich_mobile/interfaces/backup.interface.dart'; +import 'package:immich_mobile/interfaces/user.interface.dart'; import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -12,11 +16,13 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/repositories/album.repository.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; +import 'package:immich_mobile/repositories/backup.repository.dart'; +import 'package:immich_mobile/repositories/user.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/user.service.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -26,7 +32,10 @@ final albumServiceProvider = Provider( ref.watch(apiServiceProvider), ref.watch(userServiceProvider), ref.watch(syncServiceProvider), - ref.watch(dbProvider), + ref.watch(albumRepositoryProvider), + ref.watch(assetRepositoryProvider), + ref.watch(userRepositoryProvider), + ref.watch(backupRepositoryProvider), ), ); @@ -34,7 +43,10 @@ class AlbumService { final ApiService _apiService; final UserService _userService; final SyncService _syncService; - final Isar _db; + final IAlbumRepository _albumRepository; + final IAssetRepository _assetRepository; + final IUserRepository _userRepository; + final IBackupRepository _backupAlbumRepository; final Logger _log = Logger('AlbumService'); Completer _localCompleter = Completer()..complete(false); Completer _remoteCompleter = Completer()..complete(false); @@ -43,16 +55,12 @@ class AlbumService { this._apiService, this._userService, this._syncService, - this._db, + this._albumRepository, + this._assetRepository, + this._userRepository, + this._backupAlbumRepository, ); - QueryBuilder - selectedAlbumsQuery() => - _db.backupAlbums.filter().selectionEqualTo(BackupSelection.select); - QueryBuilder - excludedAlbumsQuery() => - _db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude); - /// Checks all selected device albums for changes of albums and their assets /// Updates the local database and returns `true` if there were any changes Future refreshDeviceAlbums() async { @@ -65,12 +73,12 @@ class AlbumService { final Stopwatch sw = Stopwatch()..start(); bool changes = false; try { - final List excludedIds = - await excludedAlbumsQuery().idProperty().findAll(); - final List selectedIds = - await selectedAlbumsQuery().idProperty().findAll(); + final List excludedIds = await _backupAlbumRepository + .getIdsBySelection(BackupSelection.exclude); + final List selectedIds = await _backupAlbumRepository + .getIdsBySelection(BackupSelection.select); if (selectedIds.isEmpty) { - final numLocal = await _db.albums.where().localIdIsNotNull().count(); + final numLocal = await _albumRepository.count(local: true); if (numLocal > 0) { _syncService.removeAllLocalAlbumsAndAssets(); } @@ -194,8 +202,8 @@ class AlbumService { ), ); if (remote != null) { - Album album = await Album.remote(remote); - await _db.writeTxn(() => _db.albums.store(album)); + final Album album = await Album.remote(remote); + await _albumRepository.create(album); return album; } } catch (e) { @@ -212,8 +220,7 @@ class AlbumService { for (int round = 0;; round++) { final proposedName = "$baseName${round == 0 ? "" : " ($round)"}"; - if (null == - await _db.albums.filter().nameEqualTo(proposedName).findFirst()) { + if (null == await _albumRepository.getByName(proposedName)) { return proposedName; } } @@ -268,20 +275,15 @@ class AlbumService { Future _updateAssets( int albumId, { - Iterable add = const [], - Iterable remove = const [], - }) { - return _db.writeTxn(() async { - final album = await _db.albums.get(albumId); - if (album == null) return; - await album.assets.update(link: add, unlink: remove); - album.startDate = - await album.assets.filter().fileCreatedAtProperty().min(); - album.endDate = await album.assets.filter().fileCreatedAtProperty().max(); - album.lastModifiedAssetTimestamp = - await album.assets.filter().updatedAtProperty().max(); - await _db.albums.put(album); - }); + List add = const [], + List remove = const [], + }) async { + final album = await _albumRepository.getById(albumId); + if (album == null) return; + await _albumRepository.addAssets(album, add); + await _albumRepository.removeAssets(album, remove); + await _albumRepository.recalculateMetadata(album); + await _albumRepository.update(album); } Future addAdditionalUserToAlbum( @@ -298,13 +300,9 @@ class AlbumService { AddUsersDto(albumUsers: albumUsers), ); if (result != null) { - album.sharedUsers - .addAll((await _db.users.getAllById(sharedUserIds)).cast()); + album.sharedUsers.addAll(await _userRepository.getByIds(sharedUserIds)); album.shared = result.shared; - await _db.writeTxn(() async { - await _db.albums.put(album); - await album.sharedUsers.save(); - }); + await _albumRepository.update(album); return true; } } catch (e) { @@ -321,7 +319,7 @@ class AlbumService { ); if (result != null) { album.activityEnabled = enabled; - await _db.writeTxn(() => _db.albums.put(album)); + await _albumRepository.update(album); return true; } } catch (e) { @@ -332,29 +330,29 @@ class AlbumService { Future deleteAlbum(Album album) async { try { - final userId = Store.get(StoreKey.currentUser).isarId; - if (album.owner.value?.isarId == userId) { + final user = Store.get(StoreKey.currentUser); + if (album.owner.value?.isarId == user.isarId) { await _apiService.albumsApi.deleteAlbum(album.remoteId!); } if (album.shared) { final foreignAssets = - await album.assets.filter().not().ownerIdEqualTo(userId).findAll(); - await _db.writeTxn(() => _db.albums.delete(album.id)); - final List albums = - await _db.albums.filter().sharedEqualTo(true).findAll(); + await _assetRepository.getByAlbum(album, notOwnedBy: user); + await _albumRepository.delete(album.id); + + final List albums = await _albumRepository.getAll(shared: true); final List existing = []; - for (Album a in albums) { + for (Album album in albums) { existing.addAll( - await a.assets.filter().not().ownerIdEqualTo(userId).findAll(), + await _assetRepository.getByAlbum(album, notOwnedBy: user), ); } final List idsToRemove = _syncService.sharedAssetsToRemove(foreignAssets, existing); if (idsToRemove.isNotEmpty) { - await _db.writeTxn(() => _db.assets.deleteAll(idsToRemove)); + await _assetRepository.deleteById(idsToRemove); } } else { - await _db.writeTxn(() => _db.albums.delete(album.id)); + await _albumRepository.delete(album.id); } return true; } catch (e) { @@ -390,7 +388,7 @@ class AlbumService { : response .where((e) => e.success) .map((e) => assets.firstWhere((a) => a.remoteId == e.id)); - await _updateAssets(album.id, remove: toRemove); + await _updateAssets(album.id, remove: toRemove.toList()); return true; } } catch (e) { @@ -410,12 +408,10 @@ class AlbumService { ); album.sharedUsers.remove(user); - await _db.writeTxn(() async { - await album.sharedUsers.update(unlink: [user]); - final a = await _db.albums.get(album.id); - // trigger watcher - await _db.albums.put(a!); - }); + await _albumRepository.removeUsers(album, [user]); + final a = await _albumRepository.getById(album.id); + // trigger watcher + await _albumRepository.update(a!); return true; } catch (e) { @@ -436,7 +432,7 @@ class AlbumService { ), ); album.name = newAlbumTitle; - await _db.writeTxn(() => _db.albums.put(album)); + await _albumRepository.update(album); return true; } catch (e) { @@ -445,14 +441,8 @@ class AlbumService { } } - Future getAlbumByName(String name, bool remoteOnly) async { - return _db.albums - .filter() - .optional(remoteOnly, (q) => q.localIdIsNull()) - .nameEqualTo(name) - .sharedEqualTo(false) - .findFirst(); - } + Future getAlbumByName(String name, bool remoteOnly) => + _albumRepository.getByName(name, remote: remoteOnly ? true : null); /// /// Add the uploaded asset to the selected albums diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index fc3feb174d582..0d4d547434034 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -12,6 +12,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/main.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; +import 'package:immich_mobile/repositories/album.repository.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; +import 'package:immich_mobile/repositories/backup.repository.dart'; +import 'package:immich_mobile/repositories/user.repository.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/services/localization.service.dart'; @@ -355,12 +359,23 @@ class BackgroundService { AppSettingsService settingService = AppSettingsService(); AppSettingsService settingsService = AppSettingsService(); PartnerService partnerService = PartnerService(apiService, db); + AlbumRepository albumRepository = AlbumRepository(db); + AssetRepository assetRepository = AssetRepository(db); + UserRepository userRepository = UserRepository(db); + BackupRepository backupAlbumRepository = BackupRepository(db); HashService hashService = HashService(db, this); SyncService syncSerive = SyncService(db, hashService); UserService userService = UserService(apiService, db, syncSerive, partnerService); - AlbumService albumService = - AlbumService(apiService, userService, syncSerive, db); + AlbumService albumService = AlbumService( + apiService, + userService, + syncSerive, + albumRepository, + assetRepository, + userRepository, + backupAlbumRepository, + ); BackupService backupService = BackupService(apiService, db, settingService, albumService); diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart new file mode 100644 index 0000000000000..e54d82739e5b8 --- /dev/null +++ b/mobile/test/repository.mocks.dart @@ -0,0 +1,13 @@ +import 'package:immich_mobile/interfaces/album.interface.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; +import 'package:immich_mobile/interfaces/backup.interface.dart'; +import 'package:immich_mobile/interfaces/user.interface.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockAlbumRepository extends Mock implements IAlbumRepository {} + +class MockAssetRepository extends Mock implements IAssetRepository {} + +class MockUserRepository extends Mock implements IUserRepository {} + +class MockBackupRepository extends Mock implements IBackupRepository {} diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart new file mode 100644 index 0000000000000..ba4c129e5c2bc --- /dev/null +++ b/mobile/test/service.mocks.dart @@ -0,0 +1,10 @@ +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/sync.service.dart'; +import 'package:immich_mobile/services/user.service.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockApiService extends Mock implements ApiService {} + +class MockUserService extends Mock implements UserService {} + +class MockSyncService extends Mock implements SyncService {} diff --git a/mobile/test/services/album.service.test.dart b/mobile/test/services/album.service.test.dart new file mode 100644 index 0000000000000..790a0eba356b9 --- /dev/null +++ b/mobile/test/services/album.service.test.dart @@ -0,0 +1,52 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/entities/backup_album.entity.dart'; +import 'package:immich_mobile/services/album.service.dart'; +import 'package:mocktail/mocktail.dart'; +import '../repository.mocks.dart'; +import '../service.mocks.dart'; + +void main() { + late AlbumService sut; + late MockApiService apiService; + late MockUserService userService; + late MockSyncService syncService; + late MockAlbumRepository albumRepository; + late MockAssetRepository assetRepository; + late MockUserRepository userRepository; + late MockBackupRepository backupRepository; + + setUp(() { + apiService = MockApiService(); + userService = MockUserService(); + syncService = MockSyncService(); + albumRepository = MockAlbumRepository(); + assetRepository = MockAssetRepository(); + userRepository = MockUserRepository(); + backupRepository = MockBackupRepository(); + + sut = AlbumService( + apiService, + userService, + syncService, + albumRepository, + assetRepository, + userRepository, + backupRepository, + ); + }); + + group('refreshDeviceAlbums', () { + test('empty selection with one album in db', () async { + when(() => backupRepository.getIdsBySelection(BackupSelection.exclude)) + .thenAnswer((_) async => []); + when(() => backupRepository.getIdsBySelection(BackupSelection.select)) + .thenAnswer((_) async => []); + when(() => albumRepository.count(local: true)).thenAnswer((_) async => 1); + when(() => syncService.removeAllLocalAlbumsAndAssets()) + .thenAnswer((_) async => true); + final result = await sut.refreshDeviceAlbums(); + expect(result, false); + verify(() => syncService.removeAllLocalAlbumsAndAssets()); + }); + }); +} From b74b20824a1c0aa238a08e59a327307526016ad3 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 16 Sep 2024 16:49:12 -0400 Subject: [PATCH 04/57] feat: tag cleanup job (#12654) --- mobile/openapi/README.md | 3 + mobile/openapi/lib/api.dart | 2 + mobile/openapi/lib/api/jobs_api.dart | 39 ++++++++ mobile/openapi/lib/api_client.dart | 4 + mobile/openapi/lib/api_helper.dart | 3 + mobile/openapi/lib/model/job_create_dto.dart | 98 +++++++++++++++++++ mobile/openapi/lib/model/manual_job_name.dart | 88 +++++++++++++++++ open-api/immich-openapi-specs.json | 52 ++++++++++ open-api/typescript-sdk/src/fetch-client.ts | 17 ++++ server/src/controllers/job.controller.ts | 10 +- server/src/dtos/job.dto.ts | 7 ++ server/src/enum.ts | 6 ++ server/src/interfaces/job.interface.ts | 6 ++ server/src/interfaces/tag.interface.ts | 1 + server/src/repositories/job.repository.ts | 3 + server/src/repositories/tag.repository.ts | 39 +++++++- server/src/services/job.service.ts | 28 +++++- server/src/services/microservices.service.ts | 3 + server/src/services/tag.service.ts | 6 ++ .../test/repositories/tag.repository.mock.ts | 1 + .../shared-components/combobox.svelte | 2 +- web/src/lib/i18n/en.json | 6 ++ web/src/routes/admin/jobs-status/+page.svelte | 62 +++++++++++- 23 files changed, 476 insertions(+), 10 deletions(-) create mode 100644 mobile/openapi/lib/model/job_create_dto.dart create mode 100644 mobile/openapi/lib/model/manual_job_name.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 36b2c7bbf4613..16f293f81a6d3 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -124,6 +124,7 @@ Class | Method | HTTP request | Description *FileReportsApi* | [**fixAuditFiles**](doc//FileReportsApi.md#fixauditfiles) | **POST** /reports/fix | *FileReportsApi* | [**getAuditFiles**](doc//FileReportsApi.md#getauditfiles) | **GET** /reports | *FileReportsApi* | [**getFileChecksums**](doc//FileReportsApi.md#getfilechecksums) | **POST** /reports/checksum | +*JobsApi* | [**createJob**](doc//JobsApi.md#createjob) | **POST** /jobs | *JobsApi* | [**getAllJobsStatus**](doc//JobsApi.md#getalljobsstatus) | **GET** /jobs | *JobsApi* | [**sendJobCommand**](doc//JobsApi.md#sendjobcommand) | **PUT** /jobs/{id} | *LibrariesApi* | [**createLibrary**](doc//LibrariesApi.md#createlibrary) | **POST** /libraries | @@ -330,6 +331,7 @@ Class | Method | HTTP request | Description - [JobCommand](doc//JobCommand.md) - [JobCommandDto](doc//JobCommandDto.md) - [JobCountsDto](doc//JobCountsDto.md) + - [JobCreateDto](doc//JobCreateDto.md) - [JobName](doc//JobName.md) - [JobSettingsDto](doc//JobSettingsDto.md) - [JobStatusDto](doc//JobStatusDto.md) @@ -341,6 +343,7 @@ Class | Method | HTTP request | Description - [LoginCredentialDto](doc//LoginCredentialDto.md) - [LoginResponseDto](doc//LoginResponseDto.md) - [LogoutResponseDto](doc//LogoutResponseDto.md) + - [ManualJobName](doc//ManualJobName.md) - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) - [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md) - [MapTheme](doc//MapTheme.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 091e900145ab3..915c70f08eb26 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -144,6 +144,7 @@ part 'model/image_format.dart'; part 'model/job_command.dart'; part 'model/job_command_dto.dart'; part 'model/job_counts_dto.dart'; +part 'model/job_create_dto.dart'; part 'model/job_name.dart'; part 'model/job_settings_dto.dart'; part 'model/job_status_dto.dart'; @@ -155,6 +156,7 @@ part 'model/log_level.dart'; part 'model/login_credential_dto.dart'; part 'model/login_response_dto.dart'; part 'model/logout_response_dto.dart'; +part 'model/manual_job_name.dart'; part 'model/map_marker_response_dto.dart'; part 'model/map_reverse_geocode_response_dto.dart'; part 'model/map_theme.dart'; diff --git a/mobile/openapi/lib/api/jobs_api.dart b/mobile/openapi/lib/api/jobs_api.dart index 5f9501d126f8e..78afc15c93580 100644 --- a/mobile/openapi/lib/api/jobs_api.dart +++ b/mobile/openapi/lib/api/jobs_api.dart @@ -16,6 +16,45 @@ class JobsApi { final ApiClient apiClient; + /// Performs an HTTP 'POST /jobs' operation and returns the [Response]. + /// Parameters: + /// + /// * [JobCreateDto] jobCreateDto (required): + Future createJobWithHttpInfo(JobCreateDto jobCreateDto,) async { + // ignore: prefer_const_declarations + final path = r'/jobs'; + + // ignore: prefer_final_locals + Object? postBody = jobCreateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [JobCreateDto] jobCreateDto (required): + Future createJob(JobCreateDto jobCreateDto,) async { + final response = await createJobWithHttpInfo(jobCreateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Performs an HTTP 'GET /jobs' operation and returns the [Response]. Future getAllJobsStatusWithHttpInfo() async { // ignore: prefer_const_declarations diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 9ec00aecc87aa..6a40de730c002 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -343,6 +343,8 @@ class ApiClient { return JobCommandDto.fromJson(value); case 'JobCountsDto': return JobCountsDto.fromJson(value); + case 'JobCreateDto': + return JobCreateDto.fromJson(value); case 'JobName': return JobNameTypeTransformer().decode(value); case 'JobSettingsDto': @@ -365,6 +367,8 @@ class ApiClient { return LoginResponseDto.fromJson(value); case 'LogoutResponseDto': return LogoutResponseDto.fromJson(value); + case 'ManualJobName': + return ManualJobNameTypeTransformer().decode(value); case 'MapMarkerResponseDto': return MapMarkerResponseDto.fromJson(value); case 'MapReverseGeocodeResponseDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 8dcef880f59a4..0f3cc41097276 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -97,6 +97,9 @@ String parameterToString(dynamic value) { if (value is LogLevel) { return LogLevelTypeTransformer().encode(value).toString(); } + if (value is ManualJobName) { + return ManualJobNameTypeTransformer().encode(value).toString(); + } if (value is MapTheme) { return MapThemeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/job_create_dto.dart b/mobile/openapi/lib/model/job_create_dto.dart new file mode 100644 index 0000000000000..a4734791bbced --- /dev/null +++ b/mobile/openapi/lib/model/job_create_dto.dart @@ -0,0 +1,98 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class JobCreateDto { + /// Returns a new [JobCreateDto] instance. + JobCreateDto({ + required this.name, + }); + + ManualJobName name; + + @override + bool operator ==(Object other) => identical(this, other) || other is JobCreateDto && + other.name == name; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (name.hashCode); + + @override + String toString() => 'JobCreateDto[name=$name]'; + + Map toJson() { + final json = {}; + json[r'name'] = this.name; + return json; + } + + /// Returns a new [JobCreateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static JobCreateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return JobCreateDto( + name: ManualJobName.fromJson(json[r'name'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = JobCreateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = JobCreateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of JobCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = JobCreateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'name', + }; +} + diff --git a/mobile/openapi/lib/model/manual_job_name.dart b/mobile/openapi/lib/model/manual_job_name.dart new file mode 100644 index 0000000000000..7e8d9d51b2bab --- /dev/null +++ b/mobile/openapi/lib/model/manual_job_name.dart @@ -0,0 +1,88 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class ManualJobName { + /// Instantiate a new enum with the provided [value]. + const ManualJobName._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const personCleanup = ManualJobName._(r'person-cleanup'); + static const tagCleanup = ManualJobName._(r'tag-cleanup'); + static const userCleanup = ManualJobName._(r'user-cleanup'); + + /// List of all possible values in this [enum][ManualJobName]. + static const values = [ + personCleanup, + tagCleanup, + userCleanup, + ]; + + static ManualJobName? fromJson(dynamic value) => ManualJobNameTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ManualJobName.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [ManualJobName] to String, +/// and [decode] dynamic data back to [ManualJobName]. +class ManualJobNameTypeTransformer { + factory ManualJobNameTypeTransformer() => _instance ??= const ManualJobNameTypeTransformer._(); + + const ManualJobNameTypeTransformer._(); + + String encode(ManualJobName data) => data.value; + + /// Decodes a [dynamic value][data] to a ManualJobName. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + ManualJobName? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'person-cleanup': return ManualJobName.personCleanup; + case r'tag-cleanup': return ManualJobName.tagCleanup; + case r'user-cleanup': return ManualJobName.userCleanup; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [ManualJobNameTypeTransformer] instance. + static ManualJobNameTypeTransformer? _instance; +} + diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index b4ec4505b9e2d..af79815563c70 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2561,6 +2561,39 @@ "tags": [ "Jobs" ] + }, + "post": { + "operationId": "createJob", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JobCreateDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Jobs" + ] } }, "/jobs/{id}": { @@ -9269,6 +9302,17 @@ ], "type": "object" }, + "JobCreateDto": { + "properties": { + "name": { + "$ref": "#/components/schemas/ManualJobName" + } + }, + "required": [ + "name" + ], + "type": "object" + }, "JobName": { "enum": [ "thumbnailGeneration", @@ -9511,6 +9555,14 @@ ], "type": "object" }, + "ManualJobName": { + "enum": [ + "person-cleanup", + "tag-cleanup", + "user-cleanup" + ], + "type": "string" + }, "MapMarkerResponseDto": { "properties": { "city": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 9350bd5604507..da57313692dc2 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -548,6 +548,9 @@ export type AllJobStatusResponseDto = { thumbnailGeneration: JobStatusDto; videoConversion: JobStatusDto; }; +export type JobCreateDto = { + name: ManualJobName; +}; export type JobCommandDto = { command: JobCommand; force: boolean; @@ -1941,6 +1944,15 @@ export function getAllJobsStatus(opts?: Oazapfts.RequestOpts) { ...opts })); } +export function createJob({ jobCreateDto }: { + jobCreateDto: JobCreateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/jobs", oazapfts.json({ + ...opts, + method: "POST", + body: jobCreateDto + }))); +} export function sendJobCommand({ id, jobCommandDto }: { id: JobName; jobCommandDto: JobCommandDto; @@ -3364,6 +3376,11 @@ export enum EntityType { Asset = "ASSET", Album = "ALBUM" } +export enum ManualJobName { + PersonCleanup = "person-cleanup", + TagCleanup = "tag-cleanup", + UserCleanup = "user-cleanup" +} export enum JobName { ThumbnailGeneration = "thumbnailGeneration", MetadataExtraction = "metadataExtraction", diff --git a/server/src/controllers/job.controller.ts b/server/src/controllers/job.controller.ts index 2aa5920fab7b8..7da19e207fce0 100644 --- a/server/src/controllers/job.controller.ts +++ b/server/src/controllers/job.controller.ts @@ -1,6 +1,6 @@ -import { Body, Controller, Get, Param, Put } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto'; +import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto'; import { Authenticated } from 'src/middleware/auth.guard'; import { JobService } from 'src/services/job.service'; @@ -15,6 +15,12 @@ export class JobController { return this.service.getAllJobsStatus(); } + @Post() + @Authenticated({ admin: true }) + createJob(@Body() dto: JobCreateDto): Promise { + return this.service.create(dto); + } + @Put(':id') @Authenticated({ admin: true }) sendJobCommand(@Param() { id }: JobIdParamDto, @Body() dto: JobCommandDto): Promise { diff --git a/server/src/dtos/job.dto.ts b/server/src/dtos/job.dto.ts index b7d8cf59bf55a..895f710b7a782 100644 --- a/server/src/dtos/job.dto.ts +++ b/server/src/dtos/job.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty } from 'class-validator'; +import { ManualJobName } from 'src/enum'; import { JobCommand, QueueName } from 'src/interfaces/job.interface'; import { ValidateBoolean } from 'src/validation'; @@ -20,6 +21,12 @@ export class JobCommandDto { force!: boolean; } +export class JobCreateDto { + @IsEnum(ManualJobName) + @ApiProperty({ type: 'string', enum: ManualJobName, enumName: 'ManualJobName' }) + name!: ManualJobName; +} + export class JobCountsDto { @ApiProperty({ type: 'integer' }) active!: number; diff --git a/server/src/enum.ts b/server/src/enum.ts index 32254854e4c5a..d76d97371ce48 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -186,3 +186,9 @@ export enum SourceType { MACHINE_LEARNING = 'machine-learning', EXIF = 'exif', } + +export enum ManualJobName { + PERSON_CLEANUP = 'person-cleanup', + TAG_CLEANUP = 'tag-cleanup', + USER_CLEANUP = 'user-cleanup', +} diff --git a/server/src/interfaces/job.interface.ts b/server/src/interfaces/job.interface.ts index a0533fa63f9c0..d0a15bfa5dc00 100644 --- a/server/src/interfaces/job.interface.ts +++ b/server/src/interfaces/job.interface.ts @@ -60,6 +60,9 @@ export enum JobName { STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration', STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single', + // tags + TAG_CLEANUP = 'tag-cleanup', + // migration QUEUE_MIGRATION = 'queue-migration', MIGRATE_ASSET = 'migrate-asset', @@ -262,6 +265,9 @@ export type JobItem = | { name: JobName.CLEAN_OLD_AUDIT_LOGS; data?: IBaseJob } | { name: JobName.CLEAN_OLD_SESSION_TOKENS; data?: IBaseJob } + // Tags + | { name: JobName.TAG_CLEANUP; data?: IBaseJob } + // Asset Deletion | { name: JobName.PERSON_CLEANUP; data?: IBaseJob } | { name: JobName.ASSET_DELETION; data: IAssetDeleteJob } diff --git a/server/src/interfaces/tag.interface.ts b/server/src/interfaces/tag.interface.ts index aca9c223d552b..16a34d6ac4960 100644 --- a/server/src/interfaces/tag.interface.ts +++ b/server/src/interfaces/tag.interface.ts @@ -17,4 +17,5 @@ export interface ITagRepository extends IBulkAsset { upsertAssetTags({ assetId, tagIds }: { assetId: string; tagIds: string[] }): Promise; upsertAssetIds(items: AssetTagItem[]): Promise; + deleteEmptyTags(): Promise; } diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts index f64e5175e5127..2981fa4bddcd8 100644 --- a/server/src/repositories/job.repository.ts +++ b/server/src/repositories/job.repository.ts @@ -41,6 +41,9 @@ export const JOBS_TO_QUEUE: Record = { [JobName.GENERATE_THUMBHASH]: QueueName.THUMBNAIL_GENERATION, [JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, + // tags + [JobName.TAG_CLEANUP]: QueueName.BACKGROUND_TASK, + // metadata [JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION, [JobName.METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION, diff --git a/server/src/repositories/tag.repository.ts b/server/src/repositories/tag.repository.ts index 9389aeb13b4e3..1a5415b8dbb08 100644 --- a/server/src/repositories/tag.repository.ts +++ b/server/src/repositories/tag.repository.ts @@ -1,10 +1,11 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { TagEntity } from 'src/entities/tag.entity'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AssetTagItem, ITagRepository } from 'src/interfaces/tag.interface'; import { Instrumentation } from 'src/utils/instrumentation'; -import { DataSource, In, Repository } from 'typeorm'; +import { DataSource, In, Repository, TreeRepository } from 'typeorm'; @Instrumentation() @Injectable() @@ -12,7 +13,11 @@ export class TagRepository implements ITagRepository { constructor( @InjectDataSource() private dataSource: DataSource, @InjectRepository(TagEntity) private repository: Repository, - ) {} + @InjectRepository(TagEntity) private tree: TreeRepository, + @Inject(ILoggerRepository) private logger: ILoggerRepository, + ) { + this.logger.setContext(TagRepository.name); + } get(id: string): Promise { return this.repository.findOne({ where: { id } }); @@ -174,6 +179,34 @@ export class TagRepository implements ITagRepository { }); } + async deleteEmptyTags() { + await this.dataSource.transaction(async (manager) => { + const ids = new Set(); + const tags = await manager.find(TagEntity); + for (const tag of tags) { + const count = await manager + .createQueryBuilder('assets', 'asset') + .innerJoin( + 'asset.tags', + 'asset_tags', + 'asset_tags.id IN (SELECT id_descendant FROM tags_closure WHERE id_ancestor = :tagId)', + { tagId: tag.id }, + ) + .getCount(); + + if (count === 0) { + this.logger.debug(`Found empty tag: ${tag.id} - ${tag.value}`); + ids.add(tag.id); + } + } + + if (ids.size > 0) { + await manager.delete(TagEntity, { id: In([...ids]) }); + this.logger.log(`Deleted ${ids.size} empty tags`); + } + }); + } + private async save(partial: Partial): Promise { const { id } = await this.repository.save(partial); return this.repository.findOneOrFail({ where: { id } }); diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index aa61ccf3cb229..03a6edf126e3a 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -2,8 +2,8 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { snakeCase } from 'lodash'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { mapAsset } from 'src/dtos/asset-response.dto'; -import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from 'src/dtos/job.dto'; -import { AssetType } from 'src/enum'; +import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto'; +import { AssetType, ManualJobName } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { @@ -22,6 +22,26 @@ import { IMetricRepository } from 'src/interfaces/metric.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +const asJobItem = (dto: JobCreateDto): JobItem => { + switch (dto.name) { + case ManualJobName.TAG_CLEANUP: { + return { name: JobName.TAG_CLEANUP }; + } + + case ManualJobName.PERSON_CLEANUP: { + return { name: JobName.PERSON_CLEANUP }; + } + + case ManualJobName.USER_CLEANUP: { + return { name: JobName.USER_DELETE_CHECK }; + } + + default: { + throw new BadRequestException('Invalid job name'); + } + } +}; + @Injectable() export class JobService { private configCore: SystemConfigCore; @@ -39,6 +59,10 @@ export class JobService { this.configCore = SystemConfigCore.create(systemMetadataRepository, logger); } + async create(dto: JobCreateDto): Promise { + await this.jobRepository.queue(asJobItem(dto)); + } + async handleCommand(queueName: QueueName, dto: JobCommandDto): Promise { this.logger.debug(`Handling command: queue=${queueName},force=${dto.force}`); diff --git a/server/src/services/microservices.service.ts b/server/src/services/microservices.service.ts index 025400cc9bde3..df4b072d56400 100644 --- a/server/src/services/microservices.service.ts +++ b/server/src/services/microservices.service.ts @@ -15,6 +15,7 @@ import { SessionService } from 'src/services/session.service'; import { SmartInfoService } from 'src/services/smart-info.service'; import { StorageTemplateService } from 'src/services/storage-template.service'; import { StorageService } from 'src/services/storage.service'; +import { TagService } from 'src/services/tag.service'; import { UserService } from 'src/services/user.service'; import { VersionService } from 'src/services/version.service'; import { otelShutdown } from 'src/utils/instrumentation'; @@ -34,6 +35,7 @@ export class MicroservicesService { private sessionService: SessionService, private storageTemplateService: StorageTemplateService, private storageService: StorageService, + private tagService: TagService, private userService: UserService, private duplicateService: DuplicateService, private versionService: VersionService, @@ -93,6 +95,7 @@ export class MicroservicesService { [JobName.NOTIFY_ALBUM_INVITE]: (data) => this.notificationService.handleAlbumInvite(data), [JobName.NOTIFY_ALBUM_UPDATE]: (data) => this.notificationService.handleAlbumUpdate(data), [JobName.NOTIFY_SIGNUP]: (data) => this.notificationService.handleUserSignup(data), + [JobName.TAG_CLEANUP]: () => this.tagService.handleTagCleanup(), [JobName.VERSION_CHECK]: () => this.versionService.handleVersionCheck(), }); } diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts index 97b0ef1be6843..cc6d64f749d20 100644 --- a/server/src/services/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -14,6 +14,7 @@ import { TagEntity } from 'src/entities/tag.entity'; import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; +import { JobStatus } from 'src/interfaces/job.interface'; import { AssetTagItem, ITagRepository } from 'src/interfaces/tag.interface'; import { checkAccess, requireAccess } from 'src/utils/access'; import { addAssets, removeAssets } from 'src/utils/asset.util'; @@ -138,6 +139,11 @@ export class TagService { return results; } + async handleTagCleanup() { + await this.repository.deleteEmptyTags(); + return JobStatus.SUCCESS; + } + private async findOrFail(id: string) { const tag = await this.repository.get(id); if (!tag) { diff --git a/server/test/repositories/tag.repository.mock.ts b/server/test/repositories/tag.repository.mock.ts index a3fc0e77e0312..acc2b59f6d686 100644 --- a/server/test/repositories/tag.repository.mock.ts +++ b/server/test/repositories/tag.repository.mock.ts @@ -17,5 +17,6 @@ export const newTagRepositoryMock = (): Mocked => { addAssetIds: vitest.fn(), removeAssetIds: vitest.fn(), upsertAssetIds: vitest.fn(), + deleteEmptyTags: vitest.fn(), }; }; diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index d3e022a75933c..7c71fe8aeaed7 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -220,7 +220,7 @@ role="listbox" id={listboxId} transition:fly={{ duration: 250 }} - class="absolute text-left text-sm w-full max-h-64 overflow-y-auto bg-white dark:bg-gray-800 border-t-0 border-gray-300 dark:border-gray-900 rounded-b-xl z-10" + class="absolute text-left text-sm w-full max-h-64 overflow-y-auto bg-white dark:bg-gray-800 border-t-0 border-gray-300 dark:border-gray-900 rounded-b-xl z-[10000]" class:border={isOpen} tabindex="-1" > diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index f880dab34737a..a788666050643 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -41,6 +41,7 @@ "confirm_email_below": "To confirm, type \"{email}\" below", "confirm_reprocess_all_faces": "Are you sure you want to reprocess all faces? This will also clear named people.", "confirm_user_password_reset": "Are you sure you want to reset {user}'s password?", + "create_job": "Create job", "disable_login": "Disable login", "duplicate_detection_job_description": "Run machine learning on assets to detect similar images. Relies on Smart Search", "exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.", @@ -68,6 +69,7 @@ "image_thumbnail_resolution": "Thumbnail resolution", "image_thumbnail_resolution_description": "Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.", "job_concurrency": "{job} concurrency", + "job_created": "Job created", "job_not_concurrency_safe": "This job is not concurrency-safe.", "job_settings": "Job Settings", "job_settings_description": "Manage job concurrency", @@ -196,6 +198,7 @@ "password_settings": "Password Login", "password_settings_description": "Manage password login settings", "paths_validated_successfully": "All paths validated successfully", + "person_cleanup_job": "Person cleanup", "quota_size_gib": "Quota Size (GiB)", "refreshing_all_libraries": "Refreshing all libraries", "registration": "Admin Registration", @@ -209,6 +212,7 @@ "reset_settings_to_recent_saved": "Reset settings to the recent saved settings", "scanning_library_for_changed_files": "Scanning library for changed files", "scanning_library_for_new_files": "Scanning library for new files", + "search_jobs": "Search jobs...", "send_welcome_email": "Send welcome email", "server_external_domain_settings": "External domain", "server_external_domain_settings_description": "Domain for public shared links, including http(s)://", @@ -236,6 +240,7 @@ "storage_template_settings_description": "Manage the folder structure and file name of the upload asset", "storage_template_user_label": "{label} is the user's Storage Label", "system_settings": "System Settings", + "tag_cleanup_job": "Tag cleanup", "theme_custom_css_settings": "Custom CSS", "theme_custom_css_settings_description": "Cascading Style Sheets allow the design of Immich to be customized.", "theme_settings": "Theme Settings", @@ -309,6 +314,7 @@ "trash_settings_description": "Manage trash settings", "untracked_files": "Untracked Files", "untracked_files_description": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug", + "user_cleanup_job": "User cleanup", "user_delete_delay": "{user}'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.", "user_delete_delay_settings": "Delete delay", "user_delete_delay_settings_description": "Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.", diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte index dcd6630a01c56..16c2541e61b53 100644 --- a/web/src/routes/admin/jobs-status/+page.svelte +++ b/web/src/routes/admin/jobs-status/+page.svelte @@ -3,10 +3,17 @@ import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; import Icon from '$lib/components/elements/icon.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; + import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte'; + import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte'; + import { + notificationController, + NotificationType, + } from '$lib/components/shared-components/notification/notification'; import { AppRoute } from '$lib/constants'; import { asyncTimeout } from '$lib/utils'; - import { getAllJobsStatus, type AllJobStatusResponseDto } from '@immich/sdk'; - import { mdiCog } from '@mdi/js'; + import { handleError } from '$lib/utils/handle-error'; + import { createJob, getAllJobsStatus, ManualJobName, type AllJobStatusResponseDto } from '@immich/sdk'; + import { mdiCog, mdiPlus } from '@mdi/js'; import { onDestroy, onMount } from 'svelte'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; @@ -16,6 +23,8 @@ let jobs: AllJobStatusResponseDto; let running = true; + let isOpen = false; + let selectedJob: ComboBoxOption | undefined = undefined; onMount(async () => { while (running) { @@ -27,10 +36,38 @@ onDestroy(() => { running = false; }); + + const options = [ + { title: $t('admin.person_cleanup_job'), value: ManualJobName.PersonCleanup }, + { title: $t('admin.tag_cleanup_job'), value: ManualJobName.TagCleanup }, + { title: $t('admin.user_cleanup_job'), value: ManualJobName.UserCleanup }, + ].map(({ value, title }) => ({ id: value, label: title, value })); + + const handleCancel = () => (isOpen = false); + + const handleCreate = async () => { + if (!selectedJob) { + return; + } + + try { + await createJob({ jobCreateDto: { name: selectedJob.value as ManualJobName } }); + notificationController.show({ message: $t('admin.job_created'), type: NotificationType.Info }); + handleCancel(); + } catch (error) { + handleError(error, $t('errors.unable_to_submit_job')); + } + };
+ (isOpen = true)}> +
+ + {$t('admin.create_job')} +
+
@@ -46,3 +83,24 @@ + +{#if isOpen} + +
+
+ +
+
+
+{/if} From 186b4e133336300a1ead4876c9838e0a23b310c9 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 16 Sep 2024 15:51:03 -0500 Subject: [PATCH 05/57] feat(web): improve UI/UX for settings pages (#12626) * fix(web): local date time for buckets * feat(web): improve UI/UX for setting pages * search admin settings and icon * clean up * fix translation file * Update web/src/routes/admin/system-settings/+page.svelte Co-authored-by: Ben <45583362+ben-basten@users.noreply.github.com> * Update web/src/lib/components/shared-components/settings/setting-accordion.svelte Co-authored-by: Ben <45583362+ben-basten@users.noreply.github.com> * better search bar on smaller screen * lint * template syntax --------- Co-authored-by: Jason Rasmussen Co-authored-by: Ben <45583362+ben-basten@users.noreply.github.com> --- .../settings/auth/auth-settings.svelte | 2 +- .../settings/setting-accordion.svelte | 22 +++++-- .../feature-settings.svelte | 2 +- .../user-settings-list.svelte | 57 ++++++++++++++--- web/src/lib/i18n/en.json | 1 + .../routes/admin/system-settings/+page.svelte | 62 +++++++++++++++++-- 6 files changed, 126 insertions(+), 20 deletions(-) diff --git a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte index 37f875c604f16..9b0e4b32706b5 100644 --- a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte +++ b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte @@ -71,7 +71,7 @@
-
+
-
+
{/each} diff --git a/web/src/lib/components/shared-components/settings/setting-switch.svelte b/web/src/lib/components/shared-components/settings/setting-switch.svelte index d933b27ab54fb..24b539f0a1c22 100644 --- a/web/src/lib/components/shared-components/settings/setting-switch.svelte +++ b/web/src/lib/components/shared-components/settings/setting-switch.svelte @@ -43,11 +43,5 @@
- onToggle(detail)} - ariaDescribedBy={subtitleId} - /> +
diff --git a/web/src/lib/components/shared-components/show-shortcuts.svelte b/web/src/lib/components/shared-components/show-shortcuts.svelte index ebc0dd688c1a6..2bd1b8976bd17 100644 --- a/web/src/lib/components/shared-components/show-shortcuts.svelte +++ b/web/src/lib/components/shared-components/show-shortcuts.svelte @@ -1,9 +1,8 @@ - dispatch('close')}> +
{#if shortcuts.general.length > 0}
diff --git a/web/src/lib/components/user-settings-page/device-card.svelte b/web/src/lib/components/user-settings-page/device-card.svelte index 676e9843641c3..d43977ea08a77 100644 --- a/web/src/lib/components/user-settings-page/device-card.svelte +++ b/web/src/lib/components/user-settings-page/device-card.svelte @@ -15,14 +15,10 @@ mdiUbuntu, } from '@mdi/js'; import { DateTime, type ToRelativeCalendarOptions } from 'luxon'; - import { createEventDispatcher } from 'svelte'; import { t } from 'svelte-i18n'; export let device: SessionResponseDto; - - const dispatcher = createEventDispatcher<{ - delete: void; - }>(); + export let onDelete: (() => void) | undefined = undefined; const options: ToRelativeCalendarOptions = { unit: 'days', @@ -68,14 +64,14 @@
- {#if !device.current} + {#if !device.current && onDelete}
dispatcher('delete')} + on:click={onDelete} />
{/if} diff --git a/web/src/lib/components/user-settings-page/device-list.svelte b/web/src/lib/components/user-settings-page/device-list.svelte index 57299bb46fb22..26e03c35d8acd 100644 --- a/web/src/lib/components/user-settings-page/device-list.svelte +++ b/web/src/lib/components/user-settings-page/device-list.svelte @@ -68,7 +68,7 @@ {$t('other_devices').toUpperCase()} {#each otherDevices as device, index} - handleDelete(device)} /> + handleDelete(device)} /> {#if index !== otherDevices.length - 1}
{/if} diff --git a/web/src/lib/components/user-settings-page/partner-selection-modal.svelte b/web/src/lib/components/user-settings-page/partner-selection-modal.svelte index 3cff1cd1de2bc..8ab747aa276da 100644 --- a/web/src/lib/components/user-settings-page/partner-selection-modal.svelte +++ b/web/src/lib/components/user-settings-page/partner-selection-modal.svelte @@ -1,19 +1,18 @@ - dispatch('close')}> +

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

@@ -68,14 +64,14 @@ title={$t('comments_and_likes')} subtitle={$t('let_others_respond')} checked={album.isActivityEnabled} - on:toggle={() => dispatch('toggleEnableActivity')} + onToggle={onToggleEnabledActivity} />
{$t('people').toUpperCase()}
-
{/key} @@ -152,10 +151,8 @@ rounded="full" disabled={Object.keys(selectedUsers).length === 0} on:click={() => - dispatch( - 'select', - Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })), - )}>{$t('add')} ({ userId: user.id, ...rest })))} + >{$t('add')}
{/if} @@ -166,7 +163,7 @@ -
- onSelect(detail)} /> + diff --git a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte index d781e1cc562fb..f869790ebab0b 100644 --- a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte +++ b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte @@ -4,7 +4,6 @@ import { getPeopleThumbnailUrl } from '$lib/utils'; import { type PersonResponseDto } from '@immich/sdk'; import { mdiArrowLeft, mdiMerge } from '@mdi/js'; - import { createEventDispatcher } from 'svelte'; import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; import Button from '../elements/buttons/button.svelte'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; @@ -13,25 +12,22 @@ export let personMerge1: PersonResponseDto; export let personMerge2: PersonResponseDto; export let potentialMergePeople: PersonResponseDto[]; + export let onReject: () => void; + export let onConfirm: ([personMerge1, personMerge2]: [PersonResponseDto, PersonResponseDto]) => void; + export let onClose: () => void; let choosePersonToMerge = false; const title = personMerge2.name; - const dispatch = createEventDispatcher<{ - reject: void; - confirm: [PersonResponseDto, PersonResponseDto]; - close: void; - }>(); - - const changePersonToMerge = (newperson: PersonResponseDto) => { - const index = potentialMergePeople.indexOf(newperson); + const changePersonToMerge = (newPerson: PersonResponseDto) => { + const index = potentialMergePeople.indexOf(newPerson); [potentialMergePeople[index], personMerge2] = [personMerge2, potentialMergePeople[index]]; choosePersonToMerge = false; }; - dispatch('close')}> +
{#if !choosePersonToMerge}
@@ -105,7 +101,7 @@

{$t('they_will_be_merged_together')}

- - + + diff --git a/web/src/lib/components/faces-page/people-card.svelte b/web/src/lib/components/faces-page/people-card.svelte index 21f48e42ebfc4..6791a26232e48 100644 --- a/web/src/lib/components/faces-page/people-card.svelte +++ b/web/src/lib/components/faces-page/people-card.svelte @@ -9,7 +9,6 @@ mdiDotsVertical, mdiEyeOffOutline, } from '@mdi/js'; - import { createEventDispatcher } from 'svelte'; import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; import MenuOption from '../shared-components/context-menu/menu-option.svelte'; import { t } from 'svelte-i18n'; @@ -18,19 +17,12 @@ export let person: PersonResponseDto; export let preload = false; - - type MenuItemEvent = 'change-name' | 'set-birth-date' | 'merge-people' | 'hide-person'; - let dispatch = createEventDispatcher<{ - 'change-name': void; - 'set-birth-date': void; - 'merge-people': void; - 'hide-person': void; - }>(); + export let onChangeName: () => void; + export let onSetBirthDate: () => void; + export let onMergePeople: () => void; + export let onHidePerson: () => void; let showVerticalDots = false; - const onMenuClick = (event: MenuItemEvent) => { - dispatch(event); - };
- onMenuClick('hide-person')} icon={mdiEyeOffOutline} text={$t('hide_person')} /> - onMenuClick('change-name')} icon={mdiAccountEditOutline} text={$t('change_name')} /> - onMenuClick('set-birth-date')} - icon={mdiCalendarEditOutline} - text={$t('set_date_of_birth')} - /> - onMenuClick('merge-people')} - icon={mdiAccountMultipleCheckOutline} - text={$t('merge_people')} - /> + + + +
{/if} diff --git a/web/src/lib/components/faces-page/people-list.svelte b/web/src/lib/components/faces-page/people-list.svelte index 5130baf30b89a..230c8750aedee 100644 --- a/web/src/lib/components/faces-page/people-list.svelte +++ b/web/src/lib/components/faces-page/people-list.svelte @@ -1,6 +1,5 @@ - +

{$t('birthdate_set_description')}

- handleSubmit()} autocomplete="off" id="set-birth-date-form"> + onUpdate(birthDate)} autocomplete="off" id="set-birth-date-form">
- + diff --git a/web/src/lib/components/faces-page/unmerge-face-selector.svelte b/web/src/lib/components/faces-page/unmerge-face-selector.svelte index c89c8338d3791..753e46c2199f0 100644 --- a/web/src/lib/components/faces-page/unmerge-face-selector.svelte +++ b/web/src/lib/components/faces-page/unmerge-face-selector.svelte @@ -10,7 +10,7 @@ type PersonResponseDto, } from '@immich/sdk'; import { mdiMerge, mdiPlus } from '@mdi/js'; - import { createEventDispatcher, onMount } from 'svelte'; + import { onMount } from 'svelte'; import { quintOut } from 'svelte/easing'; import { fly } from 'svelte/transition'; import Button from '../elements/buttons/button.svelte'; @@ -23,6 +23,8 @@ export let assetIds: string[]; export let personAssets: PersonResponseDto; + export let onConfirm: () => void; + export let onClose: () => void; let people: PersonResponseDto[] = []; let selectedPerson: PersonResponseDto | null = null; @@ -34,11 +36,6 @@ $: peopleToNotShow = selectedPerson ? [personAssets, selectedPerson] : [personAssets]; - let dispatch = createEventDispatcher<{ - confirm: void; - close: void; - }>(); - const selectedPeople: AssetFaceUpdateItem[] = []; for (const assetId of assetIds) { @@ -50,10 +47,6 @@ people = data.people; }); - const onClose = () => { - dispatch('close'); - }; - const handleSelectedPerson = (person: PersonResponseDto) => { if (selectedPerson && selectedPerson.id === person.id) { handleRemoveSelectedPerson(); @@ -87,7 +80,7 @@ } showLoadingSpinnerCreate = false; - dispatch('confirm'); + onConfirm(); }; const handleReassign = async () => { @@ -113,7 +106,7 @@ } showLoadingSpinnerReassign = false; - dispatch('confirm'); + onConfirm(); }; @@ -123,7 +116,7 @@ transition:fly={{ y: 500, duration: 100, easing: quintOut }} class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg" > - +
@@ -180,7 +173,7 @@
{/if} - handleSelectedPerson(detail)} /> + diff --git a/web/src/lib/components/forms/api-key-secret.svelte b/web/src/lib/components/forms/api-key-secret.svelte index b7bf8e1836270..f43e1da38e83a 100644 --- a/web/src/lib/components/forms/api-key-secret.svelte +++ b/web/src/lib/components/forms/api-key-secret.svelte @@ -1,20 +1,15 @@ - handleDone()}> +

{$t('api_key_description')} @@ -28,6 +23,6 @@ - + diff --git a/web/src/lib/components/forms/change-password-form.svelte b/web/src/lib/components/forms/change-password-form.svelte index 799dde7ef3787..cbf2ff07f0f00 100644 --- a/web/src/lib/components/forms/change-password-form.svelte +++ b/web/src/lib/components/forms/change-password-form.svelte @@ -1,10 +1,11 @@ diff --git a/web/src/lib/components/forms/create-user-form.svelte b/web/src/lib/components/forms/create-user-form.svelte index 8f049685a4930..9c4b83002b77a 100644 --- a/web/src/lib/components/forms/create-user-form.svelte +++ b/web/src/lib/components/forms/create-user-form.svelte @@ -5,13 +5,14 @@ import { ByteUnit, convertToBytes } from '$lib/utils/byte-units'; import { handleError } from '$lib/utils/handle-error'; import { createUserAdmin } from '@immich/sdk'; - import { createEventDispatcher } from 'svelte'; import { t } from 'svelte-i18n'; import Button from '../elements/buttons/button.svelte'; import Slider from '../elements/slider.svelte'; import PasswordField from '../shared-components/password-field.svelte'; export let onClose: () => void; + export let onSubmit: () => void; + export let onCancel: () => void; let error: string; let success: string; @@ -39,10 +40,6 @@ canCreateUser = true; } } - const dispatch = createEventDispatcher<{ - submit: void; - cancel: void; - }>(); async function registerUser() { if (canCreateUser && !isCreatingUser) { @@ -63,7 +60,7 @@ success = $t('new_user_created'); - dispatch('submit'); + onSubmit(); return; } catch (error) { @@ -132,7 +129,7 @@ {/if} - + diff --git a/web/src/lib/components/forms/edit-user-form.svelte b/web/src/lib/components/forms/edit-user-form.svelte index b326565122d87..0079a695bc3f7 100644 --- a/web/src/lib/components/forms/edit-user-form.svelte +++ b/web/src/lib/components/forms/edit-user-form.svelte @@ -5,7 +5,6 @@ import { handleError } from '$lib/utils/handle-error'; import { updateUserAdmin, type UserAdminResponseDto } from '@immich/sdk'; import { mdiAccountEditOutline } from '@mdi/js'; - import { createEventDispatcher } from 'svelte'; import Button from '../elements/buttons/button.svelte'; import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { t } from 'svelte-i18n'; @@ -15,6 +14,8 @@ export let canResetPassword = true; export let newPassword: string; export let onClose: () => void; + export let onResetPasswordSuccess: () => void; + export let onEditSuccess: () => void; let error: string; let success: string; @@ -27,12 +28,6 @@ !!quotaSize && convertToBytes(Number(quotaSize), ByteUnit.GiB) > $serverInfo.diskSizeRaw; - const dispatch = createEventDispatcher<{ - close: void; - resetPasswordSuccess: void; - editSuccess: void; - }>(); - const editUser = async () => { try { const { id, email, name, storageLabel } = user; @@ -46,7 +41,7 @@ }, }); - dispatch('editSuccess'); + onEditSuccess(); } catch (error) { handleError(error, $t('errors.unable_to_update_user')); } @@ -72,7 +67,7 @@ }, }); - dispatch('resetPasswordSuccess'); + onResetPasswordSuccess(); } catch (error) { handleError(error, $t('errors.unable_to_reset_password')); } diff --git a/web/src/lib/components/forms/library-exclusion-pattern-form.svelte b/web/src/lib/components/forms/library-exclusion-pattern-form.svelte index c09f1fbaf6bbb..05d47c0a0fbee 100644 --- a/web/src/lib/components/forms/library-exclusion-pattern-form.svelte +++ b/web/src/lib/components/forms/library-exclusion-pattern-form.svelte @@ -1,5 +1,4 @@ - -

handleSubmit()} autocomplete="off" id="add-exclusion-pattern-form"> + + onSubmit(exclusionPattern)} autocomplete="off" id="add-exclusion-pattern-form">

{$t('admin.exclusion_pattern_description')}

@@ -53,9 +47,9 @@

- + {#if isEditing} - + {/if} diff --git a/web/src/lib/components/forms/library-import-path-form.svelte b/web/src/lib/components/forms/library-import-path-form.svelte index f82d5733866bf..8bfca80aecb32 100644 --- a/web/src/lib/components/forms/library-import-path-form.svelte +++ b/web/src/lib/components/forms/library-import-path-form.svelte @@ -1,5 +1,4 @@ - -
handleSubmit()} autocomplete="off" id="library-import-path-form"> + + onSubmit(importPath)} autocomplete="off" id="library-import-path-form">

{$t('admin.library_import_path_description')}

@@ -47,9 +41,9 @@
- + {#if isEditing} - + {/if} diff --git a/web/src/lib/components/forms/library-import-paths-form.svelte b/web/src/lib/components/forms/library-import-paths-form.svelte index a2bb3a9686857..9e7ae11a63b48 100644 --- a/web/src/lib/components/forms/library-import-paths-form.svelte +++ b/web/src/lib/components/forms/library-import-paths-form.svelte @@ -1,5 +1,5 @@ -
handleSubmit()} autocomplete="off" class="m-4 flex flex-col gap-2"> + onSubmit({ ...library })} autocomplete="off" class="m-4 flex flex-col gap-2">
- +
diff --git a/web/src/lib/components/forms/library-scan-settings-form.svelte b/web/src/lib/components/forms/library-scan-settings-form.svelte index 5e025a406ae41..a9a42c31f7b24 100644 --- a/web/src/lib/components/forms/library-scan-settings-form.svelte +++ b/web/src/lib/components/forms/library-scan-settings-form.svelte @@ -1,7 +1,7 @@ - -
handleSubmit()} autocomplete="off" id="select-library-owner-form"> + + onSubmit(ownerId)} autocomplete="off" id="select-library-owner-form">

{$t('admin.note_cannot_be_changed_later')}

- +
diff --git a/web/src/lib/components/layouts/user-page-layout.svelte b/web/src/lib/components/layouts/user-page-layout.svelte index 5bca13b06029d..ed232b80cda28 100644 --- a/web/src/lib/components/layouts/user-page-layout.svelte +++ b/web/src/lib/components/layouts/user-page-layout.svelte @@ -21,7 +21,7 @@
{#if !hideNavbar} - openFileUploadDialog()} /> + openFileUploadDialog()} /> {/if} diff --git a/web/src/lib/components/map-page/map-settings-modal.svelte b/web/src/lib/components/map-page/map-settings-modal.svelte index b442396c84152..35df9f2285f1a 100644 --- a/web/src/lib/components/map-page/map-settings-modal.svelte +++ b/web/src/lib/components/map-page/map-settings-modal.svelte @@ -4,7 +4,6 @@ import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import type { MapSettings } from '$lib/stores/preferences.store'; import { Duration } from 'luxon'; - import { createEventDispatcher } from 'svelte'; import { t } from 'svelte-i18n'; import { fly } from 'svelte/transition'; import Button from '../elements/buttons/button.svelte'; @@ -12,19 +11,15 @@ import DateInput from '../elements/date-input.svelte'; export let settings: MapSettings; + export let onClose: () => void; + export let onSave: (settings: MapSettings) => void; + let customDateRange = !!settings.dateAfter || !!settings.dateBefore; - - const dispatch = createEventDispatcher<{ - close: void; - save: MapSettings; - }>(); - - const handleClose = () => dispatch('close'); - +
dispatch('save', settings)} + on:submit|preventDefault={() => onSave(settings)} class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary" id="map-settings-form" > @@ -108,7 +103,7 @@ {/if}
- +
diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index ae6416873eae6..919433f79b4a0 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -250,7 +250,7 @@
{#if current && current.memory.assets.length > 0} - goto(AppRoute.PHOTOS)} forceDark> + goto(AppRoute.PHOTOS)} forceDark>

{$memoryLaneTitle(current.memory.yearsAgo)} diff --git a/web/src/lib/components/photos-page/actions/add-to-album.svelte b/web/src/lib/components/photos-page/actions/add-to-album.svelte index 976f4bd9cf03d..d3998510cdc84 100644 --- a/web/src/lib/components/photos-page/actions/add-to-album.svelte +++ b/web/src/lib/components/photos-page/actions/add-to-album.svelte @@ -40,8 +40,8 @@ {#if showAlbumPicker} handleAddToNewAlbum(detail)} - on:album={({ detail }) => handleAddToAlbum(detail)} + onNewAlbum={handleAddToNewAlbum} + onAlbumClick={handleAddToAlbum} onClose={handleHideAlbumPicker} /> {/if} diff --git a/web/src/lib/components/photos-page/actions/change-date-action.svelte b/web/src/lib/components/photos-page/actions/change-date-action.svelte index 6ee775fa69835..114315348d203 100644 --- a/web/src/lib/components/photos-page/actions/change-date-action.svelte +++ b/web/src/lib/components/photos-page/actions/change-date-action.svelte @@ -31,9 +31,5 @@ (isShowChangeDate = true)} /> {/if} {#if isShowChangeDate} - handleConfirm(date)} - on:cancel={() => (isShowChangeDate = false)} - /> + (isShowChangeDate = false)} /> {/if} diff --git a/web/src/lib/components/photos-page/actions/change-location-action.svelte b/web/src/lib/components/photos-page/actions/change-location-action.svelte index 0e19696a4269d..3fe1db4327ae0 100644 --- a/web/src/lib/components/photos-page/actions/change-location-action.svelte +++ b/web/src/lib/components/photos-page/actions/change-location-action.svelte @@ -35,8 +35,5 @@ /> {/if} {#if isShowChangeLocation} - handleConfirm(point)} - on:cancel={() => (isShowChangeLocation = false)} - /> + (isShowChangeLocation = false)} /> {/if} diff --git a/web/src/lib/components/photos-page/actions/delete-assets.svelte b/web/src/lib/components/photos-page/actions/delete-assets.svelte index 5c79e7b221833..6d3275c74d594 100644 --- a/web/src/lib/components/photos-page/actions/delete-assets.svelte +++ b/web/src/lib/components/photos-page/actions/delete-assets.svelte @@ -49,7 +49,7 @@ {#if isShowConfirmation} (isShowConfirmation = false)} + onConfirm={handleDelete} + onCancel={() => (isShowConfirmation = false)} /> {/if} 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 240b6c2ba2162..b2780cc1a06b0 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -8,7 +8,7 @@ import { findTotalOffset, type DateGroup, type ScrollTargetListener } from '$lib/utils/timeline-util'; import type { AssetResponseDto } from '@immich/sdk'; import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js'; - import { createEventDispatcher, onDestroy } from 'svelte'; + import { onDestroy } from 'svelte'; import { fly } from 'svelte/transition'; import Thumbnail from '../assets/thumbnail/thumbnail.svelte'; import { TUNABLES } from '$lib/utils/tunables'; @@ -29,6 +29,9 @@ export let onScrollTarget: ScrollTargetListener | undefined = undefined; export let onAssetInGrid: ((asset: AssetResponseDto) => void) | undefined = undefined; + export let onSelect: ({ title, assets }: { title: string; assets: AssetResponseDto[] }) => void; + export let onSelectAssets: (asset: AssetResponseDto) => void; + export let onSelectAssetCandidates: (asset: AssetResponseDto | null) => void; const componentId = generateId(); $: bucketDate = bucket.bucketDate; @@ -41,11 +44,6 @@ const TITLE_HEIGHT = 51; const { selectedGroup, selectedAssets, assetSelectionCandidates, isMultiSelectState } = assetInteractionStore; - const dispatch = createEventDispatcher<{ - select: { title: string; assets: AssetResponseDto[] }; - selectAssets: AssetResponseDto; - selectAssetCandidates: AssetResponseDto | null; - }>(); let isMouseOverGroup = false; let hoveredDateGroup = ''; @@ -65,10 +63,10 @@ } }; - const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => dispatch('select', { title, assets }); + const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => onSelect({ title, assets }); const assetSelectHandler = (asset: AssetResponseDto, assetsInDateGroup: AssetResponseDto[], groupTitle: string) => { - dispatch('selectAssets', asset); + onSelectAssets(asset); // Check if all assets are selected in a group to toggle the group selection's icon let selectedAssetsInGroupCount = assetsInDateGroup.filter((asset) => $selectedAssets.has(asset)).length; @@ -86,7 +84,7 @@ hoveredDateGroup = groupTitle; if ($isMultiSelectState) { - dispatch('selectAssetCandidates', asset); + onSelectAssetCandidates(asset); } }; diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 3bf0c65bc9467..6de36c803e775 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -28,7 +28,7 @@ import { TUNABLES } from '$lib/utils/tunables'; import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk'; import { throttle } from 'lodash-es'; - import { createEventDispatcher, onDestroy, onMount } from 'svelte'; + import { onDestroy, onMount } from 'svelte'; import Portal from '../shared-components/portal/portal.svelte'; import Scrubber from '../shared-components/scrubber/scrubber.svelte'; import ShowShortcuts from '../shared-components/show-shortcuts.svelte'; @@ -64,6 +64,8 @@ export let isShared = false; export let album: AlbumResponseDto | null = null; export let isShowDeleteConfirmation = false; + export let onSelect: (asset: AssetResponseDto) => void = () => {}; + export let onEscape: () => void = () => {}; let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget } = assetViewingStore; const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } = @@ -127,8 +129,6 @@ }, } = TUNABLES; - const dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>(); - const isViewportOrigin = () => { return viewport.height === 0 && viewport.width === 0; }; @@ -447,7 +447,7 @@ const ids = await stackAssets(Array.from($selectedAssets)); if (ids) { $assetStore.removeAssets(ids); - dispatch('escape'); + onEscape(); } }; @@ -471,7 +471,7 @@ } const shortcuts: ShortcutOptions[] = [ - { shortcut: { key: 'Escape' }, onShortcut: () => dispatch('escape') }, + { shortcut: { key: 'Escape' }, onShortcut: onEscape }, { shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) }, { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) }, { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteractionStore) }, @@ -539,7 +539,7 @@ return !!nextAsset; }; - const handleClose = async ({ detail: { asset } }: { detail: { asset: AssetResponseDto } }) => { + const handleClose = async ({ asset }: { asset: AssetResponseDto }) => { assetViewingStore.showAssetViewer(false); showSkeleton = true; $gridScrollTarget = { at: asset.id }; @@ -554,7 +554,7 @@ case AssetAction.DELETE: { // find the next asset to show or close the viewer // eslint-disable-next-line @typescript-eslint/no-unused-expressions - (await handleNext()) || (await handlePrevious()) || (await handleClose({ detail: { asset: action.asset } })); + (await handleNext()) || (await handlePrevious()) || (await handleClose({ asset: action.asset })); // delete after find the next one assetStore.removeAssets([action.asset.id]); @@ -649,7 +649,7 @@ return; } - dispatch('select', asset); + onSelect(asset); if (singleSelect) { element.scrollTop = 0; @@ -754,8 +754,8 @@ {#if isShowDeleteConfirmation} (isShowDeleteConfirmation = false)} - on:confirm={() => handlePromiseError(trashOrDelete(true))} + onCancel={() => (isShowDeleteConfirmation = false)} + onConfirm={() => handlePromiseError(trashOrDelete(true))} /> {/if} @@ -847,9 +847,9 @@ {onAssetInGrid} {bucket} viewport={safeViewport} - on:select={({ detail: group }) => handleGroupSelect(group.title, group.assets)} - on:selectAssetCandidates={({ detail: asset }) => handleSelectAssetCandidates(asset)} - on:selectAssets={({ detail: asset }) => handleSelectAssets(asset)} + onSelect={({ title, assets }) => handleGroupSelect(title, assets)} + onSelectAssetCandidates={handleSelectAssetCandidates} + onSelectAssets={handleSelectAssets} /> {/if}

@@ -869,9 +869,9 @@ {isShared} {album} onAction={handleAction} - on:previous={handlePrevious} - on:next={handleNext} - on:close={handleClose} + onPrevious={handlePrevious} + onNext={handleNext} + onClose={handleClose} /> {/await} {/if} diff --git a/web/src/lib/components/photos-page/asset-select-control-bar.svelte b/web/src/lib/components/photos-page/asset-select-control-bar.svelte index c802c53454a0d..79a0ea75e6736 100644 --- a/web/src/lib/components/photos-page/asset-select-control-bar.svelte +++ b/web/src/lib/components/photos-page/asset-select-control-bar.svelte @@ -30,7 +30,7 @@ }); - +

{assets.size}

diff --git a/web/src/lib/components/photos-page/delete-asset-dialog.svelte b/web/src/lib/components/photos-page/delete-asset-dialog.svelte index 84782b2d7fcf0..3eff428a7bb5e 100644 --- a/web/src/lib/components/photos-page/delete-asset-dialog.svelte +++ b/web/src/lib/components/photos-page/delete-asset-dialog.svelte @@ -1,5 +1,4 @@ @@ -27,7 +23,7 @@ title={$t('permanently_delete_assets_count', { values: { count: size } })} confirmText={$t('delete')} onConfirm={handleConfirm} - onCancel={() => dispatch('cancel')} + {onCancel} >

diff --git a/web/src/lib/components/shared-components/album-selection-modal.svelte b/web/src/lib/components/shared-components/album-selection-modal.svelte index 0690374c01702..6d28bd12c0332 100644 --- a/web/src/lib/components/shared-components/album-selection-modal.svelte +++ b/web/src/lib/components/shared-components/album-selection-modal.svelte @@ -2,7 +2,7 @@ import Icon from '$lib/components/elements/icon.svelte'; import { getAllAlbums, type AlbumResponseDto } from '@immich/sdk'; import { mdiPlus } from '@mdi/js'; - import { createEventDispatcher, onMount } from 'svelte'; + import { onMount } from 'svelte'; import AlbumListItem from '../asset-viewer/album-list-item.svelte'; import { normalizeSearchString } from '$lib/utils/string-utils'; import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; @@ -11,17 +11,15 @@ import { sortAlbums } from '$lib/utils/album-utils'; import { albumViewSettings } from '$lib/stores/preferences.store'; + export let onNewAlbum: (search: string) => void; + export let onAlbumClick: (album: AlbumResponseDto) => void; + let albums: AlbumResponseDto[] = []; let recentAlbums: AlbumResponseDto[] = []; let filteredAlbums: AlbumResponseDto[] = []; let loading = true; let search = ''; - const dispatch = createEventDispatcher<{ - newAlbum: string; - album: AlbumResponseDto; - }>(); - export let shared: boolean; export let onClose: () => void; @@ -40,14 +38,6 @@ { sortBy: $albumViewSettings.sortBy, orderBy: $albumViewSettings.sortOrder }, ); - const handleSelect = (album: AlbumResponseDto) => { - dispatch('album', album); - }; - - const handleNew = () => { - dispatch('newAlbum', search.length > 0 ? search : ''); - }; - const getTitle = () => { if (shared) { return $t('add_to_shared_album'); @@ -81,7 +71,7 @@

@@ -180,7 +162,7 @@ center={lat && lng ? { lat, lng } : undefined} simplified={true} clickable={true} - on:clickedPoint={({ detail: point }) => handleSelect(point)} + onClickPoint={(selected) => (point = selected)} /> {/await}
diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index 7c71fe8aeaed7..241f937be0fd5 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -21,7 +21,7 @@ import { fly } from 'svelte/transition'; import Icon from '$lib/components/elements/icon.svelte'; import { mdiMagnify, mdiUnfoldMoreHorizontal, mdiClose } from '@mdi/js'; - import { createEventDispatcher, tick } from 'svelte'; + import { tick } from 'svelte'; import type { FormEventHandler } from 'svelte/elements'; import { shortcuts } from '$lib/actions/shortcut'; import { focusOutside } from '$lib/actions/focus-outside'; @@ -35,6 +35,7 @@ export let options: ComboBoxOption[] = []; export let selectedOption: ComboBoxOption | undefined = undefined; export let placeholder = ''; + export let onSelect: (option: ComboBoxOption | undefined) => void = () => {}; /** * Unique identifier for the combobox. @@ -61,10 +62,6 @@ searchQuery = selectedOption ? selectedOption.label : ''; } - const dispatch = createEventDispatcher<{ - select: ComboBoxOption | undefined; - }>(); - const activate = () => { isActive = true; searchQuery = ''; @@ -105,10 +102,10 @@ optionRefs[0]?.scrollIntoView({ block: 'nearest' }); }; - let onSelect = (option: ComboBoxOption) => { + let handleSelect = (option: ComboBoxOption) => { selectedOption = option; searchQuery = option.label; - dispatch('select', option); + onSelect(option); closeDropdown(); }; @@ -117,7 +114,7 @@ selectedIndex = undefined; selectedOption = undefined; searchQuery = ''; - dispatch('select', selectedOption); + onSelect(selectedOption); }; @@ -188,7 +185,7 @@ shortcut: { key: 'Enter' }, onShortcut: () => { if (selectedIndex !== undefined && filteredOptions.length > 0) { - onSelect(filteredOptions[selectedIndex]); + handleSelect(filteredOptions[selectedIndex]); } closeDropdown(); }, @@ -245,7 +242,7 @@ bind:this={optionRefs[index]} class="text-left w-full px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all cursor-pointer aria-selected:bg-gray-100 aria-selected:dark:bg-gray-700" id={`${listboxId}-${index}`} - on:click={() => onSelect(option)} + on:click={() => handleSelect(option)} role="option" > {option.label} diff --git a/web/src/lib/components/shared-components/control-app-bar.svelte b/web/src/lib/components/shared-components/control-app-bar.svelte index cf128104d18e0..228cd88a86e75 100644 --- a/web/src/lib/components/shared-components/control-app-bar.svelte +++ b/web/src/lib/components/shared-components/control-app-bar.svelte @@ -1,7 +1,7 @@ diff --git a/web/src/lib/components/shared-components/settings/setting-switch.svelte b/web/src/lib/components/shared-components/settings/setting-switch.svelte index 24b539f0a1c22..11716526f85dc 100644 --- a/web/src/lib/components/shared-components/settings/setting-switch.svelte +++ b/web/src/lib/components/shared-components/settings/setting-switch.svelte @@ -1,7 +1,6 @@
diff --git a/web/src/lib/components/user-settings-page/user-api-key-list.svelte b/web/src/lib/components/user-settings-page/user-api-key-list.svelte index 13ec440082e91..a63bdb3ca9cb6 100644 --- a/web/src/lib/components/user-settings-page/user-api-key-list.svelte +++ b/web/src/lib/components/user-settings-page/user-api-key-list.svelte @@ -102,7 +102,7 @@ {/if} {#if secret} - (secret = '')} /> + (secret = '')} /> {/if} {#if editKey} diff --git a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte index 2f1efc487cdd2..fd5b68d8c38a6 100644 --- a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte +++ b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte @@ -151,15 +151,15 @@ 1} - on:next={() => { + onNext={() => { const index = getAssetIndex($viewingAsset.id) + 1; setAsset(assets[index % assets.length]); }} - on:previous={() => { + onPrevious={() => { const index = getAssetIndex($viewingAsset.id) - 1 + assets.length; setAsset(assets[index % assets.length]); }} - on:close={() => { + onClose={() => { assetViewingStore.showAssetViewer(false); handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); }} diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 6e75273f3bc2c..57d09ed53a563 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -674,8 +674,8 @@ disabled={!album.isActivityEnabled} {isLiked} numberOfComments={$numberOfComments} - on:favorite={handleFavorite} - on:openActivityTab={handleOpenAndCloseActivityTab} + onFavorite={handleFavorite} + onOpenActivityTab={handleOpenAndCloseActivityTab} />
{/if} @@ -697,10 +697,10 @@ albumId={album.id} {isLiked} bind:reactions - on:addComment={() => updateNumberOfComments(1)} - on:deleteComment={() => updateNumberOfComments(-1)} - on:deleteLike={() => (isLiked = null)} - on:close={handleOpenAndCloseActivityTab} + onAddComment={() => updateNumberOfComments(1)} + onDeleteComment={() => updateNumberOfComments(-1)} + onDeleteLike={() => (isLiked = null)} + onClose={handleOpenAndCloseActivityTab} />
@@ -709,8 +709,8 @@ {#if viewMode === ViewMode.SELECT_USERS} handleAddUsers(users)} - on:share={() => (viewMode = ViewMode.LINK_SHARING)} + onSelect={handleAddUsers} + onShare={() => (viewMode = ViewMode.LINK_SHARING)} onClose={() => (viewMode = ViewMode.VIEW)} /> {/if} @@ -723,8 +723,8 @@ (viewMode = ViewMode.VIEW)} {album} - on:remove={({ detail: userId }) => handleRemoveUser(userId)} - on:refreshAlbum={refreshAlbum} + onRemove={handleRemoveUser} + onRefreshAlbum={refreshAlbum} /> {/if} @@ -737,9 +737,9 @@ albumOrder = order; await setModeToView(); }} - on:close={() => (viewMode = ViewMode.VIEW)} - on:toggleEnableActivity={handleToggleEnableActivity} - on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)} + onClose={() => (viewMode = ViewMode.VIEW)} + onToggleEnabledActivity={handleToggleEnableActivity} + onShowSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)} /> {/if} diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte index 0ea0ed18bb733..2e109823ed175 100644 --- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -122,9 +122,9 @@ 1} - on:next={navigateNext} - on:previous={navigatePrevious} - on:close={() => { + onNext={navigateNext} + onPrevious={navigatePrevious} + onClose={() => { assetViewingStore.showAssetViewer(false); handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); }} @@ -137,11 +137,11 @@ {#if showSettingsModal} (showSettingsModal = false)} - on:save={async ({ detail }) => { - const shouldUpdate = !isEqual(omit(detail, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode')); + onClose={() => (showSettingsModal = false)} + onSave={async (settings) => { + const shouldUpdate = !isEqual(omit(settings, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode')); showSettingsModal = false; - $mapSettings = detail; + $mapSettings = settings; if (shouldUpdate) { mapMarkers = await loadMapMarkers(); diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index f1a2674e24905..b6d25c48bf937 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -302,9 +302,9 @@ {personMerge1} {personMerge2} {potentialMergePeople} - on:close={() => (showMergeModal = false)} - on:reject={() => changeName()} - on:confirm={(event) => handleMergeSamePerson(event.detail)} + onClose={() => (showMergeModal = false)} + onReject={changeName} + onConfirm={handleMergeSamePerson} /> {/if} @@ -349,10 +349,10 @@ handleChangeName(person)} - on:set-birth-date={() => handleSetBirthDate(person)} - on:merge-people={() => handleMergePeople(person)} - on:hide-person={() => handleHidePerson(person)} + onChangeName={() => handleChangeName(person)} + onSetBirthDate={() => handleSetBirthDate(person)} + onMergePeople={() => handleMergePeople(person)} + onHidePerson={() => handleHidePerson(person)} /> {:else} @@ -397,8 +397,8 @@ {#if showSetBirthDateModal} (showSetBirthDateModal = false)} - on:updated={(event) => submitBirthDateChange(event.detail)} + onClose={() => (showSetBirthDateModal = false)} + onUpdate={submitBirthDateChange} /> {/if} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index daa5821e8506b..bb648228b93c7 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -347,8 +347,8 @@ a.id)} personAssets={person} - on:close={() => (viewMode = ViewMode.VIEW_ASSETS)} - on:confirm={handleUnmerge} + onClose={() => (viewMode = ViewMode.VIEW_ASSETS)} + onConfirm={handleUnmerge} /> {/if} @@ -357,22 +357,22 @@ {personMerge1} {personMerge2} {potentialMergePeople} - on:close={() => (viewMode = ViewMode.VIEW_ASSETS)} - on:reject={() => changeName()} - on:confirm={(event) => handleMergeSamePerson(event.detail)} + onClose={() => (viewMode = ViewMode.VIEW_ASSETS)} + onReject={changeName} + onConfirm={handleMergeSamePerson} /> {/if} {#if viewMode === ViewMode.BIRTH_DATE} (viewMode = ViewMode.VIEW_ASSETS)} - on:updated={(event) => handleSetBirthDate(event.detail)} + onClose={() => (viewMode = ViewMode.VIEW_ASSETS)} + onUpdate={handleSetBirthDate} /> {/if} {#if viewMode === ViewMode.MERGE_PEOPLE} - handleMerge(detail)} /> + {/if}
@@ -464,7 +464,7 @@ bind:suggestedPeople name={person.name} bind:isSearchingPeople - on:change={(event) => handleNameChange(event.detail)} + onChange={handleNameChange} {thumbnailData} /> {:else} diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index 74db5628ba3a6..5ce3296a03531 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -267,10 +267,7 @@ {#if toCreateLibrary} - handleCreate(detail.ownerId)} - on:cancel={() => (toCreateLibrary = false)} - /> + (toCreateLibrary = false)} /> {/if} @@ -385,28 +382,20 @@ {#if renameLibrary === index}
- handleUpdate(detail)} - on:cancel={() => (renameLibrary = null)} - /> + (renameLibrary = null)} />
{/if} {#if editImportPaths === index}
- handleUpdate(detail)} - on:cancel={() => (editImportPaths = null)} - /> + (editImportPaths = null)} />
{/if} {#if editScanSettings === index}
handleUpdate(library)} - on:cancel={() => (editScanSettings = null)} + onSubmit={handleUpdate} + onCancel={() => (editScanSettings = null)} />
{/if} diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index b040ce293c74a..2313b17cb1ea1 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -110,8 +110,8 @@
{#if shouldShowCreateUserForm} (shouldShowCreateUserForm = false)} + onSubmit={onUserCreated} + onCancel={() => (shouldShowCreateUserForm = false)} onClose={() => (shouldShowCreateUserForm = false)} /> {/if} @@ -121,8 +121,8 @@ user={selectedUser} bind:newPassword canResetPassword={selectedUser?.id !== $user.id} - on:editSuccess={onEditUserSuccess} - on:resetPasswordSuccess={onEditPasswordSuccess} + onEditSuccess={onEditUserSuccess} + onResetPasswordSuccess={onEditPasswordSuccess} onClose={() => (shouldShowEditUserForm = false)} /> {/if} diff --git a/web/src/routes/auth/change-password/+page.svelte b/web/src/routes/auth/change-password/+page.svelte index aa23e4e7d24e0..eaf5a88fe20be 100644 --- a/web/src/routes/auth/change-password/+page.svelte +++ b/web/src/routes/auth/change-password/+page.svelte @@ -25,5 +25,5 @@ {$t('change_password_description')}

- + From 8cd3f6b8840a8f8f66c42d40dc694aac2307e930 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Sat, 21 Sep 2024 00:24:46 +0200 Subject: [PATCH 28/57] fix(web): events as props (#12825) --- .../admin-page/settings/ffmpeg/ffmpeg-settings.svelte | 4 ++-- .../admin-page/settings/image/image-settings.svelte | 4 ++-- .../asset-viewer/video-wrapper-viewer.svelte | 10 +++++++++- web/src/lib/components/faces-page/people-list.svelte | 2 +- web/src/lib/components/faces-page/people-search.svelte | 4 ++-- .../components/faces-page/unmerge-face-selector.svelte | 2 +- web/src/lib/components/forms/tag-asset-form.svelte | 2 +- .../share-page/individual-shared-viewer.svelte | 2 +- .../purchasing/purchase-activation-success.svelte | 2 +- .../search-bar/search-camera-section.svelte | 4 ++-- .../search-bar/search-location-section.svelte | 6 +++--- .../shared-components/settings/setting-combobox.svelte | 9 +-------- .../components/user-settings-page/app-settings.svelte | 10 +++++----- .../user-settings-page/partner-settings.svelte | 2 +- .../user-settings-page/user-purchase-settings.svelte | 2 +- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 10 +++++----- .../map/[[photos=photos]]/[[assetId=id]]/+page.svelte | 2 +- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 2 +- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 8 ++++---- .../routes/(user)/photos/[[assetId=id]]/+page.svelte | 2 +- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 2 +- web/src/routes/(user)/sharing/sharedlinks/+page.svelte | 2 +- 22 files changed, 47 insertions(+), 46 deletions(-) diff --git a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte index 7ddb71cbdef20..c048a222070a3 100644 --- a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte +++ b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte @@ -99,7 +99,7 @@ ]} name="vcodec" isEdited={config.ffmpeg.targetVideoCodec !== savedConfig.ffmpeg.targetVideoCodec} - on:select={() => (config.ffmpeg.acceptedVideoCodecs = [config.ffmpeg.targetVideoCodec])} + onSelect={() => (config.ffmpeg.acceptedVideoCodecs = [config.ffmpeg.targetVideoCodec])} /> + onSelect={() => config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec) ? null : config.ffmpeg.acceptedAudioCodecs.push(config.ffmpeg.targetAudioCodec)} diff --git a/web/src/lib/components/admin-page/settings/image/image-settings.svelte b/web/src/lib/components/admin-page/settings/image/image-settings.svelte index a7b47920fd98b..d6fc814b98e4c 100644 --- a/web/src/lib/components/admin-page/settings/image/image-settings.svelte +++ b/web/src/lib/components/admin-page/settings/image/image-settings.svelte @@ -96,7 +96,7 @@ title={$t('admin.image_prefer_wide_gamut')} subtitle={$t('admin.image_prefer_wide_gamut_setting_description')} checked={config.image.colorspace === Colorspace.P3} - on:toggle={(e) => (config.image.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)} + onToggle={(isChecked) => (config.image.colorspace = isChecked ? Colorspace.P3 : Colorspace.Srgb)} isEdited={config.image.colorspace !== savedConfig.image.colorspace} {disabled} /> @@ -105,7 +105,7 @@ title={$t('admin.image_prefer_embedded_preview')} subtitle={$t('admin.image_prefer_embedded_preview_setting_description')} checked={config.image.extractEmbedded} - on:toggle={() => (config.image.extractEmbedded = !config.image.extractEmbedded)} + onToggle={() => (config.image.extractEmbedded = !config.image.extractEmbedded)} isEdited={config.image.extractEmbedded !== savedConfig.image.extractEmbedded} {disabled} /> diff --git a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte index ae9fda8c69a7f..5f03784c42258 100644 --- a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte @@ -15,5 +15,13 @@ {#if projectionType === ProjectionType.EQUIRECTANGULAR} {:else} - + {/if} diff --git a/web/src/lib/components/faces-page/people-list.svelte b/web/src/lib/components/faces-page/people-list.svelte index 230c8750aedee..10626a6a93888 100644 --- a/web/src/lib/components/faces-page/people-list.svelte +++ b/web/src/lib/components/faces-page/people-list.svelte @@ -32,7 +32,7 @@ >
{#each showPeople as person (person.id)} - onSelect(person)} circle border selectable /> + onSelect(person)} circle border selectable /> {/each}
diff --git a/web/src/lib/components/faces-page/people-search.svelte b/web/src/lib/components/faces-page/people-search.svelte index cfd4c8f29a2f5..2a952b8145b25 100644 --- a/web/src/lib/components/faces-page/people-search.svelte +++ b/web/src/lib/components/faces-page/people-search.svelte @@ -83,8 +83,8 @@ bind:name={searchName} {showLoadingSpinner} {placeholder} - on:reset={handleReset} - on:search={({ detail }) => handleSearch(detail.force ?? false)} + onReset={handleReset} + onSearch={({ force }) => handleSearch(force ?? false)} /> {:else}
diff --git a/web/src/lib/components/forms/tag-asset-form.svelte b/web/src/lib/components/forms/tag-asset-form.svelte index 7500a6faac0d3..b5e358ec9664e 100644 --- a/web/src/lib/components/forms/tag-asset-form.svelte +++ b/web/src/lib/components/forms/tag-asset-form.svelte @@ -52,7 +52,7 @@
handleSelect(option)} + onSelect={handleSelect} label={$t('tag')} options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))} placeholder={$t('search_tags')} diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte index af5c54c9880cd..1b5368b1336e1 100644 --- a/web/src/lib/components/share-page/individual-shared-viewer.svelte +++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte @@ -84,7 +84,7 @@ {/if} {:else} - goto(AppRoute.PHOTOS)} backIcon={mdiArrowLeft} showBackButton={false}> + goto(AppRoute.PHOTOS)} backIcon={mdiArrowLeft} showBackButton={false}> diff --git a/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte index 2b8c678543659..3bd462f9976ff 100644 --- a/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte +++ b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte @@ -20,7 +20,7 @@ title={$t('show_supporter_badge')} subtitle={$t('show_supporter_badge_description')} bind:checked={$preferences.purchase.showSupportBadge} - on:toggle={({ detail }) => setSupportBadgeVisibility(detail)} + onToggle={setSupportBadgeVisibility} />
diff --git a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte index f1cd0c85964cf..3ac8cb8d5aa4f 100644 --- a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte @@ -56,7 +56,7 @@
(filters.make = detail?.value)} + onSelect={(option) => (filters.make = option?.value)} options={asComboboxOptions(makes)} placeholder={$t('search_camera_make')} selectedOption={asSelectedOption(makeFilter)} @@ -66,7 +66,7 @@
(filters.model = detail?.value)} + onSelect={(option) => (filters.model = option?.value)} options={asComboboxOptions(models)} placeholder={$t('search_camera_model')} selectedOption={asSelectedOption(modelFilter)} diff --git a/web/src/lib/components/shared-components/search-bar/search-location-section.svelte b/web/src/lib/components/shared-components/search-bar/search-location-section.svelte index ce265d00306b8..71912264ed7ac 100644 --- a/web/src/lib/components/shared-components/search-bar/search-location-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-location-section.svelte @@ -73,7 +73,7 @@
(filters.country = detail?.value)} + onSelect={(option) => (filters.country = option?.value)} options={asComboboxOptions(countries)} placeholder={$t('search_country')} selectedOption={asSelectedOption(filters.country)} @@ -83,7 +83,7 @@
(filters.state = detail?.value)} + onSelect={(option) => (filters.state = option?.value)} options={asComboboxOptions(states)} placeholder={$t('search_state')} selectedOption={asSelectedOption(filters.state)} @@ -93,7 +93,7 @@
(filters.city = detail?.value)} + onSelect={(option) => (filters.city = option?.value)} options={asComboboxOptions(cities)} placeholder={$t('search_city')} selectedOption={asSelectedOption(filters.city)} diff --git a/web/src/lib/components/shared-components/settings/setting-combobox.svelte b/web/src/lib/components/shared-components/settings/setting-combobox.svelte index 502cd94cce04f..722af048a5d99 100644 --- a/web/src/lib/components/shared-components/settings/setting-combobox.svelte +++ b/web/src/lib/components/shared-components/settings/setting-combobox.svelte @@ -32,14 +32,7 @@

{subtitle}

- onSelect(detail)} - /> +
diff --git a/web/src/lib/components/user-settings-page/app-settings.svelte b/web/src/lib/components/user-settings-page/app-settings.svelte index de4bbafdd94c9..e6ce8f6aae69c 100644 --- a/web/src/lib/components/user-settings-page/app-settings.svelte +++ b/web/src/lib/components/user-settings-page/app-settings.svelte @@ -99,7 +99,7 @@ title={$t('theme_selection')} subtitle={$t('theme_selection_description')} bind:checked={$colorTheme.system} - on:toggle={handleToggleColorTheme} + onToggle={handleToggleColorTheme} />
@@ -119,7 +119,7 @@ title={$t('default_locale')} subtitle={$t('default_locale_description')} checked={$locale == undefined} - on:toggle={handleToggleLocaleBrowser} + onToggle={handleToggleLocaleBrowser} >

{selectedDate}

@@ -142,7 +142,7 @@ title={$t('display_original_photos')} subtitle={$t('display_original_photos_setting_description')} bind:checked={$alwaysLoadOriginalFile} - on:toggle={() => ($alwaysLoadOriginalFile = !$alwaysLoadOriginalFile)} + onToggle={() => ($alwaysLoadOriginalFile = !$alwaysLoadOriginalFile)} />
@@ -150,7 +150,7 @@ title={$t('video_hover_setting')} subtitle={$t('video_hover_setting_description')} bind:checked={$playVideoThumbnailOnHover} - on:toggle={() => ($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)} + onToggle={() => ($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)} />
@@ -158,7 +158,7 @@ title={$t('loop_videos')} subtitle={$t('loop_videos_description')} bind:checked={$loopVideo} - on:toggle={() => ($loopVideo = !$loopVideo)} + onToggle={() => ($loopVideo = !$loopVideo)} />
diff --git a/web/src/lib/components/user-settings-page/partner-settings.svelte b/web/src/lib/components/user-settings-page/partner-settings.svelte index ee57e4c688700..050e2c42f3cac 100644 --- a/web/src/lib/components/user-settings-page/partner-settings.svelte +++ b/web/src/lib/components/user-settings-page/partner-settings.svelte @@ -177,7 +177,7 @@ title={$t('show_in_timeline')} subtitle={$t('show_in_timeline_setting_description')} bind:checked={partner.inTimeline} - on:toggle={({ detail }) => handleShowOnTimelineChanged(partner, detail)} + onToggle={(isChecked) => handleShowOnTimelineChanged(partner, isChecked)} /> {/if}
diff --git a/web/src/lib/components/user-settings-page/user-purchase-settings.svelte b/web/src/lib/components/user-settings-page/user-purchase-settings.svelte index bf0fd3c8746c7..71f76d07c0a85 100644 --- a/web/src/lib/components/user-settings-page/user-purchase-settings.svelte +++ b/web/src/lib/components/user-settings-page/user-purchase-settings.svelte @@ -115,7 +115,7 @@ title={$t('show_supporter_badge')} subtitle={$t('show_supporter_badge_description')} bind:checked={$preferences.purchase.showSupportBadge} - on:toggle={({ detail }) => setSupportBadgeVisibility(detail)} + onToggle={setSupportBadgeVisibility} />
diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 57d09ed53a563..cbdb38192e082 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -470,7 +470,7 @@ {:else} {#if viewMode === ViewMode.VIEW} - goto(backUrl)}> + goto(backUrl)}> {#if isEditor} +

{#if $timelineSelected.size === 0} @@ -554,7 +554,7 @@ {/if} {#if viewMode === ViewMode.SELECT_THUMBNAIL} - (viewMode = ViewMode.VIEW)}> + (viewMode = ViewMode.VIEW)}> {$t('select_album_cover')} {/if} @@ -583,8 +583,8 @@ isSelectionMode={viewMode === ViewMode.SELECT_THUMBNAIL} singleSelect={viewMode === ViewMode.SELECT_THUMBNAIL} showArchiveIcon - on:select={({ detail: asset }) => handleUpdateThumbnail(asset.id)} - on:escape={handleEscape} + onSelect={({ id }) => handleUpdateThumbnail(id)} + onEscape={handleEscape} > {#if viewMode !== ViewMode.SELECT_THUMBNAIL} diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte index 2e109823ed175..adbc3cfe699a3 100644 --- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -113,7 +113,7 @@ {#if $featureFlags.loaded && $featureFlags.map}

- onViewAssets(event.detail)} /> +
diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index b580c4faa5454..2caab9de82508 100644 --- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -38,7 +38,7 @@ {:else} - goto(AppRoute.SHARING)}> + goto(AppRoute.SHARING)}>

{data.partner.name}'s photos diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index bb648228b93c7..83019d67cd869 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -400,7 +400,7 @@ {:else} {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE} - goto(previousRoute)}> + goto(previousRoute)}> (viewMode = ViewMode.VIEW_ASSETS)}> + (viewMode = ViewMode.VIEW_ASSETS)}> {$t('select_featured_photo')} {/if} @@ -444,8 +444,8 @@ {assetInteractionStore} isSelectionMode={viewMode === ViewMode.SELECT_PERSON} singleSelect={viewMode === ViewMode.SELECT_PERSON} - on:select={({ detail: asset }) => handleSelectFeaturePhoto(asset)} - on:escape={handleEscape} + onSelect={handleSelectFeaturePhoto} + onEscape={handleEscape} > {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE} diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index 4649da8205120..ba8ee13cc9f1f 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -127,7 +127,7 @@ {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE} - on:escape={handleEscape} + onEscape={handleEscape} withStacked > {#if $preferences.memories.enabled} diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index da85eb49c8267..9c6a8f9e75891 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -246,7 +246,7 @@ {:else}

- goto(previousRoute)} backIcon={mdiArrowLeft}> + goto(previousRoute)} backIcon={mdiArrowLeft}>
diff --git a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte index 5e934143dff2a..67e80f4703858 100644 --- a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte +++ b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte @@ -52,7 +52,7 @@ }; - goto(AppRoute.SHARING)}> + goto(AppRoute.SHARING)}> {$t('shared_links')} From af7011164589a34f83fa896d756b5c7b1d4c5d81 Mon Sep 17 00:00:00 2001 From: Shubham Date: Sat, 21 Sep 2024 04:31:26 +0530 Subject: [PATCH 29/57] fix(mobile): Issue Selecting Many Albuns for Backup (#12784) * Update backup.provider.dart * Revert "Update backup.provider.dart" This reverts commit ac2b7acef9c4390a61a30884a05589723f572403. * Reapply "Update backup.provider.dart" This reverts commit c9fe934b3bde472a579b465fbd3b21448b819930. * dart formatting --- mobile/lib/providers/backup/backup.provider.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 9329f9b1f7093..0885f35f77998 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -313,6 +313,9 @@ class BackupNotifier extends StateNotifier { /// Those assets are unique and are used as the total assets /// Future _updateBackupAssetCount() async { + // Save to persistent storage + await _updatePersistentAlbumsSelection(); + final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds(); final Set assetsFromSelectedAlbums = {}; final Set assetsFromExcludedAlbums = {}; @@ -408,9 +411,6 @@ class BackupNotifier extends StateNotifier { selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets, ); } - - // Save to persistent storage - await _updatePersistentAlbumsSelection(); } /// Get all necessary information for calculating the available albums, From 5a1a841365a842eab345a70c420380cc00606e2e Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Sat, 21 Sep 2024 00:16:53 +0100 Subject: [PATCH 30/57] fix: rework file handling so we always explicitly create, overwrite or both (#12812) --- server/src/interfaces/storage.interface.ts | 4 +++- server/src/repositories/storage.repository.ts | 12 +++++++++-- server/src/services/metadata.service.spec.ts | 10 ++++----- server/src/services/metadata.service.ts | 2 +- server/src/services/storage.service.spec.ts | 9 ++++++-- server/src/services/storage.service.ts | 21 +++++++++++++++---- .../repositories/storage.repository.mock.ts | 4 +++- 7 files changed, 46 insertions(+), 16 deletions(-) diff --git a/server/src/interfaces/storage.interface.ts b/server/src/interfaces/storage.interface.ts index fec3d66dd5c03..321f7b8367f23 100644 --- a/server/src/interfaces/storage.interface.ts +++ b/server/src/interfaces/storage.interface.ts @@ -35,7 +35,9 @@ export interface IStorageRepository { createZipStream(): ImmichZipStream; createReadStream(filepath: string, mimeType?: string | null): Promise; readFile(filepath: string, options?: FileReadOptions): Promise; - writeFile(filepath: string, buffer: Buffer): Promise; + createFile(filepath: string, buffer: Buffer): Promise; + createOrOverwriteFile(filepath: string, buffer: Buffer): Promise; + overwriteFile(filepath: string, buffer: Buffer): Promise; realpath(filepath: string): Promise; unlink(filepath: string): Promise; unlinkDir(folder: string, options?: { recursive?: boolean; force?: boolean }): Promise; diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts index c699047ce1575..6fd9bb8b04147 100644 --- a/server/src/repositories/storage.repository.ts +++ b/server/src/repositories/storage.repository.ts @@ -40,8 +40,16 @@ export class StorageRepository implements IStorageRepository { return fs.stat(filepath); } - writeFile(filepath: string, buffer: Buffer) { - return fs.writeFile(filepath, buffer); + createFile(filepath: string, buffer: Buffer) { + return fs.writeFile(filepath, buffer, { flag: 'wx' }); + } + + createOrOverwriteFile(filepath: string, buffer: Buffer) { + return fs.writeFile(filepath, buffer, { flag: 'w' }); + } + + overwriteFile(filepath: string, buffer: Buffer) { + return fs.writeFile(filepath, buffer, { flag: 'r+' }); } rename(source: string, target: string) { diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 19aaa2ea1a323..4eac4a4cf9574 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -511,7 +511,7 @@ describe(MetadataService.name, () => { await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id }); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]); - expect(storageMock.writeFile).not.toHaveBeenCalled(); + expect(storageMock.createOrOverwriteFile).not.toHaveBeenCalled(); expect(jobMock.queue).not.toHaveBeenCalled(); expect(jobMock.queueAll).not.toHaveBeenCalled(); expect(assetMock.update).not.toHaveBeenCalledWith( @@ -581,7 +581,7 @@ describe(MetadataService.name, () => { type: AssetType.VIDEO, }); expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); - expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); + expect(storageMock.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); expect(assetMock.update).toHaveBeenNthCalledWith(1, { id: assetStub.livePhotoWithOriginalFileName.id, livePhotoVideoId: fileStub.livePhotoMotion.uuid, @@ -624,7 +624,7 @@ describe(MetadataService.name, () => { type: AssetType.VIDEO, }); expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); - expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); + expect(storageMock.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); expect(assetMock.update).toHaveBeenNthCalledWith(1, { id: assetStub.livePhotoWithOriginalFileName.id, livePhotoVideoId: fileStub.livePhotoMotion.uuid, @@ -668,7 +668,7 @@ describe(MetadataService.name, () => { type: AssetType.VIDEO, }); expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); - expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); + expect(storageMock.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); expect(assetMock.update).toHaveBeenNthCalledWith(1, { id: assetStub.livePhotoWithOriginalFileName.id, livePhotoVideoId: fileStub.livePhotoMotion.uuid, @@ -716,7 +716,7 @@ describe(MetadataService.name, () => { await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); expect(assetMock.create).toHaveBeenCalledTimes(0); - expect(storageMock.writeFile).toHaveBeenCalledTimes(0); + expect(storageMock.createOrOverwriteFile).toHaveBeenCalledTimes(0); // The still asset gets saved by handleMetadataExtraction, but not the video expect(assetMock.update).toHaveBeenCalledTimes(1); expect(jobMock.queue).toHaveBeenCalledTimes(0); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index eaa491c3ee7d8..60a1e12a5ac7a 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -529,7 +529,7 @@ export class MetadataService { const existsOnDisk = await this.storageRepository.checkFileExists(motionAsset.originalPath); if (!existsOnDisk) { this.storageCore.ensureFolders(motionAsset.originalPath); - await this.storageRepository.writeFile(motionAsset.originalPath, video); + await this.storageRepository.createFile(motionAsset.originalPath, video); this.logger.log(`Wrote motion photo video to ${motionAsset.originalPath}`); await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: motionAsset.id } }); } diff --git a/server/src/services/storage.service.spec.ts b/server/src/services/storage.service.spec.ts index b0f38554cb032..930fb3c726e2f 100644 --- a/server/src/services/storage.service.spec.ts +++ b/server/src/services/storage.service.spec.ts @@ -41,6 +41,11 @@ describe(StorageService.name, () => { expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/library'); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/profile'); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs'); + expect(storageMock.createFile).toHaveBeenCalledWith('upload/encoded-video/.immich', expect.any(Buffer)); + expect(storageMock.createFile).toHaveBeenCalledWith('upload/library/.immich', expect.any(Buffer)); + expect(storageMock.createFile).toHaveBeenCalledWith('upload/profile/.immich', expect.any(Buffer)); + expect(storageMock.createFile).toHaveBeenCalledWith('upload/thumbs/.immich', expect.any(Buffer)); + expect(storageMock.createFile).toHaveBeenCalledWith('upload/upload/.immich', expect.any(Buffer)); }); it('should throw an error if .immich is missing', async () => { @@ -49,13 +54,13 @@ describe(StorageService.name, () => { await expect(sut.onBootstrap()).rejects.toThrow('Failed to validate folder mount'); - expect(storageMock.writeFile).not.toHaveBeenCalled(); + expect(storageMock.createOrOverwriteFile).not.toHaveBeenCalled(); expect(systemMock.set).not.toHaveBeenCalled(); }); it('should throw an error if .immich is present but read-only', async () => { systemMock.get.mockResolvedValue({ mountFiles: true }); - storageMock.writeFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'")); + storageMock.overwriteFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'")); await expect(sut.onBootstrap()).rejects.toThrow('Failed to validate folder mount'); diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index a8f6a76e747e1..15328b0c21f65 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -32,7 +32,7 @@ export class StorageService { for (const folder of Object.values(StorageFolder)) { if (!flags.mountFiles) { this.logger.log(`Writing initial mount file for the ${folder} folder`); - await this.verifyWriteAccess(folder); + await this.createMountFile(folder); } await this.verifyReadAccess(folder); @@ -81,17 +81,30 @@ export class StorageService { } } - private async verifyWriteAccess(folder: StorageFolder) { + private async createMountFile(folder: StorageFolder) { const { folderPath, filePath } = this.getMountFilePaths(folder); try { this.storageRepository.mkdirSync(folderPath); - await this.storageRepository.writeFile(filePath, Buffer.from(`${Date.now()}`)); + await this.storageRepository.createFile(filePath, Buffer.from(`${Date.now()}`)); + } catch (error) { + this.logger.error(`Failed to create ${filePath}: ${error}`); + this.logger.error( + `The "${folder}" folder cannot be written to, please make sure the volume is mounted with the correct permissions`, + ); + throw new ImmichStartupError(`Failed to validate folder mount (write to "/${folder}")`); + } + } + + private async verifyWriteAccess(folder: StorageFolder) { + const { filePath } = this.getMountFilePaths(folder); + try { + await this.storageRepository.overwriteFile(filePath, Buffer.from(`${Date.now()}`)); } catch (error) { this.logger.error(`Failed to write ${filePath}: ${error}`); this.logger.error( `The "${folder}" folder cannot be written to, please make sure the volume is mounted with the correct permissions`, ); - throw new ImmichStartupError(`Failed to validate folder mount (write to "/${folder}")`); + throw new ImmichStartupError(`Failed to validate folder mount (write to "/${folder}")`); } } diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts index 5c2951e097b24..5226e0bb1e985 100644 --- a/server/test/repositories/storage.repository.mock.ts +++ b/server/test/repositories/storage.repository.mock.ts @@ -48,7 +48,9 @@ export const newStorageRepositoryMock = (reset = true): Mocked Date: Sat, 21 Sep 2024 07:29:07 +0700 Subject: [PATCH 31/57] fix(mobile): fix uncaught error in getting file cause hashing procses to be aborted entirely (#12826) * fix(mobile): fix uncaught error in getting file cause hashing procses to be aborted entirely * log error --- mobile/lib/services/hash.service.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/mobile/lib/services/hash.service.dart b/mobile/lib/services/hash.service.dart index 2ec545453f232..94d680972fa1a 100644 --- a/mobile/lib/services/hash.service.dart +++ b/mobile/lib/services/hash.service.dart @@ -65,7 +65,19 @@ class HashService { if (hashes[i] != null) { continue; } - final file = await assets[i].local!.originFile; + + File? file; + + try { + file = await assets[i].local!.originFile; + } catch (error, stackTrace) { + _log.warning( + "Error getting file to hash for asset ${assets[i].localId}, name: ${assets[i].fileName}, created on: ${assets[i].fileCreatedAt}, skipping", + error, + stackTrace, + ); + } + if (file == null) { final fileName = assets[i].fileName; From 7c1ea2dc73219aa06c9b5d3ee90a2a04417279d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Sep 2024 07:29:30 +0700 Subject: [PATCH 32/57] chore(deps): update dependency flutter to v3.24.3 (#11738) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- mobile/.fvmrc | 2 +- mobile/pubspec.lock | 2 +- mobile/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile/.fvmrc b/mobile/.fvmrc index 971587f297946..ee6eaac06fefc 100644 --- a/mobile/.fvmrc +++ b/mobile/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.24.0" + "flutter": "3.24.3" } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 7fe33c327058a..aaea00d699bbe 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1854,4 +1854,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.24.0" + flutter: ">=3.24.3" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 8787fd85651d7..0f75463547d6b 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -6,7 +6,7 @@ version: 1.115.0+159 environment: sdk: '>=3.3.0 <4.0.0' - flutter: 3.24.0 + flutter: 3.24.3 dependencies: flutter: From 39ea73d654c79bdffe70d4e4804f813b049b512b Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey <10599762+fyfrey@users.noreply.github.com> Date: Sun, 22 Sep 2024 15:24:08 +0200 Subject: [PATCH 33/57] chore(mobile): restrict isar use via CI checks (#12840) --- mobile/analysis_options.yaml | 20 +++++++++++++++++++ mobile/lib/pages/library/favorite.page.dart | 2 +- ...e_provider.dart => favorite.provider.dart} | 0 3 files changed, 21 insertions(+), 1 deletion(-) rename mobile/lib/providers/{favorite_provider.dart => favorite.provider.dart} (100%) diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 2783e8f1d1423..8f9d41d73610e 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -58,6 +58,26 @@ custom_lint: # refactor to make the providers and services testable - lib/providers/backup/{backup,manual_upload}.provider.dart # uses only PMProgressHandler - lib/services/{background,backup}.service.dart # uses only PMProgressHandler + - import_rule_isar: + message: isar must only be used in entities and repositories + restrict: package:isar + allowed: + # required / wanted + - lib/entities/*.entity.dart + - lib/repositories/{album,asset,backup,user}.repository.dart + # acceptable exceptions for the time being + - integration_test/test_utils/general_helper.dart + - lib/main.dart + - lib/routing/router.dart + - lib/utils/{db,image_url_builder,migration,renderlist_generator}.dart + - test/**.dart + # refactor to make the providers and services testable + - lib/pages/common/{album_asset_selection,gallery_viewer}.page.dart + - lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart + - lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,backup/manual_upload,search/all_motion_photos,search/recently_added_asset}.provider.dart + - lib/services/{asset,asset_description,background,backup,backup_verification,hash,immich_logger,memory,partner,person,search,stack,sync,user}.service.dart + - lib/widgets/asset_grid/{asset_grid_data_structure,thumbnail_image}.dart + - import_rule_openapi: message: openapi must only be used through ApiRepositories restrict: package:openapi diff --git a/mobile/lib/pages/library/favorite.page.dart b/mobile/lib/pages/library/favorite.page.dart index 7462dc8f21519..cc422f88c7fbf 100644 --- a/mobile/lib/pages/library/favorite.page.dart +++ b/mobile/lib/pages/library/favorite.page.dart @@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/favorite_provider.dart'; +import 'package:immich_mobile/providers/favorite.provider.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; diff --git a/mobile/lib/providers/favorite_provider.dart b/mobile/lib/providers/favorite.provider.dart similarity index 100% rename from mobile/lib/providers/favorite_provider.dart rename to mobile/lib/providers/favorite.provider.dart From 9abfa6940ca09ec3aa069b74f50a6a67c61e063e Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey <10599762+fyfrey@users.noreply.github.com> Date: Mon, 23 Sep 2024 06:11:23 +0200 Subject: [PATCH 34/57] docs: mobile architecture diagram (#12841) --- docs/docs/developer/architecture.mdx | 10 +- .../img/immich_mobile_architecture.drawio | 104 ++++++++++++++++++ .../img/immich_mobile_architecture.svg | 3 + 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 docs/docs/developer/img/immich_mobile_architecture.drawio create mode 100644 docs/docs/developer/img/immich_mobile_architecture.svg diff --git a/docs/docs/developer/architecture.mdx b/docs/docs/developer/architecture.mdx index cf004a1119213..7b5debef4c0da 100644 --- a/docs/docs/developer/architecture.mdx +++ b/docs/docs/developer/architecture.mdx @@ -3,6 +3,7 @@ sidebar_position: 1 --- import AppArchitecture from './img/app-architecture.png'; +import MobileArchitecture from './img/immich_mobile_architecture.svg'; # Architecture @@ -28,7 +29,14 @@ All three clients use [OpenAPI](./open-api.md) to auto-generate rest clients for ### Mobile App -The mobile app is written in [Flutter](https://flutter.dev/). It uses [Isar Database](https://isar.dev/) for a local database and [Riverpod](https://riverpod.dev/) for state management. +The mobile app is written in [Dart](https://dart.dev/) using [Flutter](https://flutter.dev/). Below is an architecture overview: + + + +The diagrams shows the target architecture, the current state of the code-base is not always following the architecture yet. New code and contributions should follow this architecture. +Currently, it uses [Isar Database](https://isar.dev/) for a local database and [Riverpod](https://riverpod.dev/) for state management (providers). +Entities and Models are the two types of data classes used. While entities are stored in the on-device database, models are ephemeral and only kept in memory. +The Repositories should be the only place where other data classes are used internally (such as OpenAPI DTOs). However, their interfaces must not use foreign data classes! ### Web Client diff --git a/docs/docs/developer/img/immich_mobile_architecture.drawio b/docs/docs/developer/img/immich_mobile_architecture.drawio new file mode 100644 index 0000000000000..548cda09383f3 --- /dev/null +++ b/docs/docs/developer/img/immich_mobile_architecture.drawio @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/docs/developer/img/immich_mobile_architecture.svg b/docs/docs/developer/img/immich_mobile_architecture.svg new file mode 100644 index 0000000000000..71f28235bf649 --- /dev/null +++ b/docs/docs/developer/img/immich_mobile_architecture.svg @@ -0,0 +1,3 @@ + + +
Mobile App
Mobile App
Services
Services
Repositories
Repositories
Providers
Providers
Pages
Pages
Widgets
Widgets
User
User
platform
system
platform...
on-device
database
on-device...
server
server
OpenAPI
OpenAPI
UI part
UI part
non-UI part
non-UI part
Models
Models
Entities
Entities
\ No newline at end of file From 147747de32a7362db842d95433a4ab1688eece92 Mon Sep 17 00:00:00 2001 From: kurama <52566613+zp33dy@users.noreply.github.com> Date: Mon, 23 Sep 2024 09:40:23 +0200 Subject: [PATCH 35/57] docs: add section for Traefik Reverse Proxy (#12813) * added a section for the Traefik Proxy * minimized the configs * replaced config with a comment. * Update docs/docs/administration/reverse-proxy.md changed timeout values Co-authored-by: dvbthien <89862334+dvbthien@users.noreply.github.com> * changed timeouts back to 10 minutes * fixed typo and set default writeTimeout 600s Leaving it at 0 may be also bad practice * removed whitespace * run `npm run format -- --check -w` --------- Co-authored-by: dvbthien <89862334+dvbthien@users.noreply.github.com> --- docs/docs/administration/reverse-proxy.md | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/docs/administration/reverse-proxy.md b/docs/docs/administration/reverse-proxy.md index 1d2488f1192d8..c40fecbdc4c23 100644 --- a/docs/docs/administration/reverse-proxy.md +++ b/docs/docs/administration/reverse-proxy.md @@ -64,3 +64,43 @@ Below is an example config for Apache2 site configuration. ProxyPreserveHost On ``` + +### Traefik Proxy example config + +The example below is for Traefik version 3. + +The most important is to increase the `respondingTimeouts` of the entrypoint used by immich. In this example of entrypoint `websecure` for port `443`. Per default it's set to 60s which leeds to videos stop uploading after 1 minute (Error Code 499). With this config it will fail after 10 minutes which is in most cases enough. Increase it if needed. + +`traefik.yaml` + +```yaml +[...] +entryPoints: + websecure: + address: :443 + # this section needs to be added + transport: + respondingTimeouts: + readTimeout: 600s + idleTimeout: 600s + writeTimeout: 600s +``` + +The second part is in the `docker-compose.yml` file where immich is in. Add the Traefik specific labels like in the example. + +`docker-compose.yml` + +```yaml +services: + immich-server: + [...] + labels: + traefik.enable: true + # increase readingTimeouts for the entrypoint used here + traefik.http.routers.immich.entrypoints: websecure + traefik.http.routers.immich.rule: Host(`immich.your-domain.com`) + traefik.http.services.immich.loadbalancer.server.port: 3001 +``` + +Keep in mind, that Traefik needs to communicate with the network where immich is in, usually done +by adding the Traefik network to the `immich-server`. From b1cdf73a2425cf789aff1e3ab874e05d377dfe0f Mon Sep 17 00:00:00 2001 From: Nuno Antunes Date: Mon, 23 Sep 2024 08:50:18 +0100 Subject: [PATCH 36/57] feat(server): validate rating (#12855) * feat(server): validate exif rating tag * fix(server): change allowed range for rating * refactor: better readibility * docs: comments * remove log line --- server/src/services/metadata.service.spec.ts | 24 ++++++++++++++++++++ server/src/services/metadata.service.ts | 14 +++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 4eac4a4cf9574..ad01aa5784afe 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -1107,6 +1107,30 @@ describe(MetadataService.name, () => { }), ); }); + + it('should handle invalid rating value', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Rating: 6 }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(assetMock.upsertExif).toHaveBeenCalledWith( + expect.objectContaining({ + rating: null, + }), + ); + }); + + it('should handle valid rating value', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Rating: 5 }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(assetMock.upsertExif).toHaveBeenCalledWith( + expect.objectContaining({ + rating: 5, + }), + ); + }); }); describe('handleQueueSidecar', () => { diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 60a1e12a5ac7a..bf76be07311b2 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -83,6 +83,18 @@ const validate = (value: T): NonNullable | null => { return value ?? null; }; +const validateRange = (value: number | undefined, min: number, max: number): NonNullable | null => { + // reutilizes the validate function + const val = validate(value); + + // check if the value is within the range + if (val == null || val < min || val > max) { + return null; + } + + return val; +}; + @Injectable() export class MetadataService { private storageCore: StorageCore; @@ -261,7 +273,7 @@ export class MetadataService { // comments description: String(exifTags.ImageDescription || exifTags.Description || '').trim(), profileDescription: exifTags.ProfileDescription || null, - rating: exifTags.Rating ?? null, + rating: validateRange(exifTags.Rating, 0, 5), // grouping livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null, From 0cce7ebf25b8684709ff4a270b74ab1b1f097bec Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 23 Sep 2024 11:16:25 -0400 Subject: [PATCH 37/57] fix: web e2e (#12869) --- e2e/docker-compose.yml | 5 ----- e2e/playwright.config.ts | 4 +++- e2e/src/setup/docker-compose.ts | 3 ++- e2e/src/utils.ts | 3 +-- server/src/services/storage.service.ts | 5 +++-- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index dbb95f176d7e7..6169a4bfa1725 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -22,7 +22,6 @@ services: - IMMICH_METRICS=true - IMMICH_ENV=testing volumes: - - upload:/usr/src/app/upload - ./test-assets:/test-assets extra_hosts: - 'auth-server:host-gateway' @@ -44,7 +43,3 @@ services: POSTGRES_DB: immich ports: - 5435:5432 - -volumes: - model-cache: - upload: diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 55032bd364bee..2576a2c5c945f 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -53,8 +53,10 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: 'docker compose up --build -V --remove-orphans', + command: 'docker compose up --build --renew-anon-volumes --force-recreate --remove-orphans', url: 'http://127.0.0.1:2285', + stdout: 'pipe', + stderr: 'pipe', reuseExistingServer: true, }, }); diff --git a/e2e/src/setup/docker-compose.ts b/e2e/src/setup/docker-compose.ts index 3ae87417a2f94..49a702e776c85 100644 --- a/e2e/src/setup/docker-compose.ts +++ b/e2e/src/setup/docker-compose.ts @@ -12,7 +12,8 @@ const setup = async () => { const timeout = setTimeout(() => _reject(new Error('Timeout starting e2e environment')), 60_000); - const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' }); + const command = 'compose up --build --renew-anon-volumes --force-recreate --remove-orphans'; + const child = spawn('docker', command.split(' '), { stdio: 'pipe' }); child.stdout.on('data', (data) => { const input = data.toString(); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index c67e5696975a9..3c9d4284ce49c 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -156,8 +156,7 @@ export const utils = { for (const table of tables) { if (table === 'system_metadata') { - // prevent reverse geocoder from being re-initialized - sql.push(`DELETE FROM "system_metadata" where "key" != 'reverse-geocoding-state';`); + sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`); } else { sql.push(`DELETE FROM ${table} CASCADE;`); } diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index 15328b0c21f65..1591149dc20d8 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -25,12 +25,13 @@ export class StorageService { async onBootstrap() { await this.databaseRepository.withLock(DatabaseLock.SystemFileMounts, async () => { const flags = (await this.systemMetadata.get(SystemMetadataKey.SYSTEM_FLAGS)) || { mountFiles: false }; + const enabled = flags.mountFiles ?? false; - this.logger.log('Verifying system mount folder checks'); + this.logger.log(`Verifying system mount folder checks (enabled=${enabled})`); // check each folder exists and is writable for (const folder of Object.values(StorageFolder)) { - if (!flags.mountFiles) { + if (!enabled) { this.logger.log(`Writing initial mount file for the ${folder} folder`); await this.createMountFile(folder); } From 9a4a320cfb82b2cf5a7e273801e4955452a4e524 Mon Sep 17 00:00:00 2001 From: Caesiumhydroxid Date: Mon, 23 Sep 2024 17:38:50 +0200 Subject: [PATCH 38/57] fix(web): Fix same key for delete and stack actions (#12865) Fix same key for delete and stack actions --- .../duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte index 5207cf8445e52..e1029b7ccbfff 100644 --- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -42,7 +42,7 @@ { key: ['s'], action: $t('view') }, { key: ['d'], action: $t('unselect_all_duplicates') }, { key: ['⇧', 'c'], action: $t('resolve_duplicates') }, - { key: ['⇧', 'c'], action: $t('stack_duplicates') }, + { key: ['⇧', 's'], action: $t('stack_duplicates') }, ], }; From a7719a94fcac4e52edefe482a7661019708fde53 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:40:25 +0200 Subject: [PATCH 39/57] fix: normalize external domain (#12831) chore: normalize external domain --- server/src/cores/system-config.core.ts | 4 ++++ .../src/services/system-config.service.spec.ts | 17 +++++++++++++++++ web/src/lib/utils.ts | 6 +----- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts index 7c1434004a437..8ed53344cc02f 100644 --- a/server/src/cores/system-config.core.ts +++ b/server/src/cores/system-config.core.ts @@ -120,6 +120,10 @@ export class SystemConfigCore { } } + if (config.server.externalDomain.length > 0) { + config.server.externalDomain = new URL(config.server.externalDomain).origin; + } + if (!config.ffmpeg.acceptedVideoCodecs.includes(config.ffmpeg.targetVideoCodec)) { config.ffmpeg.acceptedVideoCodecs.push(config.ffmpeg.targetVideoCodec); } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 409cd6a52f360..7e25e0cd46c8c 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -289,6 +289,23 @@ describe(SystemConfigService.name, () => { expect(config.machineLearning.url).toEqual('immich_machine_learning'); }); + const externalDomainTests = [ + { should: 'with a trailing slash', externalDomain: 'https://demo.immich.app/' }, + { should: 'without a trailing slash', externalDomain: 'https://demo.immich.app' }, + { should: 'with a port', externalDomain: 'https://demo.immich.app:42', result: 'https://demo.immich.app:42' }, + ]; + + for (const { should, externalDomain, result } of externalDomainTests) { + it(`should normalize an external domain ${should}`, async () => { + process.env.IMMICH_CONFIG_FILE = 'immich-config.json'; + const partialConfig = { server: { externalDomain } }; + systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig)); + + const config = await sut.getConfig(); + expect(config.server.externalDomain).toEqual(result ?? 'https://demo.immich.app'); + }); + } + it('should warn for unknown options in yaml', async () => { process.env.IMMICH_CONFIG_FILE = 'immich-config.yaml'; const partialConfig = ` diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 29c7552d0c94c..dccb03c9bf55e 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -257,11 +257,7 @@ export const copyToClipboard = async (secret: string) => { }; export const makeSharedLinkUrl = (externalDomain: string, key: string) => { - let url = externalDomain || window.location.origin; - if (!url.endsWith('/')) { - url += '/'; - } - return `${url}share/${key}`; + return new URL(`share/${key}`, externalDomain || window.location.origin).href; }; export const oauth = { From 9f8a7e0beac3615fd2b7b3e2f8cbb4d91448e238 Mon Sep 17 00:00:00 2001 From: jschwalbe Date: Mon, 23 Sep 2024 12:09:26 -0400 Subject: [PATCH 40/57] feat(server): sort assets randomly from the API 'api/search/metadata' endpoint by including 'order': 'rand' in the API call. (#12741) feat(server): search metadata random sort order Co-authored-by: Jason Rasmussen --- mobile/openapi/README.md | 3 + mobile/openapi/lib/api.dart | 1 + mobile/openapi/lib/api/assets_api.dart | 7 +- mobile/openapi/lib/api/deprecated_api.dart | 59 ++ mobile/openapi/lib/api/search_api.dart | 47 ++ mobile/openapi/lib/api_client.dart | 2 + .../openapi/lib/model/random_search_dto.dart | 583 ++++++++++++++++++ open-api/immich-openapi-specs.json | 176 +++++- open-api/typescript-sdk/src/fetch-client.ts | 49 ++ server/src/controllers/asset.controller.ts | 2 + server/src/controllers/search.controller.ts | 8 + server/src/dtos/search.dto.ts | 16 +- server/src/interfaces/search.interface.ts | 1 + server/src/repositories/search.repository.ts | 7 +- server/src/services/search.service.ts | 17 + 15 files changed, 967 insertions(+), 11 deletions(-) create mode 100644 mobile/openapi/lib/model/random_search_dto.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 697239fa44e99..c8135519def56 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -116,6 +116,7 @@ Class | Method | HTTP request | Description *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | *DeprecatedApi* | [**getPersonAssets**](doc//DeprecatedApi.md#getpersonassets) | **GET** /people/{id}/assets | +*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | *DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates | @@ -172,6 +173,7 @@ Class | Method | HTTP request | Description *SearchApi* | [**searchMetadata**](doc//SearchApi.md#searchmetadata) | **POST** /search/metadata | *SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person | *SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places | +*SearchApi* | [**searchRandom**](doc//SearchApi.md#searchrandom) | **POST** /search/random | *SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart | *ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license | *ServerApi* | [**getAboutInfo**](doc//ServerApi.md#getaboutinfo) | **GET** /server/about | @@ -379,6 +381,7 @@ Class | Method | HTTP request | Description - [PurchaseResponse](doc//PurchaseResponse.md) - [PurchaseUpdate](doc//PurchaseUpdate.md) - [QueueStatusDto](doc//QueueStatusDto.md) + - [RandomSearchDto](doc//RandomSearchDto.md) - [RatingsResponse](doc//RatingsResponse.md) - [RatingsUpdate](doc//RatingsUpdate.md) - [ReactionLevel](doc//ReactionLevel.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 8a1655d35a109..7fa06b04875ee 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -192,6 +192,7 @@ part 'model/places_response_dto.dart'; part 'model/purchase_response.dart'; part 'model/purchase_update.dart'; part 'model/queue_status_dto.dart'; +part 'model/random_search_dto.dart'; part 'model/ratings_response.dart'; part 'model/ratings_update.dart'; part 'model/reaction_level.dart'; diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index ceba3574cd17a..bd1d5b84847a1 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -449,7 +449,10 @@ class AssetsApi { return null; } - /// Performs an HTTP 'GET /assets/random' operation and returns the [Response]. + /// This property was deprecated in v1.116.0 + /// + /// Note: This method returns the HTTP [Response]. + /// /// Parameters: /// /// * [num] count: @@ -482,6 +485,8 @@ class AssetsApi { ); } + /// This property was deprecated in v1.116.0 + /// /// Parameters: /// /// * [num] count: diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index 96cb3c2ef0ad2..bc8f50092a030 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -71,4 +71,63 @@ class DeprecatedApi { } return null; } + + /// This property was deprecated in v1.116.0 + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [num] count: + Future getRandomWithHttpInfo({ num? count, }) async { + // ignore: prefer_const_declarations + final path = r'/assets/random'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (count != null) { + queryParams.addAll(_queryParams('', 'count', count)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// This property was deprecated in v1.116.0 + /// + /// Parameters: + /// + /// * [num] count: + Future?> getRandom({ num? count, }) async { + final response = await getRandomWithHttpInfo( count: count, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } } diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 4b6cdfea78aa4..3b981e0ccb5bb 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -351,6 +351,53 @@ class SearchApi { return null; } + /// Performs an HTTP 'POST /search/random' operation and returns the [Response]. + /// Parameters: + /// + /// * [RandomSearchDto] randomSearchDto (required): + Future searchRandomWithHttpInfo(RandomSearchDto randomSearchDto,) async { + // ignore: prefer_const_declarations + final path = r'/search/random'; + + // ignore: prefer_final_locals + Object? postBody = randomSearchDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [RandomSearchDto] randomSearchDto (required): + Future searchRandom(RandomSearchDto randomSearchDto,) async { + final response = await searchRandomWithHttpInfo(randomSearchDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchResponseDto',) as SearchResponseDto; + + } + return null; + } + /// Performs an HTTP 'POST /search/smart' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 4976c8a75f331..597a15d5b0562 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -439,6 +439,8 @@ class ApiClient { return PurchaseUpdate.fromJson(value); case 'QueueStatusDto': return QueueStatusDto.fromJson(value); + case 'RandomSearchDto': + return RandomSearchDto.fromJson(value); case 'RatingsResponse': return RatingsResponse.fromJson(value); case 'RatingsUpdate': diff --git a/mobile/openapi/lib/model/random_search_dto.dart b/mobile/openapi/lib/model/random_search_dto.dart new file mode 100644 index 0000000000000..8dbbeb538714d --- /dev/null +++ b/mobile/openapi/lib/model/random_search_dto.dart @@ -0,0 +1,583 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class RandomSearchDto { + /// Returns a new [RandomSearchDto] instance. + RandomSearchDto({ + this.city, + this.country, + this.createdAfter, + this.createdBefore, + this.deviceId, + this.isArchived, + this.isEncoded, + this.isFavorite, + this.isMotion, + this.isNotInAlbum, + this.isOffline, + this.isVisible, + this.lensModel, + this.libraryId, + this.make, + this.model, + this.page, + this.personIds = const [], + this.size, + this.state, + this.takenAfter, + this.takenBefore, + this.trashedAfter, + this.trashedBefore, + this.type, + this.updatedAfter, + this.updatedBefore, + this.withArchived = false, + this.withDeleted, + this.withExif, + this.withPeople, + this.withStacked, + }); + + String? city; + + String? country; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? createdAfter; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? createdBefore; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? deviceId; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isArchived; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isEncoded; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isFavorite; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isMotion; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isNotInAlbum; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isOffline; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isVisible; + + String? lensModel; + + String? libraryId; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? make; + + String? model; + + /// Minimum value: 1 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? page; + + List personIds; + + /// Minimum value: 1 + /// Maximum value: 1000 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? size; + + String? state; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? takenAfter; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? takenBefore; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? trashedAfter; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? trashedBefore; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + AssetTypeEnum? type; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? updatedAfter; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? updatedBefore; + + bool withArchived; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? withDeleted; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? withExif; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? withPeople; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? withStacked; + + @override + bool operator ==(Object other) => identical(this, other) || other is RandomSearchDto && + other.city == city && + other.country == country && + other.createdAfter == createdAfter && + other.createdBefore == createdBefore && + other.deviceId == deviceId && + other.isArchived == isArchived && + other.isEncoded == isEncoded && + other.isFavorite == isFavorite && + other.isMotion == isMotion && + other.isNotInAlbum == isNotInAlbum && + other.isOffline == isOffline && + other.isVisible == isVisible && + other.lensModel == lensModel && + other.libraryId == libraryId && + other.make == make && + other.model == model && + other.page == page && + _deepEquality.equals(other.personIds, personIds) && + other.size == size && + other.state == state && + other.takenAfter == takenAfter && + other.takenBefore == takenBefore && + other.trashedAfter == trashedAfter && + other.trashedBefore == trashedBefore && + other.type == type && + other.updatedAfter == updatedAfter && + other.updatedBefore == updatedBefore && + other.withArchived == withArchived && + other.withDeleted == withDeleted && + other.withExif == withExif && + other.withPeople == withPeople && + other.withStacked == withStacked; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (city == null ? 0 : city!.hashCode) + + (country == null ? 0 : country!.hashCode) + + (createdAfter == null ? 0 : createdAfter!.hashCode) + + (createdBefore == null ? 0 : createdBefore!.hashCode) + + (deviceId == null ? 0 : deviceId!.hashCode) + + (isArchived == null ? 0 : isArchived!.hashCode) + + (isEncoded == null ? 0 : isEncoded!.hashCode) + + (isFavorite == null ? 0 : isFavorite!.hashCode) + + (isMotion == null ? 0 : isMotion!.hashCode) + + (isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) + + (isOffline == null ? 0 : isOffline!.hashCode) + + (isVisible == null ? 0 : isVisible!.hashCode) + + (lensModel == null ? 0 : lensModel!.hashCode) + + (libraryId == null ? 0 : libraryId!.hashCode) + + (make == null ? 0 : make!.hashCode) + + (model == null ? 0 : model!.hashCode) + + (page == null ? 0 : page!.hashCode) + + (personIds.hashCode) + + (size == null ? 0 : size!.hashCode) + + (state == null ? 0 : state!.hashCode) + + (takenAfter == null ? 0 : takenAfter!.hashCode) + + (takenBefore == null ? 0 : takenBefore!.hashCode) + + (trashedAfter == null ? 0 : trashedAfter!.hashCode) + + (trashedBefore == null ? 0 : trashedBefore!.hashCode) + + (type == null ? 0 : type!.hashCode) + + (updatedAfter == null ? 0 : updatedAfter!.hashCode) + + (updatedBefore == null ? 0 : updatedBefore!.hashCode) + + (withArchived.hashCode) + + (withDeleted == null ? 0 : withDeleted!.hashCode) + + (withExif == null ? 0 : withExif!.hashCode) + + (withPeople == null ? 0 : withPeople!.hashCode) + + (withStacked == null ? 0 : withStacked!.hashCode); + + @override + String toString() => 'RandomSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; + + Map toJson() { + final json = {}; + if (this.city != null) { + json[r'city'] = this.city; + } else { + // json[r'city'] = null; + } + if (this.country != null) { + json[r'country'] = this.country; + } else { + // json[r'country'] = null; + } + if (this.createdAfter != null) { + json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + } else { + // json[r'createdAfter'] = null; + } + if (this.createdBefore != null) { + json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + } else { + // json[r'createdBefore'] = null; + } + if (this.deviceId != null) { + json[r'deviceId'] = this.deviceId; + } else { + // json[r'deviceId'] = null; + } + if (this.isArchived != null) { + json[r'isArchived'] = this.isArchived; + } else { + // json[r'isArchived'] = null; + } + if (this.isEncoded != null) { + json[r'isEncoded'] = this.isEncoded; + } else { + // json[r'isEncoded'] = null; + } + if (this.isFavorite != null) { + json[r'isFavorite'] = this.isFavorite; + } else { + // json[r'isFavorite'] = null; + } + if (this.isMotion != null) { + json[r'isMotion'] = this.isMotion; + } else { + // json[r'isMotion'] = null; + } + if (this.isNotInAlbum != null) { + json[r'isNotInAlbum'] = this.isNotInAlbum; + } else { + // json[r'isNotInAlbum'] = null; + } + if (this.isOffline != null) { + json[r'isOffline'] = this.isOffline; + } else { + // json[r'isOffline'] = null; + } + if (this.isVisible != null) { + json[r'isVisible'] = this.isVisible; + } else { + // json[r'isVisible'] = null; + } + if (this.lensModel != null) { + json[r'lensModel'] = this.lensModel; + } else { + // json[r'lensModel'] = null; + } + if (this.libraryId != null) { + json[r'libraryId'] = this.libraryId; + } else { + // json[r'libraryId'] = null; + } + if (this.make != null) { + json[r'make'] = this.make; + } else { + // json[r'make'] = null; + } + if (this.model != null) { + json[r'model'] = this.model; + } else { + // json[r'model'] = null; + } + if (this.page != null) { + json[r'page'] = this.page; + } else { + // json[r'page'] = null; + } + json[r'personIds'] = this.personIds; + if (this.size != null) { + json[r'size'] = this.size; + } else { + // json[r'size'] = null; + } + if (this.state != null) { + json[r'state'] = this.state; + } else { + // json[r'state'] = null; + } + if (this.takenAfter != null) { + json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + } else { + // json[r'takenAfter'] = null; + } + if (this.takenBefore != null) { + json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + } else { + // json[r'takenBefore'] = null; + } + if (this.trashedAfter != null) { + json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + } else { + // json[r'trashedAfter'] = null; + } + if (this.trashedBefore != null) { + json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + } else { + // json[r'trashedBefore'] = null; + } + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + if (this.updatedAfter != null) { + json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + } else { + // json[r'updatedAfter'] = null; + } + if (this.updatedBefore != null) { + json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + } else { + // json[r'updatedBefore'] = null; + } + json[r'withArchived'] = this.withArchived; + if (this.withDeleted != null) { + json[r'withDeleted'] = this.withDeleted; + } else { + // json[r'withDeleted'] = null; + } + if (this.withExif != null) { + json[r'withExif'] = this.withExif; + } else { + // json[r'withExif'] = null; + } + if (this.withPeople != null) { + json[r'withPeople'] = this.withPeople; + } else { + // json[r'withPeople'] = null; + } + if (this.withStacked != null) { + json[r'withStacked'] = this.withStacked; + } else { + // json[r'withStacked'] = null; + } + return json; + } + + /// Returns a new [RandomSearchDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static RandomSearchDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return RandomSearchDto( + city: mapValueOfType(json, r'city'), + country: mapValueOfType(json, r'country'), + createdAfter: mapDateTime(json, r'createdAfter', r''), + createdBefore: mapDateTime(json, r'createdBefore', r''), + deviceId: mapValueOfType(json, r'deviceId'), + isArchived: mapValueOfType(json, r'isArchived'), + isEncoded: mapValueOfType(json, r'isEncoded'), + isFavorite: mapValueOfType(json, r'isFavorite'), + isMotion: mapValueOfType(json, r'isMotion'), + isNotInAlbum: mapValueOfType(json, r'isNotInAlbum'), + isOffline: mapValueOfType(json, r'isOffline'), + isVisible: mapValueOfType(json, r'isVisible'), + lensModel: mapValueOfType(json, r'lensModel'), + libraryId: mapValueOfType(json, r'libraryId'), + make: mapValueOfType(json, r'make'), + model: mapValueOfType(json, r'model'), + page: num.parse('${json[r'page']}'), + personIds: json[r'personIds'] is Iterable + ? (json[r'personIds'] as Iterable).cast().toList(growable: false) + : const [], + size: num.parse('${json[r'size']}'), + state: mapValueOfType(json, r'state'), + takenAfter: mapDateTime(json, r'takenAfter', r''), + takenBefore: mapDateTime(json, r'takenBefore', r''), + trashedAfter: mapDateTime(json, r'trashedAfter', r''), + trashedBefore: mapDateTime(json, r'trashedBefore', r''), + type: AssetTypeEnum.fromJson(json[r'type']), + updatedAfter: mapDateTime(json, r'updatedAfter', r''), + updatedBefore: mapDateTime(json, r'updatedBefore', r''), + withArchived: mapValueOfType(json, r'withArchived') ?? false, + withDeleted: mapValueOfType(json, r'withDeleted'), + withExif: mapValueOfType(json, r'withExif'), + withPeople: mapValueOfType(json, r'withPeople'), + withStacked: mapValueOfType(json, r'withStacked'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = RandomSearchDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = RandomSearchDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of RandomSearchDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = RandomSearchDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index f48fa989dad57..706ff5b8fb654 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1646,6 +1646,8 @@ }, "/assets/random": { "get": { + "deprecated": true, + "description": "This property was deprecated in v1.116.0", "operationId": "getRandom", "parameters": [ { @@ -1685,8 +1687,12 @@ } ], "tags": [ - "Assets" - ] + "Assets", + "Deprecated" + ], + "x-immich-lifecycle": { + "deprecatedAt": "v1.116.0" + } } }, "/assets/statistics": { @@ -4677,6 +4683,48 @@ ] } }, + "/search/random": { + "post": { + "operationId": "searchRandom", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RandomSearchDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Search" + ] + } + }, "/search/smart": { "post": { "operationId": "searchSmart", @@ -10454,6 +10502,130 @@ ], "type": "object" }, + "RandomSearchDto": { + "properties": { + "city": { + "nullable": true, + "type": "string" + }, + "country": { + "nullable": true, + "type": "string" + }, + "createdAfter": { + "format": "date-time", + "type": "string" + }, + "createdBefore": { + "format": "date-time", + "type": "string" + }, + "deviceId": { + "type": "string" + }, + "isArchived": { + "type": "boolean" + }, + "isEncoded": { + "type": "boolean" + }, + "isFavorite": { + "type": "boolean" + }, + "isMotion": { + "type": "boolean" + }, + "isNotInAlbum": { + "type": "boolean" + }, + "isOffline": { + "type": "boolean" + }, + "isVisible": { + "type": "boolean" + }, + "lensModel": { + "nullable": true, + "type": "string" + }, + "libraryId": { + "format": "uuid", + "nullable": true, + "type": "string" + }, + "make": { + "type": "string" + }, + "model": { + "nullable": true, + "type": "string" + }, + "page": { + "minimum": 1, + "type": "number" + }, + "personIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, + "size": { + "maximum": 1000, + "minimum": 1, + "type": "number" + }, + "state": { + "nullable": true, + "type": "string" + }, + "takenAfter": { + "format": "date-time", + "type": "string" + }, + "takenBefore": { + "format": "date-time", + "type": "string" + }, + "trashedAfter": { + "format": "date-time", + "type": "string" + }, + "trashedBefore": { + "format": "date-time", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/AssetTypeEnum" + }, + "updatedAfter": { + "format": "date-time", + "type": "string" + }, + "updatedBefore": { + "format": "date-time", + "type": "string" + }, + "withArchived": { + "default": false, + "type": "boolean" + }, + "withDeleted": { + "type": "boolean" + }, + "withExif": { + "type": "boolean" + }, + "withPeople": { + "type": "boolean" + }, + "withStacked": { + "type": "boolean" + } + }, + "type": "object" + }, "RatingsResponse": { "properties": { "enabled": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index c2d73bda1acaf..8e607f7570856 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -837,6 +837,40 @@ export type PlacesResponseDto = { longitude: number; name: string; }; +export type RandomSearchDto = { + city?: string | null; + country?: string | null; + createdAfter?: string; + createdBefore?: string; + deviceId?: string; + isArchived?: boolean; + isEncoded?: boolean; + isFavorite?: boolean; + isMotion?: boolean; + isNotInAlbum?: boolean; + isOffline?: boolean; + isVisible?: boolean; + lensModel?: string | null; + libraryId?: string | null; + make?: string; + model?: string | null; + page?: number; + personIds?: string[]; + size?: number; + state?: string | null; + takenAfter?: string; + takenBefore?: string; + trashedAfter?: string; + trashedBefore?: string; + "type"?: AssetTypeEnum; + updatedAfter?: string; + updatedBefore?: string; + withArchived?: boolean; + withDeleted?: boolean; + withExif?: boolean; + withPeople?: boolean; + withStacked?: boolean; +}; export type SmartSearchDto = { city?: string | null; country?: string | null; @@ -1696,6 +1730,9 @@ export function getMemoryLane({ day, month }: { ...opts })); } +/** + * This property was deprecated in v1.116.0 + */ export function getRandom({ count }: { count?: number; }, opts?: Oazapfts.RequestOpts) { @@ -2500,6 +2537,18 @@ export function searchPlaces({ name }: { ...opts })); } +export function searchRandom({ randomSearchDto }: { + randomSearchDto: RandomSearchDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SearchResponseDto; + }>("/search/random", oazapfts.json({ + ...opts, + method: "POST", + body: randomSearchDto + }))); +} export function searchSmart({ smartSearchDto }: { smartSearchDto: SmartSearchDto; }, opts?: Oazapfts.RequestOpts) { diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index c6fdac1710edc..9d3d23065724c 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -1,5 +1,6 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +import { EndpointLifecycle } from 'src/decorators'; import { AssetResponseDto, MemoryLaneResponseDto } from 'src/dtos/asset-response.dto'; import { AssetBulkDeleteDto, @@ -31,6 +32,7 @@ export class AssetController { @Get('random') @Authenticated() + @EndpointLifecycle({ deprecatedAt: 'v1.116.0' }) getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise { return this.service.getRandom(auth, dto.count ?? 1); } diff --git a/server/src/controllers/search.controller.ts b/server/src/controllers/search.controller.ts index 5b8c1eeece026..5b6deb2981bc5 100644 --- a/server/src/controllers/search.controller.ts +++ b/server/src/controllers/search.controller.ts @@ -6,6 +6,7 @@ import { PersonResponseDto } from 'src/dtos/person.dto'; import { MetadataSearchDto, PlacesResponseDto, + RandomSearchDto, SearchExploreResponseDto, SearchPeopleDto, SearchPlacesDto, @@ -28,6 +29,13 @@ export class SearchController { return this.service.searchMetadata(auth, dto); } + @Post('random') + @HttpCode(HttpStatus.OK) + @Authenticated() + searchRandom(@Auth() auth: AuthDto, @Body() dto: RandomSearchDto): Promise { + return this.service.searchRandom(auth, dto); + } + @Post('smart') @HttpCode(HttpStatus.OK) @Authenticated() diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 9e36cfee800b8..ddc6c192c5faa 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -119,7 +119,15 @@ class BaseSearchDto { personIds?: string[]; } -export class MetadataSearchDto extends BaseSearchDto { +export class RandomSearchDto extends BaseSearchDto { + @ValidateBoolean({ optional: true }) + withStacked?: boolean; + + @ValidateBoolean({ optional: true }) + withPeople?: boolean; +} + +export class MetadataSearchDto extends RandomSearchDto { @ValidateUUID({ optional: true }) id?: string; @@ -133,12 +141,6 @@ export class MetadataSearchDto extends BaseSearchDto { @Optional() checksum?: string; - @ValidateBoolean({ optional: true }) - withStacked?: boolean; - - @ValidateBoolean({ optional: true }) - withPeople?: boolean; - @IsString() @IsNotEmpty() @Optional() diff --git a/server/src/interfaces/search.interface.ts b/server/src/interfaces/search.interface.ts index 6578d0a4830eb..0ba524c00a272 100644 --- a/server/src/interfaces/search.interface.ts +++ b/server/src/interfaces/search.interface.ts @@ -116,6 +116,7 @@ export interface SearchPeopleOptions { export interface SearchOrderOptions { orderDirection?: 'ASC' | 'DESC'; + random?: boolean; } export interface SearchPaginationOptions { diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 999e9063ef2a4..8115c72cf6ac1 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -73,8 +73,13 @@ export class SearchRepository implements ISearchRepository { async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated { let builder = this.assetRepository.createQueryBuilder('asset'); builder = searchAssetBuilder(builder, options); - builder.orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC'); + + if (options.random) { + // TODO replace with complicated SQL magic after kysely migration + builder.addSelect('RANDOM() as r').orderBy('r'); + } + return paginatedBuilder(builder, { mode: PaginationMode.SKIP_TAKE, skip: (pagination.page - 1) * pagination.size, diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index 73ace233d08af..dc6e71f345943 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -6,6 +6,7 @@ import { PersonResponseDto } from 'src/dtos/person.dto'; import { MetadataSearchDto, PlacesResponseDto, + RandomSearchDto, SearchPeopleDto, SearchPlacesDto, SearchResponseDto, @@ -93,6 +94,22 @@ export class SearchService { return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth }); } + async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise { + const userIds = await this.getUserIdsToSearch(auth); + const page = dto.page ?? 1; + const size = dto.size || 250; + const { hasNextPage, items } = await this.searchRepository.searchMetadata( + { page, size }, + { + ...dto, + userIds, + random: true, + }, + ); + + return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth }); + } + async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise { const { machineLearning } = await this.configCore.getConfig({ withCache: false }); if (!isSmartSearchEnabled(machineLearning)) { From e748945b4f3ba06c5f615ad93d20b113e6ed5ee9 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 23 Sep 2024 13:22:36 -0400 Subject: [PATCH 41/57] fix(server): gracefully handle unknown jobs (#12870) --- server/src/services/job.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index 03a6edf126e3a..5ed9f3202457b 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -186,11 +186,16 @@ export class JobService { this.jobRepository.addHandler(queueName, concurrency, async (item: JobItem): Promise => { const { name, data } = item; + const handler = jobHandlers[name]; + if (!handler) { + this.logger.warn(`Skipping unknown job: "${name}"`); + return; + } + const queueMetric = `immich.queues.${snakeCase(queueName)}.active`; this.metricRepository.jobs.addToGauge(queueMetric, 1); try { - const handler = jobHandlers[name]; const status = await handler(data); const jobMetric = `immich.jobs.${name.replaceAll('-', '_')}.${status}`; this.metricRepository.jobs.addToCounter(jobMetric, 1); From 87c54d6659a73916a2c3133966b00b5b78b4e408 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 23 Sep 2024 19:37:08 +0200 Subject: [PATCH 42/57] fix: show asset count for unassigned faces (#12871) --- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 83019d67cd869..037feaf35f6f1 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -486,17 +486,10 @@
- {#if person.name} -

{person.name}

-

- {$t('assets_count', { values: { count: numberOfAssets } })} -

- {:else} -

{$t('add_a_name')}

-

- {$t('find_them_fast')} -

- {/if} +

{person.name || $t('add_a_name')}

+

+ {$t('assets_count', { values: { count: numberOfAssets } })} +

From 3008050e4c71ea6ea2be9f0831ea19b24fd37500 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 23 Sep 2024 13:51:03 -0400 Subject: [PATCH 43/57] fix: remove no longer needed LD_LIBRARY_PATH (#12872) --- docker/hwaccel.transcoding.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/hwaccel.transcoding.yml b/docker/hwaccel.transcoding.yml index bd4e2a46b8b39..33fb7b3c06273 100644 --- a/docker/hwaccel.transcoding.yml +++ b/docker/hwaccel.transcoding.yml @@ -51,5 +51,4 @@ services: volumes: - /usr/lib/wsl:/usr/lib/wsl environment: - - LD_LIBRARY_PATH=/usr/lib/wsl/lib - LIBVA_DRIVER_NAME=d3d12 From ad33ce5938c34edc7885b4244cef83edb09e39d5 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 23 Sep 2024 15:41:41 -0400 Subject: [PATCH 44/57] refactor(mobile): open api dto upgrade (#12793) --- mobile/openapi/lib/api_client.dart | 1 - .../lib/model/activity_create_dto.dart | 1 + .../lib/model/activity_response_dto.dart | 1 + .../activity_statistics_response_dto.dart | 1 + mobile/openapi/lib/model/add_users_dto.dart | 1 + .../model/admin_onboarding_update_dto.dart | 1 + .../openapi/lib/model/album_response_dto.dart | 1 + .../model/album_statistics_response_dto.dart | 1 + .../openapi/lib/model/album_user_add_dto.dart | 1 + .../lib/model/album_user_create_dto.dart | 1 + .../lib/model/album_user_response_dto.dart | 1 + .../model/all_job_status_response_dto.dart | 1 + .../openapi/lib/model/api_key_create_dto.dart | 1 + .../model/api_key_create_response_dto.dart | 1 + .../lib/model/api_key_response_dto.dart | 1 + .../openapi/lib/model/api_key_update_dto.dart | 1 + .../lib/model/asset_bulk_delete_dto.dart | 1 + .../lib/model/asset_bulk_update_dto.dart | 1 + .../model/asset_bulk_upload_check_dto.dart | 1 + .../model/asset_bulk_upload_check_item.dart | 1 + .../asset_bulk_upload_check_response_dto.dart | 1 + .../model/asset_bulk_upload_check_result.dart | 1 + .../lib/model/asset_delta_sync_dto.dart | 1 + .../model/asset_delta_sync_response_dto.dart | 1 + .../lib/model/asset_face_response_dto.dart | 1 + .../lib/model/asset_face_update_dto.dart | 1 + .../lib/model/asset_face_update_item.dart | 1 + ...sset_face_without_person_response_dto.dart | 1 + .../lib/model/asset_full_sync_dto.dart | 1 + mobile/openapi/lib/model/asset_ids_dto.dart | 1 + .../lib/model/asset_ids_response_dto.dart | 1 + mobile/openapi/lib/model/asset_jobs_dto.dart | 1 + .../lib/model/asset_media_response_dto.dart | 1 + .../openapi/lib/model/asset_response_dto.dart | 1 + .../lib/model/asset_stack_response_dto.dart | 1 + .../lib/model/asset_stats_response_dto.dart | 1 + .../lib/model/audit_deletes_response_dto.dart | 1 + mobile/openapi/lib/model/avatar_response.dart | 1 + mobile/openapi/lib/model/avatar_update.dart | 1 + .../lib/model/bulk_id_response_dto.dart | 1 + mobile/openapi/lib/model/bulk_ids_dto.dart | 1 + .../lib/model/change_password_dto.dart | 1 + .../lib/model/check_existing_assets_dto.dart | 1 + .../check_existing_assets_response_dto.dart | 1 + mobile/openapi/lib/model/clip_config.dart | 1 + .../openapi/lib/model/create_album_dto.dart | 1 + .../openapi/lib/model/create_library_dto.dart | 1 + .../create_profile_image_response_dto.dart | 1 + .../lib/model/download_archive_info.dart | 1 + .../openapi/lib/model/download_info_dto.dart | 1 + .../openapi/lib/model/download_response.dart | 1 + .../lib/model/download_response_dto.dart | 1 + mobile/openapi/lib/model/download_update.dart | 1 + .../lib/model/duplicate_detection_config.dart | 1 + .../lib/model/duplicate_response_dto.dart | 1 + .../model/email_notifications_response.dart | 1 + .../lib/model/email_notifications_update.dart | 1 + .../openapi/lib/model/exif_response_dto.dart | 1 + mobile/openapi/lib/model/face_dto.dart | 1 + .../lib/model/facial_recognition_config.dart | 1 + .../openapi/lib/model/file_checksum_dto.dart | 1 + .../lib/model/file_checksum_response_dto.dart | 1 + mobile/openapi/lib/model/file_report_dto.dart | 1 + .../lib/model/file_report_fix_dto.dart | 1 + .../lib/model/file_report_item_dto.dart | 1 + .../openapi/lib/model/folders_response.dart | 1 + mobile/openapi/lib/model/folders_update.dart | 1 + mobile/openapi/lib/model/job_command_dto.dart | 1 + mobile/openapi/lib/model/job_counts_dto.dart | 1 + mobile/openapi/lib/model/job_create_dto.dart | 1 + .../openapi/lib/model/job_settings_dto.dart | 1 + mobile/openapi/lib/model/job_status_dto.dart | 1 + .../lib/model/library_response_dto.dart | 1 + .../lib/model/library_stats_response_dto.dart | 1 + mobile/openapi/lib/model/license_key_dto.dart | 1 + .../lib/model/license_response_dto.dart | 1 + .../lib/model/login_credential_dto.dart | 1 + .../openapi/lib/model/login_response_dto.dart | 1 + .../lib/model/logout_response_dto.dart | 1 + .../lib/model/map_marker_response_dto.dart | 1 + .../map_reverse_geocode_response_dto.dart | 1 + .../openapi/lib/model/memories_response.dart | 1 + mobile/openapi/lib/model/memories_update.dart | 1 + .../openapi/lib/model/memory_create_dto.dart | 1 + .../lib/model/memory_lane_response_dto.dart | 1 + .../lib/model/memory_response_dto.dart | 1 + .../openapi/lib/model/memory_update_dto.dart | 1 + .../openapi/lib/model/merge_person_dto.dart | 1 + .../lib/model/metadata_search_dto.dart | 1 + .../model/o_auth_authorize_response_dto.dart | 1 + .../lib/model/o_auth_callback_dto.dart | 1 + .../openapi/lib/model/o_auth_config_dto.dart | 1 + mobile/openapi/lib/model/on_this_day_dto.dart | 1 + .../lib/model/partner_response_dto.dart | 1 + mobile/openapi/lib/model/people_response.dart | 1 + .../lib/model/people_response_dto.dart | 1 + mobile/openapi/lib/model/people_update.dart | 1 + .../openapi/lib/model/people_update_dto.dart | 1 + .../openapi/lib/model/people_update_item.dart | 1 + .../openapi/lib/model/person_create_dto.dart | 1 + .../lib/model/person_response_dto.dart | 1 + .../model/person_statistics_response_dto.dart | 1 + .../openapi/lib/model/person_update_dto.dart | 1 + .../model/person_with_faces_response_dto.dart | 1 + .../lib/model/places_response_dto.dart | 1 + .../openapi/lib/model/purchase_response.dart | 1 + mobile/openapi/lib/model/purchase_update.dart | 1 + .../openapi/lib/model/queue_status_dto.dart | 1 + .../openapi/lib/model/ratings_response.dart | 1 + mobile/openapi/lib/model/ratings_update.dart | 1 + .../reverse_geocoding_state_response_dto.dart | 1 + .../openapi/lib/model/scan_library_dto.dart | 1 + .../lib/model/search_album_response_dto.dart | 1 + .../lib/model/search_asset_response_dto.dart | 1 + .../lib/model/search_explore_item.dart | 1 + .../model/search_explore_response_dto.dart | 1 + .../search_facet_count_response_dto.dart | 1 + .../lib/model/search_facet_response_dto.dart | 1 + .../lib/model/search_response_dto.dart | 1 + .../lib/model/server_about_response_dto.dart | 1 + .../openapi/lib/model/server_config_dto.dart | 1 + .../lib/model/server_features_dto.dart | 1 + .../server_media_types_response_dto.dart | 1 + .../lib/model/server_ping_response.dart | 1 + .../lib/model/server_stats_response_dto.dart | 1 + .../model/server_storage_response_dto.dart | 1 + .../openapi/lib/model/server_theme_dto.dart | 1 + .../model/server_version_response_dto.dart | 1 + .../lib/model/session_response_dto.dart | 1 + .../lib/model/shared_link_create_dto.dart | 1 + .../lib/model/shared_link_edit_dto.dart | 1 + .../lib/model/shared_link_response_dto.dart | 1 + mobile/openapi/lib/model/sign_up_dto.dart | 1 + .../lib/model/smart_info_response_dto.dart | 1 + .../openapi/lib/model/smart_search_dto.dart | 1 + .../openapi/lib/model/stack_create_dto.dart | 1 + .../openapi/lib/model/stack_response_dto.dart | 1 + .../openapi/lib/model/stack_update_dto.dart | 1 + .../openapi/lib/model/system_config_dto.dart | 1 + .../lib/model/system_config_f_fmpeg_dto.dart | 1 + .../lib/model/system_config_faces_dto.dart | 1 + .../lib/model/system_config_image_dto.dart | 1 + .../lib/model/system_config_job_dto.dart | 1 + .../lib/model/system_config_library_dto.dart | 1 + .../model/system_config_library_scan_dto.dart | 1 + .../system_config_library_watch_dto.dart | 1 + .../lib/model/system_config_logging_dto.dart | 1 + .../system_config_machine_learning_dto.dart | 1 + .../lib/model/system_config_map_dto.dart | 1 + .../lib/model/system_config_metadata_dto.dart | 1 + .../system_config_new_version_check_dto.dart | 1 + .../system_config_notifications_dto.dart | 1 + .../lib/model/system_config_o_auth_dto.dart | 1 + .../system_config_password_login_dto.dart | 1 + .../system_config_reverse_geocoding_dto.dart | 1 + .../lib/model/system_config_server_dto.dart | 1 + .../lib/model/system_config_smtp_dto.dart | 1 + .../system_config_smtp_transport_dto.dart | 1 + .../system_config_storage_template_dto.dart | 1 + ...em_config_template_storage_option_dto.dart | 1 + .../lib/model/system_config_theme_dto.dart | 1 + .../lib/model/system_config_trash_dto.dart | 1 + .../lib/model/system_config_user_dto.dart | 1 + .../lib/model/tag_bulk_assets_dto.dart | 1 + .../model/tag_bulk_assets_response_dto.dart | 1 + mobile/openapi/lib/model/tag_create_dto.dart | 1 + .../openapi/lib/model/tag_response_dto.dart | 1 + mobile/openapi/lib/model/tag_update_dto.dart | 1 + mobile/openapi/lib/model/tag_upsert_dto.dart | 1 + mobile/openapi/lib/model/tags_response.dart | 1 + mobile/openapi/lib/model/tags_update.dart | 1 + .../lib/model/time_bucket_response_dto.dart | 1 + .../openapi/lib/model/trash_response_dto.dart | 1 + .../openapi/lib/model/update_album_dto.dart | 1 + .../lib/model/update_album_user_dto.dart | 1 + .../openapi/lib/model/update_asset_dto.dart | 1 + .../openapi/lib/model/update_library_dto.dart | 1 + .../openapi/lib/model/update_partner_dto.dart | 1 + .../openapi/lib/model/usage_by_user_dto.dart | 1 + .../lib/model/user_admin_create_dto.dart | 1 + .../lib/model/user_admin_delete_dto.dart | 1 + .../lib/model/user_admin_response_dto.dart | 1 + .../lib/model/user_admin_update_dto.dart | 1 + mobile/openapi/lib/model/user_license.dart | 1 + .../model/user_preferences_response_dto.dart | 1 + .../model/user_preferences_update_dto.dart | 1 + .../openapi/lib/model/user_response_dto.dart | 1 + .../openapi/lib/model/user_update_me_dto.dart | 1 + .../validate_access_token_response_dto.dart | 1 + .../lib/model/validate_library_dto.dart | 1 + ...date_library_import_path_response_dto.dart | 1 + .../model/validate_library_response_dto.dart | 1 + open-api/bin/generate-open-api.sh | 8 +- open-api/templates/mobile/api_client.mustache | 264 ------------------ .../mobile/api_client.mustache.patch | 10 - .../native/native_class.mustache | 1 + .../native/native_class.mustache.patch | 18 +- 197 files changed, 205 insertions(+), 288 deletions(-) delete mode 100644 open-api/templates/mobile/api_client.mustache delete mode 100644 open-api/templates/mobile/api_client.mustache.patch diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 597a15d5b0562..e857f51e3a875 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -166,7 +166,6 @@ class ApiClient { /// Returns a native instance of an OpenAPI class matching the [specified type][targetType]. static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) { - upgradeDto(value, targetType); try { switch (targetType) { case 'String': diff --git a/mobile/openapi/lib/model/activity_create_dto.dart b/mobile/openapi/lib/model/activity_create_dto.dart index b54fa2ca72bce..ce4b4a01766a4 100644 --- a/mobile/openapi/lib/model/activity_create_dto.dart +++ b/mobile/openapi/lib/model/activity_create_dto.dart @@ -78,6 +78,7 @@ class ActivityCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ActivityCreateDto? fromJson(dynamic value) { + upgradeDto(value, "ActivityCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/activity_response_dto.dart b/mobile/openapi/lib/model/activity_response_dto.dart index bfffd8485b0a9..25fb0f53f8707 100644 --- a/mobile/openapi/lib/model/activity_response_dto.dart +++ b/mobile/openapi/lib/model/activity_response_dto.dart @@ -78,6 +78,7 @@ class ActivityResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ActivityResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ActivityResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/activity_statistics_response_dto.dart b/mobile/openapi/lib/model/activity_statistics_response_dto.dart index 20d4696b1b60e..ad0b814a58774 100644 --- a/mobile/openapi/lib/model/activity_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/activity_statistics_response_dto.dart @@ -40,6 +40,7 @@ class ActivityStatisticsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ActivityStatisticsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ActivityStatisticsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/add_users_dto.dart b/mobile/openapi/lib/model/add_users_dto.dart index 2daa571265da6..531c1ec785b45 100644 --- a/mobile/openapi/lib/model/add_users_dto.dart +++ b/mobile/openapi/lib/model/add_users_dto.dart @@ -40,6 +40,7 @@ class AddUsersDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AddUsersDto? fromJson(dynamic value) { + upgradeDto(value, "AddUsersDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/admin_onboarding_update_dto.dart b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart index 2277f0958c813..298bf318a292b 100644 --- a/mobile/openapi/lib/model/admin_onboarding_update_dto.dart +++ b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart @@ -40,6 +40,7 @@ class AdminOnboardingUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AdminOnboardingUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "AdminOnboardingUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index c98a95775d2c8..547a6a70fd221 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -186,6 +186,7 @@ class AlbumResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AlbumResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AlbumResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/album_statistics_response_dto.dart b/mobile/openapi/lib/model/album_statistics_response_dto.dart index 90dbe520163bb..9e19002cf18c2 100644 --- a/mobile/openapi/lib/model/album_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/album_statistics_response_dto.dart @@ -52,6 +52,7 @@ class AlbumStatisticsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AlbumStatisticsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AlbumStatisticsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/album_user_add_dto.dart b/mobile/openapi/lib/model/album_user_add_dto.dart index e654a2ff5d7b0..3f72d5c893e18 100644 --- a/mobile/openapi/lib/model/album_user_add_dto.dart +++ b/mobile/openapi/lib/model/album_user_add_dto.dart @@ -56,6 +56,7 @@ class AlbumUserAddDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AlbumUserAddDto? fromJson(dynamic value) { + upgradeDto(value, "AlbumUserAddDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/album_user_create_dto.dart b/mobile/openapi/lib/model/album_user_create_dto.dart index 708acd472beed..93a0661b30251 100644 --- a/mobile/openapi/lib/model/album_user_create_dto.dart +++ b/mobile/openapi/lib/model/album_user_create_dto.dart @@ -46,6 +46,7 @@ class AlbumUserCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AlbumUserCreateDto? fromJson(dynamic value) { + upgradeDto(value, "AlbumUserCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/album_user_response_dto.dart b/mobile/openapi/lib/model/album_user_response_dto.dart index 8f86cf254ea92..bbae03fba74c3 100644 --- a/mobile/openapi/lib/model/album_user_response_dto.dart +++ b/mobile/openapi/lib/model/album_user_response_dto.dart @@ -46,6 +46,7 @@ class AlbumUserResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AlbumUserResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AlbumUserResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/all_job_status_response_dto.dart b/mobile/openapi/lib/model/all_job_status_response_dto.dart index 1ee5253c38bde..6ec248a638e80 100644 --- a/mobile/openapi/lib/model/all_job_status_response_dto.dart +++ b/mobile/openapi/lib/model/all_job_status_response_dto.dart @@ -118,6 +118,7 @@ class AllJobStatusResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AllJobStatusResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AllJobStatusResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/api_key_create_dto.dart b/mobile/openapi/lib/model/api_key_create_dto.dart index 433855c4cfe17..848774e9c9cf7 100644 --- a/mobile/openapi/lib/model/api_key_create_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_dto.dart @@ -56,6 +56,7 @@ class APIKeyCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static APIKeyCreateDto? fromJson(dynamic value) { + upgradeDto(value, "APIKeyCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/api_key_create_response_dto.dart b/mobile/openapi/lib/model/api_key_create_response_dto.dart index 93065654ac331..cdaa70e37de71 100644 --- a/mobile/openapi/lib/model/api_key_create_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_response_dto.dart @@ -46,6 +46,7 @@ class APIKeyCreateResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static APIKeyCreateResponseDto? fromJson(dynamic value) { + upgradeDto(value, "APIKeyCreateResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/api_key_response_dto.dart b/mobile/openapi/lib/model/api_key_response_dto.dart index b6ca86c050944..fd0d91f6737e3 100644 --- a/mobile/openapi/lib/model/api_key_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_response_dto.dart @@ -64,6 +64,7 @@ class APIKeyResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static APIKeyResponseDto? fromJson(dynamic value) { + upgradeDto(value, "APIKeyResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/api_key_update_dto.dart b/mobile/openapi/lib/model/api_key_update_dto.dart index 318f4936e16b2..7295d1ea1f19b 100644 --- a/mobile/openapi/lib/model/api_key_update_dto.dart +++ b/mobile/openapi/lib/model/api_key_update_dto.dart @@ -40,6 +40,7 @@ class APIKeyUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static APIKeyUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "APIKeyUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_bulk_delete_dto.dart b/mobile/openapi/lib/model/asset_bulk_delete_dto.dart index 0f6913a7f4ebc..c4453054b1b92 100644 --- a/mobile/openapi/lib/model/asset_bulk_delete_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_delete_dto.dart @@ -56,6 +56,7 @@ class AssetBulkDeleteDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetBulkDeleteDto? fromJson(dynamic value) { + upgradeDto(value, "AssetBulkDeleteDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart index c9b21683fbcec..da23d2f09d2e0 100644 --- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart @@ -148,6 +148,7 @@ class AssetBulkUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetBulkUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "AssetBulkUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart index 55ea41b598e75..36c13bfdf6889 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart @@ -40,6 +40,7 @@ class AssetBulkUploadCheckDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetBulkUploadCheckDto? fromJson(dynamic value) { + upgradeDto(value, "AssetBulkUploadCheckDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart index 16294cdae6d06..13dfa340fad0c 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart @@ -47,6 +47,7 @@ class AssetBulkUploadCheckItem { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetBulkUploadCheckItem? fromJson(dynamic value) { + upgradeDto(value, "AssetBulkUploadCheckItem"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart index 5bfacbff570d2..8c3651e9fa189 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart @@ -40,6 +40,7 @@ class AssetBulkUploadCheckResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetBulkUploadCheckResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetBulkUploadCheckResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart index a016b357e7e6b..88e46dae7daa7 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart @@ -88,6 +88,7 @@ class AssetBulkUploadCheckResult { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetBulkUploadCheckResult? fromJson(dynamic value) { + upgradeDto(value, "AssetBulkUploadCheckResult"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_delta_sync_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_dto.dart index a5ee10f33e17e..845aadcdcd3b4 100644 --- a/mobile/openapi/lib/model/asset_delta_sync_dto.dart +++ b/mobile/openapi/lib/model/asset_delta_sync_dto.dart @@ -46,6 +46,7 @@ class AssetDeltaSyncDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetDeltaSyncDto? fromJson(dynamic value) { + upgradeDto(value, "AssetDeltaSyncDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart index 3b14fa68cf8e7..a64e1a2fbee42 100644 --- a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart +++ b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart @@ -52,6 +52,7 @@ class AssetDeltaSyncResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetDeltaSyncResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetDeltaSyncResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_face_response_dto.dart b/mobile/openapi/lib/model/asset_face_response_dto.dart index 7a8588ce5c4af..c05b511649236 100644 --- a/mobile/openapi/lib/model/asset_face_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_response_dto.dart @@ -102,6 +102,7 @@ class AssetFaceResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetFaceResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetFaceResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_face_update_dto.dart b/mobile/openapi/lib/model/asset_face_update_dto.dart index 58def49ae1ae4..71bdde8e9a6a3 100644 --- a/mobile/openapi/lib/model/asset_face_update_dto.dart +++ b/mobile/openapi/lib/model/asset_face_update_dto.dart @@ -40,6 +40,7 @@ class AssetFaceUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetFaceUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "AssetFaceUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_face_update_item.dart b/mobile/openapi/lib/model/asset_face_update_item.dart index 5ea37ea4db5f5..c2c48032595dc 100644 --- a/mobile/openapi/lib/model/asset_face_update_item.dart +++ b/mobile/openapi/lib/model/asset_face_update_item.dart @@ -46,6 +46,7 @@ class AssetFaceUpdateItem { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetFaceUpdateItem? fromJson(dynamic value) { + upgradeDto(value, "AssetFaceUpdateItem"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart index ecfe06bd7d6ce..8bf07e15347ca 100644 --- a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart @@ -92,6 +92,7 @@ class AssetFaceWithoutPersonResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetFaceWithoutPersonResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetFaceWithoutPersonResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_full_sync_dto.dart b/mobile/openapi/lib/model/asset_full_sync_dto.dart index e80638f6b0869..7151094b9588c 100644 --- a/mobile/openapi/lib/model/asset_full_sync_dto.dart +++ b/mobile/openapi/lib/model/asset_full_sync_dto.dart @@ -79,6 +79,7 @@ class AssetFullSyncDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetFullSyncDto? fromJson(dynamic value) { + upgradeDto(value, "AssetFullSyncDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_ids_dto.dart b/mobile/openapi/lib/model/asset_ids_dto.dart index c8c7a69b8907c..b44888f3962b3 100644 --- a/mobile/openapi/lib/model/asset_ids_dto.dart +++ b/mobile/openapi/lib/model/asset_ids_dto.dart @@ -40,6 +40,7 @@ class AssetIdsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetIdsDto? fromJson(dynamic value) { + upgradeDto(value, "AssetIdsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_ids_response_dto.dart b/mobile/openapi/lib/model/asset_ids_response_dto.dart index a642c0924cba8..ff63091caa577 100644 --- a/mobile/openapi/lib/model/asset_ids_response_dto.dart +++ b/mobile/openapi/lib/model/asset_ids_response_dto.dart @@ -56,6 +56,7 @@ class AssetIdsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetIdsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetIdsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_jobs_dto.dart b/mobile/openapi/lib/model/asset_jobs_dto.dart index 16ed2644fd6cd..0f8bfab009fa1 100644 --- a/mobile/openapi/lib/model/asset_jobs_dto.dart +++ b/mobile/openapi/lib/model/asset_jobs_dto.dart @@ -46,6 +46,7 @@ class AssetJobsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetJobsDto? fromJson(dynamic value) { + upgradeDto(value, "AssetJobsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_media_response_dto.dart b/mobile/openapi/lib/model/asset_media_response_dto.dart index c2801c93cce55..75428ec5f61d8 100644 --- a/mobile/openapi/lib/model/asset_media_response_dto.dart +++ b/mobile/openapi/lib/model/asset_media_response_dto.dart @@ -46,6 +46,7 @@ class AssetMediaResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetMediaResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetMediaResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index bfb461efdc4c5..c11dedcbfd230 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -293,6 +293,7 @@ class AssetResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_stack_response_dto.dart b/mobile/openapi/lib/model/asset_stack_response_dto.dart index 89d30f7810682..bb4becb129c17 100644 --- a/mobile/openapi/lib/model/asset_stack_response_dto.dart +++ b/mobile/openapi/lib/model/asset_stack_response_dto.dart @@ -52,6 +52,7 @@ class AssetStackResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetStackResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetStackResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_stats_response_dto.dart b/mobile/openapi/lib/model/asset_stats_response_dto.dart index c21d7fdbffa4c..d11ce55a5cc5c 100644 --- a/mobile/openapi/lib/model/asset_stats_response_dto.dart +++ b/mobile/openapi/lib/model/asset_stats_response_dto.dart @@ -52,6 +52,7 @@ class AssetStatsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetStatsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetStatsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/audit_deletes_response_dto.dart b/mobile/openapi/lib/model/audit_deletes_response_dto.dart index 690a52e811466..6b1df74eb4d79 100644 --- a/mobile/openapi/lib/model/audit_deletes_response_dto.dart +++ b/mobile/openapi/lib/model/audit_deletes_response_dto.dart @@ -46,6 +46,7 @@ class AuditDeletesResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AuditDeletesResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AuditDeletesResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/avatar_response.dart b/mobile/openapi/lib/model/avatar_response.dart index edd242df4e3be..8ce0287565f2d 100644 --- a/mobile/openapi/lib/model/avatar_response.dart +++ b/mobile/openapi/lib/model/avatar_response.dart @@ -40,6 +40,7 @@ class AvatarResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AvatarResponse? fromJson(dynamic value) { + upgradeDto(value, "AvatarResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/avatar_update.dart b/mobile/openapi/lib/model/avatar_update.dart index b92eb8dcbdb2d..875eb138a8dbf 100644 --- a/mobile/openapi/lib/model/avatar_update.dart +++ b/mobile/openapi/lib/model/avatar_update.dart @@ -50,6 +50,7 @@ class AvatarUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AvatarUpdate? fromJson(dynamic value) { + upgradeDto(value, "AvatarUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/bulk_id_response_dto.dart b/mobile/openapi/lib/model/bulk_id_response_dto.dart index ef3cf2e0dbe00..67a587e8d001e 100644 --- a/mobile/openapi/lib/model/bulk_id_response_dto.dart +++ b/mobile/openapi/lib/model/bulk_id_response_dto.dart @@ -56,6 +56,7 @@ class BulkIdResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static BulkIdResponseDto? fromJson(dynamic value) { + upgradeDto(value, "BulkIdResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/bulk_ids_dto.dart b/mobile/openapi/lib/model/bulk_ids_dto.dart index 6942875f0a2e6..6a7f8ceeec6f6 100644 --- a/mobile/openapi/lib/model/bulk_ids_dto.dart +++ b/mobile/openapi/lib/model/bulk_ids_dto.dart @@ -40,6 +40,7 @@ class BulkIdsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static BulkIdsDto? fromJson(dynamic value) { + upgradeDto(value, "BulkIdsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/change_password_dto.dart b/mobile/openapi/lib/model/change_password_dto.dart index 1074aaf74d4f8..33b7f4a607390 100644 --- a/mobile/openapi/lib/model/change_password_dto.dart +++ b/mobile/openapi/lib/model/change_password_dto.dart @@ -46,6 +46,7 @@ class ChangePasswordDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ChangePasswordDto? fromJson(dynamic value) { + upgradeDto(value, "ChangePasswordDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/check_existing_assets_dto.dart b/mobile/openapi/lib/model/check_existing_assets_dto.dart index 49ef36cc093e0..42ce6d5c3ea0c 100644 --- a/mobile/openapi/lib/model/check_existing_assets_dto.dart +++ b/mobile/openapi/lib/model/check_existing_assets_dto.dart @@ -46,6 +46,7 @@ class CheckExistingAssetsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static CheckExistingAssetsDto? fromJson(dynamic value) { + upgradeDto(value, "CheckExistingAssetsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart b/mobile/openapi/lib/model/check_existing_assets_response_dto.dart index d8b0f43a6d0e1..ad93578ebc34c 100644 --- a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart +++ b/mobile/openapi/lib/model/check_existing_assets_response_dto.dart @@ -40,6 +40,7 @@ class CheckExistingAssetsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static CheckExistingAssetsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "CheckExistingAssetsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/clip_config.dart b/mobile/openapi/lib/model/clip_config.dart index 6e95c15fbfbe2..b500d20f2e6eb 100644 --- a/mobile/openapi/lib/model/clip_config.dart +++ b/mobile/openapi/lib/model/clip_config.dart @@ -46,6 +46,7 @@ class CLIPConfig { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static CLIPConfig? fromJson(dynamic value) { + upgradeDto(value, "CLIPConfig"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/create_album_dto.dart b/mobile/openapi/lib/model/create_album_dto.dart index fa28b782acee9..ff8c1df647fdc 100644 --- a/mobile/openapi/lib/model/create_album_dto.dart +++ b/mobile/openapi/lib/model/create_album_dto.dart @@ -68,6 +68,7 @@ class CreateAlbumDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static CreateAlbumDto? fromJson(dynamic value) { + upgradeDto(value, "CreateAlbumDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart index 65ceec8e8a4f4..bffa5f427950d 100644 --- a/mobile/openapi/lib/model/create_library_dto.dart +++ b/mobile/openapi/lib/model/create_library_dto.dart @@ -68,6 +68,7 @@ class CreateLibraryDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static CreateLibraryDto? fromJson(dynamic value) { + upgradeDto(value, "CreateLibraryDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/create_profile_image_response_dto.dart b/mobile/openapi/lib/model/create_profile_image_response_dto.dart index 86624ed06bf46..ee98142e86097 100644 --- a/mobile/openapi/lib/model/create_profile_image_response_dto.dart +++ b/mobile/openapi/lib/model/create_profile_image_response_dto.dart @@ -52,6 +52,7 @@ class CreateProfileImageResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static CreateProfileImageResponseDto? fromJson(dynamic value) { + upgradeDto(value, "CreateProfileImageResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/download_archive_info.dart b/mobile/openapi/lib/model/download_archive_info.dart index e324850bdcf0b..5f3fd1a8c1f3d 100644 --- a/mobile/openapi/lib/model/download_archive_info.dart +++ b/mobile/openapi/lib/model/download_archive_info.dart @@ -46,6 +46,7 @@ class DownloadArchiveInfo { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static DownloadArchiveInfo? fromJson(dynamic value) { + upgradeDto(value, "DownloadArchiveInfo"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/download_info_dto.dart b/mobile/openapi/lib/model/download_info_dto.dart index 4c387690102a7..6f4777975c6b2 100644 --- a/mobile/openapi/lib/model/download_info_dto.dart +++ b/mobile/openapi/lib/model/download_info_dto.dart @@ -89,6 +89,7 @@ class DownloadInfoDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static DownloadInfoDto? fromJson(dynamic value) { + upgradeDto(value, "DownloadInfoDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/download_response.dart b/mobile/openapi/lib/model/download_response.dart index 25c5159a8b655..041da44b718bc 100644 --- a/mobile/openapi/lib/model/download_response.dart +++ b/mobile/openapi/lib/model/download_response.dart @@ -46,6 +46,7 @@ class DownloadResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static DownloadResponse? fromJson(dynamic value) { + upgradeDto(value, "DownloadResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/download_response_dto.dart b/mobile/openapi/lib/model/download_response_dto.dart index f32cba92537ac..5c6bd112661e8 100644 --- a/mobile/openapi/lib/model/download_response_dto.dart +++ b/mobile/openapi/lib/model/download_response_dto.dart @@ -46,6 +46,7 @@ class DownloadResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static DownloadResponseDto? fromJson(dynamic value) { + upgradeDto(value, "DownloadResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/download_update.dart b/mobile/openapi/lib/model/download_update.dart index 2c3839a6878dc..8df825a922315 100644 --- a/mobile/openapi/lib/model/download_update.dart +++ b/mobile/openapi/lib/model/download_update.dart @@ -67,6 +67,7 @@ class DownloadUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static DownloadUpdate? fromJson(dynamic value) { + upgradeDto(value, "DownloadUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/duplicate_detection_config.dart b/mobile/openapi/lib/model/duplicate_detection_config.dart index 0bc60917848c4..e4fc352028ec0 100644 --- a/mobile/openapi/lib/model/duplicate_detection_config.dart +++ b/mobile/openapi/lib/model/duplicate_detection_config.dart @@ -48,6 +48,7 @@ class DuplicateDetectionConfig { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static DuplicateDetectionConfig? fromJson(dynamic value) { + upgradeDto(value, "DuplicateDetectionConfig"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/duplicate_response_dto.dart b/mobile/openapi/lib/model/duplicate_response_dto.dart index b93ecfe5f5775..6ac7c468711e0 100644 --- a/mobile/openapi/lib/model/duplicate_response_dto.dart +++ b/mobile/openapi/lib/model/duplicate_response_dto.dart @@ -46,6 +46,7 @@ class DuplicateResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static DuplicateResponseDto? fromJson(dynamic value) { + upgradeDto(value, "DuplicateResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/email_notifications_response.dart b/mobile/openapi/lib/model/email_notifications_response.dart index cef92957c6d78..d6dcfb9273be6 100644 --- a/mobile/openapi/lib/model/email_notifications_response.dart +++ b/mobile/openapi/lib/model/email_notifications_response.dart @@ -52,6 +52,7 @@ class EmailNotificationsResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static EmailNotificationsResponse? fromJson(dynamic value) { + upgradeDto(value, "EmailNotificationsResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/email_notifications_update.dart b/mobile/openapi/lib/model/email_notifications_update.dart index dcd1ec432206d..dad0a52fdef28 100644 --- a/mobile/openapi/lib/model/email_notifications_update.dart +++ b/mobile/openapi/lib/model/email_notifications_update.dart @@ -82,6 +82,7 @@ class EmailNotificationsUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static EmailNotificationsUpdate? fromJson(dynamic value) { + upgradeDto(value, "EmailNotificationsUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/exif_response_dto.dart b/mobile/openapi/lib/model/exif_response_dto.dart index 0185f300fac5b..17397b20815e0 100644 --- a/mobile/openapi/lib/model/exif_response_dto.dart +++ b/mobile/openapi/lib/model/exif_response_dto.dart @@ -254,6 +254,7 @@ class ExifResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ExifResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ExifResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/face_dto.dart b/mobile/openapi/lib/model/face_dto.dart index 4fcc86debf22e..c84a518b8c784 100644 --- a/mobile/openapi/lib/model/face_dto.dart +++ b/mobile/openapi/lib/model/face_dto.dart @@ -40,6 +40,7 @@ class FaceDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FaceDto? fromJson(dynamic value) { + upgradeDto(value, "FaceDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/facial_recognition_config.dart b/mobile/openapi/lib/model/facial_recognition_config.dart index 52400fd7e135d..4acfd4e20ff17 100644 --- a/mobile/openapi/lib/model/facial_recognition_config.dart +++ b/mobile/openapi/lib/model/facial_recognition_config.dart @@ -69,6 +69,7 @@ class FacialRecognitionConfig { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FacialRecognitionConfig? fromJson(dynamic value) { + upgradeDto(value, "FacialRecognitionConfig"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/file_checksum_dto.dart b/mobile/openapi/lib/model/file_checksum_dto.dart index c7e8aa1da60b3..7dc9ccdf2f93e 100644 --- a/mobile/openapi/lib/model/file_checksum_dto.dart +++ b/mobile/openapi/lib/model/file_checksum_dto.dart @@ -40,6 +40,7 @@ class FileChecksumDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FileChecksumDto? fromJson(dynamic value) { + upgradeDto(value, "FileChecksumDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/file_checksum_response_dto.dart b/mobile/openapi/lib/model/file_checksum_response_dto.dart index d4bae3c273ddc..7b963c8bd539e 100644 --- a/mobile/openapi/lib/model/file_checksum_response_dto.dart +++ b/mobile/openapi/lib/model/file_checksum_response_dto.dart @@ -46,6 +46,7 @@ class FileChecksumResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FileChecksumResponseDto? fromJson(dynamic value) { + upgradeDto(value, "FileChecksumResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/file_report_dto.dart b/mobile/openapi/lib/model/file_report_dto.dart index 422215ff6c63e..3dc892e5e7f78 100644 --- a/mobile/openapi/lib/model/file_report_dto.dart +++ b/mobile/openapi/lib/model/file_report_dto.dart @@ -46,6 +46,7 @@ class FileReportDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FileReportDto? fromJson(dynamic value) { + upgradeDto(value, "FileReportDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/file_report_fix_dto.dart b/mobile/openapi/lib/model/file_report_fix_dto.dart index cf09242b0fa15..d46cdeb4b784b 100644 --- a/mobile/openapi/lib/model/file_report_fix_dto.dart +++ b/mobile/openapi/lib/model/file_report_fix_dto.dart @@ -40,6 +40,7 @@ class FileReportFixDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FileReportFixDto? fromJson(dynamic value) { + upgradeDto(value, "FileReportFixDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/file_report_item_dto.dart b/mobile/openapi/lib/model/file_report_item_dto.dart index 5255005daaaf1..1ef08c2b48511 100644 --- a/mobile/openapi/lib/model/file_report_item_dto.dart +++ b/mobile/openapi/lib/model/file_report_item_dto.dart @@ -74,6 +74,7 @@ class FileReportItemDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FileReportItemDto? fromJson(dynamic value) { + upgradeDto(value, "FileReportItemDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/folders_response.dart b/mobile/openapi/lib/model/folders_response.dart index 5bfc4c793deed..248b64b054c83 100644 --- a/mobile/openapi/lib/model/folders_response.dart +++ b/mobile/openapi/lib/model/folders_response.dart @@ -46,6 +46,7 @@ class FoldersResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FoldersResponse? fromJson(dynamic value) { + upgradeDto(value, "FoldersResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/folders_update.dart b/mobile/openapi/lib/model/folders_update.dart index 088c98a4d8fd2..02347177545d0 100644 --- a/mobile/openapi/lib/model/folders_update.dart +++ b/mobile/openapi/lib/model/folders_update.dart @@ -66,6 +66,7 @@ class FoldersUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FoldersUpdate? fromJson(dynamic value) { + upgradeDto(value, "FoldersUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/job_command_dto.dart b/mobile/openapi/lib/model/job_command_dto.dart index 5c56715644f22..649e0128a7a97 100644 --- a/mobile/openapi/lib/model/job_command_dto.dart +++ b/mobile/openapi/lib/model/job_command_dto.dart @@ -46,6 +46,7 @@ class JobCommandDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static JobCommandDto? fromJson(dynamic value) { + upgradeDto(value, "JobCommandDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/job_counts_dto.dart b/mobile/openapi/lib/model/job_counts_dto.dart index cf1d0b457d821..afc90d108467d 100644 --- a/mobile/openapi/lib/model/job_counts_dto.dart +++ b/mobile/openapi/lib/model/job_counts_dto.dart @@ -70,6 +70,7 @@ class JobCountsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static JobCountsDto? fromJson(dynamic value) { + upgradeDto(value, "JobCountsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/job_create_dto.dart b/mobile/openapi/lib/model/job_create_dto.dart index a4734791bbced..fe6743cba09d9 100644 --- a/mobile/openapi/lib/model/job_create_dto.dart +++ b/mobile/openapi/lib/model/job_create_dto.dart @@ -40,6 +40,7 @@ class JobCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static JobCreateDto? fromJson(dynamic value) { + upgradeDto(value, "JobCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/job_settings_dto.dart b/mobile/openapi/lib/model/job_settings_dto.dart index 9c59d503cac85..af354bef9e953 100644 --- a/mobile/openapi/lib/model/job_settings_dto.dart +++ b/mobile/openapi/lib/model/job_settings_dto.dart @@ -41,6 +41,7 @@ class JobSettingsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static JobSettingsDto? fromJson(dynamic value) { + upgradeDto(value, "JobSettingsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/job_status_dto.dart b/mobile/openapi/lib/model/job_status_dto.dart index fd925bd53a5fc..18fab8dfb3fe2 100644 --- a/mobile/openapi/lib/model/job_status_dto.dart +++ b/mobile/openapi/lib/model/job_status_dto.dart @@ -46,6 +46,7 @@ class JobStatusDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static JobStatusDto? fromJson(dynamic value) { + upgradeDto(value, "JobStatusDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/library_response_dto.dart b/mobile/openapi/lib/model/library_response_dto.dart index e27b48910439c..3cf12485080ac 100644 --- a/mobile/openapi/lib/model/library_response_dto.dart +++ b/mobile/openapi/lib/model/library_response_dto.dart @@ -92,6 +92,7 @@ class LibraryResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static LibraryResponseDto? fromJson(dynamic value) { + upgradeDto(value, "LibraryResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/library_stats_response_dto.dart b/mobile/openapi/lib/model/library_stats_response_dto.dart index 8cfb292855104..afe67da31a251 100644 --- a/mobile/openapi/lib/model/library_stats_response_dto.dart +++ b/mobile/openapi/lib/model/library_stats_response_dto.dart @@ -58,6 +58,7 @@ class LibraryStatsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static LibraryStatsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "LibraryStatsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/license_key_dto.dart b/mobile/openapi/lib/model/license_key_dto.dart index aece85f81e9a0..d27d579bb4831 100644 --- a/mobile/openapi/lib/model/license_key_dto.dart +++ b/mobile/openapi/lib/model/license_key_dto.dart @@ -46,6 +46,7 @@ class LicenseKeyDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static LicenseKeyDto? fromJson(dynamic value) { + upgradeDto(value, "LicenseKeyDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/license_response_dto.dart b/mobile/openapi/lib/model/license_response_dto.dart index f83668af575c9..6d3009433fd80 100644 --- a/mobile/openapi/lib/model/license_response_dto.dart +++ b/mobile/openapi/lib/model/license_response_dto.dart @@ -52,6 +52,7 @@ class LicenseResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static LicenseResponseDto? fromJson(dynamic value) { + upgradeDto(value, "LicenseResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/login_credential_dto.dart b/mobile/openapi/lib/model/login_credential_dto.dart index ac2f5116916f2..7e892ab5fbcb6 100644 --- a/mobile/openapi/lib/model/login_credential_dto.dart +++ b/mobile/openapi/lib/model/login_credential_dto.dart @@ -46,6 +46,7 @@ class LoginCredentialDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static LoginCredentialDto? fromJson(dynamic value) { + upgradeDto(value, "LoginCredentialDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/login_response_dto.dart b/mobile/openapi/lib/model/login_response_dto.dart index 6a0eb2355ce55..dbc82d07ba1bb 100644 --- a/mobile/openapi/lib/model/login_response_dto.dart +++ b/mobile/openapi/lib/model/login_response_dto.dart @@ -76,6 +76,7 @@ class LoginResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static LoginResponseDto? fromJson(dynamic value) { + upgradeDto(value, "LoginResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/logout_response_dto.dart b/mobile/openapi/lib/model/logout_response_dto.dart index ca1e8d23bbf32..aa94904e2a7df 100644 --- a/mobile/openapi/lib/model/logout_response_dto.dart +++ b/mobile/openapi/lib/model/logout_response_dto.dart @@ -46,6 +46,7 @@ class LogoutResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static LogoutResponseDto? fromJson(dynamic value) { + upgradeDto(value, "LogoutResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/map_marker_response_dto.dart b/mobile/openapi/lib/model/map_marker_response_dto.dart index ca1ec3c8a1ced..74ac51a271478 100644 --- a/mobile/openapi/lib/model/map_marker_response_dto.dart +++ b/mobile/openapi/lib/model/map_marker_response_dto.dart @@ -82,6 +82,7 @@ class MapMarkerResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MapMarkerResponseDto? fromJson(dynamic value) { + upgradeDto(value, "MapMarkerResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart b/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart index ac99dd91a9915..6d8757d39ff61 100644 --- a/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart +++ b/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart @@ -64,6 +64,7 @@ class MapReverseGeocodeResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MapReverseGeocodeResponseDto? fromJson(dynamic value) { + upgradeDto(value, "MapReverseGeocodeResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/memories_response.dart b/mobile/openapi/lib/model/memories_response.dart index e215a66a03f67..b9f8b5d8b1862 100644 --- a/mobile/openapi/lib/model/memories_response.dart +++ b/mobile/openapi/lib/model/memories_response.dart @@ -40,6 +40,7 @@ class MemoriesResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MemoriesResponse? fromJson(dynamic value) { + upgradeDto(value, "MemoriesResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/memories_update.dart b/mobile/openapi/lib/model/memories_update.dart index d30949136197e..71efd71ae7866 100644 --- a/mobile/openapi/lib/model/memories_update.dart +++ b/mobile/openapi/lib/model/memories_update.dart @@ -50,6 +50,7 @@ class MemoriesUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MemoriesUpdate? fromJson(dynamic value) { + upgradeDto(value, "MemoriesUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/memory_create_dto.dart b/mobile/openapi/lib/model/memory_create_dto.dart index 2efdf88936093..15985f2f1c175 100644 --- a/mobile/openapi/lib/model/memory_create_dto.dart +++ b/mobile/openapi/lib/model/memory_create_dto.dart @@ -90,6 +90,7 @@ class MemoryCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MemoryCreateDto? fromJson(dynamic value) { + upgradeDto(value, "MemoryCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/memory_lane_response_dto.dart b/mobile/openapi/lib/model/memory_lane_response_dto.dart index 4abe607381f35..27248d05c1f64 100644 --- a/mobile/openapi/lib/model/memory_lane_response_dto.dart +++ b/mobile/openapi/lib/model/memory_lane_response_dto.dart @@ -46,6 +46,7 @@ class MemoryLaneResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MemoryLaneResponseDto? fromJson(dynamic value) { + upgradeDto(value, "MemoryLaneResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/memory_response_dto.dart b/mobile/openapi/lib/model/memory_response_dto.dart index f794be53cd5d3..652c993536aa4 100644 --- a/mobile/openapi/lib/model/memory_response_dto.dart +++ b/mobile/openapi/lib/model/memory_response_dto.dart @@ -120,6 +120,7 @@ class MemoryResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MemoryResponseDto? fromJson(dynamic value) { + upgradeDto(value, "MemoryResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/memory_update_dto.dart b/mobile/openapi/lib/model/memory_update_dto.dart index 318f4b42add00..e750f9faad330 100644 --- a/mobile/openapi/lib/model/memory_update_dto.dart +++ b/mobile/openapi/lib/model/memory_update_dto.dart @@ -82,6 +82,7 @@ class MemoryUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MemoryUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "MemoryUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/merge_person_dto.dart b/mobile/openapi/lib/model/merge_person_dto.dart index ea23042e2c5e0..fd225276b6f0a 100644 --- a/mobile/openapi/lib/model/merge_person_dto.dart +++ b/mobile/openapi/lib/model/merge_person_dto.dart @@ -40,6 +40,7 @@ class MergePersonDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MergePersonDto? fromJson(dynamic value) { + upgradeDto(value, "MergePersonDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index fabf7a26107ec..0aef1f623efd0 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -637,6 +637,7 @@ class MetadataSearchDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MetadataSearchDto? fromJson(dynamic value) { + upgradeDto(value, "MetadataSearchDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart b/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart index ffd017f8168d6..869c3be753f70 100644 --- a/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart +++ b/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart @@ -40,6 +40,7 @@ class OAuthAuthorizeResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static OAuthAuthorizeResponseDto? fromJson(dynamic value) { + upgradeDto(value, "OAuthAuthorizeResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/o_auth_callback_dto.dart b/mobile/openapi/lib/model/o_auth_callback_dto.dart index 89ad0f60b0b09..d0b98d5c6f503 100644 --- a/mobile/openapi/lib/model/o_auth_callback_dto.dart +++ b/mobile/openapi/lib/model/o_auth_callback_dto.dart @@ -40,6 +40,7 @@ class OAuthCallbackDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static OAuthCallbackDto? fromJson(dynamic value) { + upgradeDto(value, "OAuthCallbackDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/o_auth_config_dto.dart b/mobile/openapi/lib/model/o_auth_config_dto.dart index 7d7675886497b..86c79b4e04ff6 100644 --- a/mobile/openapi/lib/model/o_auth_config_dto.dart +++ b/mobile/openapi/lib/model/o_auth_config_dto.dart @@ -40,6 +40,7 @@ class OAuthConfigDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static OAuthConfigDto? fromJson(dynamic value) { + upgradeDto(value, "OAuthConfigDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/on_this_day_dto.dart b/mobile/openapi/lib/model/on_this_day_dto.dart index be170caf853a9..bfcc4fd630589 100644 --- a/mobile/openapi/lib/model/on_this_day_dto.dart +++ b/mobile/openapi/lib/model/on_this_day_dto.dart @@ -41,6 +41,7 @@ class OnThisDayDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static OnThisDayDto? fromJson(dynamic value) { + upgradeDto(value, "OnThisDayDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart index 375303c94a0ec..f61df86b42bf4 100644 --- a/mobile/openapi/lib/model/partner_response_dto.dart +++ b/mobile/openapi/lib/model/partner_response_dto.dart @@ -86,6 +86,7 @@ class PartnerResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PartnerResponseDto? fromJson(dynamic value) { + upgradeDto(value, "PartnerResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/people_response.dart b/mobile/openapi/lib/model/people_response.dart index e12f86eeab5ba..1312c738744ef 100644 --- a/mobile/openapi/lib/model/people_response.dart +++ b/mobile/openapi/lib/model/people_response.dart @@ -46,6 +46,7 @@ class PeopleResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PeopleResponse? fromJson(dynamic value) { + upgradeDto(value, "PeopleResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/people_response_dto.dart b/mobile/openapi/lib/model/people_response_dto.dart index 87e8c34fb0d7d..49f0e85aad421 100644 --- a/mobile/openapi/lib/model/people_response_dto.dart +++ b/mobile/openapi/lib/model/people_response_dto.dart @@ -69,6 +69,7 @@ class PeopleResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PeopleResponseDto? fromJson(dynamic value) { + upgradeDto(value, "PeopleResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/people_update.dart b/mobile/openapi/lib/model/people_update.dart index 7803e6297036a..fb4eeeb434fda 100644 --- a/mobile/openapi/lib/model/people_update.dart +++ b/mobile/openapi/lib/model/people_update.dart @@ -66,6 +66,7 @@ class PeopleUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PeopleUpdate? fromJson(dynamic value) { + upgradeDto(value, "PeopleUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/people_update_dto.dart b/mobile/openapi/lib/model/people_update_dto.dart index 9fcfdc8761018..f771084f75c63 100644 --- a/mobile/openapi/lib/model/people_update_dto.dart +++ b/mobile/openapi/lib/model/people_update_dto.dart @@ -40,6 +40,7 @@ class PeopleUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PeopleUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "PeopleUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/people_update_item.dart b/mobile/openapi/lib/model/people_update_item.dart index 8af0a8b11ab8d..042e4fa36f969 100644 --- a/mobile/openapi/lib/model/people_update_item.dart +++ b/mobile/openapi/lib/model/people_update_item.dart @@ -103,6 +103,7 @@ class PeopleUpdateItem { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PeopleUpdateItem? fromJson(dynamic value) { + upgradeDto(value, "PeopleUpdateItem"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/person_create_dto.dart b/mobile/openapi/lib/model/person_create_dto.dart index 9889328dee639..36bd6dfee9072 100644 --- a/mobile/openapi/lib/model/person_create_dto.dart +++ b/mobile/openapi/lib/model/person_create_dto.dart @@ -79,6 +79,7 @@ class PersonCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PersonCreateDto? fromJson(dynamic value) { + upgradeDto(value, "PersonCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/person_response_dto.dart b/mobile/openapi/lib/model/person_response_dto.dart index 50ee28f0af5fe..0b36fcde3b271 100644 --- a/mobile/openapi/lib/model/person_response_dto.dart +++ b/mobile/openapi/lib/model/person_response_dto.dart @@ -85,6 +85,7 @@ class PersonResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PersonResponseDto? fromJson(dynamic value) { + upgradeDto(value, "PersonResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/person_statistics_response_dto.dart b/mobile/openapi/lib/model/person_statistics_response_dto.dart index 929fbc29d2585..d9f84e9f4c226 100644 --- a/mobile/openapi/lib/model/person_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/person_statistics_response_dto.dart @@ -40,6 +40,7 @@ class PersonStatisticsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PersonStatisticsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "PersonStatisticsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/person_update_dto.dart b/mobile/openapi/lib/model/person_update_dto.dart index 1af03890a2aa3..51a7ea25d07b3 100644 --- a/mobile/openapi/lib/model/person_update_dto.dart +++ b/mobile/openapi/lib/model/person_update_dto.dart @@ -96,6 +96,7 @@ class PersonUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PersonUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "PersonUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/person_with_faces_response_dto.dart b/mobile/openapi/lib/model/person_with_faces_response_dto.dart index af2e7101c3477..b14bad789505b 100644 --- a/mobile/openapi/lib/model/person_with_faces_response_dto.dart +++ b/mobile/openapi/lib/model/person_with_faces_response_dto.dart @@ -91,6 +91,7 @@ class PersonWithFacesResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PersonWithFacesResponseDto? fromJson(dynamic value) { + upgradeDto(value, "PersonWithFacesResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/places_response_dto.dart b/mobile/openapi/lib/model/places_response_dto.dart index d3e1fc449bc5e..4f77788263450 100644 --- a/mobile/openapi/lib/model/places_response_dto.dart +++ b/mobile/openapi/lib/model/places_response_dto.dart @@ -84,6 +84,7 @@ class PlacesResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PlacesResponseDto? fromJson(dynamic value) { + upgradeDto(value, "PlacesResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/purchase_response.dart b/mobile/openapi/lib/model/purchase_response.dart index 284d8995289ec..a1172069771ea 100644 --- a/mobile/openapi/lib/model/purchase_response.dart +++ b/mobile/openapi/lib/model/purchase_response.dart @@ -46,6 +46,7 @@ class PurchaseResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PurchaseResponse? fromJson(dynamic value) { + upgradeDto(value, "PurchaseResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/purchase_update.dart b/mobile/openapi/lib/model/purchase_update.dart index ca0a27e3bc4ba..69057e6c55a46 100644 --- a/mobile/openapi/lib/model/purchase_update.dart +++ b/mobile/openapi/lib/model/purchase_update.dart @@ -66,6 +66,7 @@ class PurchaseUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PurchaseUpdate? fromJson(dynamic value) { + upgradeDto(value, "PurchaseUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/queue_status_dto.dart b/mobile/openapi/lib/model/queue_status_dto.dart index 7f7d310f6ff07..77591affe2f3d 100644 --- a/mobile/openapi/lib/model/queue_status_dto.dart +++ b/mobile/openapi/lib/model/queue_status_dto.dart @@ -46,6 +46,7 @@ class QueueStatusDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static QueueStatusDto? fromJson(dynamic value) { + upgradeDto(value, "QueueStatusDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/ratings_response.dart b/mobile/openapi/lib/model/ratings_response.dart index c8791aa91a5ee..8e1951277ae80 100644 --- a/mobile/openapi/lib/model/ratings_response.dart +++ b/mobile/openapi/lib/model/ratings_response.dart @@ -40,6 +40,7 @@ class RatingsResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static RatingsResponse? fromJson(dynamic value) { + upgradeDto(value, "RatingsResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/ratings_update.dart b/mobile/openapi/lib/model/ratings_update.dart index bde51bad1b360..5d9f9a655f0ad 100644 --- a/mobile/openapi/lib/model/ratings_update.dart +++ b/mobile/openapi/lib/model/ratings_update.dart @@ -50,6 +50,7 @@ class RatingsUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static RatingsUpdate? fromJson(dynamic value) { + upgradeDto(value, "RatingsUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart index eb414be984015..5b3648b46bb2a 100644 --- a/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart +++ b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart @@ -54,6 +54,7 @@ class ReverseGeocodingStateResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ReverseGeocodingStateResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ReverseGeocodingStateResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/scan_library_dto.dart b/mobile/openapi/lib/model/scan_library_dto.dart index 1b31aaaf01702..8ff978be05321 100644 --- a/mobile/openapi/lib/model/scan_library_dto.dart +++ b/mobile/openapi/lib/model/scan_library_dto.dart @@ -66,6 +66,7 @@ class ScanLibraryDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ScanLibraryDto? fromJson(dynamic value) { + upgradeDto(value, "ScanLibraryDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/search_album_response_dto.dart b/mobile/openapi/lib/model/search_album_response_dto.dart index 46ce5273ac947..e9b47e85ec9a3 100644 --- a/mobile/openapi/lib/model/search_album_response_dto.dart +++ b/mobile/openapi/lib/model/search_album_response_dto.dart @@ -58,6 +58,7 @@ class SearchAlbumResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SearchAlbumResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SearchAlbumResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/search_asset_response_dto.dart b/mobile/openapi/lib/model/search_asset_response_dto.dart index 21ddbbb2132f2..3d214e61d9fef 100644 --- a/mobile/openapi/lib/model/search_asset_response_dto.dart +++ b/mobile/openapi/lib/model/search_asset_response_dto.dart @@ -68,6 +68,7 @@ class SearchAssetResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SearchAssetResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SearchAssetResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/search_explore_item.dart b/mobile/openapi/lib/model/search_explore_item.dart index 951fdd1bc8ce2..d44b2cd704a9a 100644 --- a/mobile/openapi/lib/model/search_explore_item.dart +++ b/mobile/openapi/lib/model/search_explore_item.dart @@ -46,6 +46,7 @@ class SearchExploreItem { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SearchExploreItem? fromJson(dynamic value) { + upgradeDto(value, "SearchExploreItem"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/search_explore_response_dto.dart b/mobile/openapi/lib/model/search_explore_response_dto.dart index 5bc601de9e59b..3b5d4f984933a 100644 --- a/mobile/openapi/lib/model/search_explore_response_dto.dart +++ b/mobile/openapi/lib/model/search_explore_response_dto.dart @@ -46,6 +46,7 @@ class SearchExploreResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SearchExploreResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SearchExploreResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/search_facet_count_response_dto.dart b/mobile/openapi/lib/model/search_facet_count_response_dto.dart index b40710e5251e4..f8eee844859e3 100644 --- a/mobile/openapi/lib/model/search_facet_count_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_count_response_dto.dart @@ -46,6 +46,7 @@ class SearchFacetCountResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SearchFacetCountResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SearchFacetCountResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/search_facet_response_dto.dart b/mobile/openapi/lib/model/search_facet_response_dto.dart index 0784921c6b3e4..aeec873c8ddfb 100644 --- a/mobile/openapi/lib/model/search_facet_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_response_dto.dart @@ -46,6 +46,7 @@ class SearchFacetResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SearchFacetResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SearchFacetResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/search_response_dto.dart b/mobile/openapi/lib/model/search_response_dto.dart index 9b2b7fd3cf75d..ca742ae35ccbc 100644 --- a/mobile/openapi/lib/model/search_response_dto.dart +++ b/mobile/openapi/lib/model/search_response_dto.dart @@ -46,6 +46,7 @@ class SearchResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SearchResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SearchResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_about_response_dto.dart b/mobile/openapi/lib/model/server_about_response_dto.dart index 9c71d1fccdcb4..1ab51a80f1362 100644 --- a/mobile/openapi/lib/model/server_about_response_dto.dart +++ b/mobile/openapi/lib/model/server_about_response_dto.dart @@ -276,6 +276,7 @@ class ServerAboutResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerAboutResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ServerAboutResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_config_dto.dart b/mobile/openapi/lib/model/server_config_dto.dart index 47cc52fb2c3fe..c45ed32ac076b 100644 --- a/mobile/openapi/lib/model/server_config_dto.dart +++ b/mobile/openapi/lib/model/server_config_dto.dart @@ -76,6 +76,7 @@ class ServerConfigDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerConfigDto? fromJson(dynamic value) { + upgradeDto(value, "ServerConfigDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_features_dto.dart b/mobile/openapi/lib/model/server_features_dto.dart index 0a7d8a4b4774a..5149c3796a9da 100644 --- a/mobile/openapi/lib/model/server_features_dto.dart +++ b/mobile/openapi/lib/model/server_features_dto.dart @@ -118,6 +118,7 @@ class ServerFeaturesDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerFeaturesDto? fromJson(dynamic value) { + upgradeDto(value, "ServerFeaturesDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_media_types_response_dto.dart b/mobile/openapi/lib/model/server_media_types_response_dto.dart index 35ddef195601a..506cbb44b4d76 100644 --- a/mobile/openapi/lib/model/server_media_types_response_dto.dart +++ b/mobile/openapi/lib/model/server_media_types_response_dto.dart @@ -52,6 +52,7 @@ class ServerMediaTypesResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerMediaTypesResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ServerMediaTypesResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_ping_response.dart b/mobile/openapi/lib/model/server_ping_response.dart index e23dc15c61af1..621ebfa2945ad 100644 --- a/mobile/openapi/lib/model/server_ping_response.dart +++ b/mobile/openapi/lib/model/server_ping_response.dart @@ -40,6 +40,7 @@ class ServerPingResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerPingResponse? fromJson(dynamic value) { + upgradeDto(value, "ServerPingResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_stats_response_dto.dart b/mobile/openapi/lib/model/server_stats_response_dto.dart index 6996e49aa5e24..654a34ee6b0e7 100644 --- a/mobile/openapi/lib/model/server_stats_response_dto.dart +++ b/mobile/openapi/lib/model/server_stats_response_dto.dart @@ -58,6 +58,7 @@ class ServerStatsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerStatsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ServerStatsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_storage_response_dto.dart b/mobile/openapi/lib/model/server_storage_response_dto.dart index 89d97d32ead2d..8d12e77834bec 100644 --- a/mobile/openapi/lib/model/server_storage_response_dto.dart +++ b/mobile/openapi/lib/model/server_storage_response_dto.dart @@ -76,6 +76,7 @@ class ServerStorageResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerStorageResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ServerStorageResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_theme_dto.dart b/mobile/openapi/lib/model/server_theme_dto.dart index 65b9b9163e874..69e1b2d2c87a7 100644 --- a/mobile/openapi/lib/model/server_theme_dto.dart +++ b/mobile/openapi/lib/model/server_theme_dto.dart @@ -40,6 +40,7 @@ class ServerThemeDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerThemeDto? fromJson(dynamic value) { + upgradeDto(value, "ServerThemeDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_version_response_dto.dart b/mobile/openapi/lib/model/server_version_response_dto.dart index e507f3372abfd..751347fabd2c1 100644 --- a/mobile/openapi/lib/model/server_version_response_dto.dart +++ b/mobile/openapi/lib/model/server_version_response_dto.dart @@ -52,6 +52,7 @@ class ServerVersionResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerVersionResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ServerVersionResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/session_response_dto.dart b/mobile/openapi/lib/model/session_response_dto.dart index 82673b3874b67..92e2dc60676af 100644 --- a/mobile/openapi/lib/model/session_response_dto.dart +++ b/mobile/openapi/lib/model/session_response_dto.dart @@ -70,6 +70,7 @@ class SessionResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SessionResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SessionResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/shared_link_create_dto.dart b/mobile/openapi/lib/model/shared_link_create_dto.dart index 623bc3125fd48..bc96b31fd24f9 100644 --- a/mobile/openapi/lib/model/shared_link_create_dto.dart +++ b/mobile/openapi/lib/model/shared_link_create_dto.dart @@ -132,6 +132,7 @@ class SharedLinkCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SharedLinkCreateDto? fromJson(dynamic value) { + upgradeDto(value, "SharedLinkCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/shared_link_edit_dto.dart b/mobile/openapi/lib/model/shared_link_edit_dto.dart index 2369c85db1a12..a394ba9b3b8fd 100644 --- a/mobile/openapi/lib/model/shared_link_edit_dto.dart +++ b/mobile/openapi/lib/model/shared_link_edit_dto.dart @@ -141,6 +141,7 @@ class SharedLinkEditDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SharedLinkEditDto? fromJson(dynamic value) { + upgradeDto(value, "SharedLinkEditDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/shared_link_response_dto.dart b/mobile/openapi/lib/model/shared_link_response_dto.dart index 018a1a51de2a4..9cc8b3ac80add 100644 --- a/mobile/openapi/lib/model/shared_link_response_dto.dart +++ b/mobile/openapi/lib/model/shared_link_response_dto.dart @@ -144,6 +144,7 @@ class SharedLinkResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SharedLinkResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SharedLinkResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/sign_up_dto.dart b/mobile/openapi/lib/model/sign_up_dto.dart index 772749fdba48c..7e0ff4045c7b5 100644 --- a/mobile/openapi/lib/model/sign_up_dto.dart +++ b/mobile/openapi/lib/model/sign_up_dto.dart @@ -52,6 +52,7 @@ class SignUpDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SignUpDto? fromJson(dynamic value) { + upgradeDto(value, "SignUpDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/smart_info_response_dto.dart b/mobile/openapi/lib/model/smart_info_response_dto.dart index 52e7c108b8291..4631eccf2cb1c 100644 --- a/mobile/openapi/lib/model/smart_info_response_dto.dart +++ b/mobile/openapi/lib/model/smart_info_response_dto.dart @@ -54,6 +54,7 @@ class SmartInfoResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SmartInfoResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SmartInfoResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index 2a42b75768420..4e1408cafa737 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -467,6 +467,7 @@ class SmartSearchDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SmartSearchDto? fromJson(dynamic value) { + upgradeDto(value, "SmartSearchDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/stack_create_dto.dart b/mobile/openapi/lib/model/stack_create_dto.dart index 9b37bc6e2e9aa..cb51081eb1ee4 100644 --- a/mobile/openapi/lib/model/stack_create_dto.dart +++ b/mobile/openapi/lib/model/stack_create_dto.dart @@ -41,6 +41,7 @@ class StackCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static StackCreateDto? fromJson(dynamic value) { + upgradeDto(value, "StackCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/stack_response_dto.dart b/mobile/openapi/lib/model/stack_response_dto.dart index 3d0aaf91d17cc..b6cb747cafc5f 100644 --- a/mobile/openapi/lib/model/stack_response_dto.dart +++ b/mobile/openapi/lib/model/stack_response_dto.dart @@ -52,6 +52,7 @@ class StackResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static StackResponseDto? fromJson(dynamic value) { + upgradeDto(value, "StackResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/stack_update_dto.dart b/mobile/openapi/lib/model/stack_update_dto.dart index 0e9712721048a..0101499edfc51 100644 --- a/mobile/openapi/lib/model/stack_update_dto.dart +++ b/mobile/openapi/lib/model/stack_update_dto.dart @@ -50,6 +50,7 @@ class StackUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static StackUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "StackUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index aff8062c8a139..5306370d2d1f7 100644 --- a/mobile/openapi/lib/model/system_config_dto.dart +++ b/mobile/openapi/lib/model/system_config_dto.dart @@ -142,6 +142,7 @@ class SystemConfigDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart index a75a77c669168..73f7d35aecc30 100644 --- a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart +++ b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart @@ -175,6 +175,7 @@ class SystemConfigFFmpegDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigFFmpegDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigFFmpegDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_faces_dto.dart b/mobile/openapi/lib/model/system_config_faces_dto.dart index 980e494fb70b0..4e18eb8de20e4 100644 --- a/mobile/openapi/lib/model/system_config_faces_dto.dart +++ b/mobile/openapi/lib/model/system_config_faces_dto.dart @@ -40,6 +40,7 @@ class SystemConfigFacesDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigFacesDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigFacesDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart index 388949c759ce0..681a8c00c3bc0 100644 --- a/mobile/openapi/lib/model/system_config_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_image_dto.dart @@ -80,6 +80,7 @@ class SystemConfigImageDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigImageDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigImageDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_job_dto.dart b/mobile/openapi/lib/model/system_config_job_dto.dart index 1bc0f6b29c1c0..c0fed5cccc06f 100644 --- a/mobile/openapi/lib/model/system_config_job_dto.dart +++ b/mobile/openapi/lib/model/system_config_job_dto.dart @@ -100,6 +100,7 @@ class SystemConfigJobDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigJobDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigJobDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_library_dto.dart b/mobile/openapi/lib/model/system_config_library_dto.dart index 4f55e33e8087b..e728b0bf20957 100644 --- a/mobile/openapi/lib/model/system_config_library_dto.dart +++ b/mobile/openapi/lib/model/system_config_library_dto.dart @@ -46,6 +46,7 @@ class SystemConfigLibraryDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigLibraryDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigLibraryDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_library_scan_dto.dart b/mobile/openapi/lib/model/system_config_library_scan_dto.dart index 31df272594577..6a6558b4b32ee 100644 --- a/mobile/openapi/lib/model/system_config_library_scan_dto.dart +++ b/mobile/openapi/lib/model/system_config_library_scan_dto.dart @@ -46,6 +46,7 @@ class SystemConfigLibraryScanDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigLibraryScanDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigLibraryScanDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_library_watch_dto.dart b/mobile/openapi/lib/model/system_config_library_watch_dto.dart index 9d152f366a898..1a1f5d7126b34 100644 --- a/mobile/openapi/lib/model/system_config_library_watch_dto.dart +++ b/mobile/openapi/lib/model/system_config_library_watch_dto.dart @@ -40,6 +40,7 @@ class SystemConfigLibraryWatchDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigLibraryWatchDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigLibraryWatchDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_logging_dto.dart b/mobile/openapi/lib/model/system_config_logging_dto.dart index 60c0be3d2c2ff..f025221eff996 100644 --- a/mobile/openapi/lib/model/system_config_logging_dto.dart +++ b/mobile/openapi/lib/model/system_config_logging_dto.dart @@ -46,6 +46,7 @@ class SystemConfigLoggingDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigLoggingDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigLoggingDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart index 3923bacad4211..d665f0bfa56a7 100644 --- a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart +++ b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart @@ -64,6 +64,7 @@ class SystemConfigMachineLearningDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigMachineLearningDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigMachineLearningDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_map_dto.dart b/mobile/openapi/lib/model/system_config_map_dto.dart index 663188518275a..d53d5711db2af 100644 --- a/mobile/openapi/lib/model/system_config_map_dto.dart +++ b/mobile/openapi/lib/model/system_config_map_dto.dart @@ -52,6 +52,7 @@ class SystemConfigMapDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigMapDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigMapDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_metadata_dto.dart b/mobile/openapi/lib/model/system_config_metadata_dto.dart index 60ca35c835bdb..3c32fc551d4e2 100644 --- a/mobile/openapi/lib/model/system_config_metadata_dto.dart +++ b/mobile/openapi/lib/model/system_config_metadata_dto.dart @@ -40,6 +40,7 @@ class SystemConfigMetadataDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigMetadataDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigMetadataDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_new_version_check_dto.dart b/mobile/openapi/lib/model/system_config_new_version_check_dto.dart index c7b8c98695c32..c63d2abc1ba30 100644 --- a/mobile/openapi/lib/model/system_config_new_version_check_dto.dart +++ b/mobile/openapi/lib/model/system_config_new_version_check_dto.dart @@ -40,6 +40,7 @@ class SystemConfigNewVersionCheckDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigNewVersionCheckDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigNewVersionCheckDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_notifications_dto.dart b/mobile/openapi/lib/model/system_config_notifications_dto.dart index 22f08b3ab4365..35d3d318339e8 100644 --- a/mobile/openapi/lib/model/system_config_notifications_dto.dart +++ b/mobile/openapi/lib/model/system_config_notifications_dto.dart @@ -40,6 +40,7 @@ class SystemConfigNotificationsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigNotificationsDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigNotificationsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_o_auth_dto.dart b/mobile/openapi/lib/model/system_config_o_auth_dto.dart index 6ebbe8d25c05c..9125bb7bba65a 100644 --- a/mobile/openapi/lib/model/system_config_o_auth_dto.dart +++ b/mobile/openapi/lib/model/system_config_o_auth_dto.dart @@ -125,6 +125,7 @@ class SystemConfigOAuthDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigOAuthDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigOAuthDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_password_login_dto.dart b/mobile/openapi/lib/model/system_config_password_login_dto.dart index 61896a890c58e..69c8942bb6471 100644 --- a/mobile/openapi/lib/model/system_config_password_login_dto.dart +++ b/mobile/openapi/lib/model/system_config_password_login_dto.dart @@ -40,6 +40,7 @@ class SystemConfigPasswordLoginDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigPasswordLoginDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigPasswordLoginDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart b/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart index 2eb586cac689c..6c1673d46c07d 100644 --- a/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart +++ b/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart @@ -40,6 +40,7 @@ class SystemConfigReverseGeocodingDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigReverseGeocodingDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigReverseGeocodingDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_server_dto.dart b/mobile/openapi/lib/model/system_config_server_dto.dart index ccb48ee61dedb..b1b92c9515d5b 100644 --- a/mobile/openapi/lib/model/system_config_server_dto.dart +++ b/mobile/openapi/lib/model/system_config_server_dto.dart @@ -46,6 +46,7 @@ class SystemConfigServerDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigServerDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigServerDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_smtp_dto.dart b/mobile/openapi/lib/model/system_config_smtp_dto.dart index 6588d244ee5d1..fcde49cf3564e 100644 --- a/mobile/openapi/lib/model/system_config_smtp_dto.dart +++ b/mobile/openapi/lib/model/system_config_smtp_dto.dart @@ -58,6 +58,7 @@ class SystemConfigSmtpDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigSmtpDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigSmtpDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart b/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart index 63dfdca4cf07e..bdaaa426c5220 100644 --- a/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart +++ b/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart @@ -66,6 +66,7 @@ class SystemConfigSmtpTransportDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigSmtpTransportDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigSmtpTransportDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_storage_template_dto.dart b/mobile/openapi/lib/model/system_config_storage_template_dto.dart index 13323aebdaba3..596aafc1950a1 100644 --- a/mobile/openapi/lib/model/system_config_storage_template_dto.dart +++ b/mobile/openapi/lib/model/system_config_storage_template_dto.dart @@ -52,6 +52,7 @@ class SystemConfigStorageTemplateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigStorageTemplateDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigStorageTemplateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart b/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart index 82e0a6f74769b..f8586d344c5aa 100644 --- a/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart +++ b/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart @@ -82,6 +82,7 @@ class SystemConfigTemplateStorageOptionDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigTemplateStorageOptionDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigTemplateStorageOptionDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_theme_dto.dart b/mobile/openapi/lib/model/system_config_theme_dto.dart index 2f7f4d2f3b916..a97c2cf84c1f3 100644 --- a/mobile/openapi/lib/model/system_config_theme_dto.dart +++ b/mobile/openapi/lib/model/system_config_theme_dto.dart @@ -40,6 +40,7 @@ class SystemConfigThemeDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigThemeDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigThemeDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_trash_dto.dart b/mobile/openapi/lib/model/system_config_trash_dto.dart index 336019fde4203..51b39e9a55c1f 100644 --- a/mobile/openapi/lib/model/system_config_trash_dto.dart +++ b/mobile/openapi/lib/model/system_config_trash_dto.dart @@ -47,6 +47,7 @@ class SystemConfigTrashDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigTrashDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigTrashDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_user_dto.dart b/mobile/openapi/lib/model/system_config_user_dto.dart index c46637446098f..8e6bd3c9c306e 100644 --- a/mobile/openapi/lib/model/system_config_user_dto.dart +++ b/mobile/openapi/lib/model/system_config_user_dto.dart @@ -41,6 +41,7 @@ class SystemConfigUserDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigUserDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigUserDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tag_bulk_assets_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_dto.dart index c11cb66ce081f..26a575e193dc6 100644 --- a/mobile/openapi/lib/model/tag_bulk_assets_dto.dart +++ b/mobile/openapi/lib/model/tag_bulk_assets_dto.dart @@ -46,6 +46,7 @@ class TagBulkAssetsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagBulkAssetsDto? fromJson(dynamic value) { + upgradeDto(value, "TagBulkAssetsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart index d4dcb91d8c45d..009f26bfe4f49 100644 --- a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart +++ b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart @@ -40,6 +40,7 @@ class TagBulkAssetsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagBulkAssetsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "TagBulkAssetsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tag_create_dto.dart b/mobile/openapi/lib/model/tag_create_dto.dart index dd7e537a0a021..9a5171074d622 100644 --- a/mobile/openapi/lib/model/tag_create_dto.dart +++ b/mobile/openapi/lib/model/tag_create_dto.dart @@ -66,6 +66,7 @@ class TagCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagCreateDto? fromJson(dynamic value) { + upgradeDto(value, "TagCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tag_response_dto.dart b/mobile/openapi/lib/model/tag_response_dto.dart index 1d1a88c3cff29..cd684b163a27d 100644 --- a/mobile/openapi/lib/model/tag_response_dto.dart +++ b/mobile/openapi/lib/model/tag_response_dto.dart @@ -96,6 +96,7 @@ class TagResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagResponseDto? fromJson(dynamic value) { + upgradeDto(value, "TagResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tag_update_dto.dart b/mobile/openapi/lib/model/tag_update_dto.dart index 661f65896e56f..ab1adb127bacb 100644 --- a/mobile/openapi/lib/model/tag_update_dto.dart +++ b/mobile/openapi/lib/model/tag_update_dto.dart @@ -44,6 +44,7 @@ class TagUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "TagUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tag_upsert_dto.dart b/mobile/openapi/lib/model/tag_upsert_dto.dart index 941d25b6aee6c..d60a00f466e1f 100644 --- a/mobile/openapi/lib/model/tag_upsert_dto.dart +++ b/mobile/openapi/lib/model/tag_upsert_dto.dart @@ -40,6 +40,7 @@ class TagUpsertDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagUpsertDto? fromJson(dynamic value) { + upgradeDto(value, "TagUpsertDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tags_response.dart b/mobile/openapi/lib/model/tags_response.dart index 3a5ea3b20b3ec..2470edf979239 100644 --- a/mobile/openapi/lib/model/tags_response.dart +++ b/mobile/openapi/lib/model/tags_response.dart @@ -46,6 +46,7 @@ class TagsResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagsResponse? fromJson(dynamic value) { + upgradeDto(value, "TagsResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tags_update.dart b/mobile/openapi/lib/model/tags_update.dart index 8355b00a00d49..d99236914055c 100644 --- a/mobile/openapi/lib/model/tags_update.dart +++ b/mobile/openapi/lib/model/tags_update.dart @@ -66,6 +66,7 @@ class TagsUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagsUpdate? fromJson(dynamic value) { + upgradeDto(value, "TagsUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/time_bucket_response_dto.dart b/mobile/openapi/lib/model/time_bucket_response_dto.dart index 2c86a56b3c9ad..56044b27a8a81 100644 --- a/mobile/openapi/lib/model/time_bucket_response_dto.dart +++ b/mobile/openapi/lib/model/time_bucket_response_dto.dart @@ -46,6 +46,7 @@ class TimeBucketResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TimeBucketResponseDto? fromJson(dynamic value) { + upgradeDto(value, "TimeBucketResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/trash_response_dto.dart b/mobile/openapi/lib/model/trash_response_dto.dart index 52a05ff6d4db3..2df154d06c1a0 100644 --- a/mobile/openapi/lib/model/trash_response_dto.dart +++ b/mobile/openapi/lib/model/trash_response_dto.dart @@ -40,6 +40,7 @@ class TrashResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TrashResponseDto? fromJson(dynamic value) { + upgradeDto(value, "TrashResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/update_album_dto.dart b/mobile/openapi/lib/model/update_album_dto.dart index f9c9762887265..8353dba14e627 100644 --- a/mobile/openapi/lib/model/update_album_dto.dart +++ b/mobile/openapi/lib/model/update_album_dto.dart @@ -114,6 +114,7 @@ class UpdateAlbumDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UpdateAlbumDto? fromJson(dynamic value) { + upgradeDto(value, "UpdateAlbumDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/update_album_user_dto.dart b/mobile/openapi/lib/model/update_album_user_dto.dart index f77223acf5855..43218cae6e140 100644 --- a/mobile/openapi/lib/model/update_album_user_dto.dart +++ b/mobile/openapi/lib/model/update_album_user_dto.dart @@ -40,6 +40,7 @@ class UpdateAlbumUserDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UpdateAlbumUserDto? fromJson(dynamic value) { + upgradeDto(value, "UpdateAlbumUserDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart index 9aa413d24221e..9ebce5fd9232b 100644 --- a/mobile/openapi/lib/model/update_asset_dto.dart +++ b/mobile/openapi/lib/model/update_asset_dto.dart @@ -158,6 +158,7 @@ class UpdateAssetDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UpdateAssetDto? fromJson(dynamic value) { + upgradeDto(value, "UpdateAssetDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/update_library_dto.dart b/mobile/openapi/lib/model/update_library_dto.dart index 85847c0ddfb6f..b85df40172e69 100644 --- a/mobile/openapi/lib/model/update_library_dto.dart +++ b/mobile/openapi/lib/model/update_library_dto.dart @@ -62,6 +62,7 @@ class UpdateLibraryDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UpdateLibraryDto? fromJson(dynamic value) { + upgradeDto(value, "UpdateLibraryDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/update_partner_dto.dart b/mobile/openapi/lib/model/update_partner_dto.dart index f695f99535a38..3af3c83ad10bd 100644 --- a/mobile/openapi/lib/model/update_partner_dto.dart +++ b/mobile/openapi/lib/model/update_partner_dto.dart @@ -40,6 +40,7 @@ class UpdatePartnerDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UpdatePartnerDto? fromJson(dynamic value) { + upgradeDto(value, "UpdatePartnerDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/usage_by_user_dto.dart b/mobile/openapi/lib/model/usage_by_user_dto.dart index 0bbbba00bbaab..e6f9216d74572 100644 --- a/mobile/openapi/lib/model/usage_by_user_dto.dart +++ b/mobile/openapi/lib/model/usage_by_user_dto.dart @@ -74,6 +74,7 @@ class UsageByUserDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UsageByUserDto? fromJson(dynamic value) { + upgradeDto(value, "UsageByUserDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_admin_create_dto.dart b/mobile/openapi/lib/model/user_admin_create_dto.dart index db514a1d571b6..f2709be57b640 100644 --- a/mobile/openapi/lib/model/user_admin_create_dto.dart +++ b/mobile/openapi/lib/model/user_admin_create_dto.dart @@ -105,6 +105,7 @@ class UserAdminCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserAdminCreateDto? fromJson(dynamic value) { + upgradeDto(value, "UserAdminCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_admin_delete_dto.dart b/mobile/openapi/lib/model/user_admin_delete_dto.dart index 7778b15775d0f..2cf68ad7b25ee 100644 --- a/mobile/openapi/lib/model/user_admin_delete_dto.dart +++ b/mobile/openapi/lib/model/user_admin_delete_dto.dart @@ -50,6 +50,7 @@ class UserAdminDeleteDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserAdminDeleteDto? fromJson(dynamic value) { + upgradeDto(value, "UserAdminDeleteDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_admin_response_dto.dart b/mobile/openapi/lib/model/user_admin_response_dto.dart index 461596b7bf026..e5ae8e1d4ef27 100644 --- a/mobile/openapi/lib/model/user_admin_response_dto.dart +++ b/mobile/openapi/lib/model/user_admin_response_dto.dart @@ -156,6 +156,7 @@ class UserAdminResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserAdminResponseDto? fromJson(dynamic value) { + upgradeDto(value, "UserAdminResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_admin_update_dto.dart b/mobile/openapi/lib/model/user_admin_update_dto.dart index dd0db767fe6b0..6c6f73ae8e9e1 100644 --- a/mobile/openapi/lib/model/user_admin_update_dto.dart +++ b/mobile/openapi/lib/model/user_admin_update_dto.dart @@ -119,6 +119,7 @@ class UserAdminUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserAdminUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "UserAdminUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_license.dart b/mobile/openapi/lib/model/user_license.dart index c7abb085f29c7..9bed8d5c43633 100644 --- a/mobile/openapi/lib/model/user_license.dart +++ b/mobile/openapi/lib/model/user_license.dart @@ -52,6 +52,7 @@ class UserLicense { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserLicense? fromJson(dynamic value) { + upgradeDto(value, "UserLicense"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart index d3927df8d7ee3..23d9ea84ecd82 100644 --- a/mobile/openapi/lib/model/user_preferences_response_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_response_dto.dart @@ -88,6 +88,7 @@ class UserPreferencesResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserPreferencesResponseDto? fromJson(dynamic value) { + upgradeDto(value, "UserPreferencesResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart index 2841c2f572c11..208dbf686078a 100644 --- a/mobile/openapi/lib/model/user_preferences_update_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_update_dto.dart @@ -178,6 +178,7 @@ class UserPreferencesUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserPreferencesUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "UserPreferencesUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 282a5a40dce8b..a02da299481b8 100644 --- a/mobile/openapi/lib/model/user_response_dto.dart +++ b/mobile/openapi/lib/model/user_response_dto.dart @@ -70,6 +70,7 @@ class UserResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserResponseDto? fromJson(dynamic value) { + upgradeDto(value, "UserResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_update_me_dto.dart b/mobile/openapi/lib/model/user_update_me_dto.dart index 2d665fc7847b8..8f3f4df37ad81 100644 --- a/mobile/openapi/lib/model/user_update_me_dto.dart +++ b/mobile/openapi/lib/model/user_update_me_dto.dart @@ -82,6 +82,7 @@ class UserUpdateMeDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserUpdateMeDto? fromJson(dynamic value) { + upgradeDto(value, "UserUpdateMeDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/validate_access_token_response_dto.dart b/mobile/openapi/lib/model/validate_access_token_response_dto.dart index e970f7e840a80..5e36efcfedf5b 100644 --- a/mobile/openapi/lib/model/validate_access_token_response_dto.dart +++ b/mobile/openapi/lib/model/validate_access_token_response_dto.dart @@ -40,6 +40,7 @@ class ValidateAccessTokenResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ValidateAccessTokenResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ValidateAccessTokenResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/validate_library_dto.dart b/mobile/openapi/lib/model/validate_library_dto.dart index 05e122b1a1170..08199e3aa66c8 100644 --- a/mobile/openapi/lib/model/validate_library_dto.dart +++ b/mobile/openapi/lib/model/validate_library_dto.dart @@ -46,6 +46,7 @@ class ValidateLibraryDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ValidateLibraryDto? fromJson(dynamic value) { + upgradeDto(value, "ValidateLibraryDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart index 23aac0b74255a..11fbbd74c2aaa 100644 --- a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart +++ b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart @@ -62,6 +62,7 @@ class ValidateLibraryImportPathResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ValidateLibraryImportPathResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ValidateLibraryImportPathResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/validate_library_response_dto.dart b/mobile/openapi/lib/model/validate_library_response_dto.dart index b213f9ba98943..e0dc2a2d14233 100644 --- a/mobile/openapi/lib/model/validate_library_response_dto.dart +++ b/mobile/openapi/lib/model/validate_library_response_dto.dart @@ -40,6 +40,7 @@ class ValidateLibraryResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ValidateLibraryResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ValidateLibraryResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/open-api/bin/generate-open-api.sh b/open-api/bin/generate-open-api.sh index 2ca04630468f9..bf8b24b55703b 100755 --- a/open-api/bin/generate-open-api.sh +++ b/open-api/bin/generate-open-api.sh @@ -9,11 +9,7 @@ function dart { wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache patch --no-backup-if-mismatch -u native_class.mustache header}} -{{>part_of}} -class ApiClient { - ApiClient({this.basePath = '{{{basePath}}}', this.authentication,}); - - final String basePath; - final Authentication? authentication; - - var _client = Client(); - final _defaultHeaderMap = {}; - - /// Returns the current HTTP [Client] instance to use in this class. - /// - /// The return value is guaranteed to never be null. - Client get client => _client; - - /// Requests to use a new HTTP [Client] in this class. - set client(Client newClient) { - _client = newClient; - } - - Map get defaultHeaderMap => _defaultHeaderMap; - - void addDefaultHeader(String key, String value) { - _defaultHeaderMap[key] = value; - } - - // We don't use a Map for queryParams. - // If collectionFormat is 'multi', a key might appear multiple times. - Future invokeAPI( - String path, - String method, - List queryParams, - Object? body, - Map headerParams, - Map formParams, - String? contentType, - ) async { - await authentication?.applyToParams(queryParams, headerParams); - - headerParams.addAll(_defaultHeaderMap); - if (contentType != null) { - headerParams['Content-Type'] = contentType; - } - - final urlEncodedQueryParams = queryParams.map((param) => '$param'); - final queryString = urlEncodedQueryParams.isNotEmpty ? '?${urlEncodedQueryParams.join('&')}' : ''; - final uri = Uri.parse('$basePath$path$queryString'); - - try { - // Special case for uploading a single file which isn't a 'multipart/form-data'. - if ( - body is MultipartFile && (contentType == null || - !contentType.toLowerCase().startsWith('multipart/form-data')) - ) { - final request = StreamedRequest(method, uri); - request.headers.addAll(headerParams); - request.contentLength = body.length; - body.finalize().listen( - request.sink.add, - onDone: request.sink.close, - // ignore: avoid_types_on_closure_parameters - onError: (Object error, StackTrace trace) => request.sink.close(), - cancelOnError: true, - ); - final response = await _client.send(request); - return Response.fromStream(response); - } - - if (body is MultipartRequest) { - final request = MultipartRequest(method, uri); - request.fields.addAll(body.fields); - request.files.addAll(body.files); - request.headers.addAll(body.headers); - request.headers.addAll(headerParams); - final response = await _client.send(request); - return Response.fromStream(response); - } - - final msgBody = contentType == 'application/x-www-form-urlencoded' - ? formParams - : await serializeAsync(body); - final nullableHeaderParams = headerParams.isEmpty ? null : headerParams; - - switch(method) { - case 'POST': return await _client.post(uri, headers: nullableHeaderParams, body: msgBody,); - case 'PUT': return await _client.put(uri, headers: nullableHeaderParams, body: msgBody,); - case 'DELETE': return await _client.delete(uri, headers: nullableHeaderParams, body: msgBody,); - case 'PATCH': return await _client.patch(uri, headers: nullableHeaderParams, body: msgBody,); - case 'HEAD': return await _client.head(uri, headers: nullableHeaderParams,); - case 'GET': return await _client.get(uri, headers: nullableHeaderParams,); - } - } on SocketException catch (error, trace) { - throw ApiException.withInner( - HttpStatus.badRequest, - 'Socket operation failed: $method $path', - error, - trace, - ); - } on TlsException catch (error, trace) { - throw ApiException.withInner( - HttpStatus.badRequest, - 'TLS/SSL communication failed: $method $path', - error, - trace, - ); - } on IOException catch (error, trace) { - throw ApiException.withInner( - HttpStatus.badRequest, - 'I/O operation failed: $method $path', - error, - trace, - ); - } on ClientException catch (error, trace) { - throw ApiException.withInner( - HttpStatus.badRequest, - 'HTTP connection failed: $method $path', - error, - trace, - ); - } on Exception catch (error, trace) { - throw ApiException.withInner( - HttpStatus.badRequest, - 'Exception occurred: $method $path', - error, - trace, - ); - } - - throw ApiException( - HttpStatus.badRequest, - 'Invalid HTTP operation: $method $path', - ); - } -{{#native_serialization}} - - Future deserializeAsync(String value, String targetType, {bool growable = false,}) async => - // ignore: deprecated_member_use_from_same_package - deserialize(value, targetType, growable: growable); - - @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.') - dynamic deserialize(String value, String targetType, {bool growable = false,}) { - // Remove all spaces. Necessary for regular expressions as well. - targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments - - // If the expected target type is String, nothing to do... - return targetType == 'String' - ? value - : fromJson(json.decode(value), targetType, growable: growable); - } -{{/native_serialization}} - - // ignore: deprecated_member_use_from_same_package - Future serializeAsync(Object? value) async => serialize(value); - - @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use serializeAsync() instead.') - String serialize(Object? value) => value == null ? '' : json.encode(value); - -{{#native_serialization}} - /// Returns a native instance of an OpenAPI class matching the [specified type][targetType]. - static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) { - upgradeDto(value, targetType); - try { - switch (targetType) { - case 'String': - return value is String ? value : value.toString(); - case 'int': - return value is int ? value : int.parse('$value'); - case 'double': - return value is double ? value : double.parse('$value'); - case 'bool': - if (value is bool) { - return value; - } - final valueString = '$value'.toLowerCase(); - return valueString == 'true' || valueString == '1'; - case 'DateTime': - return value is DateTime ? value : DateTime.tryParse(value); - {{#models}} - {{#model}} - case '{{{classname}}}': - {{#isEnum}} - {{#native_serialization}}return {{{classname}}}TypeTransformer().decode(value);{{/native_serialization}} - {{/isEnum}} - {{^isEnum}} - return {{{classname}}}.fromJson(value); - {{/isEnum}} - {{/model}} - {{/models}} - default: - dynamic match; - if (value is List && (match = _regList.firstMatch(targetType)?.group(1)) != null) { - return value - .map((dynamic v) => fromJson(v, match, growable: growable,)) - .toList(growable: growable); - } - if (value is Set && (match = _regSet.firstMatch(targetType)?.group(1)) != null) { - return value - .map((dynamic v) => fromJson(v, match, growable: growable,)) - .toSet(); - } - if (value is Map && (match = _regMap.firstMatch(targetType)?.group(1)) != null) { - return Map.fromIterables( - value.keys.cast(), - value.values.map((dynamic v) => fromJson(v, match, growable: growable,)), - ); - } - } - } on Exception catch (error, trace) { - throw ApiException.withInner(HttpStatus.internalServerError, 'Exception during deserialization.', error, trace,); - } - throw ApiException(HttpStatus.internalServerError, 'Could not find a suitable class for deserialization',); - } -{{/native_serialization}} -} -{{#native_serialization}} - -/// Primarily intended for use in an isolate. -class DeserializationMessage { - const DeserializationMessage({ - required this.json, - required this.targetType, - this.growable = false, - }); - - /// The JSON value to deserialize. - final String json; - - /// Target type to deserialize to. - final String targetType; - - /// Whether to make deserialized lists or maps growable. - final bool growable; -} - -/// Primarily intended for use in an isolate. -Future decodeAsync(DeserializationMessage message) async { - // Remove all spaces. Necessary for regular expressions as well. - final targetType = message.targetType.replaceAll(' ', ''); - - // If the expected target type is String, nothing to do... - return targetType == 'String' - ? message.json - : json.decode(message.json); -} - -/// Primarily intended for use in an isolate. -Future deserializeAsync(DeserializationMessage message) async { - // Remove all spaces. Necessary for regular expressions as well. - final targetType = message.targetType.replaceAll(' ', ''); - - // If the expected target type is String, nothing to do... - return targetType == 'String' - ? message.json - : ApiClient.fromJson( - json.decode(message.json), - targetType, - growable: message.growable, - ); -} -{{/native_serialization}} - -/// Primarily intended for use in an isolate. -Future serializeAsync(Object? value) async => value == null ? '' : json.encode(value); diff --git a/open-api/templates/mobile/api_client.mustache.patch b/open-api/templates/mobile/api_client.mustache.patch deleted file mode 100644 index 3805cd8f7934a..0000000000000 --- a/open-api/templates/mobile/api_client.mustache.patch +++ /dev/null @@ -1,10 +0,0 @@ ---- api_client.mustache 2024-08-13 14:29:04.056364916 -0500 -+++ api_client_new.mustache 2024-08-13 14:29:36.224410735 -0500 -@@ -159,6 +159,7 @@ - {{#native_serialization}} - /// Returns a native instance of an OpenAPI class matching the [specified type][targetType]. - static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) { -+ upgradeDto(value, targetType); - try { - switch (targetType) { - case 'String': diff --git a/open-api/templates/mobile/serialization/native/native_class.mustache b/open-api/templates/mobile/serialization/native/native_class.mustache index 254843e00eefa..9a7b1439b1fdf 100644 --- a/open-api/templates/mobile/serialization/native/native_class.mustache +++ b/open-api/templates/mobile/serialization/native/native_class.mustache @@ -111,6 +111,7 @@ class {{{classname}}} { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static {{{classname}}}? fromJson(dynamic value) { + upgradeDto(value, "{{{classname}}}"); if (value is Map) { final json = value.cast(); diff --git a/open-api/templates/mobile/serialization/native/native_class.mustache.patch b/open-api/templates/mobile/serialization/native/native_class.mustache.patch index 02e07f933a30d..4ba65949665f8 100644 --- a/open-api/templates/mobile/serialization/native/native_class.mustache.patch +++ b/open-api/templates/mobile/serialization/native/native_class.mustache.patch @@ -1,5 +1,5 @@ ---- native_class.mustache 2023-08-31 23:09:59.584269162 +0200 -+++ native_class1.mustache 2023-08-31 22:59:53.633083270 +0200 +--- native_class.mustache 2024-09-19 11:41:07.855683995 -0400 ++++ native_class_temp.mustache 2024-09-19 11:41:57.113249395 -0400 @@ -91,14 +91,14 @@ {{/isDateTime}} {{#isNullable}} @@ -17,10 +17,14 @@ } {{/defaultValue}} {{/required}} -@@ -114,17 +114,6 @@ +@@ -111,20 +111,10 @@ + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static {{{classname}}}? fromJson(dynamic value) { ++ upgradeDto(value, "{{{classname}}}"); if (value is Map) { final json = value.cast(); - + - // Ensure that the map contains the required keys. - // Note 1: the values aren't checked for validity beyond being non-null. - // Note 2: this code is stripped in release mode! @@ -35,9 +39,9 @@ return {{{classname}}}( {{#vars}} {{#isDateTime}} -@@ -215,6 +204,10 @@ +@@ -215,6 +205,10 @@ ? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}} - : {{{datatypeWithEnum}}}.parse(json[r'{{{baseName}}}'].toString()), + : {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'), {{/isNumber}} + {{#isDouble}} + {{{name}}}: (mapValueOfType(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}).toDouble(), @@ -46,7 +50,7 @@ {{^isNumber}} {{^isEnum}} {{{name}}}: mapValueOfType<{{{datatypeWithEnum}}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}, -@@ -223,6 +216,7 @@ +@@ -223,6 +217,7 @@ {{{name}}}: {{{enumName}}}.fromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}, {{/isEnum}} {{/isNumber}} From e41785b1a1e6591c7b385f97d45d8417f8c451ef Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:08:01 +0200 Subject: [PATCH 45/57] fix: open api (#12878) --- mobile/openapi/lib/model/random_search_dto.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/mobile/openapi/lib/model/random_search_dto.dart b/mobile/openapi/lib/model/random_search_dto.dart index 8dbbeb538714d..419cb451e2a02 100644 --- a/mobile/openapi/lib/model/random_search_dto.dart +++ b/mobile/openapi/lib/model/random_search_dto.dart @@ -493,6 +493,7 @@ class RandomSearchDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static RandomSearchDto? fromJson(dynamic value) { + upgradeDto(value, "RandomSearchDto"); if (value is Map) { final json = value.cast(); From bcd416477b0d9dd76f3a2f11547f220354c0f9a0 Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Mon, 23 Sep 2024 21:30:23 +0100 Subject: [PATCH 46/57] feat: serve map tile styles from tiles.immich.cloud (#12858) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- e2e/package-lock.json | 6 +- e2e/src/api/specs/map.e2e-spec.ts | 65 +- e2e/src/api/specs/server-info.e2e-spec.ts | 2 + e2e/src/api/specs/server.e2e-spec.ts | 2 + mobile/.vscode/settings.json | 2 +- .../server_info/server_config.model.dart | 10 +- mobile/lib/pages/search/map/map.page.dart | 15 +- .../lib/providers/map/map_state.provider.dart | 75 +- .../lib/providers/server_info.provider.dart | 3 + mobile/lib/utils/openapi_patching.dart | 13 + mobile/openapi/README.md | 2 - mobile/openapi/lib/api.dart | 1 - mobile/openapi/lib/api/map_api.dart | 56 - mobile/openapi/lib/api_client.dart | 2 - mobile/openapi/lib/api_helper.dart | 3 - mobile/openapi/lib/model/map_theme.dart | 85 -- .../openapi/lib/model/server_config_dto.dart | 18 +- open-api/immich-openapi-specs.json | 66 +- open-api/typescript-sdk/src/fetch-client.ts | 20 +- server/package-lock.json | 1122 +++++++++-------- server/src/config.ts | 4 +- server/src/controllers/map.controller.ts | 7 - server/src/dtos/server.dto.ts | 2 + server/src/dtos/system-config.dto.ts | 6 +- server/src/services/map.service.ts | 11 - server/src/services/server.service.spec.ts | 2 + server/src/services/server.service.ts | 2 + .../services/system-config.service.spec.ts | 4 +- .../shared-components/map/map.svelte | 16 +- web/src/lib/stores/server-config.store.ts | 2 + 30 files changed, 676 insertions(+), 948 deletions(-) delete mode 100644 mobile/openapi/lib/model/map_theme.dart diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 865f154d6b065..ab4fd53fbf2ef 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -5016,9 +5016,9 @@ } }, "node_modules/path-to-regexp": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", - "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "dev": true }, "node_modules/pathe": { diff --git a/e2e/src/api/specs/map.e2e-spec.ts b/e2e/src/api/specs/map.e2e-spec.ts index 343a7c91d03e6..da5f779cffaad 100644 --- a/e2e/src/api/specs/map.e2e-spec.ts +++ b/e2e/src/api/specs/map.e2e-spec.ts @@ -1,8 +1,7 @@ -import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk'; +import { LoginResponseDto } from '@immich/sdk'; import { readFile } from 'node:fs/promises'; import { basename, join } from 'node:path'; import { Socket } from 'socket.io-client'; -import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; @@ -11,18 +10,13 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; describe('/map', () => { let websocket: Socket; let admin: LoginResponseDto; - let nonAdmin: LoginResponseDto; - let asset: AssetMediaResponseDto; beforeAll(async () => { await utils.resetDatabase(); admin = await utils.adminSetup({ onboarding: false }); - nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1); websocket = await utils.connectWebsocket(admin.accessToken); - asset = await utils.createAsset(admin.accessToken); - const files = ['formats/heic/IMG_2682.heic', 'metadata/gps-position/thompson-springs.jpg']; utils.resetEvents(); const uploadFile = async (input: string) => { @@ -103,63 +97,6 @@ describe('/map', () => { }); }); - describe('GET /map/style.json', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/map/style.json'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should allow shared link access', async () => { - const sharedLink = await utils.createSharedLink(admin.accessToken, { - type: SharedLinkType.Individual, - assetIds: [asset.id], - }); - const { status, body } = await request(app).get(`/map/style.json?key=${sharedLink.key}`).query({ theme: 'dark' }); - - expect(status).toBe(200); - expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' })); - }); - - it('should throw an error if a theme is not light or dark', async () => { - for (const theme of ['dark1', true, 123, '', null, undefined]) { - const { status, body } = await request(app) - .get('/map/style.json') - .query({ theme }) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark'])); - } - }); - - it('should return the light style.json', async () => { - const { status, body } = await request(app) - .get('/map/style.json') - .query({ theme: 'light' }) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(200); - expect(body).toEqual(expect.objectContaining({ id: 'immich-map-light' })); - }); - - it('should return the dark style.json', async () => { - const { status, body } = await request(app) - .get('/map/style.json') - .query({ theme: 'dark' }) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(200); - expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' })); - }); - - it('should not require admin authentication', async () => { - const { status, body } = await request(app) - .get('/map/style.json') - .query({ theme: 'dark' }) - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - expect(status).toBe(200); - expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' })); - }); - }); - describe('GET /map/reverse-geocode', () => { it('should require authentication', async () => { const { status, body } = await request(app).get('/map/reverse-geocode'); diff --git a/e2e/src/api/specs/server-info.e2e-spec.ts b/e2e/src/api/specs/server-info.e2e-spec.ts index 571d98cda744e..1ef8d8602ad24 100644 --- a/e2e/src/api/specs/server-info.e2e-spec.ts +++ b/e2e/src/api/specs/server-info.e2e-spec.ts @@ -128,6 +128,8 @@ describe('/server-info', () => { isInitialized: true, externalDomain: '', isOnboarded: false, + mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json', + mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json', }); }); }); diff --git a/e2e/src/api/specs/server.e2e-spec.ts b/e2e/src/api/specs/server.e2e-spec.ts index b19e6d85c4ad0..3133460adaf2a 100644 --- a/e2e/src/api/specs/server.e2e-spec.ts +++ b/e2e/src/api/specs/server.e2e-spec.ts @@ -134,6 +134,8 @@ describe('/server', () => { isInitialized: true, externalDomain: '', isOnboarded: false, + mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json', + mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json', }); }); }); diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json index aa43dab3fb008..ceaf9a6ab88dc 100644 --- a/mobile/.vscode/settings.json +++ b/mobile/.vscode/settings.json @@ -1,5 +1,5 @@ { - "dart.flutterSdkPath": ".fvm/versions/3.24.0", + "dart.flutterSdkPath": ".fvm/versions/3.24.3", "search.exclude": { "**/.fvm": true }, diff --git a/mobile/lib/models/server_info/server_config.model.dart b/mobile/lib/models/server_info/server_config.model.dart index 8936939135d26..f07ffde522f14 100644 --- a/mobile/lib/models/server_info/server_config.model.dart +++ b/mobile/lib/models/server_info/server_config.model.dart @@ -4,11 +4,15 @@ class ServerConfig { final int trashDays; final String oauthButtonText; final String externalDomain; + final String mapDarkStyleUrl; + final String mapLightStyleUrl; const ServerConfig({ required this.trashDays, required this.oauthButtonText, required this.externalDomain, + required this.mapDarkStyleUrl, + required this.mapLightStyleUrl, }); ServerConfig copyWith({ @@ -20,6 +24,8 @@ class ServerConfig { trashDays: trashDays ?? this.trashDays, oauthButtonText: oauthButtonText ?? this.oauthButtonText, externalDomain: externalDomain ?? this.externalDomain, + mapDarkStyleUrl: mapDarkStyleUrl, + mapLightStyleUrl: mapLightStyleUrl, ); } @@ -30,7 +36,9 @@ class ServerConfig { ServerConfig.fromDto(ServerConfigDto dto) : trashDays = dto.trashDays, oauthButtonText = dto.oauthButtonText, - externalDomain = dto.externalDomain; + externalDomain = dto.externalDomain, + mapDarkStyleUrl = dto.mapDarkStyleUrl, + mapLightStyleUrl = dto.mapLightStyleUrl; @override bool operator ==(covariant ServerConfig other) { diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart index d226ea55a36da..3be7e9b3e5374 100644 --- a/mobile/lib/pages/search/map/map.page.dart +++ b/mobile/lib/pages/search/map/map.page.dart @@ -1,4 +1,5 @@ import 'dart:math'; + import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -7,27 +8,27 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:geolocator/geolocator.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/latlngbounds_extension.dart'; import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; import 'package:immich_mobile/models/map/map_event.model.dart'; import 'package:immich_mobile/models/map/map_marker.model.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/map/map_marker.provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/debounce.dart'; +import 'package:immich_mobile/utils/immich_loading_overlay.dart'; import 'package:immich_mobile/utils/map_utils.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/map/map_app_bar.dart'; import 'package:immich_mobile/widgets/map/map_asset_grid.dart'; import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart'; import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/utils/immich_loading_overlay.dart'; -import 'package:immich_mobile/utils/debounce.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() @@ -304,7 +305,7 @@ class MapPage extends HookConsumerWidget { ), Positioned( right: 0, - bottom: MediaQuery.of(context).padding.bottom + 16, + bottom: MediaQuery.paddingOf(context).bottom + 16, child: ElevatedButton( onPressed: onZoomToLocation, style: ElevatedButton.styleFrom( diff --git a/mobile/lib/providers/map/map_state.provider.dart b/mobile/lib/providers/map/map_state.provider.dart index 6d1630bba2e18..189a23cd0aad1 100644 --- a/mobile/lib/providers/map/map_state.provider.dart +++ b/mobile/lib/providers/map/map_state.provider.dart @@ -1,28 +1,23 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/response_extensions.dart'; import 'package:immich_mobile/models/map/map_state.model.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'map_state.provider.g.dart'; @Riverpod(keepAlive: true) class MapStateNotifier extends _$MapStateNotifier { - final _log = Logger("MapStateNotifier"); - @override MapState build() { final appSettingsProvider = ref.read(appSettingsServiceProvider); - // Fetch and save the Style JSONs - loadStyles(); + final lightStyleUrl = + ref.read(serverInfoProvider).serverConfig.mapLightStyleUrl; + final darkStyleUrl = + ref.read(serverInfoProvider).serverConfig.mapDarkStyleUrl; + return MapState( themeMode: ThemeMode.values[ appSettingsProvider.getSetting(AppSettingsEnum.mapThemeMode)], @@ -34,65 +29,11 @@ class MapStateNotifier extends _$MapStateNotifier { appSettingsProvider.getSetting(AppSettingsEnum.mapwithPartners), relativeTime: appSettingsProvider.getSetting(AppSettingsEnum.mapRelativeDate), + lightStyleFetched: AsyncData(lightStyleUrl), + darkStyleFetched: AsyncData(darkStyleUrl), ); } - void loadStyles() async { - final documents = (await getApplicationDocumentsDirectory()).path; - - // Set to loading - state = state.copyWith(lightStyleFetched: const AsyncLoading()); - - // Fetch and save light theme - final lightResponse = await ref - .read(apiServiceProvider) - .mapApi - .getMapStyleWithHttpInfo(MapTheme.light); - - if (lightResponse.statusCode >= HttpStatus.badRequest) { - state = state.copyWith( - lightStyleFetched: AsyncError(lightResponse.body, StackTrace.current), - ); - _log.severe( - "Cannot fetch map light style", - lightResponse.toLoggerString(), - ); - return; - } - - final lightJSON = lightResponse.body; - final lightFile = await File("$documents/map-style-light.json") - .writeAsString(lightJSON, flush: true); - - // Update state with path - state = - state.copyWith(lightStyleFetched: AsyncData(lightFile.absolute.path)); - - // Set to loading - state = state.copyWith(darkStyleFetched: const AsyncLoading()); - - // Fetch and save dark theme - final darkResponse = await ref - .read(apiServiceProvider) - .mapApi - .getMapStyleWithHttpInfo(MapTheme.dark); - - if (darkResponse.statusCode >= HttpStatus.badRequest) { - state = state.copyWith( - darkStyleFetched: AsyncError(darkResponse.body, StackTrace.current), - ); - _log.severe("Cannot fetch map dark style", darkResponse.toLoggerString()); - return; - } - - final darkJSON = darkResponse.body; - final darkFile = await File("$documents/map-style-dark.json") - .writeAsString(darkJSON, flush: true); - - // Update state with path - state = state.copyWith(darkStyleFetched: AsyncData(darkFile.absolute.path)); - } - void switchTheme(ThemeMode mode) { ref.read(appSettingsServiceProvider).setSetting( AppSettingsEnum.mapThemeMode, diff --git a/mobile/lib/providers/server_info.provider.dart b/mobile/lib/providers/server_info.provider.dart index 6327f992f5cd0..14521b06f64ca 100644 --- a/mobile/lib/providers/server_info.provider.dart +++ b/mobile/lib/providers/server_info.provider.dart @@ -34,6 +34,9 @@ class ServerInfoNotifier extends StateNotifier { trashDays: 30, oauthButtonText: '', externalDomain: '', + mapLightStyleUrl: + 'https://tiles.immich.cloud/v1/style/light.json', + mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json', ), serverDiskInfo: const ServerDiskInfo( diskAvailable: "0", diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index c473fbb8333c1..255ad01247aa0 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -12,6 +12,19 @@ dynamic upgradeDto(dynamic value, String targetType) { addDefault(value, 'tags', TagsResponse().toJson()); } break; + case 'ServerConfigDto': + if (value is Map) { + addDefault( + value, + 'mapLightStyleUrl', + 'https://tiles.immich.cloud/v1/style/light.json', + ); + addDefault( + value, + 'mapDarkStyleUrl', + 'https://tiles.immich.cloud/v1/style/dark.json', + ); + } case 'UserResponseDto': if (value is Map) { addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String()); diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index c8135519def56..285514e11cd54 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -138,7 +138,6 @@ Class | Method | HTTP request | Description *LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} | *LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | *MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | -*MapApi* | [**getMapStyle**](doc//MapApi.md#getmapstyle) | **GET** /map/style.json | *MapApi* | [**reverseGeocode**](doc//MapApi.md#reversegeocode) | **GET** /map/reverse-geocode | *MemoriesApi* | [**addMemoryAssets**](doc//MemoriesApi.md#addmemoryassets) | **PUT** /memories/{id}/assets | *MemoriesApi* | [**createMemory**](doc//MemoriesApi.md#creatememory) | **POST** /memories | @@ -348,7 +347,6 @@ Class | Method | HTTP request | Description - [ManualJobName](doc//ManualJobName.md) - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) - [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md) - - [MapTheme](doc//MapTheme.md) - [MemoriesResponse](doc//MemoriesResponse.md) - [MemoriesUpdate](doc//MemoriesUpdate.md) - [MemoryCreateDto](doc//MemoryCreateDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 7fa06b04875ee..fc0224a8c2072 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -159,7 +159,6 @@ part 'model/logout_response_dto.dart'; part 'model/manual_job_name.dart'; part 'model/map_marker_response_dto.dart'; part 'model/map_reverse_geocode_response_dto.dart'; -part 'model/map_theme.dart'; part 'model/memories_response.dart'; part 'model/memories_update.dart'; part 'model/memory_create_dto.dart'; diff --git a/mobile/openapi/lib/api/map_api.dart b/mobile/openapi/lib/api/map_api.dart index 2846dae6c3582..9644fbfc5c08b 100644 --- a/mobile/openapi/lib/api/map_api.dart +++ b/mobile/openapi/lib/api/map_api.dart @@ -105,62 +105,6 @@ class MapApi { return null; } - /// Performs an HTTP 'GET /map/style.json' operation and returns the [Response]. - /// Parameters: - /// - /// * [MapTheme] theme (required): - /// - /// * [String] key: - Future getMapStyleWithHttpInfo(MapTheme theme, { String? key, }) async { - // ignore: prefer_const_declarations - final path = r'/map/style.json'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - queryParams.addAll(_queryParams('', 'theme', theme)); - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [MapTheme] theme (required): - /// - /// * [String] key: - Future getMapStyle(MapTheme theme, { String? key, }) async { - final response = await getMapStyleWithHttpInfo(theme, key: key, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'Object',) as Object; - - } - return null; - } - /// Performs an HTTP 'GET /map/reverse-geocode' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index e857f51e3a875..828c0b9ed925c 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -372,8 +372,6 @@ class ApiClient { return MapMarkerResponseDto.fromJson(value); case 'MapReverseGeocodeResponseDto': return MapReverseGeocodeResponseDto.fromJson(value); - case 'MapTheme': - return MapThemeTypeTransformer().decode(value); case 'MemoriesResponse': return MemoriesResponse.fromJson(value); case 'MemoriesUpdate': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 0f3cc41097276..b7c6ad5e010d3 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -100,9 +100,6 @@ String parameterToString(dynamic value) { if (value is ManualJobName) { return ManualJobNameTypeTransformer().encode(value).toString(); } - if (value is MapTheme) { - return MapThemeTypeTransformer().encode(value).toString(); - } if (value is MemoryType) { return MemoryTypeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/map_theme.dart b/mobile/openapi/lib/model/map_theme.dart deleted file mode 100644 index e2553790c6cc1..0000000000000 --- a/mobile/openapi/lib/model/map_theme.dart +++ /dev/null @@ -1,85 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - - -class MapTheme { - /// Instantiate a new enum with the provided [value]. - const MapTheme._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const light = MapTheme._(r'light'); - static const dark = MapTheme._(r'dark'); - - /// List of all possible values in this [enum][MapTheme]. - static const values = [ - light, - dark, - ]; - - static MapTheme? fromJson(dynamic value) => MapThemeTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = MapTheme.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [MapTheme] to String, -/// and [decode] dynamic data back to [MapTheme]. -class MapThemeTypeTransformer { - factory MapThemeTypeTransformer() => _instance ??= const MapThemeTypeTransformer._(); - - const MapThemeTypeTransformer._(); - - String encode(MapTheme data) => data.value; - - /// Decodes a [dynamic value][data] to a MapTheme. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - MapTheme? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'light': return MapTheme.light; - case r'dark': return MapTheme.dark; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [MapThemeTypeTransformer] instance. - static MapThemeTypeTransformer? _instance; -} - diff --git a/mobile/openapi/lib/model/server_config_dto.dart b/mobile/openapi/lib/model/server_config_dto.dart index c45ed32ac076b..bd5c2405e29d3 100644 --- a/mobile/openapi/lib/model/server_config_dto.dart +++ b/mobile/openapi/lib/model/server_config_dto.dart @@ -17,6 +17,8 @@ class ServerConfigDto { required this.isInitialized, required this.isOnboarded, required this.loginPageMessage, + required this.mapDarkStyleUrl, + required this.mapLightStyleUrl, required this.oauthButtonText, required this.trashDays, required this.userDeleteDelay, @@ -30,6 +32,10 @@ class ServerConfigDto { String loginPageMessage; + String mapDarkStyleUrl; + + String mapLightStyleUrl; + String oauthButtonText; int trashDays; @@ -42,6 +48,8 @@ class ServerConfigDto { other.isInitialized == isInitialized && other.isOnboarded == isOnboarded && other.loginPageMessage == loginPageMessage && + other.mapDarkStyleUrl == mapDarkStyleUrl && + other.mapLightStyleUrl == mapLightStyleUrl && other.oauthButtonText == oauthButtonText && other.trashDays == trashDays && other.userDeleteDelay == userDeleteDelay; @@ -53,12 +61,14 @@ class ServerConfigDto { (isInitialized.hashCode) + (isOnboarded.hashCode) + (loginPageMessage.hashCode) + + (mapDarkStyleUrl.hashCode) + + (mapLightStyleUrl.hashCode) + (oauthButtonText.hashCode) + (trashDays.hashCode) + (userDeleteDelay.hashCode); @override - String toString() => 'ServerConfigDto[externalDomain=$externalDomain, isInitialized=$isInitialized, isOnboarded=$isOnboarded, loginPageMessage=$loginPageMessage, oauthButtonText=$oauthButtonText, trashDays=$trashDays, userDeleteDelay=$userDeleteDelay]'; + String toString() => 'ServerConfigDto[externalDomain=$externalDomain, isInitialized=$isInitialized, isOnboarded=$isOnboarded, loginPageMessage=$loginPageMessage, mapDarkStyleUrl=$mapDarkStyleUrl, mapLightStyleUrl=$mapLightStyleUrl, oauthButtonText=$oauthButtonText, trashDays=$trashDays, userDeleteDelay=$userDeleteDelay]'; Map toJson() { final json = {}; @@ -66,6 +76,8 @@ class ServerConfigDto { json[r'isInitialized'] = this.isInitialized; json[r'isOnboarded'] = this.isOnboarded; json[r'loginPageMessage'] = this.loginPageMessage; + json[r'mapDarkStyleUrl'] = this.mapDarkStyleUrl; + json[r'mapLightStyleUrl'] = this.mapLightStyleUrl; json[r'oauthButtonText'] = this.oauthButtonText; json[r'trashDays'] = this.trashDays; json[r'userDeleteDelay'] = this.userDeleteDelay; @@ -85,6 +97,8 @@ class ServerConfigDto { isInitialized: mapValueOfType(json, r'isInitialized')!, isOnboarded: mapValueOfType(json, r'isOnboarded')!, loginPageMessage: mapValueOfType(json, r'loginPageMessage')!, + mapDarkStyleUrl: mapValueOfType(json, r'mapDarkStyleUrl')!, + mapLightStyleUrl: mapValueOfType(json, r'mapLightStyleUrl')!, oauthButtonText: mapValueOfType(json, r'oauthButtonText')!, trashDays: mapValueOfType(json, r'trashDays')!, userDeleteDelay: mapValueOfType(json, r'userDeleteDelay')!, @@ -139,6 +153,8 @@ class ServerConfigDto { 'isInitialized', 'isOnboarded', 'loginPageMessage', + 'mapDarkStyleUrl', + 'mapLightStyleUrl', 'oauthButtonText', 'trashDays', 'userDeleteDelay', diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 706ff5b8fb654..4e7c7119781fe 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -3167,55 +3167,6 @@ ] } }, - "/map/style.json": { - "get": { - "operationId": "getMapStyle", - "parameters": [ - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "theme", - "required": true, - "in": "query", - "schema": { - "$ref": "#/components/schemas/MapTheme" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Map" - ] - } - }, "/memories": { "get": { "operationId": "searchMemories", @@ -5356,8 +5307,8 @@ "name": "password", "required": false, "in": "query", - "example": "password", "schema": { + "example": "password", "type": "string" } }, @@ -9695,13 +9646,6 @@ ], "type": "object" }, - "MapTheme": { - "enum": [ - "light", - "dark" - ], - "type": "string" - }, "MemoriesResponse": { "properties": { "enabled": { @@ -10917,6 +10861,12 @@ "loginPageMessage": { "type": "string" }, + "mapDarkStyleUrl": { + "type": "string" + }, + "mapLightStyleUrl": { + "type": "string" + }, "oauthButtonText": { "type": "string" }, @@ -10932,6 +10882,8 @@ "isInitialized", "isOnboarded", "loginPageMessage", + "mapDarkStyleUrl", + "mapLightStyleUrl", "oauthButtonText", "trashDays", "userDeleteDelay" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 8e607f7570856..d1b88afabb043 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -928,6 +928,8 @@ export type ServerConfigDto = { isInitialized: boolean; isOnboarded: boolean; loginPageMessage: string; + mapDarkStyleUrl: string; + mapLightStyleUrl: string; oauthButtonText: string; trashDays: number; userDeleteDelay: number; @@ -2138,20 +2140,6 @@ export function reverseGeocode({ lat, lon }: { ...opts })); } -export function getMapStyle({ key, theme }: { - key?: string; - theme: MapTheme; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: object; - }>(`/map/style.json${QS.query(QS.explode({ - key, - theme - }))}`, { - ...opts - })); -} export function searchMemories(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -3469,10 +3457,6 @@ export enum JobCommand { Empty = "empty", ClearFailed = "clear-failed" } -export enum MapTheme { - Light = "light", - Dark = "dark" -} export enum MemoryType { OnThisDay = "on_this_day" } diff --git a/server/package-lock.json b/server/package-lock.json index ee432b9e065ff..9abfc6b5ce70f 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -733,9 +733,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], @@ -749,9 +749,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -765,9 +765,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -781,9 +781,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -797,9 +797,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -813,9 +813,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -829,9 +829,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -845,9 +845,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -861,9 +861,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -877,9 +877,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -893,9 +893,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -909,9 +909,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -925,9 +925,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -941,9 +941,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -957,9 +957,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -973,9 +973,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -989,9 +989,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -1005,9 +1005,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -1021,9 +1021,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -1037,9 +1037,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -1053,9 +1053,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -1069,9 +1069,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -1085,9 +1085,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -2085,16 +2085,16 @@ } }, "node_modules/@nestjs/core": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.1.tgz", - "integrity": "sha512-9I1WdfOBCCHdUm+ClBJupOuZQS6UxzIWHIq6Vp1brAA5ZKl/Wq6BVwSsbnUJGBy3J3PM2XHmR0EQ4fwX3nR7lA==", + "version": "10.4.4", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.4.tgz", + "integrity": "sha512-y9tjmAzU6LTh1cC/lWrRsCcOd80khSR0qAHAqwY2svbW+AhsR/XCzgpZrAAKJrm/dDfjLCZKyxJSayeirGcW5Q==", "hasInstallScript": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", - "path-to-regexp": "3.2.0", - "tslib": "2.6.3", + "path-to-regexp": "3.3.0", + "tslib": "2.7.0", "uid": "2.0.2" }, "funding": { @@ -2121,6 +2121,11 @@ } } }, + "node_modules/@nestjs/core/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, "node_modules/@nestjs/event-emitter": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.0.4.tgz", @@ -2153,15 +2158,15 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.1.tgz", - "integrity": "sha512-ccfqIDAq/bg1ShLI5KGtaLaYGykuAdvCi57ohewH7eKJSIpWY1DQjbgKlFfXokALYUq1YOMGqjeZ244OWHfDQg==", + "version": "10.4.4", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.4.tgz", + "integrity": "sha512-y52q1MxhbHaT3vAgWd08RgiYon0lJgtTa8U6g6gV0KI0IygwZhDQFJVxnrRDUdxQGIP5CKHmfQu3sk9gTNFoEA==", "dependencies": { - "body-parser": "1.20.2", + "body-parser": "1.20.3", "cors": "2.8.5", - "express": "4.19.2", + "express": "4.21.0", "multer": "1.4.4-lts.1", - "tslib": "2.6.3" + "tslib": "2.7.0" }, "funding": { "type": "opencollective", @@ -2172,6 +2177,11 @@ "@nestjs/core": "^10.0.0" } }, + "node_modules/@nestjs/platform-express/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, "node_modules/@nestjs/platform-socket.io": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.1.tgz", @@ -2238,15 +2248,15 @@ "dev": true }, "node_modules/@nestjs/swagger": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.0.tgz", - "integrity": "sha512-dCiwKkRxcR7dZs5jtrGspBAe/nqJd1AYzOBTzw9iCdbq3BGrLpwokelk6lFZPe4twpTsPQqzNKBwKzVbI6AR/g==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.2.tgz", + "integrity": "sha512-Mu6TEn1M/owIvAx2B4DUQObQXqo2028R2s9rSZ/hJEgBK95+doTwS0DjmVA2wTeZTyVtXOoN7CsoM5pONBzvKQ==", "dependencies": { "@microsoft/tsdoc": "^0.15.0", "@nestjs/mapped-types": "2.0.5", "js-yaml": "4.1.0", "lodash": "4.17.21", - "path-to-regexp": "3.2.0", + "path-to-regexp": "3.3.0", "swagger-ui-dist": "5.17.14" }, "peerDependencies": { @@ -4551,9 +4561,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.3.tgz", - "integrity": "sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", "cpu": [ "arm" ], @@ -4564,9 +4574,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.3.tgz", - "integrity": "sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", "cpu": [ "arm64" ], @@ -4577,9 +4587,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz", - "integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", "cpu": [ "arm64" ], @@ -4590,9 +4600,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.3.tgz", - "integrity": "sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", "cpu": [ "x64" ], @@ -4603,9 +4613,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.3.tgz", - "integrity": "sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", "cpu": [ "arm" ], @@ -4616,9 +4626,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.14.3.tgz", - "integrity": "sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", "cpu": [ "arm" ], @@ -4629,9 +4639,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz", - "integrity": "sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", "cpu": [ "arm64" ], @@ -4642,9 +4652,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.3.tgz", - "integrity": "sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", "cpu": [ "arm64" ], @@ -4655,9 +4665,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.3.tgz", - "integrity": "sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", "cpu": [ "ppc64" ], @@ -4668,9 +4678,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.3.tgz", - "integrity": "sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", "cpu": [ "riscv64" ], @@ -4681,9 +4691,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.3.tgz", - "integrity": "sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", "cpu": [ "s390x" ], @@ -4694,9 +4704,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz", - "integrity": "sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", "cpu": [ "x64" ], @@ -4707,9 +4717,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.3.tgz", - "integrity": "sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", "cpu": [ "x64" ], @@ -4720,9 +4730,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.3.tgz", - "integrity": "sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", "cpu": [ "arm64" ], @@ -4733,9 +4743,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.3.tgz", - "integrity": "sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", "cpu": [ "ia32" ], @@ -4746,9 +4756,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.3.tgz", - "integrity": "sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", "cpu": [ "x64" ], @@ -6689,9 +6699,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -6701,7 +6711,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -7995,9 +8005,9 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -8012,9 +8022,9 @@ } }, "node_modules/engine.io": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2.tgz", - "integrity": "sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -8025,7 +8035,7 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0" + "ws": "~8.17.1" }, "engines": { "node": ">=10.2.0" @@ -8097,9 +8107,9 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { @@ -8109,29 +8119,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/escalade": { @@ -8524,36 +8534,36 @@ ] }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -8586,9 +8596,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/extend": { "version": "3.0.2", @@ -8719,12 +8729,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -10281,9 +10291,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -10308,11 +10321,11 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -10928,9 +10941,12 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11247,9 +11263,9 @@ } }, "node_modules/path-to-regexp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", - "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==" + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==" }, "node_modules/path-type": { "version": "4.0.0", @@ -11384,9 +11400,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" }, "node_modules/picomatch": { "version": "4.0.2", @@ -11433,9 +11449,9 @@ "integrity": "sha512-3hTIM2j/v9Lio+wOyur3kckD4NxruZhpowUbEgmyikW+a2Kppjtu1eN+AhnMQtoHW46zld88JiYWv6fxpsDrTQ==" }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "funding": [ { "type": "opencollective", @@ -11452,8 +11468,8 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -11806,11 +11822,11 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -12862,9 +12878,9 @@ } }, "node_modules/rollup": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz", - "integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -12877,22 +12893,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.14.3", - "@rollup/rollup-android-arm64": "4.14.3", - "@rollup/rollup-darwin-arm64": "4.14.3", - "@rollup/rollup-darwin-x64": "4.14.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.14.3", - "@rollup/rollup-linux-arm-musleabihf": "4.14.3", - "@rollup/rollup-linux-arm64-gnu": "4.14.3", - "@rollup/rollup-linux-arm64-musl": "4.14.3", - "@rollup/rollup-linux-powerpc64le-gnu": "4.14.3", - "@rollup/rollup-linux-riscv64-gnu": "4.14.3", - "@rollup/rollup-linux-s390x-gnu": "4.14.3", - "@rollup/rollup-linux-x64-gnu": "4.14.3", - "@rollup/rollup-linux-x64-musl": "4.14.3", - "@rollup/rollup-win32-arm64-msvc": "4.14.3", - "@rollup/rollup-win32-ia32-msvc": "4.14.3", - "@rollup/rollup-win32-x64-msvc": "4.14.3", + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", "fsevents": "~2.3.2" } }, @@ -13047,9 +13063,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -13082,6 +13098,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -13092,14 +13116,14 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -13228,13 +13252,17 @@ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13314,26 +13342,6 @@ "ws": "~8.17.1" } }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -13356,9 +13364,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -13820,9 +13828,9 @@ } }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -14794,14 +14802,14 @@ } }, "node_modules/vite": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", - "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "version": "5.4.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz", + "integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==", "dev": true, "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -14820,6 +14828,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -14837,6 +14846,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -15167,15 +15179,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -15779,163 +15791,163 @@ } }, "@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "dev": true, "optional": true }, "@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "dev": true, "optional": true }, @@ -16520,16 +16532,23 @@ } }, "@nestjs/core": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.1.tgz", - "integrity": "sha512-9I1WdfOBCCHdUm+ClBJupOuZQS6UxzIWHIq6Vp1brAA5ZKl/Wq6BVwSsbnUJGBy3J3PM2XHmR0EQ4fwX3nR7lA==", + "version": "10.4.4", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.4.tgz", + "integrity": "sha512-y9tjmAzU6LTh1cC/lWrRsCcOd80khSR0qAHAqwY2svbW+AhsR/XCzgpZrAAKJrm/dDfjLCZKyxJSayeirGcW5Q==", "requires": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", - "path-to-regexp": "3.2.0", - "tslib": "2.6.3", + "path-to-regexp": "3.3.0", + "tslib": "2.7.0", "uid": "2.0.2" + }, + "dependencies": { + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + } } }, "@nestjs/event-emitter": { @@ -16547,15 +16566,22 @@ "requires": {} }, "@nestjs/platform-express": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.1.tgz", - "integrity": "sha512-ccfqIDAq/bg1ShLI5KGtaLaYGykuAdvCi57ohewH7eKJSIpWY1DQjbgKlFfXokALYUq1YOMGqjeZ244OWHfDQg==", + "version": "10.4.4", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.4.tgz", + "integrity": "sha512-y52q1MxhbHaT3vAgWd08RgiYon0lJgtTa8U6g6gV0KI0IygwZhDQFJVxnrRDUdxQGIP5CKHmfQu3sk9gTNFoEA==", "requires": { - "body-parser": "1.20.2", + "body-parser": "1.20.3", "cors": "2.8.5", - "express": "4.19.2", + "express": "4.21.0", "multer": "1.4.4-lts.1", - "tslib": "2.6.3" + "tslib": "2.7.0" + }, + "dependencies": { + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + } } }, "@nestjs/platform-socket.io": { @@ -16605,15 +16631,15 @@ } }, "@nestjs/swagger": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.0.tgz", - "integrity": "sha512-dCiwKkRxcR7dZs5jtrGspBAe/nqJd1AYzOBTzw9iCdbq3BGrLpwokelk6lFZPe4twpTsPQqzNKBwKzVbI6AR/g==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.2.tgz", + "integrity": "sha512-Mu6TEn1M/owIvAx2B4DUQObQXqo2028R2s9rSZ/hJEgBK95+doTwS0DjmVA2wTeZTyVtXOoN7CsoM5pONBzvKQ==", "requires": { "@microsoft/tsdoc": "^0.15.0", "@nestjs/mapped-types": "2.0.5", "js-yaml": "4.1.0", "lodash": "4.17.21", - "path-to-regexp": "3.2.0", + "path-to-regexp": "3.3.0", "swagger-ui-dist": "5.17.14" } }, @@ -18061,114 +18087,114 @@ } }, "@rollup/rollup-android-arm-eabi": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.3.tgz", - "integrity": "sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", "dev": true, "optional": true }, "@rollup/rollup-android-arm64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.3.tgz", - "integrity": "sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", "dev": true, "optional": true }, "@rollup/rollup-darwin-arm64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz", - "integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", "dev": true, "optional": true }, "@rollup/rollup-darwin-x64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.3.tgz", - "integrity": "sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.3.tgz", - "integrity": "sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm-musleabihf": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.14.3.tgz", - "integrity": "sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz", - "integrity": "sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-musl": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.3.tgz", - "integrity": "sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", "dev": true, "optional": true }, "@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.3.tgz", - "integrity": "sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", "dev": true, "optional": true }, "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.3.tgz", - "integrity": "sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", "dev": true, "optional": true }, "@rollup/rollup-linux-s390x-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.3.tgz", - "integrity": "sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", "dev": true, "optional": true }, "@rollup/rollup-linux-x64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz", - "integrity": "sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", "dev": true, "optional": true }, "@rollup/rollup-linux-x64-musl": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.3.tgz", - "integrity": "sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", "dev": true, "optional": true }, "@rollup/rollup-win32-arm64-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.3.tgz", - "integrity": "sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", "dev": true, "optional": true }, "@rollup/rollup-win32-ia32-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.3.tgz", - "integrity": "sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", "dev": true, "optional": true }, "@rollup/rollup-win32-x64-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.3.tgz", - "integrity": "sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", "dev": true, "optional": true }, @@ -19691,9 +19717,9 @@ } }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -19703,7 +19729,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -20626,9 +20652,9 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, "end-of-stream": { "version": "1.4.4", @@ -20640,9 +20666,9 @@ } }, "engine.io": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2.tgz", - "integrity": "sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "requires": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -20653,7 +20679,7 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0" + "ws": "~8.17.1" } }, "engine.io-parser": { @@ -20704,34 +20730,34 @@ "dev": true }, "esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "requires": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "escalade": { @@ -20995,36 +21021,36 @@ "optional": true }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -21051,9 +21077,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" } } }, @@ -21167,12 +21193,12 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -22320,9 +22346,9 @@ } }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "merge-stream": { "version": "2.0.0", @@ -22341,11 +22367,11 @@ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "dependencies": { @@ -22784,9 +22810,9 @@ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" }, "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, "obuf": { "version": "1.1.2", @@ -23022,9 +23048,9 @@ } }, "path-to-regexp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", - "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==" + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==" }, "path-type": { "version": "4.0.0", @@ -23123,9 +23149,9 @@ } }, "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" }, "picomatch": { "version": "4.0.2", @@ -23156,13 +23182,13 @@ "integrity": "sha512-3hTIM2j/v9Lio+wOyur3kckD4NxruZhpowUbEgmyikW+a2Kppjtu1eN+AhnMQtoHW46zld88JiYWv6fxpsDrTQ==" }, "postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "requires": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" } }, "postcss-import": { @@ -23389,11 +23415,11 @@ "dev": true }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "queue-microtask": { @@ -24051,27 +24077,27 @@ } }, "rollup": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz", - "integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dev": true, "requires": { - "@rollup/rollup-android-arm-eabi": "4.14.3", - "@rollup/rollup-android-arm64": "4.14.3", - "@rollup/rollup-darwin-arm64": "4.14.3", - "@rollup/rollup-darwin-x64": "4.14.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.14.3", - "@rollup/rollup-linux-arm-musleabihf": "4.14.3", - "@rollup/rollup-linux-arm64-gnu": "4.14.3", - "@rollup/rollup-linux-arm64-musl": "4.14.3", - "@rollup/rollup-linux-powerpc64le-gnu": "4.14.3", - "@rollup/rollup-linux-riscv64-gnu": "4.14.3", - "@rollup/rollup-linux-s390x-gnu": "4.14.3", - "@rollup/rollup-linux-x64-gnu": "4.14.3", - "@rollup/rollup-linux-x64-musl": "4.14.3", - "@rollup/rollup-win32-arm64-msvc": "4.14.3", - "@rollup/rollup-win32-ia32-msvc": "4.14.3", - "@rollup/rollup-win32-x64-msvc": "4.14.3", + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", "@types/estree": "1.0.5", "fsevents": "~2.3.2" } @@ -24176,9 +24202,9 @@ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -24209,6 +24235,11 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" } } }, @@ -24222,14 +24253,14 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" } }, "set-blocking": { @@ -24332,13 +24363,14 @@ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "siginfo": { @@ -24403,14 +24435,6 @@ "requires": { "debug": "~4.3.4", "ws": "~8.17.1" - }, - "dependencies": { - "ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "requires": {} - } } }, "socket.io-parser": { @@ -24429,9 +24453,9 @@ "dev": true }, "source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" }, "source-map-support": { "version": "0.5.21", @@ -24766,9 +24790,9 @@ "dev": true }, "tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -25399,15 +25423,15 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, "vite": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", - "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "version": "5.4.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz", + "integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==", "dev": true, "requires": { - "esbuild": "^0.20.1", + "esbuild": "^0.21.3", "fsevents": "~2.3.3", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" } }, "vite-node": { @@ -25627,9 +25651,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "requires": {} }, "xtend": { diff --git a/server/src/config.ts b/server/src/config.ts index 057c9a69e213c..03ea3f111b9ac 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -285,8 +285,8 @@ export const defaults = Object.freeze({ }, map: { enabled: true, - lightStyle: '', - darkStyle: '', + lightStyle: 'https://tiles.immich.cloud/v1/style/light.json', + darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json', }, reverseGeocoding: { enabled: true, diff --git a/server/src/controllers/map.controller.ts b/server/src/controllers/map.controller.ts index d6c26c58a073c..88104e6b588d8 100644 --- a/server/src/controllers/map.controller.ts +++ b/server/src/controllers/map.controller.ts @@ -7,7 +7,6 @@ import { MapReverseGeocodeDto, MapReverseGeocodeResponseDto, } from 'src/dtos/map.dto'; -import { MapThemeDto } from 'src/dtos/system-config.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { MapService } from 'src/services/map.service'; @@ -22,12 +21,6 @@ export class MapController { return this.service.getMapMarkers(auth, options); } - @Authenticated({ sharedLink: true }) - @Get('style.json') - getMapStyle(@Query() dto: MapThemeDto) { - return this.service.getMapStyle(dto.theme); - } - @Authenticated() @Get('reverse-geocode') @HttpCode(HttpStatus.OK) diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index 78e59e4d1a695..aafadff47888f 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -121,6 +121,8 @@ export class ServerConfigDto { isInitialized!: boolean; isOnboarded!: boolean; externalDomain!: string; + mapDarkStyleUrl!: string; + mapLightStyleUrl!: string; } export class ServerFeaturesDto { diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 14027aa16ad32..336f50f39bc8c 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -296,10 +296,12 @@ class SystemConfigMapDto { @ValidateBoolean() enabled!: boolean; - @IsString() + @IsNotEmpty() + @IsUrl() lightStyle!: string; - @IsString() + @IsNotEmpty() + @IsUrl() darkStyle!: string; } diff --git a/server/src/services/map.service.ts b/server/src/services/map.service.ts index ffd84a3e02bf6..5836505e54893 100644 --- a/server/src/services/map.service.ts +++ b/server/src/services/map.service.ts @@ -43,17 +43,6 @@ export class MapService { return this.mapRepository.getMapMarkers(userIds, albumIds, options); } - async getMapStyle(theme: 'light' | 'dark') { - const { map } = await this.configCore.getConfig({ withCache: false }); - const styleUrl = theme === 'dark' ? map.darkStyle : map.lightStyle; - - if (styleUrl) { - return this.mapRepository.fetchStyle(styleUrl); - } - - return JSON.parse(await this.systemMetadataRepository.readFile(`./resources/style-${theme}.json`)); - } - async reverseGeocode(dto: MapReverseGeocodeDto) { const { lat: latitude, lon: longitude } = dto; // eventually this should probably return an array of results diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index ac899f7b13ba8..4e6a8972b008c 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -186,6 +186,8 @@ describe(ServerService.name, () => { isInitialized: undefined, isOnboarded: false, externalDomain: '', + mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json', + mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json', }); expect(systemMock.get).toHaveBeenCalled(); }); diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index e57a206765f96..9db90e41b3c58 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -129,6 +129,8 @@ export class ServerService { isInitialized, isOnboarded: onboarding?.isOnboarded || false, externalDomain: config.server.externalDomain, + mapDarkStyleUrl: config.map.darkStyle, + mapLightStyleUrl: config.map.lightStyle, }; } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 7e25e0cd46c8c..52ad6d276b94e 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -100,8 +100,8 @@ const updatedConfig = Object.freeze({ }, map: { enabled: true, - lightStyle: '', - darkStyle: '', + lightStyle: 'https://tiles.immich.cloud/v1/style/light.json', + darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json', }, reverseGeocoding: { enabled: true, diff --git a/web/src/lib/components/shared-components/map/map.svelte b/web/src/lib/components/shared-components/map/map.svelte index 83ea3016fd48e..4f60131d69652 100644 --- a/web/src/lib/components/shared-components/map/map.svelte +++ b/web/src/lib/components/shared-components/map/map.svelte @@ -6,8 +6,8 @@ import Icon from '$lib/components/elements/icon.svelte'; import { Theme } from '$lib/constants'; import { colorTheme, mapSettings } from '$lib/stores/preferences.store'; - import { getAssetThumbnailUrl, getKey, handlePromiseError } from '$lib/utils'; - import { getMapStyle, MapTheme, type MapMarkerResponseDto } from '@immich/sdk'; + import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils'; + import { getServerConfig, type MapMarkerResponseDto } from '@immich/sdk'; import mapboxRtlUrl from '@mapbox/mapbox-gl-rtl-text?url'; import { mdiCog, mdiMap, mdiMapMarker } from '@mdi/js'; import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson'; @@ -57,11 +57,13 @@ let map: maplibregl.Map; let marker: maplibregl.Marker | null = null; - $: style = (() => - getMapStyle({ - theme: ($mapSettings.allowDarkMode ? $colorTheme.value : Theme.LIGHT) as unknown as MapTheme, - key: getKey(), - }) as Promise)(); + $: style = (async () => { + const config = await getServerConfig(); + const theme = $mapSettings.allowDarkMode ? $colorTheme.value : Theme.LIGHT; + const styleUrl = theme === Theme.DARK ? config.mapDarkStyleUrl : config.mapLightStyleUrl; + const style = await fetch(styleUrl).then((response) => response.json()); + return style as StyleSpecification; + })(); function handleAssetClick(assetId: string, map: Map | null) { if (!map) { diff --git a/web/src/lib/stores/server-config.store.ts b/web/src/lib/stores/server-config.store.ts index 14d1e4e66e895..358765fe0b111 100644 --- a/web/src/lib/stores/server-config.store.ts +++ b/web/src/lib/stores/server-config.store.ts @@ -32,6 +32,8 @@ export const serverConfig = writable({ isInitialized: false, isOnboarded: false, externalDomain: '', + mapDarkStyleUrl: '', + mapLightStyleUrl: '', }); export const retrieveServerConfig = async () => { From ec32a9e6109342bcb9871eac0fa22bb055cfb3af Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Tue, 24 Sep 2024 04:03:59 +0200 Subject: [PATCH 47/57] fix: set min values for face detection to reasonable values (#12877) fix: set min values for face detection to >0 --- mobile/openapi/lib/model/facial_recognition_config.dart | 4 ++-- open-api/immich-openapi-specs.json | 4 ++-- server/src/dtos/model-config.dto.ts | 4 ++-- .../machine-learning-settings.svelte | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mobile/openapi/lib/model/facial_recognition_config.dart b/mobile/openapi/lib/model/facial_recognition_config.dart index 4acfd4e20ff17..439efbbfaeeb1 100644 --- a/mobile/openapi/lib/model/facial_recognition_config.dart +++ b/mobile/openapi/lib/model/facial_recognition_config.dart @@ -22,14 +22,14 @@ class FacialRecognitionConfig { bool enabled; - /// Minimum value: 0 + /// Minimum value: 0.1 /// Maximum value: 2 double maxDistance; /// Minimum value: 1 int minFaces; - /// Minimum value: 0 + /// Minimum value: 0.1 /// Maximum value: 1 double minScore; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 4e7c7119781fe..99ea313063fce 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9119,7 +9119,7 @@ "maxDistance": { "format": "double", "maximum": 2, - "minimum": 0, + "minimum": 0.1, "type": "number" }, "minFaces": { @@ -9129,7 +9129,7 @@ "minScore": { "format": "double", "maximum": 1, - "minimum": 0, + "minimum": 0.1, "type": "number" }, "modelName": { diff --git a/server/src/dtos/model-config.dto.ts b/server/src/dtos/model-config.dto.ts index dffacc793d57b..f8b9e2043f3ea 100644 --- a/server/src/dtos/model-config.dto.ts +++ b/server/src/dtos/model-config.dto.ts @@ -27,14 +27,14 @@ export class DuplicateDetectionConfig extends TaskConfig { export class FacialRecognitionConfig extends ModelConfig { @IsNumber() - @Min(0) + @Min(0.1) @Max(1) @Type(() => Number) @ApiProperty({ type: 'number', format: 'double' }) minScore!: number; @IsNumber() - @Min(0) + @Min(0.1) @Max(2) @Type(() => Number) @ApiProperty({ type: 'number', format: 'double' }) diff --git a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte index 05a5224bd022b..aac8cd52123be 100644 --- a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte +++ b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte @@ -145,7 +145,7 @@ desc={$t('admin.machine_learning_min_detection_score_description')} bind:value={config.machineLearning.facialRecognition.minScore} step="0.1" - min={0} + min={0.1} max={1} disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled} isEdited={config.machineLearning.facialRecognition.minScore !== @@ -158,7 +158,7 @@ desc={$t('admin.machine_learning_max_recognition_distance_description')} bind:value={config.machineLearning.facialRecognition.maxDistance} step="0.1" - min={0} + min={0.1} max={2} disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled} isEdited={config.machineLearning.facialRecognition.maxDistance !== From 56f680ce04506f7969104a8866eaca330602af3a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:05:04 -0400 Subject: [PATCH 48/57] chore(deps): update typescript-projects (#12882) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/package-lock.json | 200 ++++++------- docs/package-lock.json | 6 +- e2e/package-lock.json | 424 +++++++++++++-------------- server/package-lock.json | 607 ++++++++++++++++++++------------------- web/package-lock.json | 258 ++++++++--------- 5 files changed, 718 insertions(+), 777 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index f74e86a385095..6e148fbe09a13 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1353,17 +1353,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz", - "integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz", + "integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/type-utils": "8.5.0", - "@typescript-eslint/utils": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/type-utils": "8.6.0", + "@typescript-eslint/utils": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1387,16 +1387,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz", - "integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz", + "integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/typescript-estree": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "debug": "^4.3.4" }, "engines": { @@ -1416,14 +1416,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz", - "integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz", + "integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0" + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1434,14 +1434,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz", - "integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz", + "integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.5.0", - "@typescript-eslint/utils": "8.5.0", + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/utils": "8.6.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1459,9 +1459,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz", - "integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz", + "integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==", "dev": true, "license": "MIT", "engines": { @@ -1473,14 +1473,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz", - "integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz", + "integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1502,16 +1502,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz", - "integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz", + "integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/typescript-estree": "8.5.0" + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1525,13 +1525,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz", - "integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz", + "integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/types": "8.6.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -1543,9 +1543,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.0.tgz", - "integrity": "sha512-yqCkr2nrV4o58VcVMxTVkS6Ggxzy7pmSD8JbTbhbH5PsQfUIES1QT716VUzo33wf2lX9EcWYdT3Vl2MMmjR59g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz", + "integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1566,8 +1566,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "2.1.0", - "vitest": "2.1.0" + "@vitest/browser": "2.1.1", + "vitest": "2.1.1" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1576,14 +1576,14 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.0.tgz", - "integrity": "sha512-N3/xR4fSu0+6sVZETEtPT1orUs2+Y477JOXTcU3xKuu3uBlsgbD7/7Mz2LZ1Jr1XjwilEWlrIgSCj4N1+5ZmsQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", + "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.0", - "@vitest/utils": "2.1.0", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", "chai": "^5.1.1", "tinyrainbow": "^1.2.0" }, @@ -1592,9 +1592,9 @@ } }, "node_modules/@vitest/mocker": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.0.tgz", - "integrity": "sha512-ZxENovUqhzl+QiOFpagiHUNUuZ1qPd5yYTCYHomGIZOFArzn4mgX2oxZmiAItJWAaXHG6bbpb/DpSPhlk5DgtA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", + "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", "dev": true, "license": "MIT", "dependencies": { @@ -1606,7 +1606,7 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/spy": "2.1.0", + "@vitest/spy": "2.1.1", "msw": "^2.3.5", "vite": "^5.0.0" }, @@ -1633,13 +1633,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.0.tgz", - "integrity": "sha512-D9+ZiB8MbMt7qWDRJc4CRNNUlne/8E1X7dcKhZVAbcOKG58MGGYVDqAq19xlhNfMFZsW0bpVKgztBwks38Ko0w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", + "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.0", + "@vitest/utils": "2.1.1", "pathe": "^1.1.2" }, "funding": { @@ -1647,13 +1647,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.0.tgz", - "integrity": "sha512-x69CygGMzt9VCO283K2/FYQ+nBrOj66OTKpsPykjCR4Ac3lLV+m85hj9reaIGmjBSsKzVvbxWmjWE3kF5ha3uQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", + "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.0", + "@vitest/pretty-format": "2.1.1", "magic-string": "^0.30.11", "pathe": "^1.1.2" }, @@ -1661,23 +1661,10 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz", - "integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/spy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.0.tgz", - "integrity": "sha512-IXX5NkbdgTYTog3F14i2LgnBc+20YmkXMx0IWai84mcxySUDRgm0ihbOfR4L0EVRBDFG85GjmQQEZNNKVVpkZw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", + "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", "dev": true, "license": "MIT", "dependencies": { @@ -1688,13 +1675,13 @@ } }, "node_modules/@vitest/utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.0.tgz", - "integrity": "sha512-rreyfVe0PuNqJfKYUwfPDfi6rrp0VSu0Wgvp5WBqJonP+4NvXHk48X6oBam1Lj47Hy6jbJtnMj3OcRdrkTP0tA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", + "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.0", + "@vitest/pretty-format": "2.1.1", "loupe": "^3.1.1", "tinyrainbow": "^1.2.0" }, @@ -1702,19 +1689,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz", - "integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -4241,9 +4215,9 @@ } }, "node_modules/vite-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.0.tgz", - "integrity": "sha512-+ybYqBVUjYyIscoLzMWodus2enQDZOpGhcU6HdOVD6n8WZdk12w1GFL3mbnxLs7hPtRtqs1Wo5YF6/Tsr6fmhg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", + "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", "dev": true, "license": "MIT", "dependencies": { @@ -4283,19 +4257,19 @@ } }, "node_modules/vitest": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.0.tgz", - "integrity": "sha512-XuuEeyNkqbfr0FtAvd9vFbInSSNY1ykCQTYQ0sj9wPy4hx+1gR7gqVNdW0AX2wrrM1wWlN5fnJDjF9xG6mYRSQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", + "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "2.1.0", - "@vitest/mocker": "2.1.0", - "@vitest/pretty-format": "^2.1.0", - "@vitest/runner": "2.1.0", - "@vitest/snapshot": "2.1.0", - "@vitest/spy": "2.1.0", - "@vitest/utils": "2.1.0", + "@vitest/expect": "2.1.1", + "@vitest/mocker": "2.1.1", + "@vitest/pretty-format": "^2.1.1", + "@vitest/runner": "2.1.1", + "@vitest/snapshot": "2.1.1", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", "chai": "^5.1.1", "debug": "^4.3.6", "magic-string": "^0.30.11", @@ -4306,7 +4280,7 @@ "tinypool": "^1.0.0", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.1.0", + "vite-node": "2.1.1", "why-is-node-running": "^2.3.0" }, "bin": { @@ -4321,8 +4295,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.0", - "@vitest/ui": "2.1.0", + "@vitest/browser": "2.1.1", + "@vitest/ui": "2.1.1", "happy-dom": "*", "jsdom": "*" }, diff --git a/docs/package-lock.json b/docs/package-lock.json index 5f14d39ac7ab3..3b4e6c4f9546a 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -16091,9 +16091,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.11.tgz", - "integrity": "sha512-qhEuBcLemjSJk5ajccN9xJFtM/h0AVCPaA6C92jNP+M2J8kX+eMJHI7R2HFKUvvAsMpcfLILMCFYSeDwpMmlUg==", + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz", + "integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index ab4fd53fbf2ef..73c6ac61753e9 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1149,13 +1149,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.0.tgz", - "integrity": "sha512-SgAdlSwYVpToI4e/IH19IHHWvoijAYH5hu2MWSXptRypLSnzj51PcGD+rsOXFayde4P9ZLi+loXVwArg6IUkCA==", + "version": "1.47.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.1.tgz", + "integrity": "sha512-dbWpcNQZ5nj16m+A5UNScYx7HX5trIy7g4phrcitn+Nk83S32EBX/CLU4hiF4RGKX/yRc93AAqtfaXB7JWBd4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.47.0" + "playwright": "1.47.1" }, "bin": { "playwright": "cli.js" @@ -1165,9 +1165,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz", - "integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", "cpu": [ "arm" ], @@ -1179,9 +1179,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz", - "integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", "cpu": [ "arm64" ], @@ -1193,9 +1193,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz", - "integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", "cpu": [ "arm64" ], @@ -1207,9 +1207,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz", - "integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", "cpu": [ "x64" ], @@ -1221,9 +1221,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz", - "integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", "cpu": [ "arm" ], @@ -1235,9 +1235,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz", - "integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", "cpu": [ "arm" ], @@ -1249,9 +1249,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz", - "integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", "cpu": [ "arm64" ], @@ -1263,9 +1263,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz", - "integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", "cpu": [ "arm64" ], @@ -1277,9 +1277,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz", - "integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", "cpu": [ "ppc64" ], @@ -1291,9 +1291,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz", - "integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", "cpu": [ "riscv64" ], @@ -1305,9 +1305,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz", - "integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", "cpu": [ "s390x" ], @@ -1319,9 +1319,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz", - "integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", "cpu": [ "x64" ], @@ -1333,9 +1333,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz", - "integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", "cpu": [ "x64" ], @@ -1347,9 +1347,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz", - "integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", "cpu": [ "arm64" ], @@ -1361,9 +1361,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz", - "integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", "cpu": [ "ia32" ], @@ -1375,9 +1375,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz", - "integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", "cpu": [ "x64" ], @@ -1471,9 +1471,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "license": "MIT" }, @@ -1596,9 +1596,9 @@ } }, "node_modules/@types/pg": { - "version": "8.11.9", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.9.tgz", - "integrity": "sha512-M4mYeJZRBD9lCBCGa72F44uKSV9eJrAFfjlPJagdA6pgIr2OPJULFB7nqnZzOdqXG0qzHlgtZKzTdIgbmHitSg==", + "version": "8.11.10", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz", + "integrity": "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==", "dev": true, "license": "MIT", "dependencies": { @@ -1733,17 +1733,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz", - "integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz", + "integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/type-utils": "8.5.0", - "@typescript-eslint/utils": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/type-utils": "8.6.0", + "@typescript-eslint/utils": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1767,16 +1767,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz", - "integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz", + "integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/typescript-estree": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "debug": "^4.3.4" }, "engines": { @@ -1796,14 +1796,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz", - "integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz", + "integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0" + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1814,14 +1814,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz", - "integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz", + "integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.5.0", - "@typescript-eslint/utils": "8.5.0", + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/utils": "8.6.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1839,9 +1839,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz", - "integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz", + "integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==", "dev": true, "license": "MIT", "engines": { @@ -1853,14 +1853,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz", - "integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz", + "integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1908,16 +1908,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz", - "integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz", + "integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/typescript-estree": "8.5.0" + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1931,13 +1931,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz", - "integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz", + "integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/types": "8.6.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -1949,9 +1949,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.0.tgz", - "integrity": "sha512-yqCkr2nrV4o58VcVMxTVkS6Ggxzy7pmSD8JbTbhbH5PsQfUIES1QT716VUzo33wf2lX9EcWYdT3Vl2MMmjR59g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz", + "integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1972,8 +1972,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "2.1.0", - "vitest": "2.1.0" + "@vitest/browser": "2.1.1", + "vitest": "2.1.1" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1982,14 +1982,14 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.0.tgz", - "integrity": "sha512-N3/xR4fSu0+6sVZETEtPT1orUs2+Y477JOXTcU3xKuu3uBlsgbD7/7Mz2LZ1Jr1XjwilEWlrIgSCj4N1+5ZmsQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", + "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.0", - "@vitest/utils": "2.1.0", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", "chai": "^5.1.1", "tinyrainbow": "^1.2.0" }, @@ -1998,9 +1998,9 @@ } }, "node_modules/@vitest/mocker": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.0.tgz", - "integrity": "sha512-ZxENovUqhzl+QiOFpagiHUNUuZ1qPd5yYTCYHomGIZOFArzn4mgX2oxZmiAItJWAaXHG6bbpb/DpSPhlk5DgtA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", + "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", "dev": true, "license": "MIT", "dependencies": { @@ -2012,7 +2012,7 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/spy": "2.1.0", + "@vitest/spy": "2.1.1", "msw": "^2.3.5", "vite": "^5.0.0" }, @@ -2039,13 +2039,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.0.tgz", - "integrity": "sha512-D9+ZiB8MbMt7qWDRJc4CRNNUlne/8E1X7dcKhZVAbcOKG58MGGYVDqAq19xlhNfMFZsW0bpVKgztBwks38Ko0w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", + "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.0", + "@vitest/utils": "2.1.1", "pathe": "^1.1.2" }, "funding": { @@ -2053,13 +2053,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.0.tgz", - "integrity": "sha512-x69CygGMzt9VCO283K2/FYQ+nBrOj66OTKpsPykjCR4Ac3lLV+m85hj9reaIGmjBSsKzVvbxWmjWE3kF5ha3uQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", + "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.0", + "@vitest/pretty-format": "2.1.1", "magic-string": "^0.30.11", "pathe": "^1.1.2" }, @@ -2067,23 +2067,10 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz", - "integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/spy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.0.tgz", - "integrity": "sha512-IXX5NkbdgTYTog3F14i2LgnBc+20YmkXMx0IWai84mcxySUDRgm0ihbOfR4L0EVRBDFG85GjmQQEZNNKVVpkZw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", + "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", "dev": true, "license": "MIT", "dependencies": { @@ -2094,13 +2081,13 @@ } }, "node_modules/@vitest/utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.0.tgz", - "integrity": "sha512-rreyfVe0PuNqJfKYUwfPDfi6rrp0VSu0Wgvp5WBqJonP+4NvXHk48X6oBam1Lj47Hy6jbJtnMj3OcRdrkTP0tA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", + "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.0", + "@vitest/pretty-format": "2.1.1", "loupe": "^3.1.1", "tinyrainbow": "^1.2.0" }, @@ -2108,19 +2095,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz", - "integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -4168,9 +4142,9 @@ } }, "node_modules/jose": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.8.0.tgz", - "integrity": "sha512-E7CqYpL/t7MMnfGnK/eg416OsFCVUrU/Y3Vwe7QjKhu/BkS1Ms455+2xsqZQVN57/U2MHMBvEb5SrmAZWAIntA==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.2.tgz", + "integrity": "sha512-ILI2xx/I57b20sd7rHZvgiiQrmp2mcotwsAH+5ajbpFQbrYVQdNHYlQhoA5cFb78CgtBOxtC05TeA+mcgkuCqQ==", "dev": true, "license": "MIT", "funding": { @@ -5039,15 +5013,15 @@ } }, "node_modules/pg": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", - "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.0.tgz", + "integrity": "sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw==", "dev": true, "license": "MIT", "dependencies": { - "pg-connection-string": "^2.6.4", - "pg-pool": "^3.6.2", - "pg-protocol": "^1.6.1", + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, @@ -5074,10 +5048,11 @@ "optional": true }, "node_modules/pg-connection-string": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", - "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==", - "dev": true + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==", + "dev": true, + "license": "MIT" }, "node_modules/pg-int8": { "version": "1.0.1", @@ -5098,19 +5073,21 @@ } }, "node_modules/pg-pool": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", - "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", "dev": true, + "license": "MIT", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", - "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==", - "dev": true + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==", + "dev": true, + "license": "MIT" }, "node_modules/pg-types": { "version": "2.2.0", @@ -5158,13 +5135,13 @@ } }, "node_modules/playwright": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.0.tgz", - "integrity": "sha512-jOWiRq2pdNAX/mwLiwFYnPHpEZ4rM+fRSQpRHwEwZlP2PUANvL3+aJOF/bvISMhFD30rqMxUB4RJx9aQbfh4Ww==", + "version": "1.47.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.1.tgz", + "integrity": "sha512-SUEKi6947IqYbKxRiqnbUobVZY4bF1uu+ZnZNJX9DfU1tlf2UhWfvVjLf01pQx9URsOr18bFVUKXmanYWhbfkw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.47.0" + "playwright-core": "1.47.1" }, "bin": { "playwright": "cli.js" @@ -5177,9 +5154,9 @@ } }, "node_modules/playwright-core": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.0.tgz", - "integrity": "sha512-1DyHT8OqkcfCkYUD9zzUTfg7EfTd+6a8MkD/NWOvjo0u/SCNd5YmY/lJwFvUZOxJbWNds+ei7ic2+R/cRz/PDg==", + "version": "1.47.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.1.tgz", + "integrity": "sha512-i1iyJdLftqtt51mEk6AhYFaAJCDx0xQ/O5NU8EKaWFgMjItPVma542Nh/Aq8aLCjIJSzjaiEQGW/nyqLkGF1OQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5629,9 +5606,9 @@ } }, "node_modules/rollup": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz", - "integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dev": true, "license": "MIT", "dependencies": { @@ -5645,25 +5622,32 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.3", - "@rollup/rollup-android-arm64": "4.21.3", - "@rollup/rollup-darwin-arm64": "4.21.3", - "@rollup/rollup-darwin-x64": "4.21.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.3", - "@rollup/rollup-linux-arm-musleabihf": "4.21.3", - "@rollup/rollup-linux-arm64-gnu": "4.21.3", - "@rollup/rollup-linux-arm64-musl": "4.21.3", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.3", - "@rollup/rollup-linux-riscv64-gnu": "4.21.3", - "@rollup/rollup-linux-s390x-gnu": "4.21.3", - "@rollup/rollup-linux-x64-gnu": "4.21.3", - "@rollup/rollup-linux-x64-musl": "4.21.3", - "@rollup/rollup-win32-arm64-msvc": "4.21.3", - "@rollup/rollup-win32-ia32-msvc": "4.21.3", - "@rollup/rollup-win32-x64-msvc": "4.21.3", + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6403,9 +6387,9 @@ } }, "node_modules/vite": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", - "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "version": "5.4.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz", + "integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6463,9 +6447,9 @@ } }, "node_modules/vite-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.0.tgz", - "integrity": "sha512-+ybYqBVUjYyIscoLzMWodus2enQDZOpGhcU6HdOVD6n8WZdk12w1GFL3mbnxLs7hPtRtqs1Wo5YF6/Tsr6fmhg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", + "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", "dev": true, "license": "MIT", "dependencies": { @@ -6500,19 +6484,19 @@ } }, "node_modules/vitest": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.0.tgz", - "integrity": "sha512-XuuEeyNkqbfr0FtAvd9vFbInSSNY1ykCQTYQ0sj9wPy4hx+1gR7gqVNdW0AX2wrrM1wWlN5fnJDjF9xG6mYRSQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", + "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "2.1.0", - "@vitest/mocker": "2.1.0", - "@vitest/pretty-format": "^2.1.0", - "@vitest/runner": "2.1.0", - "@vitest/snapshot": "2.1.0", - "@vitest/spy": "2.1.0", - "@vitest/utils": "2.1.0", + "@vitest/expect": "2.1.1", + "@vitest/mocker": "2.1.1", + "@vitest/pretty-format": "^2.1.1", + "@vitest/runner": "2.1.1", + "@vitest/snapshot": "2.1.1", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", "chai": "^5.1.1", "debug": "^4.3.6", "magic-string": "^0.30.11", @@ -6523,7 +6507,7 @@ "tinypool": "^1.0.0", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.1.0", + "vite-node": "2.1.1", "why-is-node-running": "^2.3.0" }, "bin": { @@ -6538,8 +6522,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.0", - "@vitest/ui": "2.1.0", + "@vitest/browser": "2.1.1", + "@vitest/ui": "2.1.1", "happy-dom": "*", "jsdom": "*" }, diff --git a/server/package-lock.json b/server/package-lock.json index 9abfc6b5ce70f..ba9f33dc1e425 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -2043,12 +2043,12 @@ } }, "node_modules/@nestjs/common": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.1.tgz", - "integrity": "sha512-4CkrDx0s4XuWqFjX8WvOFV7Y6RGJd0P2OBblkhZS7nwoctoSuW5pyEa8SWak6YHNGrHRpFb6ymm5Ai4LncwRVA==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.3.tgz", + "integrity": "sha512-4hbLd3XIJubHSylYd/1WSi4VQvG68KM/ECYpMDqA3k3J1/T17SAg40sDoq3ZoO5OZgU0xuNyjuISdOTjs11qVg==", "dependencies": { "iterare": "1.2.1", - "tslib": "2.6.3", + "tslib": "2.7.0", "uid": "2.0.2" }, "funding": { @@ -2070,6 +2070,11 @@ } } }, + "node_modules/@nestjs/common/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, "node_modules/@nestjs/config": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz", @@ -2183,12 +2188,12 @@ "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, "node_modules/@nestjs/platform-socket.io": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.1.tgz", - "integrity": "sha512-cxn5vKBAbqtEVPl0qVcJpR4sC12+hzcY/mYXGW6ippOKQDBNc2OF8oZXu6V3O1MvAl+VM7eNNEsLmP9DRKQlnw==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.3.tgz", + "integrity": "sha512-jTatT8q15LB5CFWsaIez3IigMixt7tNGJ4QLlRJ5NggPOPKRZssJnloODyEadFNHJjZiyufp5/NoPKBtNMf+lg==", "dependencies": { "socket.io": "4.7.5", - "tslib": "2.6.3" + "tslib": "2.7.0" }, "funding": { "type": "opencollective", @@ -2200,10 +2205,15 @@ "rxjs": "^7.1.0" } }, + "node_modules/@nestjs/platform-socket.io/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, "node_modules/@nestjs/schedule": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.0.tgz", - "integrity": "sha512-WEc96WTXZW+VI/Ng+uBpiBUwm6TWtAbQ4RKWkfbmzKvmbRGzA/9k/UyAWDS9k0pp+ZcbC+MaZQtt7TjQHrwX6g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.1.tgz", + "integrity": "sha512-VxAnCiU4HP0wWw8IdWAVfsGC/FGjyToNjjUtXDEQL6oj+w/N5QDd2VT9k6d7Jbr8PlZuBZNdWtDKSkH5bZ+RXQ==", "dependencies": { "cron": "3.1.7", "uuid": "10.0.0" @@ -2280,12 +2290,12 @@ } }, "node_modules/@nestjs/testing": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.1.tgz", - "integrity": "sha512-pR+su5+YGqCLH0RhhVkPowQK7FCORU0/PWAywPK7LScAOtD67ZoviZ7hAU4vnGdwkg4HCB0D7W8Bkg19CGU8Xw==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.3.tgz", + "integrity": "sha512-SBNWrMU51YAlYmW86wyjlGZ2uLnASNiOPD0lBcNIlxxei0b05/aI3nh7OPuxbXQUdedUJfPq2d2jZj4TRG4S0w==", "dev": true, "dependencies": { - "tslib": "2.6.3" + "tslib": "2.7.0" }, "funding": { "type": "opencollective", @@ -2306,6 +2316,12 @@ } } }, + "node_modules/@nestjs/testing/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + }, "node_modules/@nestjs/typeorm": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.2.tgz", @@ -2322,13 +2338,13 @@ } }, "node_modules/@nestjs/websockets": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.1.tgz", - "integrity": "sha512-p0Eq94WneczV2bnLEu9hl24iCIfH5eUCGgBuYOkVDySBwvya5L+gD4wUoqIqGoX1c6rkhQa+pMR7pi1EY4t93w==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.3.tgz", + "integrity": "sha512-EW5/GR0jImJwrb8+YpHPoFN2tlhYQzVE2yAN5Se5sygUr/ZFMNAG84sd79NmWGd4RxoxR0aFH9nRycQ/0Ebe5w==", "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", - "tslib": "2.6.3" + "tslib": "2.7.0" }, "peerDependencies": { "@nestjs/common": "^10.0.0", @@ -2343,6 +2359,11 @@ } } }, + "node_modules/@nestjs/websockets/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, "node_modules/@next/env": { "version": "14.2.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", @@ -5376,9 +5397,9 @@ } }, "node_modules/@types/nodemailer": { - "version": "6.4.15", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz", - "integrity": "sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==", + "version": "6.4.16", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.16.tgz", + "integrity": "sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -5485,9 +5506,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.3.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz", - "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.7.tgz", + "integrity": "sha512-KUnDCJF5+AiZd8owLIeVHqmW9yM4sqmDVf2JRJiBMFkGvkoZ4/WyV2lL4zVsoinmRS/W3FeEdZLEWFRofnT2FQ==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -5604,16 +5625,16 @@ "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz", - "integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz", + "integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/type-utils": "8.5.0", - "@typescript-eslint/utils": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/type-utils": "8.6.0", + "@typescript-eslint/utils": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -5637,15 +5658,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz", - "integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz", + "integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/typescript-estree": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "debug": "^4.3.4" }, "engines": { @@ -5665,13 +5686,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz", - "integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz", + "integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0" + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5682,13 +5703,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz", - "integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz", + "integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.5.0", - "@typescript-eslint/utils": "8.5.0", + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/utils": "8.6.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -5706,9 +5727,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz", - "integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz", + "integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5719,13 +5740,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz", - "integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz", + "integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -5771,15 +5792,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz", - "integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz", + "integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/typescript-estree": "8.5.0" + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5793,12 +5814,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz", - "integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz", + "integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/types": "8.6.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -5810,9 +5831,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.0.tgz", - "integrity": "sha512-yqCkr2nrV4o58VcVMxTVkS6Ggxzy7pmSD8JbTbhbH5PsQfUIES1QT716VUzo33wf2lX9EcWYdT3Vl2MMmjR59g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz", + "integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -5832,8 +5853,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "2.1.0", - "vitest": "2.1.0" + "@vitest/browser": "2.1.1", + "vitest": "2.1.1" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -5851,13 +5872,13 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.0.tgz", - "integrity": "sha512-N3/xR4fSu0+6sVZETEtPT1orUs2+Y477JOXTcU3xKuu3uBlsgbD7/7Mz2LZ1Jr1XjwilEWlrIgSCj4N1+5ZmsQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", + "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", "dev": true, "dependencies": { - "@vitest/spy": "2.1.0", - "@vitest/utils": "2.1.0", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", "chai": "^5.1.1", "tinyrainbow": "^1.2.0" }, @@ -5866,9 +5887,9 @@ } }, "node_modules/@vitest/mocker": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.0.tgz", - "integrity": "sha512-ZxENovUqhzl+QiOFpagiHUNUuZ1qPd5yYTCYHomGIZOFArzn4mgX2oxZmiAItJWAaXHG6bbpb/DpSPhlk5DgtA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", + "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", "dev": true, "dependencies": { "@vitest/spy": "^2.1.0-beta.1", @@ -5879,7 +5900,7 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/spy": "2.1.0", + "@vitest/spy": "2.1.1", "msw": "^2.3.5", "vite": "^5.0.0" }, @@ -5914,12 +5935,12 @@ } }, "node_modules/@vitest/runner": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.0.tgz", - "integrity": "sha512-D9+ZiB8MbMt7qWDRJc4CRNNUlne/8E1X7dcKhZVAbcOKG58MGGYVDqAq19xlhNfMFZsW0bpVKgztBwks38Ko0w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", + "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", "dev": true, "dependencies": { - "@vitest/utils": "2.1.0", + "@vitest/utils": "2.1.1", "pathe": "^1.1.2" }, "funding": { @@ -5927,12 +5948,12 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.0.tgz", - "integrity": "sha512-x69CygGMzt9VCO283K2/FYQ+nBrOj66OTKpsPykjCR4Ac3lLV+m85hj9reaIGmjBSsKzVvbxWmjWE3kF5ha3uQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", + "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", "dev": true, "dependencies": { - "@vitest/pretty-format": "2.1.0", + "@vitest/pretty-format": "2.1.1", "magic-string": "^0.30.11", "pathe": "^1.1.2" }, @@ -5940,18 +5961,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz", - "integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==", - "dev": true, - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/snapshot/node_modules/magic-string": { "version": "0.30.11", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", @@ -5962,9 +5971,9 @@ } }, "node_modules/@vitest/spy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.0.tgz", - "integrity": "sha512-IXX5NkbdgTYTog3F14i2LgnBc+20YmkXMx0IWai84mcxySUDRgm0ihbOfR4L0EVRBDFG85GjmQQEZNNKVVpkZw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", + "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", "dev": true, "dependencies": { "tinyspy": "^3.0.0" @@ -5974,12 +5983,12 @@ } }, "node_modules/@vitest/utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.0.tgz", - "integrity": "sha512-rreyfVe0PuNqJfKYUwfPDfi6rrp0VSu0Wgvp5WBqJonP+4NvXHk48X6oBam1Lj47Hy6jbJtnMj3OcRdrkTP0tA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", + "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", "dev": true, "dependencies": { - "@vitest/pretty-format": "2.1.0", + "@vitest/pretty-format": "2.1.1", "loupe": "^3.1.1", "tinyrainbow": "^1.2.0" }, @@ -5987,18 +5996,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz", - "integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==", - "dev": true, - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -11311,13 +11308,13 @@ } }, "node_modules/pg": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", - "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.0.tgz", + "integrity": "sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw==", "dependencies": { - "pg-connection-string": "^2.6.4", - "pg-pool": "^3.6.2", - "pg-protocol": "^1.6.1", + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, @@ -11343,9 +11340,9 @@ "optional": true }, "node_modules/pg-connection-string": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", - "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" }, "node_modules/pg-int8": { "version": "1.0.1", @@ -11364,17 +11361,17 @@ } }, "node_modules/pg-pool": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", - "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", - "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==" }, "node_modules/pg-types": { "version": "2.2.0", @@ -14565,9 +14562,9 @@ } }, "node_modules/ua-parser-js": { - "version": "1.0.38", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz", - "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==", + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.39.tgz", + "integrity": "sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==", "funding": [ { "type": "opencollective", @@ -14582,6 +14579,9 @@ "url": "https://github.com/sponsors/faisalman" } ], + "bin": { + "ua-parser-js": "script/cli.js" + }, "engines": { "node": "*" } @@ -14861,9 +14861,9 @@ } }, "node_modules/vite-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.0.tgz", - "integrity": "sha512-+ybYqBVUjYyIscoLzMWodus2enQDZOpGhcU6HdOVD6n8WZdk12w1GFL3mbnxLs7hPtRtqs1Wo5YF6/Tsr6fmhg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", + "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -14901,18 +14901,18 @@ } }, "node_modules/vitest": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.0.tgz", - "integrity": "sha512-XuuEeyNkqbfr0FtAvd9vFbInSSNY1ykCQTYQ0sj9wPy4hx+1gR7gqVNdW0AX2wrrM1wWlN5fnJDjF9xG6mYRSQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", + "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", "dev": true, "dependencies": { - "@vitest/expect": "2.1.0", - "@vitest/mocker": "2.1.0", - "@vitest/pretty-format": "^2.1.0", - "@vitest/runner": "2.1.0", - "@vitest/snapshot": "2.1.0", - "@vitest/spy": "2.1.0", - "@vitest/utils": "2.1.0", + "@vitest/expect": "2.1.1", + "@vitest/mocker": "2.1.1", + "@vitest/pretty-format": "^2.1.1", + "@vitest/runner": "2.1.1", + "@vitest/snapshot": "2.1.1", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", "chai": "^5.1.1", "debug": "^4.3.6", "magic-string": "^0.30.11", @@ -14923,7 +14923,7 @@ "tinypool": "^1.0.0", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.1.0", + "vite-node": "2.1.1", "why-is-node-running": "^2.3.0" }, "bin": { @@ -14938,8 +14938,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.0", - "@vitest/ui": "2.1.0", + "@vitest/browser": "2.1.1", + "@vitest/ui": "2.1.1", "happy-dom": "*", "jsdom": "*" }, @@ -16512,13 +16512,20 @@ } }, "@nestjs/common": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.1.tgz", - "integrity": "sha512-4CkrDx0s4XuWqFjX8WvOFV7Y6RGJd0P2OBblkhZS7nwoctoSuW5pyEa8SWak6YHNGrHRpFb6ymm5Ai4LncwRVA==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.3.tgz", + "integrity": "sha512-4hbLd3XIJubHSylYd/1WSi4VQvG68KM/ECYpMDqA3k3J1/T17SAg40sDoq3ZoO5OZgU0xuNyjuISdOTjs11qVg==", "requires": { "iterare": "1.2.1", - "tslib": "2.6.3", + "tslib": "2.7.0", "uid": "2.0.2" + }, + "dependencies": { + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + } } }, "@nestjs/config": { @@ -16585,18 +16592,25 @@ } }, "@nestjs/platform-socket.io": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.1.tgz", - "integrity": "sha512-cxn5vKBAbqtEVPl0qVcJpR4sC12+hzcY/mYXGW6ippOKQDBNc2OF8oZXu6V3O1MvAl+VM7eNNEsLmP9DRKQlnw==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.3.tgz", + "integrity": "sha512-jTatT8q15LB5CFWsaIez3IigMixt7tNGJ4QLlRJ5NggPOPKRZssJnloODyEadFNHJjZiyufp5/NoPKBtNMf+lg==", "requires": { "socket.io": "4.7.5", - "tslib": "2.6.3" + "tslib": "2.7.0" + }, + "dependencies": { + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + } } }, "@nestjs/schedule": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.0.tgz", - "integrity": "sha512-WEc96WTXZW+VI/Ng+uBpiBUwm6TWtAbQ4RKWkfbmzKvmbRGzA/9k/UyAWDS9k0pp+ZcbC+MaZQtt7TjQHrwX6g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.1.tgz", + "integrity": "sha512-VxAnCiU4HP0wWw8IdWAVfsGC/FGjyToNjjUtXDEQL6oj+w/N5QDd2VT9k6d7Jbr8PlZuBZNdWtDKSkH5bZ+RXQ==", "requires": { "cron": "3.1.7", "uuid": "10.0.0" @@ -16644,12 +16658,20 @@ } }, "@nestjs/testing": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.1.tgz", - "integrity": "sha512-pR+su5+YGqCLH0RhhVkPowQK7FCORU0/PWAywPK7LScAOtD67ZoviZ7hAU4vnGdwkg4HCB0D7W8Bkg19CGU8Xw==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.3.tgz", + "integrity": "sha512-SBNWrMU51YAlYmW86wyjlGZ2uLnASNiOPD0lBcNIlxxei0b05/aI3nh7OPuxbXQUdedUJfPq2d2jZj4TRG4S0w==", "dev": true, "requires": { - "tslib": "2.6.3" + "tslib": "2.7.0" + }, + "dependencies": { + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + } } }, "@nestjs/typeorm": { @@ -16661,13 +16683,20 @@ } }, "@nestjs/websockets": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.1.tgz", - "integrity": "sha512-p0Eq94WneczV2bnLEu9hl24iCIfH5eUCGgBuYOkVDySBwvya5L+gD4wUoqIqGoX1c6rkhQa+pMR7pi1EY4t93w==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.3.tgz", + "integrity": "sha512-EW5/GR0jImJwrb8+YpHPoFN2tlhYQzVE2yAN5Se5sygUr/ZFMNAG84sd79NmWGd4RxoxR0aFH9nRycQ/0Ebe5w==", "requires": { "iterare": "1.2.1", "object-hash": "3.0.0", - "tslib": "2.6.3" + "tslib": "2.7.0" + }, + "dependencies": { + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + } } }, "@next/env": { @@ -18680,9 +18709,9 @@ } }, "@types/nodemailer": { - "version": "6.4.15", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz", - "integrity": "sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==", + "version": "6.4.16", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.16.tgz", + "integrity": "sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==", "dev": true, "requires": { "@types/node": "*" @@ -18776,9 +18805,9 @@ "dev": true }, "@types/react": { - "version": "18.3.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz", - "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.7.tgz", + "integrity": "sha512-KUnDCJF5+AiZd8owLIeVHqmW9yM4sqmDVf2JRJiBMFkGvkoZ4/WyV2lL4zVsoinmRS/W3FeEdZLEWFRofnT2FQ==", "dev": true, "requires": { "@types/prop-types": "*", @@ -18895,16 +18924,16 @@ "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ==" }, "@typescript-eslint/eslint-plugin": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz", - "integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz", + "integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/type-utils": "8.5.0", - "@typescript-eslint/utils": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/type-utils": "8.6.0", + "@typescript-eslint/utils": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -18912,54 +18941,54 @@ } }, "@typescript-eslint/parser": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz", - "integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz", + "integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/typescript-estree": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz", - "integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz", + "integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==", "dev": true, "requires": { - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0" + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0" } }, "@typescript-eslint/type-utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz", - "integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz", + "integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "8.5.0", - "@typescript-eslint/utils": "8.5.0", + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/utils": "8.6.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" } }, "@typescript-eslint/types": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz", - "integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz", + "integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz", - "integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz", + "integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==", "dev": true, "requires": { - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -18989,31 +19018,31 @@ } }, "@typescript-eslint/utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz", - "integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz", + "integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/typescript-estree": "8.5.0" + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0" } }, "@typescript-eslint/visitor-keys": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz", - "integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz", + "integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==", "dev": true, "requires": { - "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/types": "8.6.0", "eslint-visitor-keys": "^3.4.3" } }, "@vitest/coverage-v8": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.0.tgz", - "integrity": "sha512-yqCkr2nrV4o58VcVMxTVkS6Ggxzy7pmSD8JbTbhbH5PsQfUIES1QT716VUzo33wf2lX9EcWYdT3Vl2MMmjR59g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz", + "integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==", "dev": true, "requires": { "@ampproject/remapping": "^2.3.0", @@ -19042,21 +19071,21 @@ } }, "@vitest/expect": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.0.tgz", - "integrity": "sha512-N3/xR4fSu0+6sVZETEtPT1orUs2+Y477JOXTcU3xKuu3uBlsgbD7/7Mz2LZ1Jr1XjwilEWlrIgSCj4N1+5ZmsQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", + "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", "dev": true, "requires": { - "@vitest/spy": "2.1.0", - "@vitest/utils": "2.1.0", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", "chai": "^5.1.1", "tinyrainbow": "^1.2.0" } }, "@vitest/mocker": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.0.tgz", - "integrity": "sha512-ZxENovUqhzl+QiOFpagiHUNUuZ1qPd5yYTCYHomGIZOFArzn4mgX2oxZmiAItJWAaXHG6bbpb/DpSPhlk5DgtA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", + "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", "dev": true, "requires": { "@vitest/spy": "^2.1.0-beta.1", @@ -19085,35 +19114,26 @@ } }, "@vitest/runner": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.0.tgz", - "integrity": "sha512-D9+ZiB8MbMt7qWDRJc4CRNNUlne/8E1X7dcKhZVAbcOKG58MGGYVDqAq19xlhNfMFZsW0bpVKgztBwks38Ko0w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", + "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", "dev": true, "requires": { - "@vitest/utils": "2.1.0", + "@vitest/utils": "2.1.1", "pathe": "^1.1.2" } }, "@vitest/snapshot": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.0.tgz", - "integrity": "sha512-x69CygGMzt9VCO283K2/FYQ+nBrOj66OTKpsPykjCR4Ac3lLV+m85hj9reaIGmjBSsKzVvbxWmjWE3kF5ha3uQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", + "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", "dev": true, "requires": { - "@vitest/pretty-format": "2.1.0", + "@vitest/pretty-format": "2.1.1", "magic-string": "^0.30.11", "pathe": "^1.1.2" }, "dependencies": { - "@vitest/pretty-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz", - "integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==", - "dev": true, - "requires": { - "tinyrainbow": "^1.2.0" - } - }, "magic-string": { "version": "0.30.11", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", @@ -19126,34 +19146,23 @@ } }, "@vitest/spy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.0.tgz", - "integrity": "sha512-IXX5NkbdgTYTog3F14i2LgnBc+20YmkXMx0IWai84mcxySUDRgm0ihbOfR4L0EVRBDFG85GjmQQEZNNKVVpkZw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", + "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", "dev": true, "requires": { "tinyspy": "^3.0.0" } }, "@vitest/utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.0.tgz", - "integrity": "sha512-rreyfVe0PuNqJfKYUwfPDfi6rrp0VSu0Wgvp5WBqJonP+4NvXHk48X6oBam1Lj47Hy6jbJtnMj3OcRdrkTP0tA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", + "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", "dev": true, "requires": { - "@vitest/pretty-format": "2.1.0", + "@vitest/pretty-format": "2.1.1", "loupe": "^3.1.1", "tinyrainbow": "^1.2.0" - }, - "dependencies": { - "@vitest/pretty-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz", - "integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==", - "dev": true, - "requires": { - "tinyrainbow": "^1.2.0" - } - } } }, "@webassemblyjs/ast": { @@ -23084,14 +23093,14 @@ "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==" }, "pg": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", - "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.0.tgz", + "integrity": "sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw==", "requires": { "pg-cloudflare": "^1.1.1", - "pg-connection-string": "^2.6.4", - "pg-pool": "^3.6.2", - "pg-protocol": "^1.6.1", + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", "pg-types": "^2.1.0", "pgpass": "1.x" } @@ -23103,9 +23112,9 @@ "optional": true }, "pg-connection-string": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", - "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" }, "pg-int8": { "version": "1.0.1", @@ -23118,15 +23127,15 @@ "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==" }, "pg-pool": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", - "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", "requires": {} }, "pg-protocol": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", - "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==" }, "pg-types": { "version": "2.2.0", @@ -25268,9 +25277,9 @@ "devOptional": true }, "ua-parser-js": { - "version": "1.0.38", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz", - "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==" + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.39.tgz", + "integrity": "sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==" }, "uglify-js": { "version": "3.17.4", @@ -25435,9 +25444,9 @@ } }, "vite-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.0.tgz", - "integrity": "sha512-+ybYqBVUjYyIscoLzMWodus2enQDZOpGhcU6HdOVD6n8WZdk12w1GFL3mbnxLs7hPtRtqs1Wo5YF6/Tsr6fmhg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", + "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", "dev": true, "requires": { "cac": "^6.7.14", @@ -25458,18 +25467,18 @@ } }, "vitest": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.0.tgz", - "integrity": "sha512-XuuEeyNkqbfr0FtAvd9vFbInSSNY1ykCQTYQ0sj9wPy4hx+1gR7gqVNdW0AX2wrrM1wWlN5fnJDjF9xG6mYRSQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", + "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", "dev": true, "requires": { - "@vitest/expect": "2.1.0", - "@vitest/mocker": "2.1.0", - "@vitest/pretty-format": "^2.1.0", - "@vitest/runner": "2.1.0", - "@vitest/snapshot": "2.1.0", - "@vitest/spy": "2.1.0", - "@vitest/utils": "2.1.0", + "@vitest/expect": "2.1.1", + "@vitest/mocker": "2.1.1", + "@vitest/pretty-format": "^2.1.1", + "@vitest/runner": "2.1.1", + "@vitest/snapshot": "2.1.1", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", "chai": "^5.1.1", "debug": "^4.3.6", "magic-string": "^0.30.11", @@ -25480,7 +25489,7 @@ "tinypool": "^1.0.0", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.1.0", + "vite-node": "2.1.1", "why-is-node-running": "^2.3.0" }, "dependencies": { diff --git a/web/package-lock.json b/web/package-lock.json index ce30d1ccb473d..b652f58ce0624 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -759,9 +759,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.0.tgz", - "integrity": "sha512-dTDHJSmz6c1OJ6HO7jiUiIb4sB20Dlkb3pxYsKm0qTXm2Bmj97rlXIhlvaFsW2rvCi+OLlwKLVSS6ZxFUVZvjQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.1.tgz", + "integrity": "sha512-4mDeYIgM3By7X6t5E6eYwLAa+2h4DeZDF7thhzIg6XB76jeEvMwadYAMCFJL/R4AnEBcAUO9+gL0vhy3s+qvZA==", "dev": true, "funding": [ { @@ -1875,9 +1875,9 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" }, "node_modules/@sveltejs/adapter-static": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.4.tgz", - "integrity": "sha512-Qm4GAHCnRXwfWG9/AtnQ7mqjyjTs7i0Opyb8H2KH9rMR7fLxqiPx/tXeoE6HHo66+72CjyOb4nFH3lrejY4vzA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.5.tgz", + "integrity": "sha512-kFJR7RxeB6FBvrKZWAEzIALatgy11ISaaZbcPup8JdWUdrmmfUHHTJ738YHJTEfnCiiXi6aX8Q6ePY7tnSMD6Q==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1885,9 +1885,9 @@ } }, "node_modules/@sveltejs/enhanced-img": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.3.4.tgz", - "integrity": "sha512-eX+ob5uWr0bTLMKeG9nhhM84aR88hqiLiyEfWZPX7ijhk/wlmYSUX9nOiaVHh2ct1U+Ju9Hhb90Copw+ZNOB8w==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.3.8.tgz", + "integrity": "sha512-n66u46ZeqHltiTm0BEjWptYmCrCY0EltEEvakmC7d5o5ZejDbOvOWm914mebbRKaP2Bezv65TNCod/wqvw/0KA==", "dev": true, "license": "MIT", "dependencies": { @@ -1901,9 +1901,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.5.26", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.26.tgz", - "integrity": "sha512-8l1JTIM2L+bS8ebq1E+nGjv/YSKSnD9Q19bYIUkc41vaEG2JjVUx6ikvPIJv2hkQAuqJLzoPrXlKk4KcyWOv3Q==", + "version": "2.5.28", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.28.tgz", + "integrity": "sha512-/O7pvFGBsQPcFa9UrW8eUC5uHTOXLsUp3SN0dY6YmRAL9nfPSrJsSJk//j5vMpinSshzUjteAFcfQTU+04Ka1w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2318,17 +2318,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz", - "integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz", + "integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/type-utils": "8.5.0", - "@typescript-eslint/utils": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/type-utils": "8.6.0", + "@typescript-eslint/utils": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2352,16 +2352,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz", - "integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz", + "integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/typescript-estree": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "debug": "^4.3.4" }, "engines": { @@ -2381,14 +2381,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz", - "integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz", + "integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0" + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2399,14 +2399,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz", - "integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz", + "integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.5.0", - "@typescript-eslint/utils": "8.5.0", + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/utils": "8.6.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2424,9 +2424,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz", - "integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz", + "integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==", "dev": true, "license": "MIT", "engines": { @@ -2438,14 +2438,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz", - "integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz", + "integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2493,16 +2493,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz", - "integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz", + "integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/typescript-estree": "8.5.0" + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2516,13 +2516,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz", - "integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz", + "integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/types": "8.6.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2534,9 +2534,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.0.tgz", - "integrity": "sha512-yqCkr2nrV4o58VcVMxTVkS6Ggxzy7pmSD8JbTbhbH5PsQfUIES1QT716VUzo33wf2lX9EcWYdT3Vl2MMmjR59g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz", + "integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2557,8 +2557,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "2.1.0", - "vitest": "2.1.0" + "@vitest/browser": "2.1.1", + "vitest": "2.1.1" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2567,14 +2567,14 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.0.tgz", - "integrity": "sha512-N3/xR4fSu0+6sVZETEtPT1orUs2+Y477JOXTcU3xKuu3uBlsgbD7/7Mz2LZ1Jr1XjwilEWlrIgSCj4N1+5ZmsQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", + "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.0", - "@vitest/utils": "2.1.0", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", "chai": "^5.1.1", "tinyrainbow": "^1.2.0" }, @@ -2583,9 +2583,9 @@ } }, "node_modules/@vitest/mocker": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.0.tgz", - "integrity": "sha512-ZxENovUqhzl+QiOFpagiHUNUuZ1qPd5yYTCYHomGIZOFArzn4mgX2oxZmiAItJWAaXHG6bbpb/DpSPhlk5DgtA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", + "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", "dev": true, "license": "MIT", "dependencies": { @@ -2597,7 +2597,7 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/spy": "2.1.0", + "@vitest/spy": "2.1.1", "msw": "^2.3.5", "vite": "^5.0.0" }, @@ -2624,13 +2624,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.0.tgz", - "integrity": "sha512-D9+ZiB8MbMt7qWDRJc4CRNNUlne/8E1X7dcKhZVAbcOKG58MGGYVDqAq19xlhNfMFZsW0bpVKgztBwks38Ko0w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", + "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.0", + "@vitest/utils": "2.1.1", "pathe": "^1.1.2" }, "funding": { @@ -2638,13 +2638,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.0.tgz", - "integrity": "sha512-x69CygGMzt9VCO283K2/FYQ+nBrOj66OTKpsPykjCR4Ac3lLV+m85hj9reaIGmjBSsKzVvbxWmjWE3kF5ha3uQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", + "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.0", + "@vitest/pretty-format": "2.1.1", "magic-string": "^0.30.11", "pathe": "^1.1.2" }, @@ -2652,23 +2652,10 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz", - "integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/spy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.0.tgz", - "integrity": "sha512-IXX5NkbdgTYTog3F14i2LgnBc+20YmkXMx0IWai84mcxySUDRgm0ihbOfR4L0EVRBDFG85GjmQQEZNNKVVpkZw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", + "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", "dev": true, "license": "MIT", "dependencies": { @@ -2679,13 +2666,13 @@ } }, "node_modules/@vitest/utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.0.tgz", - "integrity": "sha512-rreyfVe0PuNqJfKYUwfPDfi6rrp0VSu0Wgvp5WBqJonP+4NvXHk48X6oBam1Lj47Hy6jbJtnMj3OcRdrkTP0tA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", + "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.0", + "@vitest/pretty-format": "2.1.1", "loupe": "^3.1.1", "tinyrainbow": "^1.2.0" }, @@ -2693,23 +2680,10 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz", - "integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@zoom-image/core": { - "version": "0.37.1", - "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.37.1.tgz", - "integrity": "sha512-mIJaZJBi3jvOD2gtzoSe4yhnxfvx7GcYlVTLoJE6VPawb3Ei5dvHuRRXa8/dNHtCf1Xf2RNSEm1Za2+TqkAiBQ==", + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.38.0.tgz", + "integrity": "sha512-rA6/qTGfsRtWRs+WfMF0dIs+Ft9GBFusxXzEqqFsQa/0iYtN0MmOiuKzXGYPcIFKTbmQW/qqk0afIBtWd9163g==", "license": "MIT", "dependencies": { "@namnode/store": "^0.1.0" @@ -2720,12 +2694,12 @@ } }, "node_modules/@zoom-image/svelte": { - "version": "0.2.21", - "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.21.tgz", - "integrity": "sha512-242xKpIaVZC/cymvNF4+JlcKwAaM9l3W2QS4DHSsnqT8xvPBgBgns+1lqOuYYKSAa85DB1UL0NMBhTg8Gk4RpA==", + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.22.tgz", + "integrity": "sha512-lExo4M511/HtkmCsBzV5f8ABs8bEMZGtIrwl1pJro77iJ+5j9Yt7KUlPs6o+Yp028T6fqGJUsOCxCNWNZn9BIg==", "license": "MIT", "dependencies": { - "@zoom-image/core": "0.37.1" + "@zoom-image/core": "0.38.0" }, "funding": { "type": "github", @@ -3864,9 +3838,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "2.43.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.43.0.tgz", - "integrity": "sha512-REkxQWvg2pp7QVLxQNa+dJ97xUqRe7Y2JJbSWkHSuszu0VcblZtXkPBPckkivk99y5CdLw4slqfPylL2d/X4jQ==", + "version": "2.44.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.44.0.tgz", + "integrity": "sha512-wav4MOs02vBb1WjvTCYItwJCxMkuk2Z4p+K/eyjL0N/z7ahXLP+0LtQQjiKc2ezuif7GnZLbD1F3o1VHzSvdVg==", "dev": true, "license": "MIT", "dependencies": { @@ -3880,7 +3854,7 @@ "postcss-safe-parser": "^6.0.0", "postcss-selector-parser": "^6.1.0", "semver": "^7.6.2", - "svelte-eslint-parser": "^0.41.0" + "svelte-eslint-parser": "^0.41.1" }, "engines": { "node": "^14.17.0 || >=16.0.0" @@ -7160,9 +7134,9 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.41.0.tgz", - "integrity": "sha512-L6f4hOL+AbgfBIB52Z310pg1d2QjRqm7wy3kI1W6hhdhX5bvu7+f0R6w4ykp5HoDdzq+vGhIJmsisaiJDGmVfA==", + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.41.1.tgz", + "integrity": "sha512-08ndI6zTghzI8SuJAFpvMbA/haPSGn3xz19pjre19yYMw8Nw/wQJ2PrZBI/L8ijGTgtkWCQQiLLy+Z1tfaCwNA==", "dev": true, "license": "MIT", "dependencies": { @@ -7678,9 +7652,9 @@ "peer": true }, "node_modules/tailwindcss": { - "version": "3.4.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.11.tgz", - "integrity": "sha512-qhEuBcLemjSJk5ajccN9xJFtM/h0AVCPaA6C92jNP+M2J8kX+eMJHI7R2HFKUvvAsMpcfLILMCFYSeDwpMmlUg==", + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz", + "integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==", "dev": true, "license": "MIT", "dependencies": { @@ -8246,9 +8220,9 @@ } }, "node_modules/vite-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.0.tgz", - "integrity": "sha512-+ybYqBVUjYyIscoLzMWodus2enQDZOpGhcU6HdOVD6n8WZdk12w1GFL3mbnxLs7hPtRtqs1Wo5YF6/Tsr6fmhg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", + "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", "dev": true, "license": "MIT", "dependencies": { @@ -8282,19 +8256,19 @@ } }, "node_modules/vitest": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.0.tgz", - "integrity": "sha512-XuuEeyNkqbfr0FtAvd9vFbInSSNY1ykCQTYQ0sj9wPy4hx+1gR7gqVNdW0AX2wrrM1wWlN5fnJDjF9xG6mYRSQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", + "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "2.1.0", - "@vitest/mocker": "2.1.0", - "@vitest/pretty-format": "^2.1.0", - "@vitest/runner": "2.1.0", - "@vitest/snapshot": "2.1.0", - "@vitest/spy": "2.1.0", - "@vitest/utils": "2.1.0", + "@vitest/expect": "2.1.1", + "@vitest/mocker": "2.1.1", + "@vitest/pretty-format": "^2.1.1", + "@vitest/runner": "2.1.1", + "@vitest/snapshot": "2.1.1", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", "chai": "^5.1.1", "debug": "^4.3.6", "magic-string": "^0.30.11", @@ -8305,7 +8279,7 @@ "tinypool": "^1.0.0", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.1.0", + "vite-node": "2.1.1", "why-is-node-running": "^2.3.0" }, "bin": { @@ -8320,8 +8294,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.0", - "@vitest/ui": "2.1.0", + "@vitest/browser": "2.1.1", + "@vitest/ui": "2.1.1", "happy-dom": "*", "jsdom": "*" }, From e0fa3cdbc75817226bebe6eb58dd9a069e112d39 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey <10599762+fyfrey@users.noreply.github.com> Date: Tue, 24 Sep 2024 08:24:48 +0200 Subject: [PATCH 49/57] refactor(mobile): more repositories (#12879) * ExifInfoRepository * ActivityApiRepository * initial AssetApiRepository --- mobile/analysis_options.yaml | 9 +-- .../interfaces/activity_api.interface.dart | 16 ++++ mobile/lib/interfaces/asset.interface.dart | 12 +++ .../lib/interfaces/asset_api.interface.dart | 16 ++++ .../lib/interfaces/exif_info.interface.dart | 9 +++ .../lib/models/activities/activity.model.dart | 17 ++-- .../providers/activity_service.provider.dart | 4 +- .../activity_statistics.provider.dart | 2 +- .../repositories/activity_api.repository.dart | 67 +++++++++++++++ .../repositories/album_api.repository.dart | 25 +++--- mobile/lib/repositories/asset.repository.dart | 80 ++++++++++++++++++ .../repositories/asset_api.repository.dart | 25 ++++++ .../lib/repositories/base_api.repository.dart | 11 +++ .../repositories/exif_info.repository.dart | 28 +++++++ mobile/lib/services/activity.service.dart | 48 ++++------- mobile/lib/services/asset.service.dart | 52 ++++++++++++ .../services/asset_description.service.dart | 66 --------------- .../services/backup_verification.service.dart | 81 +++++++------------ .../asset_viewer/description_input.dart | 10 ++- .../activity_statistics_provider_test.dart | 7 +- 20 files changed, 392 insertions(+), 193 deletions(-) create mode 100644 mobile/lib/interfaces/activity_api.interface.dart create mode 100644 mobile/lib/interfaces/asset_api.interface.dart create mode 100644 mobile/lib/interfaces/exif_info.interface.dart create mode 100644 mobile/lib/repositories/activity_api.repository.dart create mode 100644 mobile/lib/repositories/asset_api.repository.dart create mode 100644 mobile/lib/repositories/base_api.repository.dart create mode 100644 mobile/lib/repositories/exif_info.repository.dart delete mode 100644 mobile/lib/services/asset_description.service.dart diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 8f9d41d73610e..e996a54372b6e 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -64,7 +64,7 @@ custom_lint: allowed: # required / wanted - lib/entities/*.entity.dart - - lib/repositories/{album,asset,backup,user}.repository.dart + - lib/repositories/{album,asset,backup,exif_info,user}.repository.dart # acceptable exceptions for the time being - integration_test/test_utils/general_helper.dart - lib/main.dart @@ -75,7 +75,7 @@ custom_lint: - lib/pages/common/{album_asset_selection,gallery_viewer}.page.dart - lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart - lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,backup/manual_upload,search/all_motion_photos,search/recently_added_asset}.provider.dart - - lib/services/{asset,asset_description,background,backup,backup_verification,hash,immich_logger,memory,partner,person,search,stack,sync,user}.service.dart + - lib/services/{asset,background,backup,hash,immich_logger,memory,partner,person,search,stack,sync,user}.service.dart - lib/widgets/asset_grid/{asset_grid_data_structure,thumbnail_image}.dart - import_rule_openapi: @@ -83,13 +83,12 @@ custom_lint: restrict: package:openapi allowed: # requried / wanted - - lib/repositories/album_api.repository.dart + - lib/repositories/*_api.repository.dart # acceptable exceptions for the time being - lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities - lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine - test/modules/utils/openapi_patching_test.dart # filename is self-explanatory... # refactor - - lib/models/activities/activity.model.dart - lib/models/map/map_marker.model.dart - lib/models/search/search_filter.model.dart - lib/models/server_info/server_{config,disk_info,features,version}.model.dart @@ -102,7 +101,7 @@ custom_lint: - lib/providers/search/{people,search,search_filter}.provider.dart - lib/providers/websocket.provider.dart - lib/routing/auth_guard.dart - - lib/services/{activity,api,asset,asset_description,backup,memory,oauth,partner,person,search,shared_link,stack,trash,user}.service.dart + - lib/services/{api,asset,backup,memory,oauth,partner,person,search,shared_link,stack,trash,user}.service.dart - lib/widgets/album/album_thumbnail_listtile.dart - lib/widgets/forms/login/login_form.dart - lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart diff --git a/mobile/lib/interfaces/activity_api.interface.dart b/mobile/lib/interfaces/activity_api.interface.dart new file mode 100644 index 0000000000000..99aef6f4d4668 --- /dev/null +++ b/mobile/lib/interfaces/activity_api.interface.dart @@ -0,0 +1,16 @@ +import 'package:immich_mobile/models/activities/activity.model.dart'; + +abstract interface class IActivityApiRepository { + Future> getAll( + String albumId, { + String? assetId, + }); + Future create( + String albumId, + ActivityType type, { + String? assetId, + String? comment, + }); + Future delete(String id); + Future getStats(String albumId, {String? assetId}); +} diff --git a/mobile/lib/interfaces/asset.interface.dart b/mobile/lib/interfaces/asset.interface.dart index 2574e52112a9a..98f4c7687cdfe 100644 --- a/mobile/lib/interfaces/asset.interface.dart +++ b/mobile/lib/interfaces/asset.interface.dart @@ -7,4 +7,16 @@ abstract interface class IAssetRepository { Future> getAllByRemoteId(Iterable ids); Future> getByAlbum(Album album, {User? notOwnedBy}); Future deleteById(List ids); + Future> getAll({ + required int ownerId, + bool? remote, + int limit = 100, + }); + + Future> getMatches({ + required List assets, + required int ownerId, + bool? remote, + int limit = 100, + }); } diff --git a/mobile/lib/interfaces/asset_api.interface.dart b/mobile/lib/interfaces/asset_api.interface.dart new file mode 100644 index 0000000000000..201c85cea7324 --- /dev/null +++ b/mobile/lib/interfaces/asset_api.interface.dart @@ -0,0 +1,16 @@ +import 'package:immich_mobile/entities/asset.entity.dart'; + +abstract interface class IAssetApiRepository { + // Future get(String id); + + // Future> getAll(); + + // Future create(Asset asset); + + Future update( + String id, { + String? description, + }); + + // Future delete(String id); +} diff --git a/mobile/lib/interfaces/exif_info.interface.dart b/mobile/lib/interfaces/exif_info.interface.dart new file mode 100644 index 0000000000000..fa8ca08f9d55f --- /dev/null +++ b/mobile/lib/interfaces/exif_info.interface.dart @@ -0,0 +1,9 @@ +import 'package:immich_mobile/entities/exif_info.entity.dart'; + +abstract interface class IExifInfoRepository { + Future get(int id); + + Future update(ExifInfo exifInfo); + + Future delete(int id); +} diff --git a/mobile/lib/models/activities/activity.model.dart b/mobile/lib/models/activities/activity.model.dart index 6adb80dca9233..4702753f41cdd 100644 --- a/mobile/lib/models/activities/activity.model.dart +++ b/mobile/lib/models/activities/activity.model.dart @@ -1,5 +1,4 @@ import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:openapi/api.dart'; enum ActivityType { comment, like } @@ -38,16 +37,6 @@ class Activity { ); } - Activity.fromDto(ActivityResponseDto dto) - : id = dto.id, - assetId = dto.assetId, - comment = dto.comment, - createdAt = dto.createdAt, - type = dto.type == ReactionType.comment - ? ActivityType.comment - : ActivityType.like, - user = User.fromSimpleUserDto(dto.user); - @override String toString() { return 'Activity(id: $id, assetId: $assetId, comment: $comment, createdAt: $createdAt, type: $type, user: $user)'; @@ -75,3 +64,9 @@ class Activity { user.hashCode; } } + +class ActivityStats { + final int comments; + + const ActivityStats({required this.comments}); +} diff --git a/mobile/lib/providers/activity_service.provider.dart b/mobile/lib/providers/activity_service.provider.dart index dcfaac883fd7c..6bd139c56504e 100644 --- a/mobile/lib/providers/activity_service.provider.dart +++ b/mobile/lib/providers/activity_service.provider.dart @@ -1,9 +1,9 @@ +import 'package:immich_mobile/repositories/activity_api.repository.dart'; import 'package:immich_mobile/services/activity.service.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'activity_service.provider.g.dart'; @riverpod ActivityService activityService(ActivityServiceRef ref) => - ActivityService(ref.watch(apiServiceProvider)); + ActivityService(ref.watch(activityApiRepositoryProvider)); diff --git a/mobile/lib/providers/activity_statistics.provider.dart b/mobile/lib/providers/activity_statistics.provider.dart index afb43e8cba3d3..b1d2b4b9871f4 100644 --- a/mobile/lib/providers/activity_statistics.provider.dart +++ b/mobile/lib/providers/activity_statistics.provider.dart @@ -11,7 +11,7 @@ class ActivityStatistics extends _$ActivityStatistics { ref .watch(activityServiceProvider) .getStatistics(albumId, assetId: assetId) - .then((comments) => state = comments); + .then((stats) => state = stats.comments); return 0; } diff --git a/mobile/lib/repositories/activity_api.repository.dart b/mobile/lib/repositories/activity_api.repository.dart new file mode 100644 index 0000000000000..0b1b4d99f36df --- /dev/null +++ b/mobile/lib/repositories/activity_api.repository.dart @@ -0,0 +1,67 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/interfaces/activity_api.interface.dart'; +import 'package:immich_mobile/models/activities/activity.model.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/base_api.repository.dart'; +import 'package:openapi/api.dart'; + +final activityApiRepositoryProvider = Provider( + (ref) => ActivityApiRepository(ref.watch(apiServiceProvider).activitiesApi), +); + +class ActivityApiRepository extends BaseApiRepository + implements IActivityApiRepository { + final ActivitiesApi _api; + + ActivityApiRepository(this._api); + + @override + Future> getAll(String albumId, {String? assetId}) async { + final response = + await checkNull(_api.getActivities(albumId, assetId: assetId)); + return response.map(_toActivity).toList(); + } + + @override + Future create( + String albumId, + ActivityType type, { + String? assetId, + String? comment, + }) async { + final dto = ActivityCreateDto( + albumId: albumId, + type: type == ActivityType.comment + ? ReactionType.comment + : ReactionType.like, + assetId: assetId, + comment: comment, + ); + final response = await checkNull(_api.createActivity(dto)); + return _toActivity(response); + } + + @override + Future delete(String id) { + return checkNull(_api.deleteActivity(id)); + } + + @override + Future getStats(String albumId, {String? assetId}) async { + final response = + await checkNull(_api.getActivityStatistics(albumId, assetId: assetId)); + return ActivityStats(comments: response.comments); + } + + static Activity _toActivity(ActivityResponseDto dto) => Activity( + id: dto.id, + createdAt: dto.createdAt, + type: dto.type == ReactionType.comment + ? ActivityType.comment + : ActivityType.like, + user: User.fromSimpleUserDto(dto.user), + assetId: dto.assetId, + comment: dto.comment, + ); +} diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart index 6b7865f8e4573..0e27e44684a67 100644 --- a/mobile/lib/repositories/album_api.repository.dart +++ b/mobile/lib/repositories/album_api.repository.dart @@ -1,30 +1,31 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/errors.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/album_api.interface.dart'; import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/base_api.repository.dart'; import 'package:openapi/api.dart'; final albumApiRepositoryProvider = Provider( (ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi), ); -class AlbumApiRepository implements IAlbumApiRepository { +class AlbumApiRepository extends BaseApiRepository + implements IAlbumApiRepository { final AlbumsApi _api; AlbumApiRepository(this._api); @override Future get(String id) async { - final dto = await _checkNull(_api.getAlbumInfo(id)); + final dto = await checkNull(_api.getAlbumInfo(id)); return _toAlbum(dto); } @override Future> getAll({bool? shared}) async { - final dtos = await _checkNull(_api.getAllAlbums(shared: shared)); + final dtos = await checkNull(_api.getAllAlbums(shared: shared)); return dtos.map(_toAlbum).toList().cast(); } @@ -37,7 +38,7 @@ class AlbumApiRepository implements IAlbumApiRepository { final users = sharedUserIds.map( (id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor), ); - final responseDto = await _checkNull( + final responseDto = await checkNull( _api.createAlbum( CreateAlbumDto( albumName: name, @@ -57,7 +58,7 @@ class AlbumApiRepository implements IAlbumApiRepository { String? description, bool? activityEnabled, }) async { - final response = await _checkNull( + final response = await checkNull( _api.updateAlbumInfo( albumId, UpdateAlbumDto( @@ -81,7 +82,7 @@ class AlbumApiRepository implements IAlbumApiRepository { String albumId, Iterable assetIds, ) async { - final response = await _checkNull( + final response = await checkNull( _api.addAssetsToAlbum( albumId, BulkIdsDto(ids: assetIds.toList()), @@ -106,7 +107,7 @@ class AlbumApiRepository implements IAlbumApiRepository { String albumId, Iterable assetIds, ) async { - final response = await _checkNull( + final response = await checkNull( _api.removeAssetFromAlbum( albumId, BulkIdsDto(ids: assetIds.toList()), @@ -127,7 +128,7 @@ class AlbumApiRepository implements IAlbumApiRepository { Future addUsers(String albumId, Iterable userIds) async { final albumUsers = userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList(); - final response = await _checkNull( + final response = await checkNull( _api.addUsersToAlbum( albumId, AddUsersDto(albumUsers: albumUsers), @@ -141,12 +142,6 @@ class AlbumApiRepository implements IAlbumApiRepository { return _api.removeUserFromAlbum(albumId, userId); } - static Future _checkNull(Future future) async { - final response = await future; - if (response == null) throw NoResponseDtoError(); - return response; - } - static Album _toAlbum(AlbumResponseDto dto) { final Album album = Album( remoteId: dto.id, diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart index 8ec028f7288de..c6012af3717eb 100644 --- a/mobile/lib/repositories/asset.repository.dart +++ b/mobile/lib/repositories/asset.repository.dart @@ -35,4 +35,84 @@ class AssetRepository implements IAssetRepository { @override Future> getAllByRemoteId(Iterable ids) => _db.assets.getAllByRemoteId(ids); + + @override + Future> getAll({ + required int ownerId, + bool? remote, + int limit = 100, + }) { + if (remote == null) { + return _db.assets + .where() + .ownerIdEqualToAnyChecksum(ownerId) + .limit(limit) + .findAll(); + } + final QueryBuilder query; + if (remote) { + query = _db.assets + .where() + .localIdIsNull() + .filter() + .remoteIdIsNotNull() + .ownerIdEqualTo(ownerId); + } else { + query = _db.assets + .where() + .remoteIdIsNull() + .filter() + .localIdIsNotNull() + .ownerIdEqualTo(ownerId); + } + + return query.limit(limit).findAll(); + } + + @override + Future> getMatches({ + required List assets, + required int ownerId, + bool? remote, + int limit = 100, + }) { + final QueryBuilder query; + if (remote == null) { + query = _db.assets.filter().remoteIdIsNotNull().or().localIdIsNotNull(); + } else if (remote) { + query = _db.assets.where().localIdIsNull().filter().remoteIdIsNotNull(); + } else { + query = _db.assets.where().remoteIdIsNull().filter().localIdIsNotNull(); + } + return _getMatchesImpl(query, ownerId, assets, limit); + } } + +Future> _getMatchesImpl( + QueryBuilder query, + int ownerId, + List assets, + int limit, +) => + query + .ownerIdEqualTo(ownerId) + .anyOf( + assets, + (q, Asset a) => q + .fileNameEqualTo(a.fileName) + .and() + .durationInSecondsEqualTo(a.durationInSeconds) + .and() + .fileCreatedAtBetween( + a.fileCreatedAt.subtract(const Duration(hours: 12)), + a.fileCreatedAt.add(const Duration(hours: 12)), + ) + .and() + .not() + .checksumEqualTo(a.checksum), + ) + .sortByFileName() + .thenByFileCreatedAt() + .thenByFileModifiedAt() + .limit(limit) + .findAll(); diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart new file mode 100644 index 0000000000000..3ad0e1cba0d19 --- /dev/null +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -0,0 +1,25 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/interfaces/asset_api.interface.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/base_api.repository.dart'; +import 'package:openapi/api.dart'; + +final assetApiRepositoryProvider = Provider( + (ref) => AssetApiRepository(ref.watch(apiServiceProvider).assetsApi), +); + +class AssetApiRepository extends BaseApiRepository + implements IAssetApiRepository { + final AssetsApi _api; + + AssetApiRepository(this._api); + + @override + Future update(String id, {String? description}) async { + final response = await checkNull( + _api.updateAsset(id, UpdateAssetDto(description: description)), + ); + return Asset.remote(response); + } +} diff --git a/mobile/lib/repositories/base_api.repository.dart b/mobile/lib/repositories/base_api.repository.dart new file mode 100644 index 0000000000000..418cba84f886c --- /dev/null +++ b/mobile/lib/repositories/base_api.repository.dart @@ -0,0 +1,11 @@ +import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/constants/errors.dart'; + +abstract class BaseApiRepository { + @protected + Future checkNull(Future future) async { + final response = await future; + if (response == null) throw NoResponseDtoError(); + return response; + } +} diff --git a/mobile/lib/repositories/exif_info.repository.dart b/mobile/lib/repositories/exif_info.repository.dart new file mode 100644 index 0000000000000..a165e98bdbfe3 --- /dev/null +++ b/mobile/lib/repositories/exif_info.repository.dart @@ -0,0 +1,28 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/exif_info.entity.dart'; +import 'package:immich_mobile/interfaces/exif_info.interface.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:isar/isar.dart'; + +final exifInfoRepositoryProvider = + Provider((ref) => ExifInfoRepository(ref.watch(dbProvider))); + +class ExifInfoRepository implements IExifInfoRepository { + final Isar _db; + + ExifInfoRepository( + this._db, + ); + + @override + Future delete(int id) => _db.exifInfos.delete(id); + + @override + Future get(int id) => _db.exifInfos.get(id); + + @override + Future update(ExifInfo exifInfo) async { + await _db.writeTxn(() => _db.exifInfos.put(exifInfo)); + return exifInfo; + } +} diff --git a/mobile/lib/services/activity.service.dart b/mobile/lib/services/activity.service.dart index 58af26e204663..5496041416558 100644 --- a/mobile/lib/services/activity.service.dart +++ b/mobile/lib/services/activity.service.dart @@ -1,41 +1,31 @@ -import 'package:immich_mobile/constants/errors.dart'; +import 'package:immich_mobile/interfaces/activity_api.interface.dart'; import 'package:immich_mobile/mixins/error_logger.mixin.dart'; import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; class ActivityService with ErrorLoggerMixin { - final ApiService _apiService; + final IActivityApiRepository _activityApiRepository; @override final Logger logger = Logger("ActivityService"); - ActivityService(this._apiService); + ActivityService(this._activityApiRepository); Future> getAllActivities( String albumId, { String? assetId, }) async { return logError( - () async { - final list = await _apiService.activitiesApi - .getActivities(albumId, assetId: assetId); - return list != null ? list.map(Activity.fromDto).toList() : []; - }, + () => _activityApiRepository.getAll(albumId, assetId: assetId), defaultValue: [], errorMessage: "Failed to get all activities for album $albumId", ); } - Future getStatistics(String albumId, {String? assetId}) async { + Future getStatistics(String albumId, {String? assetId}) async { return logError( - () async { - final dto = await _apiService.activitiesApi - .getActivityStatistics(albumId, assetId: assetId); - return dto?.comments ?? 0; - }, - defaultValue: 0, + () => _activityApiRepository.getStats(albumId, assetId: assetId), + defaultValue: const ActivityStats(comments: 0), errorMessage: "Failed to statistics for album $albumId", ); } @@ -43,7 +33,7 @@ class ActivityService with ErrorLoggerMixin { Future removeActivity(String id) async { return logError( () async { - await _apiService.activitiesApi.deleteActivity(id); + await _activityApiRepository.delete(id); return true; }, defaultValue: false, @@ -58,22 +48,12 @@ class ActivityService with ErrorLoggerMixin { String? comment, }) async { return guardError( - () async { - final dto = await _apiService.activitiesApi.createActivity( - ActivityCreateDto( - albumId: albumId, - type: type == ActivityType.comment - ? ReactionType.comment - : ReactionType.like, - assetId: assetId, - comment: comment, - ), - ); - if (dto != null) { - return Activity.fromDto(dto); - } - throw NoResponseDtoError(); - }, + () => _activityApiRepository.create( + albumId, + type, + assetId: assetId, + comment: comment, + ), errorMessage: "Failed to create $type for album $albumId", ); } diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index 90c46ae90a6d9..262040026e6a2 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -9,9 +9,13 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/interfaces/asset_api.interface.dart'; +import 'package:immich_mobile/interfaces/exif_info.interface.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/exif_info.repository.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/backup.service.dart'; @@ -24,6 +28,8 @@ import 'package:openapi/api.dart'; final assetServiceProvider = Provider( (ref) => AssetService( + ref.watch(assetApiRepositoryProvider), + ref.watch(exifInfoRepositoryProvider), ref.watch(apiServiceProvider), ref.watch(syncServiceProvider), ref.watch(userServiceProvider), @@ -34,6 +40,8 @@ final assetServiceProvider = Provider( ); class AssetService { + final IAssetApiRepository _assetApiRepository; + final IExifInfoRepository _exifInfoRepository; final ApiService _apiService; final SyncService _syncService; final UserService _userService; @@ -43,6 +51,8 @@ class AssetService { final Isar _db; AssetService( + this._assetApiRepository, + this._exifInfoRepository, this._apiService, this._syncService, this._userService, @@ -342,4 +352,46 @@ class AssetService { log.severe("Error while syncing uploaded asset to albums", error, stack); } } + + Future setDescription( + Asset asset, + String newDescription, + ) async { + final remoteAssetId = asset.remoteId; + final localExifId = asset.exifInfo?.id; + + // Guard [remoteAssetId] and [localExifId] null + if (remoteAssetId == null || localExifId == null) { + return; + } + + final result = await _assetApiRepository.update( + remoteAssetId, + description: newDescription, + ); + + final description = result.exifInfo?.description; + + if (description != null) { + var exifInfo = await _exifInfoRepository.get(localExifId); + + if (exifInfo != null) { + exifInfo.description = description; + await _exifInfoRepository.update(exifInfo); + } + } + } + + Future getDescription(Asset asset) async { + final localExifId = asset.exifInfo?.id; + + // Guard [remoteAssetId] and [localExifId] null + if (localExifId == null) { + return ""; + } + + final exifInfo = await _exifInfoRepository.get(localExifId); + + return exifInfo?.description ?? ""; + } } diff --git a/mobile/lib/services/asset_description.service.dart b/mobile/lib/services/asset_description.service.dart deleted file mode 100644 index 196e29dc6a97d..0000000000000 --- a/mobile/lib/services/asset_description.service.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/exif_info.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:isar/isar.dart'; -import 'package:openapi/api.dart'; - -class AssetDescriptionService { - AssetDescriptionService(this._db, this._api); - - final Isar _db; - final ApiService _api; - - Future setDescription( - Asset asset, - String newDescription, - ) async { - final remoteAssetId = asset.remoteId; - final localExifId = asset.exifInfo?.id; - - // Guard [remoteAssetId] and [localExifId] null - if (remoteAssetId == null || localExifId == null) { - return; - } - - final result = await _api.assetsApi.updateAsset( - remoteAssetId, - UpdateAssetDto(description: newDescription), - ); - - final description = result?.exifInfo?.description; - - if (description != null) { - var exifInfo = await _db.exifInfos.get(localExifId); - - if (exifInfo != null) { - exifInfo.description = description; - await _db.writeTxn( - () => _db.exifInfos.put(exifInfo), - ); - } - } - } - - String getAssetDescription(Asset asset) { - final localExifId = asset.exifInfo?.id; - - // Guard [remoteAssetId] and [localExifId] null - if (localExifId == null) { - return ""; - } - - final exifInfo = _db.exifInfos.getSync(localExifId); - - return exifInfo?.description ?? ""; - } -} - -final assetDescriptionServiceProvider = Provider( - (ref) => AssetDescriptionService( - ref.watch(dbProvider), - ref.watch(apiServiceProvider), - ), -); diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart index 66a61d29142cb..da9d8da1649e4 100644 --- a/mobile/lib/services/backup_verification.service.dart +++ b/mobile/lib/services/backup_verification.service.dart @@ -8,41 +8,46 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; +import 'package:immich_mobile/interfaces/exif_info.interface.dart'; import 'package:immich_mobile/interfaces/file_media.interface.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; +import 'package:immich_mobile/repositories/exif_info.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/diff.dart'; -import 'package:isar/isar.dart'; /// Finds duplicates originating from missing EXIF information class BackupVerificationService { - final Isar _db; final IFileMediaRepository _fileMediaRepository; + final IAssetRepository _assetRepository; + final IExifInfoRepository _exifInfoRepository; - BackupVerificationService(this._db, this._fileMediaRepository); + BackupVerificationService( + this._fileMediaRepository, + this._assetRepository, + this._exifInfoRepository, + ); /// Returns at most [limit] assets that were backed up without exif Future> findWronglyBackedUpAssets({int limit = 100}) async { final owner = Store.get(StoreKey.currentUser).isarId; - final List onlyLocal = await _db.assets - .where() - .remoteIdIsNull() - .filter() - .ownerIdEqualTo(owner) - .localIdIsNotNull() - .findAll(); - final List remoteMatches = await _getMatches( - _db.assets.where().localIdIsNull().filter().remoteIdIsNotNull(), - owner, - onlyLocal, - limit, + final List onlyLocal = await _assetRepository.getAll( + ownerId: owner, + remote: false, + limit: limit, ); - final List localMatches = await _getMatches( - _db.assets.where().remoteIdIsNull().filter().localIdIsNotNull(), - owner, - remoteMatches, - limit, + final List remoteMatches = await _assetRepository.getMatches( + assets: onlyLocal, + ownerId: owner, + remote: true, + limit: limit, + ); + final List localMatches = await _assetRepository.getMatches( + assets: remoteMatches, + ownerId: owner, + remote: false, + limit: limit, ); final List deleteCandidates = [], originals = []; @@ -52,7 +57,7 @@ class BackupVerificationService { localMatches, compare: (a, b) => a.fileName.compareTo(b.fileName), both: (a, b) async { - a.exifInfo = await _db.exifInfos.get(a.id); + a.exifInfo = await _exifInfoRepository.get(a.id); deleteCandidates.add(a); originals.add(b); return false; @@ -192,35 +197,6 @@ class BackupVerificationService { return bytes.buffer.asUint64List(start); } - static Future> _getMatches( - QueryBuilder query, - int ownerId, - List assets, - int limit, - ) => - query - .ownerIdEqualTo(ownerId) - .anyOf( - assets, - (q, Asset a) => q - .fileNameEqualTo(a.fileName) - .and() - .durationInSecondsEqualTo(a.durationInSeconds) - .and() - .fileCreatedAtBetween( - a.fileCreatedAt.subtract(const Duration(hours: 12)), - a.fileCreatedAt.add(const Duration(hours: 12)), - ) - .and() - .not() - .checksumEqualTo(a.checksum), - ) - .sortByFileName() - .thenByFileCreatedAt() - .thenByFileModifiedAt() - .limit(limit) - .findAll(); - static bool _sameExceptTimeZone(DateTime a, DateTime b) { final ms = a.isAfter(b) ? a.millisecondsSinceEpoch - b.millisecondsSinceEpoch @@ -233,7 +209,8 @@ class BackupVerificationService { final backupVerificationServiceProvider = Provider( (ref) => BackupVerificationService( - ref.watch(dbProvider), ref.watch(fileMediaRepositoryProvider), + ref.watch(assetRepositoryProvider), + ref.watch(exifInfoRepositoryProvider), ), ); diff --git a/mobile/lib/widgets/asset_viewer/description_input.dart b/mobile/lib/widgets/asset_viewer/description_input.dart index 18ef394e2d266..3fdd40130a710 100644 --- a/mobile/lib/widgets/asset_viewer/description_input.dart +++ b/mobile/lib/widgets/asset_viewer/description_input.dart @@ -8,7 +8,7 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/asset_description.service.dart'; +import 'package:immich_mobile/services/asset.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:logging/logging.dart'; @@ -29,14 +29,16 @@ class DescriptionInput extends HookConsumerWidget { final focusNode = useFocusNode(); final isFocus = useState(false); final isTextEmpty = useState(controller.text.isEmpty); - final descriptionProvider = ref.watch(assetDescriptionServiceProvider); + final assetService = ref.watch(assetServiceProvider); final owner = ref.watch(currentUserProvider); final hasError = useState(false); final assetWithExif = ref.watch(assetDetailProvider(asset)); useEffect( () { - controller.text = descriptionProvider.getAssetDescription(asset); + assetService + .getDescription(asset) + .then((value) => controller.text = value); return null; }, [assetWithExif.value], @@ -45,7 +47,7 @@ class DescriptionInput extends HookConsumerWidget { submitDescription(String description) async { hasError.value = false; try { - await descriptionProvider.setDescription( + await assetService.setDescription( asset, description, ); diff --git a/mobile/test/modules/activity/activity_statistics_provider_test.dart b/mobile/test/modules/activity/activity_statistics_provider_test.dart index 9edabcc0d0b66..0216528ddd31f 100644 --- a/mobile/test/modules/activity/activity_statistics_provider_test.dart +++ b/mobile/test/modules/activity/activity_statistics_provider_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/models/activities/activity.model.dart'; import 'package:immich_mobile/providers/activity_service.provider.dart'; import 'package:immich_mobile/providers/activity_statistics.provider.dart'; import 'package:mocktail/mocktail.dart'; @@ -25,7 +26,7 @@ void main() { test('Returns the proper count family', () async { when( () => activityMock.getStatistics('test-album', assetId: 'test-asset'), - ).thenAnswer((_) async => 5); + ).thenAnswer((_) async => const ActivityStats(comments: 5)); // Read here to make the getStatistics call container.read(activityStatisticsProvider('test-album', 'test-asset')); @@ -50,7 +51,7 @@ void main() { test('Adds activity', () async { when( () => activityMock.getStatistics('test-album'), - ).thenAnswer((_) async => 10); + ).thenAnswer((_) async => const ActivityStats(comments: 10)); final provider = activityStatisticsProvider('test-album'); container.listen( @@ -71,7 +72,7 @@ void main() { test('Removes activity', () async { when( () => activityMock.getStatistics('new-album', assetId: 'test-asset'), - ).thenAnswer((_) async => 10); + ).thenAnswer((_) async => const ActivityStats(comments: 10)); final provider = activityStatisticsProvider('new-album', 'test-asset'); container.listen( From 202082f62ee6ad4f9a4a8fb19e2c3b486ec7bf9e Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey <10599762+fyfrey@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:50:21 +0200 Subject: [PATCH 50/57] refactor(mobile): use repositories in a number of services (#12891) * UserService * PartnerService * HashService * MemoryService * PersonService * SearchService * StackService --- mobile/analysis_options.yaml | 14 ++-- mobile/lib/constants/constants.dart | 1 + mobile/lib/interfaces/asset.interface.dart | 5 ++ .../lib/interfaces/asset_api.interface.dart | 2 + .../lib/interfaces/partner_api.interface.dart | 13 +++ .../lib/interfaces/person_api.interface.dart | 22 +++++ mobile/lib/interfaces/user.interface.dart | 2 + mobile/lib/interfaces/user_api.interface.dart | 11 +++ .../models/search/search_filter.model.dart | 6 +- .../lib/pages/common/gallery_viewer.page.dart | 4 +- .../lib/pages/search/search_input.page.dart | 4 +- .../activity_service.provider.g.dart | 2 +- .../activity_statistics.provider.g.dart | 2 +- .../suggested_shared_users.provider.dart | 2 +- .../providers/map/map_state.provider.g.dart | 2 +- .../lib/providers/search/people.provider.dart | 4 +- .../providers/search/people.provider.g.dart | 7 +- mobile/lib/repositories/asset.repository.dart | 25 ++++++ .../repositories/asset_api.repository.dart | 31 ++++++- .../repositories/partner_api.repository.dart | 51 ++++++++++++ .../repositories/person_api.repository.dart | 38 +++++++++ mobile/lib/repositories/user.repository.dart | 16 ++++ .../lib/repositories/user_api.repository.dart | 41 ++++++++++ mobile/lib/services/background.service.dart | 19 +++-- mobile/lib/services/hash.service.dart | 29 +++---- mobile/lib/services/memory.service.dart | 13 ++- mobile/lib/services/partner.service.dart | 62 ++++++-------- mobile/lib/services/person.service.dart | 77 +++++++----------- mobile/lib/services/person.service.g.dart | 2 +- mobile/lib/services/search.service.dart | 12 +-- mobile/lib/services/stack.service.dart | 15 ++-- mobile/lib/services/user.service.dart | 80 ++++++++----------- mobile/lib/utils/image_url_builder.dart | 4 +- .../widgets/asset_grid/thumbnail_image.dart | 4 +- .../search/search_filter/people_picker.dart | 8 +- 35 files changed, 416 insertions(+), 214 deletions(-) create mode 100644 mobile/lib/constants/constants.dart create mode 100644 mobile/lib/interfaces/partner_api.interface.dart create mode 100644 mobile/lib/interfaces/person_api.interface.dart create mode 100644 mobile/lib/interfaces/user_api.interface.dart create mode 100644 mobile/lib/repositories/partner_api.repository.dart create mode 100644 mobile/lib/repositories/person_api.repository.dart create mode 100644 mobile/lib/repositories/user_api.repository.dart diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index e996a54372b6e..6a7d7a6b4df89 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -69,14 +69,14 @@ custom_lint: - integration_test/test_utils/general_helper.dart - lib/main.dart - lib/routing/router.dart - - lib/utils/{db,image_url_builder,migration,renderlist_generator}.dart + - lib/utils/{db,migration,renderlist_generator}.dart - test/**.dart # refactor to make the providers and services testable - - lib/pages/common/{album_asset_selection,gallery_viewer}.page.dart + - lib/pages/common/album_asset_selection.page.dart - lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart - lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,backup/manual_upload,search/all_motion_photos,search/recently_added_asset}.provider.dart - - lib/services/{asset,background,backup,hash,immich_logger,memory,partner,person,search,stack,sync,user}.service.dart - - lib/widgets/asset_grid/{asset_grid_data_structure,thumbnail_image}.dart + - lib/services/{asset,background,backup,immich_logger,sync}.service.dart + - lib/widgets/asset_grid/asset_grid_data_structure.dart - import_rule_openapi: message: openapi must only be used through ApiRepositories @@ -90,18 +90,16 @@ custom_lint: - test/modules/utils/openapi_patching_test.dart # filename is self-explanatory... # refactor - lib/models/map/map_marker.model.dart - - lib/models/search/search_filter.model.dart - lib/models/server_info/server_{config,disk_info,features,version}.model.dart - lib/models/shared_link/shared_link.model.dart - - lib/pages/search/search_input.page.dart - lib/providers/asset_viewer/asset_people.provider.dart - lib/providers/authentication.provider.dart - lib/providers/image/immich_remote_{image,thumbnail}_provider.dart - lib/providers/map/map_state.provider.dart - - lib/providers/search/{people,search,search_filter}.provider.dart + - lib/providers/search/{search,search_filter}.provider.dart - lib/providers/websocket.provider.dart - lib/routing/auth_guard.dart - - lib/services/{api,asset,backup,memory,oauth,partner,person,search,shared_link,stack,trash,user}.service.dart + - lib/services/{api,asset,backup,memory,oauth,search,shared_link,stack,trash}.service.dart - lib/widgets/album/album_thumbnail_listtile.dart - lib/widgets/forms/login/login_form.dart - lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart new file mode 100644 index 0000000000000..8b74b1a66fb2f --- /dev/null +++ b/mobile/lib/constants/constants.dart @@ -0,0 +1 @@ +const int noDbId = -9223372036854775808; // from Isar diff --git a/mobile/lib/interfaces/asset.interface.dart b/mobile/lib/interfaces/asset.interface.dart index 98f4c7687cdfe..0d2dcfa1b5b35 100644 --- a/mobile/lib/interfaces/asset.interface.dart +++ b/mobile/lib/interfaces/asset.interface.dart @@ -1,5 +1,6 @@ import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/device_asset.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; abstract interface class IAssetRepository { @@ -12,6 +13,7 @@ abstract interface class IAssetRepository { bool? remote, int limit = 100, }); + Future> updateAll(List assets); Future> getMatches({ required List assets, @@ -19,4 +21,7 @@ abstract interface class IAssetRepository { bool? remote, int limit = 100, }); + + Future> getDeviceAssetsById(List ids); + Future upsertDeviceAssets(List deviceAssets); } diff --git a/mobile/lib/interfaces/asset_api.interface.dart b/mobile/lib/interfaces/asset_api.interface.dart index 201c85cea7324..fe3320c9bb101 100644 --- a/mobile/lib/interfaces/asset_api.interface.dart +++ b/mobile/lib/interfaces/asset_api.interface.dart @@ -13,4 +13,6 @@ abstract interface class IAssetApiRepository { }); // Future delete(String id); + + Future> search({List personIds = const []}); } diff --git a/mobile/lib/interfaces/partner_api.interface.dart b/mobile/lib/interfaces/partner_api.interface.dart new file mode 100644 index 0000000000000..bca1baf66d251 --- /dev/null +++ b/mobile/lib/interfaces/partner_api.interface.dart @@ -0,0 +1,13 @@ +import 'package:immich_mobile/entities/user.entity.dart'; + +abstract interface class IPartnerApiRepository { + Future> getAll(Direction direction); + Future create(String id); + Future update(String id, {required bool inTimeline}); + Future delete(String id); +} + +enum Direction { + sharedWithMe, + sharedByMe, +} diff --git a/mobile/lib/interfaces/person_api.interface.dart b/mobile/lib/interfaces/person_api.interface.dart new file mode 100644 index 0000000000000..b2fa28df8cc19 --- /dev/null +++ b/mobile/lib/interfaces/person_api.interface.dart @@ -0,0 +1,22 @@ +abstract interface class IPersonApiRepository { + Future> getAll(); + Future update(String id, {String? name}); +} + +class Person { + Person({ + required this.id, + required this.isHidden, + required this.name, + required this.thumbnailPath, + this.birthDate, + this.updatedAt, + }); + + final String id; + final DateTime? birthDate; + final bool isHidden; + final String name; + final String thumbnailPath; + final DateTime? updatedAt; +} diff --git a/mobile/lib/interfaces/user.interface.dart b/mobile/lib/interfaces/user.interface.dart index 4e847ea0229e7..828a7b2398a50 100644 --- a/mobile/lib/interfaces/user.interface.dart +++ b/mobile/lib/interfaces/user.interface.dart @@ -3,4 +3,6 @@ import 'package:immich_mobile/entities/user.entity.dart'; abstract interface class IUserRepository { Future> getByIds(List ids); Future get(String id); + Future> getAll({bool self = true}); + Future update(User user); } diff --git a/mobile/lib/interfaces/user_api.interface.dart b/mobile/lib/interfaces/user_api.interface.dart new file mode 100644 index 0000000000000..67ac3c08831be --- /dev/null +++ b/mobile/lib/interfaces/user_api.interface.dart @@ -0,0 +1,11 @@ +import 'dart:typed_data'; + +import 'package:immich_mobile/entities/user.entity.dart'; + +abstract interface class IUserApiRepository { + Future> getAll(); + Future<({String profileImagePath})> createProfileImage({ + required String name, + required Uint8List data, + }); +} diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 6a7c612b1563c..297a819b6a335 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:openapi/api.dart'; +import 'package:immich_mobile/interfaces/person_api.interface.dart'; class SearchLocationFilter { String? country; @@ -235,7 +235,7 @@ class SearchDisplayFilters { class SearchFilter { String? context; String? filename; - Set people; + Set people; SearchLocationFilter location; SearchCameraFilter camera; SearchDateFilter date; @@ -258,7 +258,7 @@ class SearchFilter { SearchFilter copyWith({ String? context, String? filename, - Set? people, + Set? people, SearchLocationFilter? location, SearchCameraFilter? camera, SearchDateFilter? date, diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index d8ea7cd89b47f..1434d1cca5f59 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/pages/common/video_viewer.page.dart'; @@ -30,7 +31,6 @@ import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart'; import 'package:immich_mobile/widgets/photo_view/src/photo_view_computed_scale.dart'; import 'package:immich_mobile/widgets/photo_view/src/photo_view_scale_state.dart'; import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attributes.dart'; -import 'package:isar/isar.dart'; @RoutePage() // ignore: must_be_immutable @@ -73,7 +73,7 @@ class GalleryViewerPage extends HookConsumerWidget { : []; final stackElements = showStack ? [currentAsset, ...stack] : []; // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id - final isFromDto = currentAsset.id == Isar.autoIncrement; + final isFromDto = currentAsset.id == noDbId; Asset asset = stackIndex.value == -1 ? currentAsset diff --git a/mobile/lib/pages/search/search_input.page.dart b/mobile/lib/pages/search/search_input.page.dart index acabc75aa4950..2ca2a379180dd 100644 --- a/mobile/lib/pages/search/search_input.page.dart +++ b/mobile/lib/pages/search/search_input.page.dart @@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/interfaces/person_api.interface.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/providers/search/paginated_search.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; @@ -19,7 +20,6 @@ import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dar import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart'; import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart'; import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart'; -import 'package:openapi/api.dart'; @RoutePage() class SearchInputPage extends HookConsumerWidget { @@ -110,7 +110,7 @@ class SearchInputPage extends HookConsumerWidget { } showPeoplePicker() { - handleOnSelect(Set value) { + handleOnSelect(Set value) { filter.value = filter.value.copyWith( people: value, ); diff --git a/mobile/lib/providers/activity_service.provider.g.dart b/mobile/lib/providers/activity_service.provider.g.dart index 8e5ef43260119..d42b2a39e45f7 100644 --- a/mobile/lib/providers/activity_service.provider.g.dart +++ b/mobile/lib/providers/activity_service.provider.g.dart @@ -6,7 +6,7 @@ part of 'activity_service.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$activityServiceHash() => r'5dd4955d14f5bf01c00d7f8750d07e7ace7cc4b0'; +String _$activityServiceHash() => r'23a3ee7db71676d2719daa64217a683cc5c7eab0'; /// See also [activityService]. @ProviderFor(activityService) diff --git a/mobile/lib/providers/activity_statistics.provider.g.dart b/mobile/lib/providers/activity_statistics.provider.g.dart index 79856c525b77c..16a3c0e81b374 100644 --- a/mobile/lib/providers/activity_statistics.provider.g.dart +++ b/mobile/lib/providers/activity_statistics.provider.g.dart @@ -7,7 +7,7 @@ part of 'activity_statistics.provider.dart'; // ************************************************************************** String _$activityStatisticsHash() => - r'a5f7bbee1891c33b72919a34e632ca9ef9cd8dbf'; + r'1f43f0bcb11c754ca3cb586a13570db25023b9a8'; /// Copied from Dart SDK class _SystemHash { diff --git a/mobile/lib/providers/album/suggested_shared_users.provider.dart b/mobile/lib/providers/album/suggested_shared_users.provider.dart index 77518f47d0c04..fe8a1fccce861 100644 --- a/mobile/lib/providers/album/suggested_shared_users.provider.dart +++ b/mobile/lib/providers/album/suggested_shared_users.provider.dart @@ -5,5 +5,5 @@ import 'package:immich_mobile/services/user.service.dart'; final otherUsersProvider = FutureProvider.autoDispose>((ref) { UserService userService = ref.watch(userServiceProvider); - return userService.getUsersInDb(); + return userService.getUsers(); }); diff --git a/mobile/lib/providers/map/map_state.provider.g.dart b/mobile/lib/providers/map/map_state.provider.g.dart index eff7b4b68e60f..23a570d1c8789 100644 --- a/mobile/lib/providers/map/map_state.provider.g.dart +++ b/mobile/lib/providers/map/map_state.provider.g.dart @@ -6,7 +6,7 @@ part of 'map_state.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$mapStateNotifierHash() => r'31fafe17aa85c48379a22ed3db3cc94af59ce5b8'; +String _$mapStateNotifierHash() => r'22e4e571bd0730dbc34b109255a62b920e9c7d66'; /// See also [MapStateNotifier]. @ProviderFor(MapStateNotifier) diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart index e2c243354b536..7c956f0a37b52 100644 --- a/mobile/lib/providers/search/people.provider.dart +++ b/mobile/lib/providers/search/people.provider.dart @@ -1,14 +1,14 @@ +import 'package:immich_mobile/interfaces/person_api.interface.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/services/person.service.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:openapi/api.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'people.provider.g.dart'; @riverpod -Future> getAllPeople( +Future> getAllPeople( GetAllPeopleRef ref, ) async { final PersonService personService = ref.read(personServiceProvider); diff --git a/mobile/lib/providers/search/people.provider.g.dart b/mobile/lib/providers/search/people.provider.g.dart index db2edfb9567aa..c5ff6287cd7a8 100644 --- a/mobile/lib/providers/search/people.provider.g.dart +++ b/mobile/lib/providers/search/people.provider.g.dart @@ -6,12 +6,11 @@ part of 'people.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$getAllPeopleHash() => r'4eff6666be5a74710d1e8587e01d8154310d85bd'; +String _$getAllPeopleHash() => r'3417b7e0c211382d4480a415e352139995d57b6d'; /// See also [getAllPeople]. @ProviderFor(getAllPeople) -final getAllPeopleProvider = - AutoDisposeFutureProvider>.internal( +final getAllPeopleProvider = AutoDisposeFutureProvider>.internal( getAllPeople, name: r'getAllPeopleProvider', debugGetCreateSourceHash: @@ -20,7 +19,7 @@ final getAllPeopleProvider = allTransitiveDependencies: null, ); -typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; +typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; String _$personAssetsHash() => r'3dfecb67a54d07e4208bcb9581b2625acd2e1832'; /// Copied from Dart SDK diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart index c6012af3717eb..087344302a417 100644 --- a/mobile/lib/repositories/asset.repository.dart +++ b/mobile/lib/repositories/asset.repository.dart @@ -1,6 +1,11 @@ +import 'dart:io'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/entities/android_device_asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/device_asset.entity.dart'; +import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/providers/db.provider.dart'; @@ -69,6 +74,12 @@ class AssetRepository implements IAssetRepository { return query.limit(limit).findAll(); } + @override + Future> updateAll(List assets) async { + await _db.writeTxn(() => _db.assets.putAll(assets)); + return assets; + } + @override Future> getMatches({ required List assets, @@ -86,6 +97,20 @@ class AssetRepository implements IAssetRepository { } return _getMatchesImpl(query, ownerId, assets, limit); } + + @override + Future> getDeviceAssetsById(List ids) => + Platform.isAndroid + ? _db.androidDeviceAssets.getAll(ids.cast()) + : _db.iOSDeviceAssets.getAllById(ids.cast()); + + @override + Future upsertDeviceAssets(List deviceAssets) => + _db.writeTxn( + () => Platform.isAndroid + ? _db.androidDeviceAssets.putAll(deviceAssets.cast()) + : _db.iOSDeviceAssets.putAll(deviceAssets.cast()), + ); } Future> _getMatchesImpl( diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index 3ad0e1cba0d19..eb796f6c6b5d2 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -6,14 +6,18 @@ import 'package:immich_mobile/repositories/base_api.repository.dart'; import 'package:openapi/api.dart'; final assetApiRepositoryProvider = Provider( - (ref) => AssetApiRepository(ref.watch(apiServiceProvider).assetsApi), + (ref) => AssetApiRepository( + ref.watch(apiServiceProvider).assetsApi, + ref.watch(apiServiceProvider).searchApi, + ), ); class AssetApiRepository extends BaseApiRepository implements IAssetApiRepository { final AssetsApi _api; + final SearchApi _searchApi; - AssetApiRepository(this._api); + AssetApiRepository(this._api, this._searchApi); @override Future update(String id, {String? description}) async { @@ -22,4 +26,27 @@ class AssetApiRepository extends BaseApiRepository ); return Asset.remote(response); } + + @override + Future> search({List personIds = const []}) async { + // TODO this always fetches all assets, change API and usage to actually do pagination + final List result = []; + bool hasNext = true; + int currentPage = 1; + while (hasNext) { + final response = await checkNull( + _searchApi.searchMetadata( + MetadataSearchDto( + personIds: personIds, + page: currentPage, + size: 1000, + ), + ), + ); + result.addAll(response.assets.items.map(Asset.remote)); + hasNext = response.assets.nextPage != null; + currentPage++; + } + return result; + } } diff --git a/mobile/lib/repositories/partner_api.repository.dart b/mobile/lib/repositories/partner_api.repository.dart new file mode 100644 index 0000000000000..3419a2bc77244 --- /dev/null +++ b/mobile/lib/repositories/partner_api.repository.dart @@ -0,0 +1,51 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/interfaces/partner_api.interface.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/base_api.repository.dart'; +import 'package:openapi/api.dart'; + +final partnerApiRepositoryProvider = Provider( + (ref) => PartnerApiRepository( + ref.watch(apiServiceProvider).partnersApi, + ), +); + +class PartnerApiRepository extends BaseApiRepository + implements IPartnerApiRepository { + final PartnersApi _api; + + PartnerApiRepository(this._api); + + @override + Future> getAll(Direction direction) async { + final response = await checkNull( + _api.getPartners( + direction == Direction.sharedByMe + ? PartnerDirection.by + : PartnerDirection.with_, + ), + ); + return response.map(User.fromPartnerDto).toList(); + } + + @override + Future create(String id) async { + final dto = await checkNull(_api.createPartner(id)); + return User.fromPartnerDto(dto); + } + + @override + Future delete(String id) => checkNull(_api.removePartner(id)); + + @override + Future update(String id, {required bool inTimeline}) async { + final dto = await checkNull( + _api.updatePartner( + id, + UpdatePartnerDto(inTimeline: inTimeline), + ), + ); + return User.fromPartnerDto(dto); + } +} diff --git a/mobile/lib/repositories/person_api.repository.dart b/mobile/lib/repositories/person_api.repository.dart new file mode 100644 index 0000000000000..8071c33dc2cea --- /dev/null +++ b/mobile/lib/repositories/person_api.repository.dart @@ -0,0 +1,38 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/person_api.interface.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/base_api.repository.dart'; +import 'package:openapi/api.dart'; + +final personApiRepositoryProvider = Provider( + (ref) => PersonApiRepository(ref.watch(apiServiceProvider).peopleApi), +); + +class PersonApiRepository extends BaseApiRepository + implements IPersonApiRepository { + final PeopleApi _api; + + PersonApiRepository(this._api); + + @override + Future> getAll() async { + final dto = await checkNull(_api.getAllPeople()); + return dto.people.map(_toPerson).toList(); + } + + @override + Future update(String id, {String? name}) async { + final dto = await checkNull( + _api.updatePerson(id, PersonUpdateDto(name: name)), + ); + return _toPerson(dto); + } + + static Person _toPerson(PersonResponseDto dto) => Person( + birthDate: dto.birthDate, + id: dto.id, + isHidden: dto.isHidden, + name: dto.name, + thumbnailPath: dto.thumbnailPath, + ); +} diff --git a/mobile/lib/repositories/user.repository.dart b/mobile/lib/repositories/user.repository.dart index b05af9a57f89c..796b1f421b863 100644 --- a/mobile/lib/repositories/user.repository.dart +++ b/mobile/lib/repositories/user.repository.dart @@ -1,4 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/user.interface.dart'; import 'package:immich_mobile/providers/db.provider.dart'; @@ -20,4 +21,19 @@ class UserRepository implements IUserRepository { @override Future get(String id) => _db.users.getById(id); + + @override + Future> getAll({bool self = true}) { + if (self) { + return _db.users.where().findAll(); + } + final int userId = Store.get(StoreKey.currentUser).isarId; + return _db.users.where().isarIdNotEqualTo(userId).findAll(); + } + + @override + Future update(User user) async { + await _db.writeTxn(() => _db.users.put(user)); + return user; + } } diff --git a/mobile/lib/repositories/user_api.repository.dart b/mobile/lib/repositories/user_api.repository.dart new file mode 100644 index 0000000000000..ffc50ae4c3e6c --- /dev/null +++ b/mobile/lib/repositories/user_api.repository.dart @@ -0,0 +1,41 @@ +import 'dart:typed_data'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:http/http.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/interfaces/user_api.interface.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/base_api.repository.dart'; +import 'package:openapi/api.dart'; + +final userApiRepositoryProvider = Provider( + (ref) => UserApiRepository( + ref.watch(apiServiceProvider).usersApi, + ), +); + +class UserApiRepository extends BaseApiRepository + implements IUserApiRepository { + final UsersApi _api; + + UserApiRepository(this._api); + + @override + Future> getAll() async { + final dto = await checkNull(_api.searchUsers()); + return dto.map(User.fromSimpleUserDto).toList(); + } + + @override + Future<({String profileImagePath})> createProfileImage({ + required String name, + required Uint8List data, + }) async { + final response = await checkNull( + _api.createProfileImage( + MultipartFile.fromBytes('file', data, filename: name), + ), + ); + return (profileImagePath: response.profileImagePath); + } +} diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index 09030a621bc9d..d06bc86d4871b 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -18,7 +18,9 @@ import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/backup.repository.dart'; import 'package:immich_mobile/repositories/album_media.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; +import 'package:immich_mobile/repositories/partner_api.repository.dart'; import 'package:immich_mobile/repositories/user.repository.dart'; +import 'package:immich_mobile/repositories/user_api.repository.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/entity.service.dart'; import 'package:immich_mobile/services/hash.service.dart'; @@ -30,7 +32,6 @@ import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/partner.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/user.service.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; @@ -362,16 +363,20 @@ class BackgroundService { apiService.setAccessToken(Store.get(StoreKey.accessToken)); AppSettingsService settingService = AppSettingsService(); AppSettingsService settingsService = AppSettingsService(); - PartnerService partnerService = PartnerService(apiService, db); AlbumRepository albumRepository = AlbumRepository(db); AssetRepository assetRepository = AssetRepository(db); BackupRepository backupAlbumRepository = BackupRepository(db); AlbumMediaRepository albumMediaRepository = AlbumMediaRepository(); FileMediaRepository fileMediaRepository = FileMediaRepository(); UserRepository userRepository = UserRepository(db); + UserApiRepository userApiRepository = + UserApiRepository(apiService.usersApi); AlbumApiRepository albumApiRepository = AlbumApiRepository(apiService.albumsApi); - HashService hashService = HashService(db, this, albumMediaRepository); + PartnerApiRepository partnerApiRepository = + PartnerApiRepository(apiService.partnersApi); + HashService hashService = + HashService(assetRepository, this, albumMediaRepository); EntityService entityService = EntityService(assetRepository, userRepository); SyncService syncSerive = SyncService( @@ -381,8 +386,12 @@ class BackgroundService { albumMediaRepository, albumApiRepository, ); - UserService userService = - UserService(apiService, db, syncSerive, partnerService); + UserService userService = UserService( + partnerApiRepository, + userApiRepository, + userRepository, + syncSerive, + ); AlbumService albumService = AlbumService( userService, syncSerive, diff --git a/mobile/lib/services/hash.service.dart b/mobile/lib/services/hash.service.dart index 94d680972fa1a..3827e421e6108 100644 --- a/mobile/lib/services/hash.service.dart +++ b/mobile/lib/services/hash.service.dart @@ -4,20 +4,24 @@ import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/interfaces/album_media.interface.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/repositories/album_media.repository.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/entities/android_device_asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/device_asset.entity.dart'; import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; class HashService { - HashService(this._db, this._backgroundService, this._albumMediaRepository); - final Isar _db; + HashService( + this._assetRepository, + this._backgroundService, + this._albumMediaRepository, + ); + final IAssetRepository _assetRepository; final BackgroundService _backgroundService; final IAlbumMediaRepository _albumMediaRepository; final _log = Logger('HashService'); @@ -55,7 +59,8 @@ class HashService { final ids = assets .map(Platform.isAndroid ? (a) => a.localId!.toInt() : (a) => a.localId!) .toList(); - final List hashes = await _lookupHashes(ids); + final List hashes = + await _assetRepository.getDeviceAssetsById(ids); final List toAdd = []; final List toHash = []; @@ -106,12 +111,6 @@ class HashService { return _getHashedAssets(assets, hashes); } - /// Lookup hashes of assets by their local ID - Future> _lookupHashes(List ids) => - Platform.isAndroid - ? _db.androidDeviceAssets.getAll(ids.cast()) - : _db.iOSDeviceAssets.getAllById(ids.cast()); - /// Processes a batch of files and saves any successfully hashed /// values to the DB table. Future _processBatch( @@ -131,11 +130,7 @@ class HashService { final validHashes = anyNull ? toAdd.where((e) => e.hash.length == 20).toList(growable: false) : toAdd; - await _db.writeTxn( - () => Platform.isAndroid - ? _db.androidDeviceAssets.putAll(validHashes.cast()) - : _db.iOSDeviceAssets.putAll(validHashes.cast()), - ); + await _assetRepository.upsertDeviceAssets(validHashes); _log.fine("Hashed ${validHashes.length}/${toHash.length} assets"); } @@ -168,7 +163,7 @@ class HashService { final hashServiceProvider = Provider( (ref) => HashService( - ref.watch(dbProvider), + ref.watch(assetRepositoryProvider), ref.watch(backgroundServiceProvider), ref.watch(albumMediaRepositoryProvider), ), diff --git a/mobile/lib/services/memory.service.dart b/mobile/lib/services/memory.service.dart index ea07f7c019ea8..b95899df678ec 100644 --- a/mobile/lib/services/memory.service.dart +++ b/mobile/lib/services/memory.service.dart @@ -1,18 +1,17 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/models/memories/memory.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; final memoryServiceProvider = StateProvider((ref) { return MemoryService( ref.watch(apiServiceProvider), - ref.watch(dbProvider), + ref.watch(assetRepositoryProvider), ); }); @@ -20,9 +19,9 @@ class MemoryService { final log = Logger("MemoryService"); final ApiService _apiService; - final Isar _db; + final IAssetRepository _assetRepository; - MemoryService(this._apiService, this._db); + MemoryService(this._apiService, this._assetRepository); Future?> getMemoryLane() async { try { @@ -39,7 +38,7 @@ class MemoryService { List memories = []; for (final MemoryLaneResponseDto(:yearsAgo, :assets) in data) { final dbAssets = - await _db.assets.getAllByRemoteId(assets.map((e) => e.id)); + await _assetRepository.getAllByRemoteId(assets.map((e) => e.id)); if (dbAssets.isNotEmpty) { final String title = yearsAgo <= 1 ? 'memories_year_ago'.tr() diff --git a/mobile/lib/services/partner.service.dart b/mobile/lib/services/partner.service.dart index 8cd2fe424f190..67d7f4e1d1f56 100644 --- a/mobile/lib/services/partner.service.dart +++ b/mobile/lib/services/partner.service.dart @@ -1,43 +1,33 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:isar/isar.dart'; +import 'package:immich_mobile/interfaces/partner_api.interface.dart'; +import 'package:immich_mobile/interfaces/user.interface.dart'; +import 'package:immich_mobile/repositories/partner_api.repository.dart'; +import 'package:immich_mobile/repositories/user.repository.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; final partnerServiceProvider = Provider( (ref) => PartnerService( - ref.watch(apiServiceProvider), - ref.watch(dbProvider), + ref.watch(partnerApiRepositoryProvider), + ref.watch(userRepositoryProvider), ), ); class PartnerService { - final ApiService _apiService; - final Isar _db; + final IPartnerApiRepository _partnerApiRepository; + final IUserRepository _userRepository; final Logger _log = Logger("PartnerService"); - PartnerService(this._apiService, this._db); - - Future?> getPartners(PartnerDirection direction) async { - try { - final userDtos = await _apiService.partnersApi.getPartners(direction); - if (userDtos != null) { - return userDtos.map((u) => User.fromPartnerDto(u)).toList(); - } - } catch (e) { - _log.warning("Failed to get partners for direction $direction", e); - } - return null; - } + PartnerService( + this._partnerApiRepository, + this._userRepository, + ); Future removePartner(User partner) async { try { - await _apiService.partnersApi.removePartner(partner.id); + await _partnerApiRepository.delete(partner.id); partner.isPartnerSharedBy = false; - await _db.writeTxn(() => _db.users.put(partner)); + await _userRepository.update(partner); } catch (e) { _log.warning("Failed to remove partner ${partner.id}", e); return false; @@ -47,12 +37,10 @@ class PartnerService { Future addPartner(User partner) async { try { - final dto = await _apiService.partnersApi.createPartner(partner.id); - if (dto != null) { - partner.isPartnerSharedBy = true; - await _db.writeTxn(() => _db.users.put(partner)); - return true; - } + await _partnerApiRepository.create(partner.id); + partner.isPartnerSharedBy = true; + await _userRepository.update(partner); + return true; } catch (e) { _log.warning("Failed to add partner ${partner.id}", e); } @@ -61,13 +49,13 @@ class PartnerService { Future updatePartner(User partner, {required bool inTimeline}) async { try { - final dto = await _apiService.partnersApi - .updatePartner(partner.id, UpdatePartnerDto(inTimeline: inTimeline)); - if (dto != null) { - partner.inTimeline = dto.inTimeline ?? partner.inTimeline; - await _db.writeTxn(() => _db.users.put(partner)); - return true; - } + final dto = await _partnerApiRepository.update( + partner.id, + inTimeline: inTimeline, + ); + partner.inTimeline = dto.inTimeline; + await _userRepository.update(partner); + return true; } catch (e) { _log.warning("Failed to update partner ${partner.id}", e); } diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart index ddb61f5e48a40..5b325acdc591b 100644 --- a/mobile/lib/services/person.service.dart +++ b/mobile/lib/services/person.service.dart @@ -1,29 +1,37 @@ import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:isar/isar.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; +import 'package:immich_mobile/interfaces/asset_api.interface.dart'; +import 'package:immich_mobile/interfaces/person_api.interface.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; +import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/person_api.repository.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'person.service.g.dart'; @riverpod -PersonService personService(PersonServiceRef ref) => - PersonService(ref.read(apiServiceProvider), ref.read(dbProvider)); +PersonService personService(PersonServiceRef ref) => PersonService( + ref.watch(personApiRepositoryProvider), + ref.watch(assetApiRepositoryProvider), + ref.read(assetRepositoryProvider), + ); class PersonService { final Logger _log = Logger("PersonService"); - final ApiService _apiService; - final Isar _db; + final IPersonApiRepository _personApiRepository; + final IAssetApiRepository _assetApiRepository; + final IAssetRepository _assetRepository; - PersonService(this._apiService, this._db); + PersonService( + this._personApiRepository, + this._assetApiRepository, + this._assetRepository, + ); - Future> getAllPeople() async { + Future> getAllPeople() async { try { - final peopleResponseDto = await _apiService.peopleApi.getAllPeople(); - return peopleResponseDto?.people ?? []; + return await _personApiRepository.getAll(); } catch (error, stack) { _log.severe("Error while fetching curated people", error, stack); return []; @@ -31,50 +39,19 @@ class PersonService { } Future> getPersonAssets(String id) async { - List result = []; - var hasNext = true; - var currentPage = 1; - try { - while (hasNext) { - final response = await _apiService.searchApi.searchMetadata( - MetadataSearchDto( - personIds: [id], - page: currentPage, - size: 1000, - ), - ); - - if (response == null) { - break; - } - - if (response.assets.nextPage == null) { - hasNext = false; - } - - final assets = response.assets.items; - final mapAssets = - await _db.assets.getAllByRemoteId(assets.map((e) => e.id)); - result.addAll(mapAssets); - - currentPage++; - } + final assets = await _assetApiRepository.search(personIds: [id]); + return await _assetRepository + .getAllByRemoteId(assets.map((a) => a.remoteId!)); } catch (error, stack) { _log.severe("Error while fetching person assets", error, stack); } - - return result; + return []; } - Future updateName(String id, String name) async { + Future updateName(String id, String name) async { try { - return await _apiService.peopleApi.updatePerson( - id, - PersonUpdateDto( - name: name, - ), - ); + return await _personApiRepository.update(id, name: name); } catch (error, stack) { _log.severe("Error while updating person name", error, stack); } diff --git a/mobile/lib/services/person.service.g.dart b/mobile/lib/services/person.service.g.dart index 01a5ed8f30943..9a24069fbffa5 100644 --- a/mobile/lib/services/person.service.g.dart +++ b/mobile/lib/services/person.service.g.dart @@ -6,7 +6,7 @@ part of 'person.service.dart'; // RiverpodGenerator // ************************************************************************** -String _$personServiceHash() => r'54e6df4b8eea744f6de009f8315c9fe6230f6798'; +String _$personServiceHash() => r'32f28cb5a3de0553c17447e33a0efde7409a43ed'; /// See also [personService]. @ProviderFor(personService) diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index cf3905e5ca240..336fe450108d3 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -1,27 +1,27 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; final searchServiceProvider = Provider( (ref) => SearchService( ref.watch(apiServiceProvider), - ref.watch(dbProvider), + ref.watch(assetRepositoryProvider), ), ); class SearchService { final ApiService _apiService; - final Isar _db; + final IAssetRepository _assetRepository; final _log = Logger("SearchService"); - SearchService(this._apiService, this._db); + SearchService(this._apiService, this._assetRepository); Future?> getSearchSuggestions( SearchSuggestionType type, { @@ -103,7 +103,7 @@ class SearchService { return null; } - return _db.assets + return _assetRepository .getAllByRemoteId(response.assets.items.map((e) => e.id)); } catch (error, stackTrace) { _log.severe("Failed to search for assets", error, stackTrace); diff --git a/mobile/lib/services/stack.service.dart b/mobile/lib/services/stack.service.dart index 75074101c2ff8..8bff21fef61a6 100644 --- a/mobile/lib/services/stack.service.dart +++ b/mobile/lib/services/stack.service.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:isar/isar.dart'; import 'package:openapi/api.dart'; class StackService { - StackService(this._api, this._db); + StackService(this._api, this._assetRepository); final ApiService _api; - final Isar _db; + final IAssetRepository _assetRepository; Future getStack(String stackId) async { try { @@ -61,10 +61,7 @@ class StackService { removeAssets.add(asset); } - - _db.writeTxn(() async { - await _db.assets.putAll(removeAssets); - }); + await _assetRepository.updateAll(removeAssets); } catch (error) { debugPrint("Error while deleting stack: $error"); } @@ -74,6 +71,6 @@ class StackService { final stackServiceProvider = Provider( (ref) => StackService( ref.watch(apiServiceProvider), - ref.watch(dbProvider), + ref.watch(assetRepositoryProvider), ), ); diff --git a/mobile/lib/services/user.service.dart b/mobile/lib/services/user.service.dart index 9631141c416c9..4c2b3cbbd00fb 100644 --- a/mobile/lib/services/user.service.dart +++ b/mobile/lib/services/user.service.dart @@ -1,68 +1,48 @@ import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:http/http.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:immich_mobile/services/partner.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/interfaces/partner_api.interface.dart'; +import 'package:immich_mobile/interfaces/user.interface.dart'; +import 'package:immich_mobile/interfaces/user_api.interface.dart'; +import 'package:immich_mobile/repositories/partner_api.repository.dart'; +import 'package:immich_mobile/repositories/user.repository.dart'; +import 'package:immich_mobile/repositories/user_api.repository.dart'; import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/utils/diff.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; final userServiceProvider = Provider( (ref) => UserService( - ref.watch(apiServiceProvider), - ref.watch(dbProvider), + ref.watch(partnerApiRepositoryProvider), + ref.watch(userApiRepositoryProvider), + ref.watch(userRepositoryProvider), ref.watch(syncServiceProvider), - ref.watch(partnerServiceProvider), ), ); class UserService { - final ApiService _apiService; - final Isar _db; + final IPartnerApiRepository _partnerApiRepository; + final IUserApiRepository _userApiRepository; + final IUserRepository _userRepository; final SyncService _syncService; - final PartnerService _partnerService; final Logger _log = Logger("UserService"); UserService( - this._apiService, - this._db, + this._partnerApiRepository, + this._userApiRepository, + this._userRepository, this._syncService, - this._partnerService, ); - Future?> _getAllUsers() async { - try { - final dto = await _apiService.usersApi.searchUsers(); - return dto?.map(User.fromSimpleUserDto).toList(); - } catch (e) { - _log.warning("Failed get all users", e); - return null; - } - } + Future> getUsers({bool self = false}) => + _userRepository.getAll(self: self); - Future> getUsersInDb({bool self = false}) async { - if (self) { - return _db.users.where().findAll(); - } - final int userId = Store.get(StoreKey.currentUser).isarId; - return _db.users.where().isarIdNotEqualTo(userId).findAll(); - } - - Future uploadProfileImage(XFile image) async { + Future<({String profileImagePath})?> uploadProfileImage(XFile image) async { try { - return await _apiService.usersApi.createProfileImage( - MultipartFile.fromBytes( - 'file', - await image.readAsBytes(), - filename: image.name, - ), + return await _userApiRepository.createProfileImage( + name: image.name, + data: await image.readAsBytes(), ); } catch (e) { _log.warning("Failed to upload profile image", e); @@ -71,13 +51,19 @@ class UserService { } Future?> getUsersFromServer() async { - final List? users = await _getAllUsers(); - final List? sharedBy = - await _partnerService.getPartners(PartnerDirection.by); - final List? sharedWith = - await _partnerService.getPartners(PartnerDirection.with_); + List? users; + try { + users = await _userApiRepository.getAll(); + } catch (e) { + _log.warning("Failed to fetch users", e); + users = null; + } + final List sharedBy = + await _partnerApiRepository.getAll(Direction.sharedByMe); + final List sharedWith = + await _partnerApiRepository.getAll(Direction.sharedWithMe); - if (users == null || sharedBy == null || sharedWith == null) { + if (users == null) { _log.warning("Failed to refresh users"); return null; } diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index e7a1b9e39eefe..9fc7b13eed1c6 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -1,7 +1,7 @@ +import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:isar/isar.dart'; import 'package:openapi/api.dart'; String getThumbnailUrl( @@ -61,7 +61,7 @@ String getOriginalUrlForRemoteId(final String id) { String getImageCacheKey(final Asset asset) { // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id - final isFromDto = asset.id == Isar.autoIncrement; + final isFromDto = asset.id == noDbId; return '${isFromDto ? asset.remoteId : asset.id}_fullStage'; } diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart index 8e818f64fb7cc..6cadef763d730 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_image.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; import 'package:immich_mobile/utils/storage_indicator.dart'; -import 'package:isar/isar.dart'; class ThumbnailImage extends ConsumerWidget { /// The asset to show the thumbnail image for @@ -46,7 +46,7 @@ class ThumbnailImage extends ConsumerWidget { ? context.primaryColor.darken(amount: 0.6) : context.primaryColor.lighten(amount: 0.8); // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id - final isFromDto = asset.id == Isar.autoIncrement; + final isFromDto = asset.id == noDbId; Widget buildSelectionIcon(Asset asset) { if (isSelected) { diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index d79ae5bd95d3b..dfc435c807158 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -3,23 +3,23 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/interfaces/person_api.interface.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:openapi/api.dart'; class PeoplePicker extends HookConsumerWidget { const PeoplePicker({super.key, required this.onSelect, this.filter}); - final Function(Set) onSelect; - final Set? filter; + final Function(Set) onSelect; + final Set? filter; @override Widget build(BuildContext context, WidgetRef ref) { var imageSize = 45.0; final people = ref.watch(getAllPeopleProvider); final headers = ApiService.getRequestHeaders(); - final selectedPeople = useState>(filter ?? {}); + final selectedPeople = useState>(filter ?? {}); return people.widgetWhen( onData: (people) { From f031c096870e75e8a31ca357935fd8a24273613f Mon Sep 17 00:00:00 2001 From: JonOcto <22536384+JonOcto@users.noreply.github.com> Date: Wed, 25 Sep 2024 00:18:07 +1000 Subject: [PATCH 51/57] fix(docs): typo in remote-access.md (#12895) Fixed typo in remote-access.md Fixed spelling of "tutorial". --- docs/docs/guides/remote-access.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/guides/remote-access.md b/docs/docs/guides/remote-access.md index 1ea068c3a0a79..6f401dfc5a5c3 100644 --- a/docs/docs/guides/remote-access.md +++ b/docs/docs/guides/remote-access.md @@ -27,7 +27,7 @@ You may use a VPN service to open an encrypted connection to your Immich instanc If you are unable to open a port on your router for Wireguard or OpenVPN to your server, [Tailscale](https://tailscale.com/) is a good option. Tailscale mediates a peer-to-peer wireguard tunnel between your server and remote device, even if one or both of them are behind a [NAT firewall](https://en.wikipedia.org/wiki/Network_address_translation). -:::tip Video toturial +:::tip Video tutorial You can learn how to set up Tailscale together with Immich with the [tutorial video](https://www.youtube.com/watch?v=Vt4PDUXB_fg) they created. ::: From b85d8943e7ce65f826e2c56d8a23922994dc22fa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 10:36:25 -0400 Subject: [PATCH 52/57] chore(deps): update base-image to v20240924 (major) (#12893) chore(deps): update base-image to v20240924 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- server/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Dockerfile b/server/Dockerfile index 64dcab758b5b2..66965c0edb73e 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20240917@sha256:3d92952d37cd68f5bf641aa80e5cc034e0d11f3774147f5db8db93138cfa5b3b AS dev +FROM ghcr.io/immich-app/base-server-dev:20240924@sha256:fff4358d435065a626c64a4c015cbfce6ee714b05fabe39aa0d83d8cff3951f2 AS dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -41,7 +41,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20240917@sha256:67a40250f03812fe1e6f6b6345a3c7b71b3a9f24c65ed4862e82be8b3e53d23a +FROM ghcr.io/immich-app/base-server-prod:20240924@sha256:af3089fe48d7ff162594bd7edfffa56ba4e7014ad10ad69c4ebfd428e39b06ff WORKDIR /usr/src/app ENV NODE_ENV=production \ From af8f3774d0f6dc582c4a8449e315b18d629d68cf Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Tue, 24 Sep 2024 10:38:13 -0400 Subject: [PATCH 53/57] docs: details for windows users how to change docker volume (#12551) * details for windows users * Update requirements.md --- docs/docs/install/docker-compose.mdx | 1 + docs/docs/install/environment-variables.md | 28 ++++++------------- docs/docs/install/requirements.md | 32 ++++++++++++++++++++-- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/docs/docs/install/docker-compose.mdx b/docs/docs/install/docker-compose.mdx index a3bd703a01c8c..b73d51b4d240a 100644 --- a/docs/docs/install/docker-compose.mdx +++ b/docs/docs/install/docker-compose.mdx @@ -58,6 +58,7 @@ Optionally, you can enable hardware acceleration for machine learning and transc - Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets. - Consider changing `DB_PASSWORD` to a custom value. Postgres is not publically exposed, so this password is only used for local authentication. To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`. +- Set your timezone by uncommenting the `TZ=` line. ### Step 3 - Start the containers diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index a0cf71e044724..3944f6755b65a 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -27,23 +27,14 @@ If this should not work, try running `docker compose up -d --force-recreate`. These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly. ::: -### Supported filesystems - -The Immich Postgres database (`DB_DATA_LOCATION`) must be located on a filesystem that supports user/group -ownership and permissions (EXT2/3/4, ZFS, APFS, BTRFS, XFS, etc.). It will not work on any filesystem formatted in NTFS or ex/FAT/32. -It will not work in WSL (Windows Subsystem for Linux) when using a mounted host directory (commonly under `/mnt`). -If this is an issue, you can change the bind mount to a Docker volume instead. - -Regardless of filesystem, it is not recommended to use a network share for your database location due to performance and possible data loss issues. - ## General | Variable | Description | Default | Containers | Workers | | :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- | -| `TZ` | Timezone | | server | microservices | +| `TZ` | Timezone | \*1 | server | microservices | | `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices | | `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | -| `IMMICH_MEDIA_LOCATION` | Media Location inside the container ⚠️**You probably shouldn't set this**\*1⚠️ | `./upload`\*2 | server | api, microservices | +| `IMMICH_MEDIA_LOCATION` | Media Location inside the container ⚠️**You probably shouldn't set this**\*2⚠️ | `./upload`\*3 | server | api, microservices | | `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | | `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | | | `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | | @@ -52,16 +43,13 @@ Regardless of filesystem, it is not recommended to use a network share for your | `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices | | `IMMICH_TRUSTED_PROXIES` | List of comma separated IPs set as trusted proxies | | server | api | -\*1: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead. - -\*2: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`. -It only need to be set if the Immich deployment method is changing. - -:::tip -`TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`. - +\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`. `TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution. -::: + +\*2: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead. + +\*3: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`. +It only need to be set if the Immich deployment method is changing. ## Workers diff --git a/docs/docs/install/requirements.md b/docs/docs/install/requirements.md index 88d85c7bee8cc..b96705203aa8f 100644 --- a/docs/docs/install/requirements.md +++ b/docs/docs/install/requirements.md @@ -23,7 +23,33 @@ Immich requires the command `docker compose` - the similarly named `docker-compo - **RAM**: Minimum 4GB, recommended 6GB. - **CPU**: Minimum 2 cores, recommended 4 cores. - **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions. - - This can present an issue for Windows users. See [here](/docs/install/environment-variables#supported-filesystems) - for more details and alternatives. + - This can present an issue for Windows users. See below for details and an alternative setup. - The generation of thumbnails and transcoded video can increase the size of the photo library by 10-20% on average. - - Network shares are supported for the storage of image and video assets only. + - Network shares are supported for the storage of image and video assets only. It is not recommended to use a network share for your database location due to performance and possible data loss issues. + +### Special requirements for Windows users + +
+Database storage on Windows systems + +The Immich Postgres database (`DB_DATA_LOCATION`) must be located on a filesystem that supports user/group +ownership and permissions (EXT2/3/4, ZFS, APFS, BTRFS, XFS, etc.). It will not work on any filesystem formatted in NTFS or ex/FAT/32. +It will not work in WSL (Windows Subsystem for Linux) when using a mounted host directory (commonly under `/mnt`). +If this is an issue, you can change the bind mount to a Docker volume instead as follows: + +Make the following change to `.env`: + +```diff +- DB_DATA_LOCATION=./postgres ++ DB_DATA_LOCATION=pgdata +``` + +Add the following line to the bottom of `docker-compose.yml`: + +```diff +volumes: + model-cache: ++ pgdata: +``` + +
From b45fce8ddf773f9e4033d4819de18ea85603209b Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Tue, 24 Sep 2024 17:13:37 +0200 Subject: [PATCH 54/57] fix: album title state weirdness (#12874) --- web/src/lib/components/album-page/album-title.svelte | 7 ++++--- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 7 ++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/web/src/lib/components/album-page/album-title.svelte b/web/src/lib/components/album-page/album-title.svelte index 22c26aa10c5a6..1e69ecf1a3c2d 100644 --- a/web/src/lib/components/album-page/album-title.svelte +++ b/web/src/lib/components/album-page/album-title.svelte @@ -7,6 +7,7 @@ export let id: string; export let albumName: string; export let isOwned: boolean; + export let onUpdate: (albumName: string) => void; $: newAlbumName = albumName; @@ -16,17 +17,17 @@ } try { - await updateAlbumInfo({ + ({ albumName } = await updateAlbumInfo({ id, updateAlbumDto: { albumName: newAlbumName, }, - }); + })); + onUpdate(albumName); } catch (error) { handleError(error, $t('errors.unable_to_save_album')); return; } - albumName = newAlbumName; }; diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index cbdb38192e082..b11bf9b8aa9ef 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -589,7 +589,12 @@ {#if viewMode !== ViewMode.SELECT_THUMBNAIL}
- + (album.albumName = albumName)} + /> {#if album.assetCount > 0} From 05d8c4c132b08052293ec6e45b8a1ebe7a2eb8e6 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 24 Sep 2024 17:53:57 -0400 Subject: [PATCH 55/57] fix: do not use trashed assets as album covers (#12905) --- server/src/queries/album.repository.sql | 27 ++++++++++--------- server/src/repositories/album.repository.ts | 30 +++++++++------------ 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index cc052e9de63b4..c4f6fbdd3218b 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -483,16 +483,13 @@ UPDATE "albums" SET "albumThumbnailAssetId" = ( SELECT - "albums_assets2"."assetsId" + "album_assets"."assetsId" FROM - "assets" "assets", - "albums_assets_assets" "albums_assets2" + "albums_assets_assets" "album_assets" + INNER JOIN "assets" "assets" ON "album_assets"."assetsId" = "assets"."id" + AND "assets"."deletedAt" IS NULL WHERE - ( - "albums_assets2"."assetsId" = "assets"."id" - AND "albums_assets2"."albumsId" = "albums"."id" - ) - AND ("assets"."deletedAt" IS NULL) + "album_assets"."albumsId" = "albums"."id" ORDER BY "assets"."fileCreatedAt" DESC LIMIT @@ -505,17 +502,21 @@ WHERE SELECT 1 FROM - "albums_assets_assets" "albums_assets" + "albums_assets_assets" "album_assets" + INNER JOIN "assets" "assets" ON "album_assets"."assetsId" = "assets"."id" + AND "assets"."deletedAt" IS NULL WHERE - "albums"."id" = "albums_assets"."albumsId" + "album_assets"."albumsId" = "albums"."id" ) OR "albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM - "albums_assets_assets" "albums_assets" + "albums_assets_assets" "album_assets" + INNER JOIN "assets" "assets" ON "album_assets"."assetsId" = "assets"."id" + AND "assets"."deletedAt" IS NULL WHERE - "albums"."id" = "albums_assets"."albumsId" - AND "albums"."albumThumbnailAssetId" = "albums_assets"."assetsId" + "album_assets"."albumsId" = "albums"."id" + AND "albums"."albumThumbnailAssetId" = "album_assets"."assetsId" ) diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index 4101d78c8ed3c..f7b4cb44aa976 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -277,32 +277,26 @@ export class AlbumRepository implements IAlbumRepository { @GenerateSql() async updateThumbnails(): Promise { // Subquery for getting a new thumbnail. - const newThumbnail = this.assetRepository - .createQueryBuilder('assets') - .select('albums_assets2.assetsId') - .addFrom('albums_assets_assets', 'albums_assets2') - .where('albums_assets2.assetsId = assets.id') - .andWhere('albums_assets2.albumsId = "albums"."id"') // Reference to albums.id outside this query - .orderBy('assets.fileCreatedAt', 'DESC') - .limit(1); - // Using dataSource, because there is no direct access to albums_assets_assets. - const albumHasAssets = this.dataSource - .createQueryBuilder() - .select('1') - .from('albums_assets_assets', 'albums_assets') - .where('"albums"."id" = "albums_assets"."albumsId"'); + const builder = this.dataSource + .createQueryBuilder('albums_assets_assets', 'album_assets') + .innerJoin('assets', 'assets', '"album_assets"."assetsId" = "assets"."id"') + .where('"album_assets"."albumsId" = "albums"."id"'); - const albumContainsThumbnail = albumHasAssets + const newThumbnail = builder .clone() - .andWhere('"albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"'); + .select('"album_assets"."assetsId"') + .orderBy('"assets"."fileCreatedAt"', 'DESC') + .limit(1); + const hasAssets = builder.clone().select('1'); + const hasInvalidAsset = hasAssets.clone().andWhere('"albums"."albumThumbnailAssetId" = "album_assets"."assetsId"'); const updateAlbums = this.repository .createQueryBuilder('albums') .update(AlbumEntity) .set({ albumThumbnailAssetId: () => `(${newThumbnail.getQuery()})` }) - .where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${albumHasAssets.getQuery()})`) - .orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${albumContainsThumbnail.getQuery()})`); + .where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${hasAssets.getQuery()})`) + .orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${hasInvalidAsset.getQuery()})`); const result = await updateAlbums.execute(); From 06f1376de38682a11fe18fb305ca579c7f54b804 Mon Sep 17 00:00:00 2001 From: Cary Keesler <44330591+carykees98@users.noreply.github.com> Date: Wed, 25 Sep 2024 08:59:35 -0400 Subject: [PATCH 56/57] fix(web): Updated web README.md (#12899) Updated web README.md --- web/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/README.md b/web/README.md index e9693ceb01410..603c7ad64e720 100644 --- a/web/README.md +++ b/web/README.md @@ -2,4 +2,4 @@ This project uses the [SvelteKit](https://kit.svelte.dev/) web framework. Please refer to [the SvelteKit docs](https://kit.svelte.dev/docs) for information on getting started as a contributor to this project. In particular, it will help you navigate the project's code if you understand the basics of [SvelteKit routing](https://kit.svelte.dev/docs/routing). -When developing locally, you will run a SvelteKit Node.js server. When this project is deployed to production, it is built as a SPA and deployed as part of [../server](the server project). +When developing locally, you will run a SvelteKit Node.js server. When this project is deployed to production, it is built as a SPA and deployed as part of [the server project](../server). From 46fe60693e309cf57eea72c397b3ecf1ba523783 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Sep 2024 09:56:02 -0400 Subject: [PATCH 57/57] chore(deps): update dependency @types/react to v18.3.8 (#12918) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- server/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index ba9f33dc1e425..65e9df8d9eb90 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -5506,9 +5506,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.7.tgz", - "integrity": "sha512-KUnDCJF5+AiZd8owLIeVHqmW9yM4sqmDVf2JRJiBMFkGvkoZ4/WyV2lL4zVsoinmRS/W3FeEdZLEWFRofnT2FQ==", + "version": "18.3.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz", + "integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -18805,9 +18805,9 @@ "dev": true }, "@types/react": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.7.tgz", - "integrity": "sha512-KUnDCJF5+AiZd8owLIeVHqmW9yM4sqmDVf2JRJiBMFkGvkoZ4/WyV2lL4zVsoinmRS/W3FeEdZLEWFRofnT2FQ==", + "version": "18.3.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz", + "integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==", "dev": true, "requires": { "@types/prop-types": "*",