From 44e0aba60946dfcdf852b605f83bcbb36fc4bd4e Mon Sep 17 00:00:00 2001 From: phantomptr Date: Sun, 11 Oct 2026 10:26:11 -0700 Subject: [PATCH] Connections: WebDAV and WebDAV (HTTPS) servers (#442) PROPFIND listings, ranged GET reads, Basic auth, optional self-signed certificate per connection. Same read-only RemoteFs as SMB/FTP/SFTP, so uploads and installs stream from the server. Loopback-server test covers sign-in, listing, walk and ranged reads. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01EU1Z2mdhAxBrH1yb48pxik --- client/src/api/remote.ts | 5 +- client/src/components/BrowseButton.tsx | 2 + client/src/i18n/locales/ar.ts | 15 +- client/src/i18n/locales/bn.ts | 15 +- client/src/i18n/locales/de.ts | 15 +- client/src/i18n/locales/en.ts | 15 +- client/src/i18n/locales/es.ts | 15 +- client/src/i18n/locales/fa.ts | 15 +- client/src/i18n/locales/fr.ts | 15 +- client/src/i18n/locales/hi.ts | 15 +- client/src/i18n/locales/hu.ts | 15 +- client/src/i18n/locales/id.ts | 15 +- client/src/i18n/locales/it.ts | 15 +- client/src/i18n/locales/ja.ts | 15 +- client/src/i18n/locales/ko.ts | 15 +- client/src/i18n/locales/pl.ts | 15 +- client/src/i18n/locales/pt-BR.ts | 15 +- client/src/i18n/locales/ru.ts | 15 +- client/src/i18n/locales/th.ts | 15 +- client/src/i18n/locales/tr.ts | 15 +- client/src/i18n/locales/vi.ts | 15 +- client/src/i18n/locales/zh-CN.ts | 15 +- client/src/i18n/locales/zh-TW.ts | 15 +- .../Connections/ConnectionForm.test.tsx | 14 + .../screens/Connections/ConnectionForm.tsx | 58 +- client/src/screens/Connections/index.tsx | 3 + client/src/screens/Home/ServersCard.tsx | 2 + .../crates/ps5upload-engine/src/remote/api.rs | 5 + .../ps5upload-engine/src/remote/hints.rs | 6 +- .../crates/ps5upload-engine/src/remote/mod.rs | 3 +- .../ps5upload-engine/src/remote/pool.rs | 3 + .../ps5upload-engine/src/remote/store.rs | 11 + .../ps5upload-engine/src/remote/webdav_fs.rs | 719 ++++++++++++++++++ 33 files changed, 1035 insertions(+), 111 deletions(-) create mode 100644 engine/crates/ps5upload-engine/src/remote/webdav_fs.rs diff --git a/client/src/api/remote.ts b/client/src/api/remote.ts index 5529ed37..b95b5918 100644 --- a/client/src/api/remote.ts +++ b/client/src/api/remote.ts @@ -3,7 +3,7 @@ import { getEngineUrl } from "../state/engine"; -export type Protocol = "smb" | "ftp" | "ftps" | "sftp"; +export type Protocol = "smb" | "ftp" | "ftps" | "sftp" | "webdav" | "webdavs"; export interface Connection { id: string; @@ -15,6 +15,8 @@ export interface Connection { user: string; start_path: string; host_key: string | null; + /** WebDAV over HTTPS: the server's self-signed certificate is accepted unverified. */ + insecure_tls?: boolean; has_secret: boolean; } @@ -26,6 +28,7 @@ export interface ConnectionInput { share?: string; user?: string; start_path?: string; + insecure_tls?: boolean; } export interface SecretInput { diff --git a/client/src/components/BrowseButton.tsx b/client/src/components/BrowseButton.tsx index 9b51f1d8..7022a4a6 100644 --- a/client/src/components/BrowseButton.tsx +++ b/client/src/components/BrowseButton.tsx @@ -17,6 +17,8 @@ const PROTOCOL: Record = { ftp: "FTP", ftps: "FTPS", sftp: "SFTP", + webdav: "WebDAV", + webdavs: "WebDAV (HTTPS)", }; export interface BrowseButtonProps { diff --git a/client/src/i18n/locales/ar.ts b/client/src/i18n/locales/ar.ts index ce7b6951..98330399 100644 --- a/client/src/i18n/locales/ar.ts +++ b/client/src/i18n/locales/ar.ts @@ -2603,7 +2603,7 @@ helper_mismatch_go: "إعادة تحميل المساعد", "conn_edit_title": "تعديل {name}", "conn_add_title": "إضافة خادم", "conn_empty_title": "لا توجد خوادم بعد", -"conn_empty_body": "أضف جهاز NAS أو مجلدًا مشتركًا على حاسوب أو خادم FTP/SFTP، واختر الألعاب منه مباشرة.", +"conn_empty_body": "أضف جهاز NAS أو مجلدًا مشتركًا على حاسوب أو خادم FTP/SFTP/WebDAV، واختر الألعاب منه مباشرة.", "conn_browse": "تصفح", "conn_test": "اختبار", "conn_edit": "تعديل", @@ -2611,6 +2611,11 @@ helper_mismatch_go: "إعادة تحميل المساعد", "conn_anonymous": "مجهول (دون مستخدم أو كلمة مرور)", "conn_protocol": "البروتوكول", "conn_ftp_unencrypted": "غير مشفّر — استخدم FTPS أو SFTP إن كان الخادم يدعمهما.", +conn_webdav_unencrypted: "غير مشفّر — استخدم WebDAV (HTTPS) حيث يدعمه الخادم.", +conn_webdav_root_hint: "ضع جذر WebDAV للخادم في مجلد البداية: /remote.php/dav/files/USER لـ Nextcloud، و/dav لـ Alist، و/ لـ rclone serve webdav. تسجيل الدخول بمصادقة Basic.", +conn_insecure_tls: "قبول شهادة موقّعة ذاتيًا", +conn_insecure_tls_hint: "فقط لجهاز NAS في شبكتك الخاصة بشهادة موقّعة ذاتيًا. يمكن حينها لأي شخص بين هذا الحاسوب والخادم قراءة الاتصال.", +conn_no_signin: "بدون تسجيل دخول (مشاركة عامة)", "conn_name": "الاسم", "conn_host": "عنوان الخادم", "conn_port": "المنفذ", @@ -2632,7 +2637,7 @@ helper_mismatch_go: "إعادة تحميل المساعد", "conn_accept_key": "قبول", "conn_save": "حفظ", "v5_home_servers": "الخوادم", -"v5_home_servers_empty": "اختر الألعاب مباشرة من جهاز NAS أو مجلد مشترك، عبر SMB أو FTP أو SFTP.", +"v5_home_servers_empty": "اختر الألعاب مباشرة من جهاز NAS أو مجلد مشترك، عبر SMB أو FTP أو SFTP/WebDAV.", "v5_home_servers_connect": "توصيل NAS أو خادم", "fpkg.stage.copy": "النسخ من الخادم", "fpkg.stage.park": "تنحية النسخة جانبًا", @@ -3213,9 +3218,9 @@ upload_as_image_open_convert: "فتح التحويل", upload_as_image_queued: "أُنشئت الصورة وهي في قائمة انتظار الرفع.", upload_as_image_title: "أو أرسلها كصورة لعبة واحدة", "collection.add_server_folder": "إضافة مجلد على {name}", -"collection.no_servers_hint": "ألعاب على NAS؟ احفظه في الاتصالات (SMB أو FTP أو SFTP)، وتُضاف مجلداته هنا دون تركيبها.", +"collection.no_servers_hint": "ألعاب على NAS؟ احفظه في الاتصالات (SMB أو FTP أو SFTP/WebDAV)، وتُضاف مجلداته هنا دون تركيبها.", "collection.pick_server_folder": "المجلد على {name} الذي يحوي ألعابك", -"collection.server_folders_hint": "يُقرأ المجلد على خادم محفوظ (SMB أو FTP أو SFTP) في مكانه: لا يُنسخ شيء إلى هذا الحاسوب، وتُثبَّت ألعابه مباشرة من الخادم. النقل إلى سلة المهملات والتنظيم يعملان على مجلدات هذا الحاسوب فقط.", +"collection.server_folders_hint": "يُقرأ المجلد على خادم محفوظ (SMB أو FTP أو SFTP/WebDAV) في مكانه: لا يُنسخ شيء إلى هذا الحاسوب، وتُثبَّت ألعابه مباشرة من الخادم. النقل إلى سلة المهملات والتنظيم يعملان على مجلدات هذا الحاسوب فقط.", bottleneck_source_v2: "قراءة المصدر", android_local_network_allow: "السماح", android_local_network_body: "يمنع Android التطبيق من الوصول إلى جهاز PS5 حتى تسمح بذلك.", @@ -3577,7 +3582,7 @@ sendpayload_probe_no_signature: "لم يُعثر على توقيع PS5Upload — sendpayload_probe_ok: "يبدو ملف الحمولة سليمًا.", sendpayload_probe_read_error: "تعذّرت قراءة هذا الملف ({error}). تأكد أنه لا يزال موجودًا ويمكن فتحه، ثم اختره مجددًا.", sendpayload_probe_too_small: "هذا الملف فارغ، لذا لا يوجد ما يُرسل.", -servers_subtitle: "أجهزة NAS والخوادم الخاصة بك (SMB، FTP، FTPS، SFTP). أينما تتصفح بحثًا عن لعبة أو ملف، اختر منها عبر ▾ بجوار «استعراض».", +servers_subtitle: "أجهزة NAS والخوادم الخاصة بك (SMB، FTP، FTPS، SFTP/WebDAV). أينما تتصفح بحثًا عن لعبة أو ملف، اختر منها عبر ▾ بجوار «استعراض».", settings_artwork_cache_explain_v2: "تُحفظ أغلفة الألعاب حيث يعمل المحرك (هذا الكمبيوتر، أو الخادم في نسخة الويب) لتُحمَّل فورًا بدلًا من قراءتها من PS5 في كل مرة. تُحدَّث تلقائيًا، وتُزال لجهاز ما عند تثبيت لعبة أو إلغاء تثبيتها عليه.", settings_backup_downloaded: "تم تنزيل {name}", settings_backup_hint_v2: "اجمع السمة وقائمة أجهزة PS5 وسجل الإشعارات وسجل النشاط وتفضيلات أخرى في ملف JSON واحد، لاستعادتها بعد مسح تخزين التطبيق أو على جهاز آخر. لا تُضمَّن إقرانات الأجهزة والمجموعة وكلمات مرور الخوادم المحفوظة ومكتبات النقل الخلفي.", diff --git a/client/src/i18n/locales/bn.ts b/client/src/i18n/locales/bn.ts index 5ae2a06f..f4192b52 100644 --- a/client/src/i18n/locales/bn.ts +++ b/client/src/i18n/locales/bn.ts @@ -2603,7 +2603,7 @@ helper_mismatch_go: "হেল্পার লোড করুন", "conn_edit_title": "{name} সম্পাদনা", "conn_add_title": "সার্ভার যোগ করুন", "conn_empty_title": "এখনও কোনো সার্ভার নেই", -"conn_empty_body": "আপনার NAS, কোনো PC-র শেয়ার করা ফোল্ডার বা FTP/SFTP সার্ভার যোগ করুন, আর সরাসরি সেখান থেকে গেম বেছে নিন।", +"conn_empty_body": "আপনার NAS, কোনো PC-র শেয়ার করা ফোল্ডার বা FTP/SFTP/WebDAV সার্ভার যোগ করুন, আর সরাসরি সেখান থেকে গেম বেছে নিন।", "conn_browse": "ব্রাউজ", "conn_test": "পরীক্ষা", "conn_edit": "সম্পাদনা", @@ -2611,6 +2611,11 @@ helper_mismatch_go: "হেল্পার লোড করুন", "conn_anonymous": "বেনামী (ব্যবহারকারী বা পাসওয়ার্ড নেই)", "conn_protocol": "প্রোটোকল", "conn_ftp_unencrypted": "এনক্রিপ্ট করা নয় — সার্ভার সমর্থন করলে FTPS বা SFTP ব্যবহার করুন।", +conn_webdav_unencrypted: "এনক্রিপ্ট করা নয় — সার্ভার সমর্থন করলে WebDAV (HTTPS) ব্যবহার করুন।", +conn_webdav_root_hint: "সার্ভারের WebDAV রুট শুরুর ফোল্ডারে দিন: Nextcloud-এর জন্য /remote.php/dav/files/USER, Alist-এর জন্য /dav, rclone serve webdav-এর জন্য /। সাইন-ইন Basic প্রমাণীকরণ।", +conn_insecure_tls: "স্ব-স্বাক্ষরিত সার্টিফিকেট গ্রহণ করুন", +conn_insecure_tls_hint: "শুধু নিজের নেটওয়ার্কের এমন NAS-এর জন্য যার সার্টিফিকেট স্ব-স্বাক্ষরিত। তখন এই কম্পিউটার ও সার্ভারের মাঝে যে কেউ সংযোগ পড়তে পারবে।", +conn_no_signin: "সাইন-ইন ছাড়া (পাবলিক শেয়ার)", "conn_name": "নাম", "conn_host": "সার্ভারের ঠিকানা", "conn_port": "পোর্ট", @@ -2632,7 +2637,7 @@ helper_mismatch_go: "হেল্পার লোড করুন", "conn_accept_key": "গ্রহণ", "conn_save": "সংরক্ষণ", "v5_home_servers": "সার্ভার", -"v5_home_servers_empty": "SMB, FTP বা SFTP দিয়ে আপনার NAS বা শেয়ার করা ফোল্ডার থেকে সরাসরি গেম বেছে নিন।", +"v5_home_servers_empty": "SMB, FTP বা SFTP/WebDAV দিয়ে আপনার NAS বা শেয়ার করা ফোল্ডার থেকে সরাসরি গেম বেছে নিন।", "v5_home_servers_connect": "NAS বা সার্ভার সংযোগ", "fpkg.stage.copy": "সার্ভার থেকে কপি", "fpkg.stage.park": "ডাম্প সরিয়ে রাখা", @@ -3213,9 +3218,9 @@ upload_as_image_open_convert: "রূপান্তর খুলুন", upload_as_image_queued: "ইমেজ তৈরি হয়েছে এবং আপলোড সারিতে আছে।", upload_as_image_title: "অথবা একটি গেম ইমেজ হিসেবে পাঠান", "collection.add_server_folder": "{name}-এ একটি ফোল্ডার যোগ করুন", -"collection.no_servers_hint": "গেম NAS-এ? সংযোগে (SMB, FTP বা SFTP) সেটি সংরক্ষণ করুন, তারপর মাউন্ট না করেই এর ফোল্ডার এখানে যোগ করা যাবে।", +"collection.no_servers_hint": "গেম NAS-এ? সংযোগে (SMB, FTP বা SFTP/WebDAV) সেটি সংরক্ষণ করুন, তারপর মাউন্ট না করেই এর ফোল্ডার এখানে যোগ করা যাবে।", "collection.pick_server_folder": "{name}-এ আপনার গেম থাকা ফোল্ডার", -"collection.server_folders_hint": "সংরক্ষিত সার্ভারের (SMB, FTP, SFTP) ফোল্ডার জায়গাতেই পড়া হয়: এই কম্পিউটারে কিছু কপি হয় না, আর এর গেম সরাসরি সার্ভার থেকে ইনস্টল হয়। ট্র্যাশে সরান ও গোছান শুধু এই কম্পিউটারের ফোল্ডারে কাজ করে।", +"collection.server_folders_hint": "সংরক্ষিত সার্ভারের (SMB, FTP, SFTP/WebDAV) ফোল্ডার জায়গাতেই পড়া হয়: এই কম্পিউটারে কিছু কপি হয় না, আর এর গেম সরাসরি সার্ভার থেকে ইনস্টল হয়। ট্র্যাশে সরান ও গোছান শুধু এই কম্পিউটারের ফোল্ডারে কাজ করে।", bottleneck_source_v2: "উৎস পড়া", android_local_network_allow: "অনুমতি দিন", android_local_network_body: "আপনি অনুমতি না দেওয়া পর্যন্ত Android অ্যাপটিকে আপনার PS5-এ পৌঁছাতে দেয় না।", @@ -3577,7 +3582,7 @@ sendpayload_probe_no_signature: "কোনো PS5Upload স্বাক্ষর sendpayload_probe_ok: "পেলোড ফাইল ঠিক আছে বলে মনে হচ্ছে।", sendpayload_probe_read_error: "এই ফাইলটি পড়া যায়নি ({error})। এটি এখনো আছে এবং খোলা যায় কিনা দেখুন, তারপর আবার বেছে নিন।", sendpayload_probe_too_small: "এই ফাইলটি খালি, তাই পাঠানোর কিছু নেই।", -servers_subtitle: "আপনার NAS ও সার্ভার (SMB, FTP, FTPS, SFTP)। যেখানেই কোনো গেম বা ফাইলের জন্য ব্রাউজ করুন, ব্রাউজের পাশের ▾ দিয়ে এগুলো থেকে বেছে নিন।", +servers_subtitle: "আপনার NAS ও সার্ভার (SMB, FTP, FTPS, SFTP/WebDAV)। যেখানেই কোনো গেম বা ফাইলের জন্য ব্রাউজ করুন, ব্রাউজের পাশের ▾ দিয়ে এগুলো থেকে বেছে নিন।", settings_artwork_cache_explain_v2: "গেম কভার ইঞ্জিন যেখানে চলে সেখানে রাখা হয় (এই কম্পিউটার, বা ওয়েব সংস্করণের জন্য সার্ভার) যাতে প্রতিবার আপনার PS5 থেকে পড়ার বদলে সেগুলো সাথে সাথে লোড হয়। এগুলো স্বয়ংক্রিয়ভাবে রিফ্রেশ হয়, এবং কোনো কনসোলে গেম ইনস্টল বা আনইনস্টল করলে সেই কনসোলের জন্য সরানো হয়।", settings_backup_downloaded: "{name} ডাউনলোড হয়েছে", settings_backup_hint_v2: "থিম, PS5 তালিকা, নোটিফিকেশন ইতিহাস, কার্যকলাপ লগ এবং অন্যান্য পছন্দ একটি JSON ফাইলে বান্ডল করুন, অ্যাপের স্টোরেজ মোছার পর বা অন্য মেশিনে রিস্টোর করার জন্য। কনসোল পেয়ারিং, সংগ্রহ, সংরক্ষিত সার্ভারের পাসওয়ার্ড এবং ব্যাকপোর্ট লাইব্রেরি অন্তর্ভুক্ত নয়।", diff --git a/client/src/i18n/locales/de.ts b/client/src/i18n/locales/de.ts index 57b9552b..70ac27f8 100644 --- a/client/src/i18n/locales/de.ts +++ b/client/src/i18n/locales/de.ts @@ -2611,7 +2611,7 @@ helper_mismatch_go: "Helfer neu laden", "conn_edit_title": "{name} bearbeiten", "conn_add_title": "Server hinzufügen", "conn_empty_title": "Noch keine Server", -"conn_empty_body": "Füge dein NAS, einen freigegebenen Ordner auf einem PC oder einen FTP/SFTP-Server hinzu und wähle Spiele direkt daraus.", +"conn_empty_body": "Füge dein NAS, einen freigegebenen Ordner auf einem PC oder einen FTP/SFTP/WebDAV-Server hinzu und wähle Spiele direkt daraus.", "conn_browse": "Durchsuchen", "conn_test": "Testen", "conn_edit": "Bearbeiten", @@ -2619,6 +2619,11 @@ helper_mismatch_go: "Helfer neu laden", "conn_anonymous": "Anonym (ohne Benutzer und Passwort)", "conn_protocol": "Protokoll", "conn_ftp_unencrypted": "Unverschlüsselt – nutze FTPS oder SFTP, wo der Server es unterstützt.", +conn_webdav_unencrypted: "Unverschlüsselt — nutze WebDAV (HTTPS), wenn der Server es unterstützt.", +conn_webdav_root_hint: "Trage die WebDAV-Wurzel des Servers als Startordner ein: /remote.php/dav/files/USER bei Nextcloud, /dav bei Alist, / bei rclone serve webdav. Angemeldet wird per Basic-Authentifizierung.", +conn_insecure_tls: "Selbstsigniertes Zertifikat akzeptieren", +conn_insecure_tls_hint: "Nur für ein NAS im eigenen Netzwerk mit selbstsigniertem Zertifikat. Jeder zwischen diesem Computer und dem Server könnte die Verbindung dann mitlesen.", +conn_no_signin: "Keine Anmeldung (öffentliche Freigabe)", "conn_name": "Name", "conn_host": "Serveradresse", "conn_port": "Port", @@ -2640,7 +2645,7 @@ helper_mismatch_go: "Helfer neu laden", "conn_accept_key": "Akzeptieren", "conn_save": "Speichern", "v5_home_servers": "Server", -"v5_home_servers_empty": "Wähle Spiele direkt von deinem NAS oder einem freigegebenen Ordner, über SMB, FTP oder SFTP.", +"v5_home_servers_empty": "Wähle Spiele direkt von deinem NAS oder einem freigegebenen Ordner, über SMB, FTP oder SFTP/WebDAV.", "v5_home_servers_connect": "NAS oder Server verbinden", "fpkg.stage.copy": "Vom Server kopieren", "fpkg.stage.park": "Dump beiseitelegen", @@ -3221,9 +3226,9 @@ upload_as_image_open_convert: "Konvertieren öffnen", upload_as_image_queued: "Das Image ist erstellt und in der Upload-Warteschlange.", upload_as_image_title: "Oder als ein Spiel-Image senden", "collection.add_server_folder": "Ordner auf {name} hinzufügen", -"collection.no_servers_hint": "Spiele auf einem NAS? Speichere es unter Verbindungen (SMB, FTP oder SFTP), dann lassen sich seine Ordner hier hinzufügen, ohne sie einzubinden.", +"collection.no_servers_hint": "Spiele auf einem NAS? Speichere es unter Verbindungen (SMB, FTP oder SFTP/WebDAV), dann lassen sich seine Ordner hier hinzufügen, ohne sie einzubinden.", "collection.pick_server_folder": "Der Ordner auf {name} mit deinen Spielen", -"collection.server_folders_hint": "Ein Ordner auf einem gespeicherten Server (SMB, FTP, SFTP) wird an Ort und Stelle gelesen: Nichts wird auf diesen Computer kopiert, und seine Spiele installieren direkt vom Server. In den Papierkorb und Aufräumen gelten nur für Ordner auf diesem Computer.", +"collection.server_folders_hint": "Ein Ordner auf einem gespeicherten Server (SMB, FTP, SFTP/WebDAV) wird an Ort und Stelle gelesen: Nichts wird auf diesen Computer kopiert, und seine Spiele installieren direkt vom Server. In den Papierkorb und Aufräumen gelten nur für Ordner auf diesem Computer.", bottleneck_source_v2: "Lesen der Quelle", android_local_network_allow: "Zulassen", android_local_network_body: "Android verhindert, dass die App deine PS5 erreicht, bis du es zulässt.", @@ -3585,7 +3590,7 @@ sendpayload_probe_no_signature: "Keine PS5Upload-Signatur gefunden — nur verwe sendpayload_probe_ok: "Payload-Datei sieht gut aus.", sendpayload_probe_read_error: "Diese Datei konnte nicht gelesen werden ({error}). Prüfe, ob sie noch existiert und geöffnet werden kann, und wähle sie dann erneut.", sendpayload_probe_too_small: "Diese Datei ist leer, es gibt also nichts zu senden.", -servers_subtitle: "Deine NAS und Server (SMB, FTP, FTPS, SFTP). Überall, wo du nach einem Spiel oder einer Datei suchst, wählst du sie mit dem ▾ neben Durchsuchen.", +servers_subtitle: "Deine NAS und Server (SMB, FTP, FTPS, SFTP/WebDAV). Überall, wo du nach einem Spiel oder einer Datei suchst, wählst du sie mit dem ▾ neben Durchsuchen.", settings_artwork_cache_explain_v2: "Spielcover werden dort aufbewahrt, wo die Engine läuft (dieser Computer oder bei der Webversion der Server), damit sie sofort laden, statt jedes Mal von deiner PS5 gelesen zu werden. Sie werden automatisch aktualisiert und für eine Konsole entfernt, wenn du dort ein Spiel installierst oder deinstallierst.", settings_backup_downloaded: "{name} heruntergeladen", settings_backup_hint_v2: "Theme, PS5-Liste, Benachrichtigungsverlauf, Aktivitätsprotokoll und andere Einstellungen in einer JSON-Datei bündeln, um sie nach dem Löschen des App-Speichers oder auf einem anderen Rechner wiederherzustellen. Konsolen-Kopplungen, die Sammlung, Passwörter gespeicherter Server und Backport-Bibliotheken sind nicht enthalten.", diff --git a/client/src/i18n/locales/en.ts b/client/src/i18n/locales/en.ts index 74099732..0e5b8df9 100644 --- a/client/src/i18n/locales/en.ts +++ b/client/src/i18n/locales/en.ts @@ -3163,14 +3163,14 @@ conn_delete_title: "Remove {name}?", conn_delete_body: "Its saved password or key is deleted too. Nothing on the server changes.", conn_delete: "Remove", conn_need_ps5: "Connect to a PS5 to install.", -servers_subtitle: "Your NAS and servers (SMB, FTP, FTPS, SFTP). Anywhere you browse for a game or file, pick from them with the ▾ next to Browse.", +servers_subtitle: "Your NAS and servers (SMB, FTP, FTPS, SFTP/WebDAV). Anywhere you browse for a game or file, pick from them with the ▾ next to Browse.", connections_failed: "Something went wrong with your servers", connections_retry: "Retry", conn_add: "Add a server", conn_edit_title: "Edit {name}", conn_add_title: "Add a server", conn_empty_title: "No servers yet", -conn_empty_body: "Add your NAS, a shared folder on a PC, or an FTP/SFTP server, and pick games straight from it.", +conn_empty_body: "Add your NAS, a shared folder on a PC, or an FTP/SFTP/WebDAV server, and pick games straight from it.", conn_browse: "Browse", conn_test: "Test", conn_edit: "Edit", @@ -3178,6 +3178,11 @@ conn_guest: "Guest (no user or password)", conn_anonymous: "Anonymous (no user or password)", conn_protocol: "Protocol", conn_ftp_unencrypted: "Unencrypted — use FTPS or SFTP where the server supports it.", +conn_webdav_unencrypted: "Unencrypted — use WebDAV (HTTPS) where the server supports it.", +conn_webdav_root_hint: "Put the server's WebDAV root in Start folder: /remote.php/dav/files/USER for Nextcloud, /dav for Alist, / for rclone serve webdav. Sign-in is Basic authentication.", +conn_insecure_tls: "Accept a self-signed certificate", +conn_insecure_tls_hint: "Only for a NAS on your own network whose certificate is self-signed. Anyone between this computer and the server could then read the connection.", +conn_no_signin: "No sign-in (public share)", conn_name: "Name", conn_host: "Server address", conn_port: "Port", @@ -3199,7 +3204,7 @@ conn_host_key_new: "First connection to this server. Its key fingerprint is:", conn_accept_key: "Accept", conn_save: "Save", v5_home_servers: "Servers", -v5_home_servers_empty: "Pick games straight from your NAS or a shared folder, over SMB, FTP or SFTP.", +v5_home_servers_empty: "Pick games straight from your NAS or a shared folder, over SMB, FTP or SFTP/WebDAV.", v5_home_servers_connect: "Connect a NAS or server", "fpkg.stage.copy": "Copy from server", "fpkg.stage.park": "Set the dump aside", @@ -3873,9 +3878,9 @@ upload_as_image_open_convert: "Open Convert", upload_as_image_queued: "The image is built and in the Upload queue.", upload_as_image_title: "Or send it as one game image", "collection.add_server_folder": "Add a folder on {name}", -"collection.no_servers_hint": "Games on a NAS? Save it under Connections (SMB, FTP or SFTP), and its folders can be added here without mounting them.", +"collection.no_servers_hint": "Games on a NAS? Save it under Connections (SMB, FTP or SFTP/WebDAV), and its folders can be added here without mounting them.", "collection.pick_server_folder": "The folder on {name} that holds your games", -"collection.server_folders_hint": "A folder on a saved server (SMB, FTP, SFTP) is read in place: nothing is copied to this computer, and its games install straight from the server. Move to Trash and Organize work on this computer's folders only.", +"collection.server_folders_hint": "A folder on a saved server (SMB, FTP, SFTP/WebDAV) is read in place: nothing is copied to this computer, and its games install straight from the server. Move to Trash and Organize work on this computer's folders only.", android_local_network_allow: "Allow", android_local_network_body: "Android blocks the app from reaching your PS5 until you allow it.", android_local_network_title: "Allow PS5Upload on your local network", diff --git a/client/src/i18n/locales/es.ts b/client/src/i18n/locales/es.ts index 9524bc07..90bb2d13 100644 --- a/client/src/i18n/locales/es.ts +++ b/client/src/i18n/locales/es.ts @@ -2553,7 +2553,7 @@ helper_mismatch_go: "Recargar asistente", "conn_edit_title": "Editar {name}", "conn_add_title": "Añadir un servidor", "conn_empty_title": "Aún no hay servidores", -"conn_empty_body": "Añade tu NAS, una carpeta compartida de un PC o un servidor FTP/SFTP, y elige juegos directamente desde ahí.", +"conn_empty_body": "Añade tu NAS, una carpeta compartida de un PC o un servidor FTP/SFTP/WebDAV, y elige juegos directamente desde ahí.", "conn_browse": "Explorar", "conn_test": "Probar", "conn_edit": "Editar", @@ -2561,6 +2561,11 @@ helper_mismatch_go: "Recargar asistente", "conn_anonymous": "Anónimo (sin usuario ni contraseña)", "conn_protocol": "Protocolo", "conn_ftp_unencrypted": "Sin cifrar: usa FTPS o SFTP si el servidor los admite.", +conn_webdav_unencrypted: "Sin cifrar: usa WebDAV (HTTPS) si el servidor lo admite.", +conn_webdav_root_hint: "Pon la raíz WebDAV del servidor en Carpeta inicial: /remote.php/dav/files/USUARIO en Nextcloud, /dav en Alist, / en rclone serve webdav. El inicio de sesión es autenticación Basic.", +conn_insecure_tls: "Aceptar un certificado autofirmado", +conn_insecure_tls_hint: "Solo para un NAS en tu propia red con certificado autofirmado. Cualquiera entre este equipo y el servidor podría leer la conexión.", +conn_no_signin: "Sin inicio de sesión (recurso público)", "conn_name": "Nombre", "conn_host": "Dirección del servidor", "conn_port": "Puerto", @@ -2582,7 +2587,7 @@ helper_mismatch_go: "Recargar asistente", "conn_accept_key": "Aceptar", "conn_save": "Guardar", "v5_home_servers": "Servidores", -"v5_home_servers_empty": "Elige juegos directamente de tu NAS o de una carpeta compartida, por SMB, FTP o SFTP.", +"v5_home_servers_empty": "Elige juegos directamente de tu NAS o de una carpeta compartida, por SMB, FTP o SFTP/WebDAV.", "v5_home_servers_connect": "Conectar un NAS o servidor", "fpkg.stage.copy": "Copiar desde el servidor", "fpkg.stage.park": "Apartar el volcado", @@ -3163,9 +3168,9 @@ upload_as_image_open_convert: "Abrir Convertir", upload_as_image_queued: "La imagen está creada y en la cola de subida.", upload_as_image_title: "O envíalo como una sola imagen de juego", "collection.add_server_folder": "Añadir una carpeta de {name}", -"collection.no_servers_hint": "¿Juegos en un NAS? Guárdalo en Conexiones (SMB, FTP o SFTP) y podrás añadir aquí sus carpetas sin montarlas.", +"collection.no_servers_hint": "¿Juegos en un NAS? Guárdalo en Conexiones (SMB, FTP o SFTP/WebDAV) y podrás añadir aquí sus carpetas sin montarlas.", "collection.pick_server_folder": "La carpeta de {name} que contiene tus juegos", -"collection.server_folders_hint": "Una carpeta en un servidor guardado (SMB, FTP, SFTP) se lee donde está: no se copia nada a este ordenador y sus juegos se instalan directamente desde el servidor. Mover a la Papelera y Organizar solo funcionan con carpetas de este ordenador.", +"collection.server_folders_hint": "Una carpeta en un servidor guardado (SMB, FTP, SFTP/WebDAV) se lee donde está: no se copia nada a este ordenador y sus juegos se instalan directamente desde el servidor. Mover a la Papelera y Organizar solo funcionan con carpetas de este ordenador.", bottleneck_source_v2: "la lectura del origen", android_local_network_allow: "Permitir", android_local_network_body: "Android impide que la app llegue a tu PS5 hasta que lo permitas.", @@ -3527,7 +3532,7 @@ sendpayload_probe_no_signature: "No se encontró la firma de PS5Upload — úsal sendpayload_probe_ok: "El archivo de payload parece correcto.", sendpayload_probe_read_error: "No se pudo leer este archivo ({error}). Comprueba que sigue existiendo y se puede abrir, y vuelve a elegirlo.", sendpayload_probe_too_small: "Este archivo está vacío, así que no hay nada que enviar.", -servers_subtitle: "Tus NAS y servidores (SMB, FTP, FTPS, SFTP). Allí donde busques un juego o archivo, elígelos con el ▾ junto a Explorar.", +servers_subtitle: "Tus NAS y servidores (SMB, FTP, FTPS, SFTP/WebDAV). Allí donde busques un juego o archivo, elígelos con el ▾ junto a Explorar.", settings_artwork_cache_explain_v2: "Las portadas de los juegos se guardan donde se ejecuta el motor (este ordenador, o el servidor en la versión web) para que carguen al instante en lugar de leerse de tu PS5 cada vez. Se actualizan automáticamente y se eliminan para una consola cuando instalas o desinstalas un juego en ella.", settings_backup_downloaded: "{name} descargado", settings_backup_hint_v2: "Agrupa el tema, la lista de PS5, el historial de notificaciones, el registro de actividad y otras preferencias en un archivo JSON, para restaurarlo tras borrar el almacenamiento de la app o en otro equipo. No se incluyen las vinculaciones de consolas, la Colección, las contraseñas de servidores guardados ni las bibliotecas de backport.", diff --git a/client/src/i18n/locales/fa.ts b/client/src/i18n/locales/fa.ts index 9539e63f..8b3cf388 100644 --- a/client/src/i18n/locales/fa.ts +++ b/client/src/i18n/locales/fa.ts @@ -2920,7 +2920,7 @@ conn_add: "افزودن سرور", conn_edit_title: "ویرایش {name}", conn_add_title: "افزودن یک سرور", conn_empty_title: "هنوز سروری نیست", -conn_empty_body: "NAS خود، یک پوشه‌ی اشتراکی روی یک PC، یا یک سرور FTP/SFTP اضافه کنید و بازی‌ها را مستقیم از آن انتخاب کنید.", +conn_empty_body: "NAS خود، یک پوشه‌ی اشتراکی روی یک PC، یا یک سرور FTP/SFTP/WebDAV اضافه کنید و بازی‌ها را مستقیم از آن انتخاب کنید.", conn_browse: "مرور", conn_test: "تست", conn_edit: "ویرایش", @@ -2928,6 +2928,11 @@ conn_guest: "مهمان (بدون کاربر یا رمز)", conn_anonymous: "ناشناس (بدون کاربر یا رمز)", conn_protocol: "پروتکل", conn_ftp_unencrypted: "رمزگذاری‌نشده — هر جا سرور پشتیبانی می‌کند از FTPS یا SFTP استفاده کنید.", +conn_webdav_unencrypted: "رمزگذاری‌نشده — اگر سرور پشتیبانی می‌کند از WebDAV (HTTPS) استفاده کنید.", +conn_webdav_root_hint: "ریشهٔ WebDAV سرور را در پوشهٔ شروع بگذارید: /remote.php/dav/files/USER برای Nextcloud، /dav برای Alist، / برای rclone serve webdav. ورود با احراز هویت Basic است.", +conn_insecure_tls: "پذیرش گواهی خودامضا", +conn_insecure_tls_hint: "فقط برای NAS در شبکهٔ خودتان با گواهی خودامضا. در این صورت هر کسی میان این رایانه و سرور می‌تواند اتصال را بخواند.", +conn_no_signin: "بدون ورود (اشتراک عمومی)", conn_name: "نام", conn_host: "آدرس سرور", conn_port: "پورت", @@ -2949,7 +2954,7 @@ conn_host_key_new: "اولین اتصال به این سرور. اثر انگش conn_accept_key: "پذیرش", conn_save: "ذخیره", v5_home_servers: "سرورها", -v5_home_servers_empty: "بازی‌ها را مستقیم از NAS یا پوشه‌ی اشتراکی خود انتخاب کنید، از طریق SMB، FTP یا SFTP.", +v5_home_servers_empty: "بازی‌ها را مستقیم از NAS یا پوشه‌ی اشتراکی خود انتخاب کنید، از طریق SMB، FTP یا SFTP/WebDAV.", v5_home_servers_connect: "اتصال به NAS یا سرور", "fpkg.stage.copy": "کپی از سرور", "fpkg.stage.park": "کنار گذاشتن dump", @@ -3546,9 +3551,9 @@ upload_as_image_open_convert: "باز کردن تبدیل", upload_as_image_queued: "ایمیج ساخته شده و در صف بارگذاری است.", upload_as_image_title: "یا آن را به صورت یک ایمیج بازی بفرستید", "collection.add_server_folder": "افزودن پوشه‌ای روی {name}", -"collection.no_servers_hint": "بازی‌ها روی NAS هستند؟ آن را در اتصال‌ها (SMB، FTP یا SFTP) ذخیره کنید تا پوشه‌هایش بدون مونت کردن اینجا اضافه شوند.", +"collection.no_servers_hint": "بازی‌ها روی NAS هستند؟ آن را در اتصال‌ها (SMB، FTP یا SFTP/WebDAV) ذخیره کنید تا پوشه‌هایش بدون مونت کردن اینجا اضافه شوند.", "collection.pick_server_folder": "پوشه‌ای روی {name} که بازی‌هایتان در آن است", -"collection.server_folders_hint": "پوشه روی سرور ذخیره‌شده (SMB، FTP، SFTP) در همان جا خوانده می‌شود: چیزی به این رایانه کپی نمی‌شود و بازی‌هایش مستقیم از سرور نصب می‌شوند. انتقال به سطل زباله و مرتب‌سازی فقط روی پوشه‌های این رایانه کار می‌کنند.", +"collection.server_folders_hint": "پوشه روی سرور ذخیره‌شده (SMB، FTP، SFTP/WebDAV) در همان جا خوانده می‌شود: چیزی به این رایانه کپی نمی‌شود و بازی‌هایش مستقیم از سرور نصب می‌شوند. انتقال به سطل زباله و مرتب‌سازی فقط روی پوشه‌های این رایانه کار می‌کنند.", bottleneck_source_v2: "خواندن منبع", android_local_network_allow: "اجازه دادن", android_local_network_body: "تا وقتی اجازه ندهید، Android جلوی دسترسی برنامه به PS5 را می‌گیرد.", @@ -3910,7 +3915,7 @@ sendpayload_probe_no_signature: "امضای PS5Upload پیدا نشد — فقط sendpayload_probe_ok: "فایل payload سالم به نظر می‌رسد.", sendpayload_probe_read_error: "این فایل خوانده نشد ({error}). بررسی کنید که هنوز وجود دارد و باز می‌شود، سپس دوباره انتخابش کنید.", sendpayload_probe_too_small: "این فایل خالی است، پس چیزی برای ارسال نیست.", -servers_subtitle: "NAS و سرورهای شما (SMB، FTP، FTPS، SFTP). هر جا که برای یک بازی یا فایل مرور می‌کنید، با ▾ کنار «مرور» از میان آن‌ها انتخاب کنید.", +servers_subtitle: "NAS و سرورهای شما (SMB، FTP، FTPS، SFTP/WebDAV). هر جا که برای یک بازی یا فایل مرور می‌کنید، با ▾ کنار «مرور» از میان آن‌ها انتخاب کنید.", settings_artwork_cache_explain_v2: "کاورهای بازی جایی که موتور اجرا می‌شود (این کامپیوتر، یا سرور در نسخه‌ی وب) نگه داشته می‌شوند تا به‌جای خواندن هر باره از PS5، فوراً بارگذاری شوند. خودکار تازه می‌شوند و وقتی روی یک کنسول بازی‌ای نصب یا حذف می‌کنید، برای آن کنسول حذف می‌شوند.", settings_backup_downloaded: "{name} دانلود شد", settings_backup_hint_v2: "پوسته، فهرست PS5ها، تاریخچه‌ی اعلان‌ها، گزارش فعالیت و دیگر ترجیحات را در یک فایل JSON بسته‌بندی کنید تا پس از پاک کردن حافظه‌ی برنامه یا روی دستگاه دیگری بازیابی کنید. جفت‌سازی کنسول‌ها، مجموعه، رمزهای سرورهای ذخیره‌شده و کتابخانه‌های backport شامل نمی‌شوند.", diff --git a/client/src/i18n/locales/fr.ts b/client/src/i18n/locales/fr.ts index c02f87a0..8d07863a 100644 --- a/client/src/i18n/locales/fr.ts +++ b/client/src/i18n/locales/fr.ts @@ -2625,7 +2625,7 @@ helper_mismatch_go: "Recharger l'assistant", "conn_edit_title": "Modifier {name}", "conn_add_title": "Ajouter un serveur", "conn_empty_title": "Aucun serveur pour l’instant", -"conn_empty_body": "Ajoute ton NAS, un dossier partagé sur un PC ou un serveur FTP/SFTP, et choisis des jeux directement dessus.", +"conn_empty_body": "Ajoute ton NAS, un dossier partagé sur un PC ou un serveur FTP/SFTP/WebDAV, et choisis des jeux directement dessus.", "conn_browse": "Parcourir", "conn_test": "Tester", "conn_edit": "Modifier", @@ -2633,6 +2633,11 @@ helper_mismatch_go: "Recharger l'assistant", "conn_anonymous": "Anonyme (sans utilisateur ni mot de passe)", "conn_protocol": "Protocole", "conn_ftp_unencrypted": "Non chiffré — utilise FTPS ou SFTP si le serveur les prend en charge.", +conn_webdav_unencrypted: "Non chiffré : utilisez WebDAV (HTTPS) si le serveur le permet.", +conn_webdav_root_hint: "Indiquez la racine WebDAV du serveur dans Dossier de départ : /remote.php/dav/files/UTILISATEUR pour Nextcloud, /dav pour Alist, / pour rclone serve webdav. La connexion utilise l'authentification Basic.", +conn_insecure_tls: "Accepter un certificat auto-signé", +conn_insecure_tls_hint: "Uniquement pour un NAS sur votre propre réseau dont le certificat est auto-signé. Toute personne entre cet ordinateur et le serveur pourrait alors lire la connexion.", +conn_no_signin: "Sans identification (partage public)", "conn_name": "Nom", "conn_host": "Adresse du serveur", "conn_port": "Port", @@ -2654,7 +2659,7 @@ helper_mismatch_go: "Recharger l'assistant", "conn_accept_key": "Accepter", "conn_save": "Enregistrer", "v5_home_servers": "Serveurs", -"v5_home_servers_empty": "Choisis des jeux directement sur ton NAS ou un dossier partagé, via SMB, FTP ou SFTP.", +"v5_home_servers_empty": "Choisis des jeux directement sur ton NAS ou un dossier partagé, via SMB, FTP ou SFTP/WebDAV.", "v5_home_servers_connect": "Connecter un NAS ou un serveur", "fpkg.stage.copy": "Copier depuis le serveur", "fpkg.stage.park": "Mettre le dump de côté", @@ -3231,9 +3236,9 @@ upload_as_image_open_convert: "Ouvrir Convertir", upload_as_image_queued: "L’image est créée et dans la file d’envoi.", upload_as_image_title: "Ou l’envoyer en une seule image de jeu", "collection.add_server_folder": "Ajouter un dossier de {name}", -"collection.no_servers_hint": "Des jeux sur un NAS ? Enregistrez-le dans Connexions (SMB, FTP ou SFTP) et ses dossiers pourront être ajoutés ici sans les monter.", +"collection.no_servers_hint": "Des jeux sur un NAS ? Enregistrez-le dans Connexions (SMB, FTP ou SFTP/WebDAV) et ses dossiers pourront être ajoutés ici sans les monter.", "collection.pick_server_folder": "Le dossier de {name} qui contient vos jeux", -"collection.server_folders_hint": "Un dossier sur un serveur enregistré (SMB, FTP, SFTP) est lu sur place : rien n’est copié sur cet ordinateur et ses jeux s’installent directement depuis le serveur. Mettre à la Corbeille et Ranger ne concernent que les dossiers de cet ordinateur.", +"collection.server_folders_hint": "Un dossier sur un serveur enregistré (SMB, FTP, SFTP/WebDAV) est lu sur place : rien n’est copié sur cet ordinateur et ses jeux s’installent directement depuis le serveur. Mettre à la Corbeille et Ranger ne concernent que les dossiers de cet ordinateur.", bottleneck_source_v2: "la lecture de la source", android_local_network_allow: "Autoriser", android_local_network_body: "Android empêche l’app d’atteindre ta PS5 tant que tu ne l’autorises pas.", @@ -3595,7 +3600,7 @@ sendpayload_probe_no_signature: "Aucune signature PS5Upload trouvée — à util sendpayload_probe_ok: "Le fichier payload semble correct.", sendpayload_probe_read_error: "Impossible de lire ce fichier ({error}). Vérifiez qu'il existe toujours et peut être ouvert, puis sélectionnez-le à nouveau.", sendpayload_probe_too_small: "Ce fichier est vide, il n'y a donc rien à envoyer.", -servers_subtitle: "Vos NAS et serveurs (SMB, FTP, FTPS, SFTP). Partout où vous cherchez un jeu ou un fichier, choisissez-les avec le ▾ à côté de Parcourir.", +servers_subtitle: "Vos NAS et serveurs (SMB, FTP, FTPS, SFTP/WebDAV). Partout où vous cherchez un jeu ou un fichier, choisissez-les avec le ▾ à côté de Parcourir.", settings_artwork_cache_explain_v2: "Les jaquettes des jeux sont conservées là où tourne le moteur (cet ordinateur, ou le serveur pour la version web) afin de se charger instantanément au lieu d'être lues depuis votre PS5 à chaque fois. Elles sont actualisées automatiquement et supprimées pour une console lorsque vous y installez ou désinstallez un jeu.", settings_backup_downloaded: "{name} téléchargé", settings_backup_hint_v2: "Regrouper le thème, la liste des PS5, l'historique des notifications, le journal d'activité et les autres préférences dans un fichier JSON, à restaurer après avoir effacé le stockage de l'application ou sur une autre machine. Les associations de consoles, la Collection, les mots de passe des serveurs enregistrés et les bibliothèques de backport ne sont pas inclus.", diff --git a/client/src/i18n/locales/hi.ts b/client/src/i18n/locales/hi.ts index 59c74032..d417ce70 100644 --- a/client/src/i18n/locales/hi.ts +++ b/client/src/i18n/locales/hi.ts @@ -2623,7 +2623,7 @@ helper_mismatch_go: "हेल्पर फिर लोड करें", "conn_edit_title": "{name} संपादित करें", "conn_add_title": "सर्वर जोड़ें", "conn_empty_title": "अभी कोई सर्वर नहीं", -"conn_empty_body": "अपना NAS, किसी PC का साझा फ़ोल्डर, या FTP/SFTP सर्वर जोड़ें, और सीधे वहीं से गेम चुनें।", +"conn_empty_body": "अपना NAS, किसी PC का साझा फ़ोल्डर, या FTP/SFTP/WebDAV सर्वर जोड़ें, और सीधे वहीं से गेम चुनें।", "conn_browse": "ब्राउज़ करें", "conn_test": "जाँचें", "conn_edit": "संपादित करें", @@ -2631,6 +2631,11 @@ helper_mismatch_go: "हेल्पर फिर लोड करें", "conn_anonymous": "अनाम (कोई उपयोगकर्ता या पासवर्ड नहीं)", "conn_protocol": "प्रोटोकॉल", "conn_ftp_unencrypted": "एन्क्रिप्ट नहीं — जहाँ सर्वर सपोर्ट करे वहाँ FTPS या SFTP का उपयोग करें।", +conn_webdav_unencrypted: "अनएन्क्रिप्टेड — जहाँ सर्वर समर्थन करे, WebDAV (HTTPS) उपयोग करें।", +conn_webdav_root_hint: "सर्वर का WebDAV रूट प्रारंभ फ़ोल्डर में डालें: Nextcloud के लिए /remote.php/dav/files/USER, Alist के लिए /dav, rclone serve webdav के लिए /। साइन-इन Basic प्रमाणीकरण है।", +conn_insecure_tls: "स्व-हस्ताक्षरित प्रमाणपत्र स्वीकार करें", +conn_insecure_tls_hint: "केवल अपने नेटवर्क के उस NAS के लिए जिसका प्रमाणपत्र स्व-हस्ताक्षरित हो। तब इस कंप्यूटर और सर्वर के बीच कोई भी कनेक्शन पढ़ सकता है।", +conn_no_signin: "बिना साइन-इन (सार्वजनिक शेयर)", "conn_name": "नाम", "conn_host": "सर्वर पता", "conn_port": "पोर्ट", @@ -2652,7 +2657,7 @@ helper_mismatch_go: "हेल्पर फिर लोड करें", "conn_accept_key": "स्वीकार करें", "conn_save": "सहेजें", "v5_home_servers": "सर्वर", -"v5_home_servers_empty": "SMB, FTP या SFTP पर अपने NAS या साझा फ़ोल्डर से सीधे गेम चुनें।", +"v5_home_servers_empty": "SMB, FTP या SFTP/WebDAV पर अपने NAS या साझा फ़ोल्डर से सीधे गेम चुनें।", "v5_home_servers_connect": "NAS या सर्वर जोड़ें", "fpkg.stage.copy": "सर्वर से कॉपी करें", "fpkg.stage.park": "डंप को अलग रखें", @@ -3229,9 +3234,9 @@ upload_as_image_open_convert: "कन्वर्ट खोलें", upload_as_image_queued: "इमेज बन गई है और अपलोड कतार में है।", upload_as_image_title: "या इसे एक गेम इमेज के रूप में भेजें", "collection.add_server_folder": "{name} पर फ़ोल्डर जोड़ें", -"collection.no_servers_hint": "गेम NAS पर हैं? इसे कनेक्शन में सहेजें (SMB, FTP या SFTP), फिर इसके फ़ोल्डर बिना माउंट किए यहाँ जोड़े जा सकते हैं।", +"collection.no_servers_hint": "गेम NAS पर हैं? इसे कनेक्शन में सहेजें (SMB, FTP या SFTP/WebDAV), फिर इसके फ़ोल्डर बिना माउंट किए यहाँ जोड़े जा सकते हैं।", "collection.pick_server_folder": "{name} पर आपके गेम वाला फ़ोल्डर", -"collection.server_folders_hint": "सहेजे गए सर्वर (SMB, FTP, SFTP) का फ़ोल्डर वहीं से पढ़ा जाता है: इस कंप्यूटर पर कुछ कॉपी नहीं होता, और उसके गेम सीधे सर्वर से इंस्टॉल होते हैं। ट्रैश में भेजें और व्यवस्थित करें केवल इस कंप्यूटर के फ़ोल्डरों पर काम करते हैं।", +"collection.server_folders_hint": "सहेजे गए सर्वर (SMB, FTP, SFTP/WebDAV) का फ़ोल्डर वहीं से पढ़ा जाता है: इस कंप्यूटर पर कुछ कॉपी नहीं होता, और उसके गेम सीधे सर्वर से इंस्टॉल होते हैं। ट्रैश में भेजें और व्यवस्थित करें केवल इस कंप्यूटर के फ़ोल्डरों पर काम करते हैं।", bottleneck_source_v2: "स्रोत पढ़ना", android_local_network_allow: "अनुमति दें", android_local_network_body: "जब तक आप अनुमति नहीं देते, Android ऐप को आपके PS5 तक पहुँचने से रोकता है।", @@ -3593,7 +3598,7 @@ sendpayload_probe_no_signature: "कोई PS5Upload हस्ताक्षर sendpayload_probe_ok: "पेलोड फ़ाइल ठीक लगती है।", sendpayload_probe_read_error: "यह फ़ाइल पढ़ी नहीं जा सकी ({error})। जाँचें कि यह अब भी मौजूद है और खोली जा सकती है, फिर इसे दोबारा चुनें।", sendpayload_probe_too_small: "यह फ़ाइल खाली है, इसलिए भेजने के लिए कुछ नहीं है।", -servers_subtitle: "आपके NAS और सर्वर (SMB, FTP, FTPS, SFTP)। जहाँ भी आप किसी गेम या फ़ाइल के लिए ब्राउज़ करें, ब्राउज़ के पास ▾ से इनमें से चुनें।", +servers_subtitle: "आपके NAS और सर्वर (SMB, FTP, FTPS, SFTP/WebDAV)। जहाँ भी आप किसी गेम या फ़ाइल के लिए ब्राउज़ करें, ब्राउज़ के पास ▾ से इनमें से चुनें।", settings_artwork_cache_explain_v2: "गेम कवर वहाँ रखे जाते हैं जहाँ इंजन चलता है (यह कंप्यूटर, या वेब संस्करण के लिए सर्वर) ताकि वे हर बार आपके PS5 से पढ़े जाने के बजाय तुरंत लोड हों। ये अपने आप रीफ़्रेश होते हैं, और किसी कंसोल पर गेम इंस्टॉल या अनइंस्टॉल करने पर उस कंसोल के लिए हटा दिए जाते हैं।", settings_backup_downloaded: "{name} डाउनलोड हुआ", settings_backup_hint_v2: "थीम, PS5 सूची, सूचना इतिहास, गतिविधि लॉग और अन्य प्राथमिकताओं को एक JSON फ़ाइल में बंडल करें, ताकि ऐप का स्टोरेज मिटाने के बाद या किसी दूसरी मशीन पर रीस्टोर कर सकें। कंसोल पेयरिंग, संग्रह, सेव किए सर्वरों के पासवर्ड और बैकपोर्ट लाइब्रेरी शामिल नहीं हैं।", diff --git a/client/src/i18n/locales/hu.ts b/client/src/i18n/locales/hu.ts index e949e6db..ab0faa94 100644 --- a/client/src/i18n/locales/hu.ts +++ b/client/src/i18n/locales/hu.ts @@ -2936,7 +2936,7 @@ task_recheck: "Újraellenőrzés", "conn_edit_title": "{name} szerkesztése", "conn_add_title": "Kiszolgáló hozzáadása", "conn_empty_title": "Még nincs kiszolgáló", -"conn_empty_body": "Add hozzá a NAS-odat, egy PC megosztott mappáját vagy egy FTP/SFTP kiszolgálót, és válassz játékot közvetlenül onnan.", +"conn_empty_body": "Add hozzá a NAS-odat, egy PC megosztott mappáját vagy egy FTP/SFTP/WebDAV kiszolgálót, és válassz játékot közvetlenül onnan.", "conn_browse": "Tallózás", "conn_test": "Teszt", "conn_edit": "Szerkesztés", @@ -2944,6 +2944,11 @@ task_recheck: "Újraellenőrzés", "conn_anonymous": "Névtelen (felhasználó és jelszó nélkül)", "conn_protocol": "Protokoll", "conn_ftp_unencrypted": "Titkosítatlan — használj FTPS-t vagy SFTP-t, ha a kiszolgáló támogatja.", +conn_webdav_unencrypted: "Titkosítatlan — használj WebDAV (HTTPS)-t, ha a szerver támogatja.", +conn_webdav_root_hint: "Add meg a szerver WebDAV-gyökerét a Kezdőmappában: /remote.php/dav/files/FELHASZNÁLÓ Nextcloudnál, /dav Alistnél, / rclone serve webdav esetén. A bejelentkezés Basic hitelesítés.", +conn_insecure_tls: "Önaláírt tanúsítvány elfogadása", +conn_insecure_tls_hint: "Csak saját hálózaton lévő, önaláírt tanúsítványú NAS-hoz. Ekkor bárki a gép és a szerver között olvashatná a kapcsolatot.", +conn_no_signin: "Bejelentkezés nélkül (nyilvános megosztás)", "conn_name": "Név", "conn_host": "Kiszolgáló címe", "conn_port": "Port", @@ -2965,7 +2970,7 @@ task_recheck: "Újraellenőrzés", "conn_accept_key": "Elfogadás", "conn_save": "Mentés", "v5_home_servers": "Kiszolgálók", -"v5_home_servers_empty": "Válassz játékot közvetlenül a NAS-odról vagy egy megosztott mappából, SMB-n, FTP-n vagy SFTP-n keresztül.", +"v5_home_servers_empty": "Válassz játékot közvetlenül a NAS-odról vagy egy megosztott mappából, SMB-n, FTP-n vagy SFTP/WebDAV-n keresztül.", "v5_home_servers_connect": "NAS vagy kiszolgáló csatlakoztatása", "fpkg.stage.copy": "Másolás a kiszolgálóról", "fpkg.stage.park": "A dump félretétele", @@ -3529,9 +3534,9 @@ upload_as_image_open_convert: "Átalakítás megnyitása", upload_as_image_queued: "A képfájl elkészült, és a feltöltési sorban van.", upload_as_image_title: "Vagy küldd el egyetlen játékképfájlként", "collection.add_server_folder": "Mappa hozzáadása innen: {name}", -"collection.no_servers_hint": "Játékok egy NAS-on? Mentsd el a Kapcsolatok között (SMB, FTP vagy SFTP), és a mappái csatolás nélkül hozzáadhatók itt.", +"collection.no_servers_hint": "Játékok egy NAS-on? Mentsd el a Kapcsolatok között (SMB, FTP vagy SFTP/WebDAV), és a mappái csatolás nélkül hozzáadhatók itt.", "collection.pick_server_folder": "A játékaidat tartalmazó mappa itt: {name}", -"collection.server_folders_hint": "Egy mentett szerveren (SMB, FTP, SFTP) lévő mappát a helyén olvasunk: semmi sem másolódik erre a számítógépre, a játékai pedig közvetlenül a szerverről települnek. A Kukába helyezés és a Rendezés csak ennek a számítógépnek a mappáira vonatkozik.", +"collection.server_folders_hint": "Egy mentett szerveren (SMB, FTP, SFTP/WebDAV) lévő mappát a helyén olvasunk: semmi sem másolódik erre a számítógépre, a játékai pedig közvetlenül a szerverről települnek. A Kukába helyezés és a Rendezés csak ennek a számítógépnek a mappáira vonatkozik.", bottleneck_source_v2: "a forrás olvasása", android_local_network_allow: "Engedélyezés", android_local_network_body: "Amíg nem engedélyezed, az Android nem engedi, hogy az alkalmazás elérje a PS5-öt.", @@ -3870,7 +3875,7 @@ sendpayload_probe_no_signature: "Nem található PS5Upload aláírás — csak a sendpayload_probe_ok: "A payload fájl rendben lévőnek tűnik.", sendpayload_probe_read_error: "Nem sikerült beolvasni ezt a fájlt ({error}). Ellenőrizd, hogy még létezik és megnyitható, majd válaszd ki újra.", sendpayload_probe_too_small: "Ez a fájl üres, így nincs mit elküldeni.", -servers_subtitle: "A NAS-aid és kiszolgálóid (SMB, FTP, FTPS, SFTP). Bárhol, ahol játékot vagy fájlt tallózol, a Tallózás melletti ▾ segítségével választhatsz közülük.", +servers_subtitle: "A NAS-aid és kiszolgálóid (SMB, FTP, FTPS, SFTP/WebDAV). Bárhol, ahol játékot vagy fájlt tallózol, a Tallózás melletti ▾ segítségével választhatsz közülük.", settings_artwork_cache_explain_v2: "A játékborítók ott tárolódnak, ahol a motor fut (ezen a számítógépen, vagy a webes verziónál a kiszolgálón), így azonnal betöltődnek ahelyett, hogy minden alkalommal a PS5-ről kellene beolvasni őket. Automatikusan frissülnek, és egy konzolnál törlődnek, amikor ott játékot telepítesz vagy eltávolítasz.", settings_backup_downloaded: "Letöltve: {name}", settings_backup_hint_v2: "A téma, a PS5-lista, az értesítési előzmények, a tevékenységnapló és más beállítások egyetlen JSON-fájlba csomagolása, hogy visszaállíthasd őket az alkalmazás tárhelyének törlése után vagy egy másik gépen. A konzolpárosítások, a Gyűjtemény, a mentett kiszolgálók jelszavai és a backport könyvtárak nem kerülnek bele.", diff --git a/client/src/i18n/locales/id.ts b/client/src/i18n/locales/id.ts index dca8dc3a..be0274c4 100644 --- a/client/src/i18n/locales/id.ts +++ b/client/src/i18n/locales/id.ts @@ -2616,7 +2616,7 @@ helper_mismatch_go: "Muat ulang helper", "conn_edit_title": "Ubah {name}", "conn_add_title": "Tambah server", "conn_empty_title": "Belum ada server", -"conn_empty_body": "Tambahkan NAS-mu, folder bersama di PC, atau server FTP/SFTP, lalu pilih game langsung dari sana.", +"conn_empty_body": "Tambahkan NAS-mu, folder bersama di PC, atau server FTP/SFTP/WebDAV, lalu pilih game langsung dari sana.", "conn_browse": "Jelajahi", "conn_test": "Uji", "conn_edit": "Ubah", @@ -2624,6 +2624,11 @@ helper_mismatch_go: "Muat ulang helper", "conn_anonymous": "Anonim (tanpa pengguna atau kata sandi)", "conn_protocol": "Protokol", "conn_ftp_unencrypted": "Tidak terenkripsi — gunakan FTPS atau SFTP jika server mendukung.", +conn_webdav_unencrypted: "Tidak terenkripsi — gunakan WebDAV (HTTPS) jika server mendukungnya.", +conn_webdav_root_hint: "Masukkan root WebDAV server di Folder awal: /remote.php/dav/files/PENGGUNA untuk Nextcloud, /dav untuk Alist, / untuk rclone serve webdav. Masuk memakai autentikasi Basic.", +conn_insecure_tls: "Terima sertifikat yang ditandatangani sendiri", +conn_insecure_tls_hint: "Hanya untuk NAS di jaringan Anda sendiri dengan sertifikat yang ditandatangani sendiri. Siapa pun di antara komputer ini dan server bisa membaca koneksinya.", +conn_no_signin: "Tanpa masuk (berbagi publik)", "conn_name": "Nama", "conn_host": "Alamat server", "conn_port": "Port", @@ -2645,7 +2650,7 @@ helper_mismatch_go: "Muat ulang helper", "conn_accept_key": "Terima", "conn_save": "Simpan", "v5_home_servers": "Server", -"v5_home_servers_empty": "Pilih game langsung dari NAS-mu atau folder bersama, lewat SMB, FTP, atau SFTP.", +"v5_home_servers_empty": "Pilih game langsung dari NAS-mu atau folder bersama, lewat SMB, FTP, atau SFTP/WebDAV.", "v5_home_servers_connect": "Hubungkan NAS atau server", "fpkg.stage.copy": "Salin dari server", "fpkg.stage.park": "Sisihkan dump", @@ -3222,9 +3227,9 @@ upload_as_image_open_convert: "Buka Konversi", upload_as_image_queued: "Image sudah dibuat dan ada di antrean unggah.", upload_as_image_title: "Atau kirim sebagai satu image game", "collection.add_server_folder": "Tambah folder di {name}", -"collection.no_servers_hint": "Game di NAS? Simpan di Koneksi (SMB, FTP, atau SFTP), lalu foldernya bisa ditambahkan di sini tanpa dipasang.", +"collection.no_servers_hint": "Game di NAS? Simpan di Koneksi (SMB, FTP, atau SFTP/WebDAV), lalu foldernya bisa ditambahkan di sini tanpa dipasang.", "collection.pick_server_folder": "Folder di {name} yang berisi game kamu", -"collection.server_folders_hint": "Folder di server tersimpan (SMB, FTP, SFTP) dibaca di tempatnya: tidak ada yang disalin ke komputer ini, dan game-nya terpasang langsung dari server. Pindahkan ke Tempat Sampah dan Rapikan hanya berlaku untuk folder di komputer ini.", +"collection.server_folders_hint": "Folder di server tersimpan (SMB, FTP, SFTP/WebDAV) dibaca di tempatnya: tidak ada yang disalin ke komputer ini, dan game-nya terpasang langsung dari server. Pindahkan ke Tempat Sampah dan Rapikan hanya berlaku untuk folder di komputer ini.", bottleneck_source_v2: "membaca sumber", android_local_network_allow: "Izinkan", android_local_network_body: "Android memblokir aplikasi agar tidak bisa menjangkau PS5 sampai kamu mengizinkannya.", @@ -3586,7 +3591,7 @@ sendpayload_probe_no_signature: "Tanda tangan PS5Upload tidak ditemukan — guna sendpayload_probe_ok: "File payload tampak baik.", sendpayload_probe_read_error: "Tidak dapat membaca file ini ({error}). Pastikan file masih ada dan bisa dibuka, lalu pilih lagi.", sendpayload_probe_too_small: "File ini kosong, jadi tidak ada yang bisa dikirim.", -servers_subtitle: "NAS dan servermu (SMB, FTP, FTPS, SFTP). Di mana pun kamu menelusuri game atau file, pilih dari daftar ini dengan ▾ di samping Telusuri.", +servers_subtitle: "NAS dan servermu (SMB, FTP, FTPS, SFTP/WebDAV). Di mana pun kamu menelusuri game atau file, pilih dari daftar ini dengan ▾ di samping Telusuri.", settings_artwork_cache_explain_v2: "Sampul game disimpan di tempat engine berjalan (komputer ini, atau server untuk versi web) sehingga langsung dimuat alih-alih dibaca dari PS5 setiap kali. Sampul diperbarui otomatis, dan dihapus untuk sebuah konsol saat kamu memasang atau mencopot game di sana.", settings_backup_downloaded: "{name} diunduh", settings_backup_hint_v2: "Gabungkan tema, daftar PS5, riwayat notifikasi, log aktivitas, dan preferensi lain ke satu file JSON, untuk dipulihkan setelah menghapus penyimpanan aplikasi atau di mesin lain. Pemasangan konsol, Koleksi, kata sandi server tersimpan, dan pustaka backport tidak disertakan.", diff --git a/client/src/i18n/locales/it.ts b/client/src/i18n/locales/it.ts index 5fe68187..5f84e1d5 100644 --- a/client/src/i18n/locales/it.ts +++ b/client/src/i18n/locales/it.ts @@ -2557,7 +2557,7 @@ helper_mismatch_go: "Ricarica helper", "conn_edit_title": "Modifica {name}", "conn_add_title": "Aggiungi un server", "conn_empty_title": "Ancora nessun server", -"conn_empty_body": "Aggiungi il tuo NAS, una cartella condivisa su un PC o un server FTP/SFTP, e scegli i giochi direttamente da lì.", +"conn_empty_body": "Aggiungi il tuo NAS, una cartella condivisa su un PC o un server FTP/SFTP/WebDAV, e scegli i giochi direttamente da lì.", "conn_browse": "Sfoglia", "conn_test": "Prova", "conn_edit": "Modifica", @@ -2565,6 +2565,11 @@ helper_mismatch_go: "Ricarica helper", "conn_anonymous": "Anonimo (senza utente né password)", "conn_protocol": "Protocollo", "conn_ftp_unencrypted": "Non cifrato: usa FTPS o SFTP se il server li supporta.", +conn_webdav_unencrypted: "Non cifrato: usa WebDAV (HTTPS) se il server lo supporta.", +conn_webdav_root_hint: "Metti la radice WebDAV del server in Cartella iniziale: /remote.php/dav/files/UTENTE per Nextcloud, /dav per Alist, / per rclone serve webdav. L'accesso usa l'autenticazione Basic.", +conn_insecure_tls: "Accetta un certificato autofirmato", +conn_insecure_tls_hint: "Solo per un NAS nella tua rete con certificato autofirmato. Chiunque tra questo computer e il server potrebbe leggere la connessione.", +conn_no_signin: "Senza accesso (condivisione pubblica)", "conn_name": "Nome", "conn_host": "Indirizzo del server", "conn_port": "Porta", @@ -2586,7 +2591,7 @@ helper_mismatch_go: "Ricarica helper", "conn_accept_key": "Accetta", "conn_save": "Salva", "v5_home_servers": "Server", -"v5_home_servers_empty": "Scegli i giochi direttamente dal tuo NAS o da una cartella condivisa, via SMB, FTP o SFTP.", +"v5_home_servers_empty": "Scegli i giochi direttamente dal tuo NAS o da una cartella condivisa, via SMB, FTP o SFTP/WebDAV.", "v5_home_servers_connect": "Collega un NAS o un server", "fpkg.stage.copy": "Copia dal server", "fpkg.stage.park": "Metti da parte il dump", @@ -3163,9 +3168,9 @@ upload_as_image_open_convert: "Apri Converti", upload_as_image_queued: "L’immagine è creata ed è nella coda di caricamento.", upload_as_image_title: "Oppure invialo come un’unica immagine di gioco", "collection.add_server_folder": "Aggiungi una cartella su {name}", -"collection.no_servers_hint": "Giochi su un NAS? Salvalo in Connessioni (SMB, FTP o SFTP) e le sue cartelle si potranno aggiungere qui senza montarle.", +"collection.no_servers_hint": "Giochi su un NAS? Salvalo in Connessioni (SMB, FTP o SFTP/WebDAV) e le sue cartelle si potranno aggiungere qui senza montarle.", "collection.pick_server_folder": "La cartella su {name} che contiene i tuoi giochi", -"collection.server_folders_hint": "Una cartella su un server salvato (SMB, FTP, SFTP) viene letta dov’è: non viene copiato nulla su questo computer e i suoi giochi si installano direttamente dal server. Sposta nel Cestino e Riordina valgono solo per le cartelle di questo computer.", +"collection.server_folders_hint": "Una cartella su un server salvato (SMB, FTP, SFTP/WebDAV) viene letta dov’è: non viene copiato nulla su questo computer e i suoi giochi si installano direttamente dal server. Sposta nel Cestino e Riordina valgono solo per le cartelle di questo computer.", bottleneck_source_v2: "la lettura della sorgente", android_local_network_allow: "Consenti", android_local_network_body: "Android impedisce all’app di raggiungere la tua PS5 finché non lo consenti.", @@ -3527,7 +3532,7 @@ sendpayload_probe_no_signature: "Nessuna firma PS5Upload trovata — usalo solo sendpayload_probe_ok: "Il file payload sembra a posto.", sendpayload_probe_read_error: "Impossibile leggere questo file ({error}). Controlla che esista ancora e si possa aprire, poi selezionalo di nuovo.", sendpayload_probe_too_small: "Questo file è vuoto, quindi non c'è nulla da inviare.", -servers_subtitle: "I tuoi NAS e server (SMB, FTP, FTPS, SFTP). Ovunque cerchi un gioco o un file, sceglili con la ▾ accanto a Sfoglia.", +servers_subtitle: "I tuoi NAS e server (SMB, FTP, FTPS, SFTP/WebDAV). Ovunque cerchi un gioco o un file, sceglili con la ▾ accanto a Sfoglia.", settings_artwork_cache_explain_v2: "Le copertine dei giochi vengono conservate dove gira il motore (questo computer, o il server per la versione web) così si caricano subito invece di essere lette dalla PS5 ogni volta. Vengono aggiornate automaticamente e rimosse per una console quando vi installi o disinstalli un gioco.", settings_backup_downloaded: "{name} scaricato", settings_backup_hint_v2: "Raccogli tema, elenco delle PS5, cronologia notifiche, registro attività e altre preferenze in un unico file JSON, da ripristinare dopo aver cancellato l'archiviazione dell'app o su un altro computer. Associazioni delle console, la Collezione, le password dei server salvati e le librerie di backport non sono incluse.", diff --git a/client/src/i18n/locales/ja.ts b/client/src/i18n/locales/ja.ts index c94965a4..f7d09ffa 100644 --- a/client/src/i18n/locales/ja.ts +++ b/client/src/i18n/locales/ja.ts @@ -2628,7 +2628,7 @@ helper_mismatch_go: "ヘルパーを再読み込み", "conn_edit_title": "{name} を編集", "conn_add_title": "サーバーを追加", "conn_empty_title": "サーバーはまだありません", -"conn_empty_body": "NAS、PC の共有フォルダー、FTP/SFTP サーバーを追加して、そこから直接ゲームを選べます。", +"conn_empty_body": "NAS、PC の共有フォルダー、FTP/SFTP/WebDAV サーバーを追加して、そこから直接ゲームを選べます。", "conn_browse": "参照", "conn_test": "テスト", "conn_edit": "編集", @@ -2636,6 +2636,11 @@ helper_mismatch_go: "ヘルパーを再読み込み", "conn_anonymous": "匿名(ユーザー名とパスワードなし)", "conn_protocol": "プロトコル", "conn_ftp_unencrypted": "暗号化されていません。サーバーが対応していれば FTPS か SFTP を使ってください。", +conn_webdav_unencrypted: "暗号化なし — サーバーが対応していれば WebDAV (HTTPS) を使ってください。", +conn_webdav_root_hint: "サーバーの WebDAV ルートを開始フォルダーに入れてください: Nextcloud は /remote.php/dav/files/USER、Alist は /dav、rclone serve webdav は /。サインインは Basic 認証です。", +conn_insecure_tls: "自己署名証明書を受け入れる", +conn_insecure_tls_hint: "自宅ネットワーク内の、自己署名証明書を使う NAS にのみ。このコンピューターとサーバーの間にいる誰でも通信を読める可能性があります。", +conn_no_signin: "サインインなし(公開共有)", "conn_name": "名前", "conn_host": "サーバーのアドレス", "conn_port": "ポート", @@ -2657,7 +2662,7 @@ helper_mismatch_go: "ヘルパーを再読み込み", "conn_accept_key": "承認", "conn_save": "保存", "v5_home_servers": "サーバー", -"v5_home_servers_empty": "NAS や共有フォルダーから SMB、FTP、SFTP で直接ゲームを選べます。", +"v5_home_servers_empty": "NAS や共有フォルダーから SMB、FTP、SFTP/WebDAV で直接ゲームを選べます。", "v5_home_servers_connect": "NAS またはサーバーに接続", "fpkg.stage.copy": "サーバーからコピー", "fpkg.stage.park": "ダンプを退避", @@ -3234,9 +3239,9 @@ upload_as_image_open_convert: "変換を開く", upload_as_image_queued: "イメージが作成され、アップロード待ちに入りました。", upload_as_image_title: "または 1 つのゲームイメージとして送る", "collection.add_server_folder": "{name} のフォルダーを追加", -"collection.no_servers_hint": "NAS にゲームがありますか? 接続 (SMB、FTP、SFTP) に保存すると、マウントせずにそのフォルダーをここに追加できます。", +"collection.no_servers_hint": "NAS にゲームがありますか? 接続 (SMB、FTP、SFTP/WebDAV) に保存すると、マウントせずにそのフォルダーをここに追加できます。", "collection.pick_server_folder": "{name} 上のゲームが入っているフォルダー", -"collection.server_folders_hint": "保存したサーバー (SMB、FTP、SFTP) のフォルダーはその場で読み取ります。このコンピューターには何もコピーされず、ゲームはサーバーから直接インストールされます。ゴミ箱に移すと整理は、このコンピューターのフォルダーだけが対象です。", +"collection.server_folders_hint": "保存したサーバー (SMB、FTP、SFTP/WebDAV) のフォルダーはその場で読み取ります。このコンピューターには何もコピーされず、ゲームはサーバーから直接インストールされます。ゴミ箱に移すと整理は、このコンピューターのフォルダーだけが対象です。", bottleneck_source_v2: "ソースの読み取り", android_local_network_allow: "許可", android_local_network_body: "許可するまで、Android はこのアプリが PS5 に接続するのをブロックします。", @@ -3598,7 +3603,7 @@ sendpayload_probe_no_signature: "PS5Upload の署名が見つかりません — sendpayload_probe_ok: "ペイロードファイルに問題はなさそうです。", sendpayload_probe_read_error: "このファイルを読み取れませんでした({error})。ファイルがまだ存在し開けることを確認してから、もう一度選んでください。", sendpayload_probe_too_small: "このファイルは空なので、送信するものがありません。", -servers_subtitle: "NAS とサーバー(SMB、FTP、FTPS、SFTP)。ゲームやファイルを参照するどこからでも、「参照」の横の ▾ から選べます。", +servers_subtitle: "NAS とサーバー(SMB、FTP、FTPS、SFTP/WebDAV)。ゲームやファイルを参照するどこからでも、「参照」の横の ▾ から選べます。", settings_artwork_cache_explain_v2: "ゲームのカバー画像はエンジンが動作している場所(このコンピューター、Web 版ではサーバー)に保存され、毎回 PS5 から読み込む代わりにすぐに表示されます。自動で更新され、本体でゲームをインストールまたはアンインストールするとその本体の分は削除されます。", settings_backup_downloaded: "{name} をダウンロードしました", settings_backup_hint_v2: "テーマ、PS5 の一覧、通知履歴、アクティビティログなどの設定を 1 つの JSON ファイルにまとめ、アプリのストレージを消去した後や別のマシンで復元できます。本体のペアリング、コレクション、保存したサーバーのパスワード、バックポート用ライブラリは含まれません。", diff --git a/client/src/i18n/locales/ko.ts b/client/src/i18n/locales/ko.ts index 2aefed94..da372da9 100644 --- a/client/src/i18n/locales/ko.ts +++ b/client/src/i18n/locales/ko.ts @@ -2626,7 +2626,7 @@ helper_mismatch_go: "헬퍼 다시 불러오기", "conn_edit_title": "{name} 편집", "conn_add_title": "서버 추가", "conn_empty_title": "아직 서버가 없습니다", -"conn_empty_body": "NAS, PC의 공유 폴더 또는 FTP/SFTP 서버를 추가하고 거기서 바로 게임을 고르세요.", +"conn_empty_body": "NAS, PC의 공유 폴더 또는 FTP/SFTP/WebDAV 서버를 추가하고 거기서 바로 게임을 고르세요.", "conn_browse": "탐색", "conn_test": "테스트", "conn_edit": "편집", @@ -2634,6 +2634,11 @@ helper_mismatch_go: "헬퍼 다시 불러오기", "conn_anonymous": "익명(사용자 이름·비밀번호 없음)", "conn_protocol": "프로토콜", "conn_ftp_unencrypted": "암호화되지 않음 — 서버가 지원하면 FTPS나 SFTP를 사용하세요.", +conn_webdav_unencrypted: "암호화되지 않음 — 서버가 지원하면 WebDAV (HTTPS)를 사용하세요.", +conn_webdav_root_hint: "서버의 WebDAV 루트를 시작 폴더에 입력하세요: Nextcloud는 /remote.php/dav/files/USER, Alist는 /dav, rclone serve webdav는 /. 로그인은 Basic 인증입니다.", +conn_insecure_tls: "자체 서명 인증서 허용", +conn_insecure_tls_hint: "자체 서명 인증서를 쓰는 내 네트워크의 NAS에만 사용하세요. 이 컴퓨터와 서버 사이의 누구든 연결 내용을 읽을 수 있습니다.", +conn_no_signin: "로그인 없음 (공개 공유)", "conn_name": "이름", "conn_host": "서버 주소", "conn_port": "포트", @@ -2655,7 +2660,7 @@ helper_mismatch_go: "헬퍼 다시 불러오기", "conn_accept_key": "수락", "conn_save": "저장", "v5_home_servers": "서버", -"v5_home_servers_empty": "NAS나 공유 폴더에서 SMB, FTP, SFTP로 바로 게임을 고르세요.", +"v5_home_servers_empty": "NAS나 공유 폴더에서 SMB, FTP, SFTP/WebDAV로 바로 게임을 고르세요.", "v5_home_servers_connect": "NAS 또는 서버 연결", "fpkg.stage.copy": "서버에서 복사", "fpkg.stage.park": "덤프 옮겨 두기", @@ -3232,9 +3237,9 @@ upload_as_image_open_convert: "변환 열기", upload_as_image_queued: "이미지가 만들어져 업로드 대기열에 있습니다.", upload_as_image_title: "또는 게임 이미지 하나로 보내기", "collection.add_server_folder": "{name}의 폴더 추가", -"collection.no_servers_hint": "NAS에 게임이 있나요? 연결(SMB, FTP, SFTP)에 저장하면 마운트하지 않고도 그 폴더를 여기에 추가할 수 있습니다.", +"collection.no_servers_hint": "NAS에 게임이 있나요? 연결(SMB, FTP, SFTP/WebDAV)에 저장하면 마운트하지 않고도 그 폴더를 여기에 추가할 수 있습니다.", "collection.pick_server_folder": "게임이 들어 있는 {name}의 폴더", -"collection.server_folders_hint": "저장된 서버(SMB, FTP, SFTP)의 폴더는 그 자리에서 읽습니다. 이 컴퓨터로 아무것도 복사하지 않고, 게임은 서버에서 바로 설치됩니다. 휴지통으로 이동과 정리는 이 컴퓨터의 폴더에만 적용됩니다.", +"collection.server_folders_hint": "저장된 서버(SMB, FTP, SFTP/WebDAV)의 폴더는 그 자리에서 읽습니다. 이 컴퓨터로 아무것도 복사하지 않고, 게임은 서버에서 바로 설치됩니다. 휴지통으로 이동과 정리는 이 컴퓨터의 폴더에만 적용됩니다.", bottleneck_source_v2: "원본 읽기", android_local_network_allow: "허용", android_local_network_body: "허용하기 전까지 Android가 앱이 PS5에 연결하지 못하게 막습니다.", @@ -3596,7 +3601,7 @@ sendpayload_probe_no_signature: "PS5Upload 서명이 없습니다 — 신뢰하 sendpayload_probe_ok: "페이로드 파일이 정상으로 보입니다.", sendpayload_probe_read_error: "이 파일을 읽을 수 없습니다({error}). 파일이 아직 있고 열 수 있는지 확인한 뒤 다시 선택하세요.", sendpayload_probe_too_small: "이 파일은 비어 있어 보낼 것이 없습니다.", -servers_subtitle: "NAS와 서버(SMB, FTP, FTPS, SFTP)입니다. 게임이나 파일을 찾아볼 때 어디서든 찾아보기 옆의 ▾에서 고를 수 있습니다.", +servers_subtitle: "NAS와 서버(SMB, FTP, FTPS, SFTP/WebDAV)입니다. 게임이나 파일을 찾아볼 때 어디서든 찾아보기 옆의 ▾에서 고를 수 있습니다.", settings_artwork_cache_explain_v2: "게임 표지는 엔진이 실행되는 곳(이 컴퓨터, 웹 버전은 서버)에 보관되어 매번 PS5에서 읽는 대신 즉시 표시됩니다. 자동으로 새로 고쳐지며, 본체에서 게임을 설치하거나 제거하면 해당 본체의 표지가 삭제됩니다.", settings_backup_downloaded: "{name} 다운로드됨", settings_backup_hint_v2: "테마, PS5 목록, 알림 기록, 활동 로그 및 기타 설정을 하나의 JSON 파일로 묶어 앱 저장소를 지운 뒤나 다른 기기에서 복원할 수 있습니다. 본체 페어링, 컬렉션, 저장된 서버 비밀번호, 백포트 라이브러리는 포함되지 않습니다.", diff --git a/client/src/i18n/locales/pl.ts b/client/src/i18n/locales/pl.ts index bab97bc0..18997777 100644 --- a/client/src/i18n/locales/pl.ts +++ b/client/src/i18n/locales/pl.ts @@ -2863,7 +2863,7 @@ helper_mismatch_go: "Załaduj pomocnika", "conn_edit_title": "Edytuj {name}", "conn_add_title": "Dodaj serwer", "conn_empty_title": "Brak serwerów", -"conn_empty_body": "Dodaj swój NAS, folder udostępniony na PC lub serwer FTP/SFTP i wybieraj gry prosto z niego.", +"conn_empty_body": "Dodaj swój NAS, folder udostępniony na PC lub serwer FTP/SFTP/WebDAV i wybieraj gry prosto z niego.", "conn_browse": "Przeglądaj", "conn_test": "Testuj", "conn_edit": "Edytuj", @@ -2871,6 +2871,11 @@ helper_mismatch_go: "Załaduj pomocnika", "conn_anonymous": "Anonimowo (bez użytkownika i hasła)", "conn_protocol": "Protokół", "conn_ftp_unencrypted": "Bez szyfrowania — używaj FTPS lub SFTP, jeśli serwer je obsługuje.", +conn_webdav_unencrypted: "Bez szyfrowania — użyj WebDAV (HTTPS), jeśli serwer to obsługuje.", +conn_webdav_root_hint: "Wpisz katalog główny WebDAV serwera w Folderze startowym: /remote.php/dav/files/UŻYTKOWNIK dla Nextcloud, /dav dla Alist, / dla rclone serve webdav. Logowanie to uwierzytelnianie Basic.", +conn_insecure_tls: "Akceptuj certyfikat z własnym podpisem", +conn_insecure_tls_hint: "Tylko dla NAS we własnej sieci z certyfikatem z własnym podpisem. Każdy między tym komputerem a serwerem mógłby wtedy odczytać połączenie.", +conn_no_signin: "Bez logowania (udział publiczny)", "conn_name": "Nazwa", "conn_host": "Adres serwera", "conn_port": "Port", @@ -2892,7 +2897,7 @@ helper_mismatch_go: "Załaduj pomocnika", "conn_accept_key": "Akceptuj", "conn_save": "Zapisz", "v5_home_servers": "Serwery", -"v5_home_servers_empty": "Wybieraj gry prosto ze swojego NAS-a lub folderu udostępnionego przez SMB, FTP lub SFTP.", +"v5_home_servers_empty": "Wybieraj gry prosto ze swojego NAS-a lub folderu udostępnionego przez SMB, FTP lub SFTP/WebDAV.", "v5_home_servers_connect": "Połącz NAS lub serwer", "fpkg.stage.copy": "Kopiowanie z serwera", "fpkg.stage.park": "Odłożenie zrzutu", @@ -3469,9 +3474,9 @@ upload_as_image_open_convert: "Otwórz Konwersję", upload_as_image_queued: "Obraz jest gotowy i czeka w kolejce wysyłania.", upload_as_image_title: "Albo wyślij jako jeden obraz gry", "collection.add_server_folder": "Dodaj folder na {name}", -"collection.no_servers_hint": "Gry na NAS? Zapisz go w Połączeniach (SMB, FTP lub SFTP), a jego foldery będzie można dodać tutaj bez montowania.", +"collection.no_servers_hint": "Gry na NAS? Zapisz go w Połączeniach (SMB, FTP lub SFTP/WebDAV), a jego foldery będzie można dodać tutaj bez montowania.", "collection.pick_server_folder": "Folder na {name} z twoimi grami", -"collection.server_folders_hint": "Folder na zapisanym serwerze (SMB, FTP, SFTP) jest czytany na miejscu: nic nie jest kopiowane na ten komputer, a jego gry instalują się prosto z serwera. Przenieś do Kosza i Uporządkuj działają tylko na folderach tego komputera.", +"collection.server_folders_hint": "Folder na zapisanym serwerze (SMB, FTP, SFTP/WebDAV) jest czytany na miejscu: nic nie jest kopiowane na ten komputer, a jego gry instalują się prosto z serwera. Przenieś do Kosza i Uporządkuj działają tylko na folderach tego komputera.", bottleneck_source_v2: "odczyt źródła", android_local_network_allow: "Zezwól", android_local_network_body: "Android blokuje aplikacji dostęp do PS5, dopóki na to nie zezwolisz.", @@ -3810,7 +3815,7 @@ sendpayload_probe_no_signature: "Nie znaleziono podpisu PS5Upload — używaj ty sendpayload_probe_ok: "Plik payloadu wygląda dobrze.", sendpayload_probe_read_error: "Nie można odczytać tego pliku ({error}). Sprawdź, czy nadal istnieje i da się go otworzyć, a potem wybierz go ponownie.", sendpayload_probe_too_small: "Ten plik jest pusty, więc nie ma czego wysłać.", -servers_subtitle: "Twoje NAS-y i serwery (SMB, FTP, FTPS, SFTP). Wszędzie, gdzie wyszukujesz grę lub plik, wybierz je za pomocą ▾ obok Przeglądaj.", +servers_subtitle: "Twoje NAS-y i serwery (SMB, FTP, FTPS, SFTP/WebDAV). Wszędzie, gdzie wyszukujesz grę lub plik, wybierz je za pomocą ▾ obok Przeglądaj.", settings_artwork_cache_explain_v2: "Okładki gier są przechowywane tam, gdzie działa silnik (na tym komputerze lub na serwerze w wersji webowej), więc ładują się natychmiast zamiast za każdym razem z PS5. Są odświeżane automatycznie i usuwane dla konsoli, gdy zainstalujesz lub odinstalujesz na niej grę.", settings_backup_downloaded: "Pobrano {name}", settings_backup_hint_v2: "Spakuj motyw, listę PS5, historię powiadomień, dziennik aktywności i inne preferencje do jednego pliku JSON, aby przywrócić je po wyczyszczeniu pamięci aplikacji lub na innym komputerze. Parowania konsol, Kolekcja, hasła zapisanych serwerów i biblioteki backportu nie są uwzględniane.", diff --git a/client/src/i18n/locales/pt-BR.ts b/client/src/i18n/locales/pt-BR.ts index fce173c6..ffb1f7d3 100644 --- a/client/src/i18n/locales/pt-BR.ts +++ b/client/src/i18n/locales/pt-BR.ts @@ -2624,7 +2624,7 @@ helper_mismatch_go: "Recarregar auxiliar", "conn_edit_title": "Editar {name}", "conn_add_title": "Adicionar um servidor", "conn_empty_title": "Nenhum servidor ainda", -"conn_empty_body": "Adicione seu NAS, uma pasta compartilhada em um PC ou um servidor FTP/SFTP e escolha jogos direto dele.", +"conn_empty_body": "Adicione seu NAS, uma pasta compartilhada em um PC ou um servidor FTP/SFTP/WebDAV e escolha jogos direto dele.", "conn_browse": "Navegar", "conn_test": "Testar", "conn_edit": "Editar", @@ -2632,6 +2632,11 @@ helper_mismatch_go: "Recarregar auxiliar", "conn_anonymous": "Anônimo (sem usuário ou senha)", "conn_protocol": "Protocolo", "conn_ftp_unencrypted": "Sem criptografia — use FTPS ou SFTP quando o servidor suportar.", +conn_webdav_unencrypted: "Sem criptografia: use WebDAV (HTTPS) se o servidor permitir.", +conn_webdav_root_hint: "Coloque a raiz WebDAV do servidor em Pasta inicial: /remote.php/dav/files/USUARIO no Nextcloud, /dav no Alist, / no rclone serve webdav. O login usa autenticação Basic.", +conn_insecure_tls: "Aceitar certificado autoassinado", +conn_insecure_tls_hint: "Só para um NAS na sua própria rede com certificado autoassinado. Qualquer um entre este computador e o servidor poderia ler a conexão.", +conn_no_signin: "Sem login (compartilhamento público)", "conn_name": "Nome", "conn_host": "Endereço do servidor", "conn_port": "Porta", @@ -2653,7 +2658,7 @@ helper_mismatch_go: "Recarregar auxiliar", "conn_accept_key": "Aceitar", "conn_save": "Salvar", "v5_home_servers": "Servidores", -"v5_home_servers_empty": "Escolha jogos direto do seu NAS ou de uma pasta compartilhada, por SMB, FTP ou SFTP.", +"v5_home_servers_empty": "Escolha jogos direto do seu NAS ou de uma pasta compartilhada, por SMB, FTP ou SFTP/WebDAV.", "v5_home_servers_connect": "Conectar um NAS ou servidor", "fpkg.stage.copy": "Copiar do servidor", "fpkg.stage.park": "Separar o dump", @@ -3230,9 +3235,9 @@ upload_as_image_open_convert: "Abrir Converter", upload_as_image_queued: "A imagem foi criada e está na fila de envio.", upload_as_image_title: "Ou envie como uma única imagem de jogo", "collection.add_server_folder": "Adicionar uma pasta de {name}", -"collection.no_servers_hint": "Jogos em um NAS? Salve-o em Conexões (SMB, FTP ou SFTP) e as pastas dele podem ser adicionadas aqui sem montar.", +"collection.no_servers_hint": "Jogos em um NAS? Salve-o em Conexões (SMB, FTP ou SFTP/WebDAV) e as pastas dele podem ser adicionadas aqui sem montar.", "collection.pick_server_folder": "A pasta de {name} que contém seus jogos", -"collection.server_folders_hint": "Uma pasta num servidor salvo (SMB, FTP, SFTP) é lida onde está: nada é copiado para este computador e os jogos dela instalam direto do servidor. Mover para a Lixeira e Organizar valem só para pastas deste computador.", +"collection.server_folders_hint": "Uma pasta num servidor salvo (SMB, FTP, SFTP/WebDAV) é lida onde está: nada é copiado para este computador e os jogos dela instalam direto do servidor. Mover para a Lixeira e Organizar valem só para pastas deste computador.", bottleneck_source_v2: "a leitura da origem", android_local_network_allow: "Permitir", android_local_network_body: "O Android impede que o app alcance seu PS5 até você permitir.", @@ -3594,7 +3599,7 @@ sendpayload_probe_no_signature: "Nenhuma assinatura do PS5Upload encontrada — sendpayload_probe_ok: "O arquivo de payload parece OK.", sendpayload_probe_read_error: "Não foi possível ler este arquivo ({error}). Verifique se ele ainda existe e pode ser aberto e selecione-o de novo.", sendpayload_probe_too_small: "Este arquivo está vazio, então não há nada para enviar.", -servers_subtitle: "Seus NAS e servidores (SMB, FTP, FTPS, SFTP). Em qualquer lugar onde você procurar um jogo ou arquivo, escolha-os com o ▾ ao lado de Navegar.", +servers_subtitle: "Seus NAS e servidores (SMB, FTP, FTPS, SFTP/WebDAV). Em qualquer lugar onde você procurar um jogo ou arquivo, escolha-os com o ▾ ao lado de Navegar.", settings_artwork_cache_explain_v2: "As capas dos jogos ficam guardadas onde o motor roda (este computador, ou o servidor na versão web) para carregarem na hora em vez de serem lidas do seu PS5 toda vez. Elas são atualizadas automaticamente e removidas para um console quando você instala ou desinstala um jogo nele.", settings_backup_downloaded: "{name} baixado", settings_backup_hint_v2: "Reunir tema, lista de PS5, histórico de notificações, registro de atividade e outras preferências em um arquivo JSON, para restaurar depois de limpar o armazenamento do app ou em outra máquina. Pareamentos de consoles, a Coleção, senhas de servidores salvos e bibliotecas de backport não estão incluídos.", diff --git a/client/src/i18n/locales/ru.ts b/client/src/i18n/locales/ru.ts index 7ee58d71..a109254a 100644 --- a/client/src/i18n/locales/ru.ts +++ b/client/src/i18n/locales/ru.ts @@ -2557,7 +2557,7 @@ helper_mismatch_go: "Перезагрузить помощник", "conn_edit_title": "Изменить {name}", "conn_add_title": "Добавить сервер", "conn_empty_title": "Серверов пока нет", -"conn_empty_body": "Добавьте свой NAS, общую папку на ПК или сервер FTP/SFTP и выбирайте игры прямо оттуда.", +"conn_empty_body": "Добавьте свой NAS, общую папку на ПК или сервер FTP/SFTP/WebDAV и выбирайте игры прямо оттуда.", "conn_browse": "Обзор", "conn_test": "Проверить", "conn_edit": "Изменить", @@ -2565,6 +2565,11 @@ helper_mismatch_go: "Перезагрузить помощник", "conn_anonymous": "Анонимно (без пользователя и пароля)", "conn_protocol": "Протокол", "conn_ftp_unencrypted": "Без шифрования — используйте FTPS или SFTP, если сервер их поддерживает.", +conn_webdav_unencrypted: "Без шифрования — используйте WebDAV (HTTPS), если сервер поддерживает.", +conn_webdav_root_hint: "Укажите корень WebDAV сервера в «Начальной папке»: /remote.php/dav/files/ПОЛЬЗОВАТЕЛЬ для Nextcloud, /dav для Alist, / для rclone serve webdav. Вход — Basic-аутентификация.", +conn_insecure_tls: "Принимать самоподписанный сертификат", +conn_insecure_tls_hint: "Только для NAS в вашей сети с самоподписанным сертификатом. Любой между этим компьютером и сервером сможет читать соединение.", +conn_no_signin: "Без входа (публичный ресурс)", "conn_name": "Название", "conn_host": "Адрес сервера", "conn_port": "Порт", @@ -2586,7 +2591,7 @@ helper_mismatch_go: "Перезагрузить помощник", "conn_accept_key": "Принять", "conn_save": "Сохранить", "v5_home_servers": "Серверы", -"v5_home_servers_empty": "Выбирайте игры прямо со своего NAS или общей папки по SMB, FTP или SFTP.", +"v5_home_servers_empty": "Выбирайте игры прямо со своего NAS или общей папки по SMB, FTP или SFTP/WebDAV.", "v5_home_servers_connect": "Подключить NAS или сервер", "fpkg.stage.copy": "Копирование с сервера", "fpkg.stage.park": "Убрать дамп в сторону", @@ -3163,9 +3168,9 @@ upload_as_image_open_convert: "Открыть «Конвертацию»", upload_as_image_queued: "Образ создан и стоит в очереди загрузки.", upload_as_image_title: "Или отправить одним образом игры", "collection.add_server_folder": "Добавить папку на {name}", -"collection.no_servers_hint": "Игры на NAS? Сохраните его в «Подключениях» (SMB, FTP или SFTP), и его папки можно будет добавить сюда без монтирования.", +"collection.no_servers_hint": "Игры на NAS? Сохраните его в «Подключениях» (SMB, FTP или SFTP/WebDAV), и его папки можно будет добавить сюда без монтирования.", "collection.pick_server_folder": "Папка на {name} с вашими играми", -"collection.server_folders_hint": "Папка на сохранённом сервере (SMB, FTP, SFTP) читается на месте: на этот компьютер ничего не копируется, а её игры устанавливаются прямо с сервера. «В корзину» и «Упорядочить» работают только с папками этого компьютера.", +"collection.server_folders_hint": "Папка на сохранённом сервере (SMB, FTP, SFTP/WebDAV) читается на месте: на этот компьютер ничего не копируется, а её игры устанавливаются прямо с сервера. «В корзину» и «Упорядочить» работают только с папками этого компьютера.", bottleneck_source_v2: "чтение источника", android_local_network_allow: "Разрешить", android_local_network_body: "Android не даёт приложению связаться с PS5, пока вы это не разрешите.", @@ -3504,7 +3509,7 @@ sendpayload_probe_no_signature: "Подпись PS5Upload не найдена sendpayload_probe_ok: "Файл payload в порядке.", sendpayload_probe_read_error: "Не удалось прочитать этот файл ({error}). Проверьте, что он существует и открывается, затем выберите его снова.", sendpayload_probe_too_small: "Этот файл пуст, отправлять нечего.", -servers_subtitle: "Ваши NAS и серверы (SMB, FTP, FTPS, SFTP). Везде, где вы ищете игру или файл, выбирайте из них с помощью ▾ рядом с «Обзор».", +servers_subtitle: "Ваши NAS и серверы (SMB, FTP, FTPS, SFTP/WebDAV). Везде, где вы ищете игру или файл, выбирайте из них с помощью ▾ рядом с «Обзор».", settings_artwork_cache_explain_v2: "Обложки игр хранятся там, где работает движок (на этом компьютере или на сервере для веб-версии), поэтому загружаются мгновенно, а не считываются с PS5 каждый раз. Они обновляются автоматически и удаляются для консоли, когда вы устанавливаете или удаляете на ней игру.", settings_backup_downloaded: "Скачано {name}", settings_backup_hint_v2: "Собрать тему, список PS5, историю уведомлений, журнал активности и другие настройки в один JSON-файл, чтобы восстановить их после очистки хранилища приложения или на другом компьютере. Сопряжения консолей, Коллекция, пароли сохранённых серверов и библиотеки бэкпорта не включаются.", diff --git a/client/src/i18n/locales/th.ts b/client/src/i18n/locales/th.ts index 4ad8100c..c7396eed 100644 --- a/client/src/i18n/locales/th.ts +++ b/client/src/i18n/locales/th.ts @@ -2616,7 +2616,7 @@ helper_mismatch_go: "โหลดตัวช่วยใหม่", "conn_edit_title": "แก้ไข {name}", "conn_add_title": "เพิ่มเซิร์ฟเวอร์", "conn_empty_title": "ยังไม่มีเซิร์ฟเวอร์", -"conn_empty_body": "เพิ่ม NAS โฟลเดอร์แชร์บน PC หรือเซิร์ฟเวอร์ FTP/SFTP แล้วเลือกเกมจากที่นั่นได้โดยตรง", +"conn_empty_body": "เพิ่ม NAS โฟลเดอร์แชร์บน PC หรือเซิร์ฟเวอร์ FTP/SFTP/WebDAV แล้วเลือกเกมจากที่นั่นได้โดยตรง", "conn_browse": "เรียกดู", "conn_test": "ทดสอบ", "conn_edit": "แก้ไข", @@ -2624,6 +2624,11 @@ helper_mismatch_go: "โหลดตัวช่วยใหม่", "conn_anonymous": "ไม่ระบุตัวตน (ไม่มีผู้ใช้หรือรหัสผ่าน)", "conn_protocol": "โปรโตคอล", "conn_ftp_unencrypted": "ไม่เข้ารหัส — ใช้ FTPS หรือ SFTP หากเซิร์ฟเวอร์รองรับ", +conn_webdav_unencrypted: "ไม่เข้ารหัส — ใช้ WebDAV (HTTPS) หากเซิร์ฟเวอร์รองรับ", +conn_webdav_root_hint: "ใส่รากของ WebDAV บนเซิร์ฟเวอร์ในโฟลเดอร์เริ่มต้น: /remote.php/dav/files/USER สำหรับ Nextcloud, /dav สำหรับ Alist, / สำหรับ rclone serve webdav การลงชื่อเข้าใช้เป็นแบบ Basic", +conn_insecure_tls: "ยอมรับใบรับรองแบบลงนามเอง", +conn_insecure_tls_hint: "ใช้เฉพาะกับ NAS ในเครือข่ายของคุณเองที่ใบรับรองลงนามเอง ใครก็ตามระหว่างคอมพิวเตอร์นี้กับเซิร์ฟเวอร์อาจอ่านการเชื่อมต่อได้", +conn_no_signin: "ไม่ต้องลงชื่อเข้าใช้ (แชร์สาธารณะ)", "conn_name": "ชื่อ", "conn_host": "ที่อยู่เซิร์ฟเวอร์", "conn_port": "พอร์ต", @@ -2645,7 +2650,7 @@ helper_mismatch_go: "โหลดตัวช่วยใหม่", "conn_accept_key": "ยอมรับ", "conn_save": "บันทึก", "v5_home_servers": "เซิร์ฟเวอร์", -"v5_home_servers_empty": "เลือกเกมจาก NAS หรือโฟลเดอร์แชร์ได้โดยตรง ผ่าน SMB, FTP หรือ SFTP", +"v5_home_servers_empty": "เลือกเกมจาก NAS หรือโฟลเดอร์แชร์ได้โดยตรง ผ่าน SMB, FTP หรือ SFTP/WebDAV", "v5_home_servers_connect": "เชื่อมต่อ NAS หรือเซิร์ฟเวอร์", "fpkg.stage.copy": "คัดลอกจากเซิร์ฟเวอร์", "fpkg.stage.park": "ย้ายดัมพ์ออกไปก่อน", @@ -3222,9 +3227,9 @@ upload_as_image_open_convert: "เปิดการแปลง", upload_as_image_queued: "สร้างอิมเมจแล้วและอยู่ในคิวอัปโหลด", upload_as_image_title: "หรือส่งเป็นอิมเมจเกมเดียว", "collection.add_server_folder": "เพิ่มโฟลเดอร์บน {name}", -"collection.no_servers_hint": "มีเกมใน NAS? บันทึกไว้ในการเชื่อมต่อ (SMB, FTP หรือ SFTP) แล้วเพิ่มโฟลเดอร์ของมันที่นี่ได้โดยไม่ต้องเมานต์", +"collection.no_servers_hint": "มีเกมใน NAS? บันทึกไว้ในการเชื่อมต่อ (SMB, FTP หรือ SFTP/WebDAV) แล้วเพิ่มโฟลเดอร์ของมันที่นี่ได้โดยไม่ต้องเมานต์", "collection.pick_server_folder": "โฟลเดอร์บน {name} ที่เก็บเกมของคุณ", -"collection.server_folders_hint": "โฟลเดอร์บนเซิร์ฟเวอร์ที่บันทึกไว้ (SMB, FTP, SFTP) อ่านจากที่เดิม: ไม่มีการคัดลอกมายังคอมพิวเตอร์นี้ และเกมติดตั้งจากเซิร์ฟเวอร์โดยตรง ย้ายไปถังขยะและการจัดใช้ได้กับโฟลเดอร์บนคอมพิวเตอร์นี้เท่านั้น", +"collection.server_folders_hint": "โฟลเดอร์บนเซิร์ฟเวอร์ที่บันทึกไว้ (SMB, FTP, SFTP/WebDAV) อ่านจากที่เดิม: ไม่มีการคัดลอกมายังคอมพิวเตอร์นี้ และเกมติดตั้งจากเซิร์ฟเวอร์โดยตรง ย้ายไปถังขยะและการจัดใช้ได้กับโฟลเดอร์บนคอมพิวเตอร์นี้เท่านั้น", bottleneck_source_v2: "การอ่านต้นทาง", android_local_network_allow: "อนุญาต", android_local_network_body: "Android จะบล็อกไม่ให้แอปเชื่อมต่อ PS5 จนกว่าคุณจะอนุญาต", @@ -3586,7 +3591,7 @@ sendpayload_probe_no_signature: "ไม่พบลายเซ็น PS5Upload sendpayload_probe_ok: "ไฟล์เพย์โหลดดูเรียบร้อย", sendpayload_probe_read_error: "อ่านไฟล์นี้ไม่ได้ ({error}) ตรวจสอบว่าไฟล์ยังอยู่และเปิดได้ แล้วเลือกอีกครั้ง", sendpayload_probe_too_small: "ไฟล์นี้ว่างเปล่า จึงไม่มีอะไรให้ส่ง", -servers_subtitle: "NAS และเซิร์ฟเวอร์ของคุณ (SMB, FTP, FTPS, SFTP) ทุกที่ที่คุณเรียกดูเกมหรือไฟล์ เลือกจากรายการเหล่านี้ได้ด้วย ▾ ข้างปุ่มเรียกดู", +servers_subtitle: "NAS และเซิร์ฟเวอร์ของคุณ (SMB, FTP, FTPS, SFTP/WebDAV) ทุกที่ที่คุณเรียกดูเกมหรือไฟล์ เลือกจากรายการเหล่านี้ได้ด้วย ▾ ข้างปุ่มเรียกดู", settings_artwork_cache_explain_v2: "ภาพปกเกมถูกเก็บไว้ที่ที่เอนจินทำงาน (คอมพิวเตอร์เครื่องนี้ หรือเซิร์ฟเวอร์สำหรับเวอร์ชันเว็บ) เพื่อให้โหลดได้ทันทีแทนการอ่านจาก PS5 ทุกครั้ง ภาพจะรีเฟรชอัตโนมัติ และถูกลบสำหรับเครื่องนั้นเมื่อคุณติดตั้งหรือถอนการติดตั้งเกมบนเครื่องนั้น", settings_backup_downloaded: "ดาวน์โหลด {name} แล้ว", settings_backup_hint_v2: "รวมธีม รายชื่อ PS5 ประวัติการแจ้งเตือน บันทึกกิจกรรม และค่ากำหนดอื่นๆ ไว้ในไฟล์ JSON เดียว เพื่อกู้คืนหลังล้างพื้นที่จัดเก็บของแอปหรือบนเครื่องอื่น ไม่รวมการจับคู่เครื่อง คอลเลกชัน รหัสผ่านของเซิร์ฟเวอร์ที่บันทึก และไลบรารีแบ็กพอร์ต", diff --git a/client/src/i18n/locales/tr.ts b/client/src/i18n/locales/tr.ts index ef0a33c8..399fd479 100644 --- a/client/src/i18n/locales/tr.ts +++ b/client/src/i18n/locales/tr.ts @@ -2614,7 +2614,7 @@ helper_mismatch_go: "Yardımcıyı yükle", "conn_edit_title": "{name} düzenle", "conn_add_title": "Sunucu ekle", "conn_empty_title": "Henüz sunucu yok", -"conn_empty_body": "NAS'ını, bir PC'deki paylaşılan klasörü ya da bir FTP/SFTP sunucusunu ekle ve oyunları doğrudan oradan seç.", +"conn_empty_body": "NAS'ını, bir PC'deki paylaşılan klasörü ya da bir FTP/SFTP/WebDAV sunucusunu ekle ve oyunları doğrudan oradan seç.", "conn_browse": "Gözat", "conn_test": "Dene", "conn_edit": "Düzenle", @@ -2622,6 +2622,11 @@ helper_mismatch_go: "Yardımcıyı yükle", "conn_anonymous": "Anonim (kullanıcı ya da parola yok)", "conn_protocol": "Protokol", "conn_ftp_unencrypted": "Şifrelenmemiş — sunucu destekliyorsa FTPS ya da SFTP kullan.", +conn_webdav_unencrypted: "Şifrelenmemiş — sunucu destekliyorsa WebDAV (HTTPS) kullanın.", +conn_webdav_root_hint: "Sunucunun WebDAV kökünü Başlangıç klasörüne yazın: Nextcloud için /remote.php/dav/files/KULLANICI, Alist için /dav, rclone serve webdav için /. Oturum açma Basic kimlik doğrulamasıdır.", +conn_insecure_tls: "Kendinden imzalı sertifikayı kabul et", +conn_insecure_tls_hint: "Yalnızca kendi ağınızdaki, sertifikası kendinden imzalı bir NAS için. Bu bilgisayar ile sunucu arasındaki herkes bağlantıyı okuyabilir.", +conn_no_signin: "Oturum açmadan (herkese açık paylaşım)", "conn_name": "Ad", "conn_host": "Sunucu adresi", "conn_port": "Bağlantı noktası", @@ -2643,7 +2648,7 @@ helper_mismatch_go: "Yardımcıyı yükle", "conn_accept_key": "Kabul et", "conn_save": "Kaydet", "v5_home_servers": "Sunucular", -"v5_home_servers_empty": "Oyunları doğrudan NAS'ından ya da paylaşılan bir klasörden SMB, FTP veya SFTP ile seç.", +"v5_home_servers_empty": "Oyunları doğrudan NAS'ından ya da paylaşılan bir klasörden SMB, FTP veya SFTP/WebDAV ile seç.", "v5_home_servers_connect": "Bir NAS ya da sunucu bağla", "fpkg.stage.copy": "Sunucudan kopyala", "fpkg.stage.park": "Dökümü kenara al", @@ -3220,9 +3225,9 @@ upload_as_image_open_convert: "Dönüştür'ü aç", upload_as_image_queued: "İmaj oluşturuldu ve yükleme kuyruğunda.", upload_as_image_title: "Ya da tek bir oyun imajı olarak gönder", "collection.add_server_folder": "{name} üzerinde klasör ekle", -"collection.no_servers_hint": "Oyunlar bir NAS'ta mı? Bağlantılar'a kaydet (SMB, FTP veya SFTP); klasörleri bağlamadan buraya eklenebilir.", +"collection.no_servers_hint": "Oyunlar bir NAS'ta mı? Bağlantılar'a kaydet (SMB, FTP veya SFTP/WebDAV); klasörleri bağlamadan buraya eklenebilir.", "collection.pick_server_folder": "{name} üzerindeki oyunlarının bulunduğu klasör", -"collection.server_folders_hint": "Kayıtlı bir sunucudaki (SMB, FTP, SFTP) klasör yerinde okunur: bu bilgisayara hiçbir şey kopyalanmaz ve oyunları doğrudan sunucudan yüklenir. Çöp Sepeti'ne taşı ve Düzenle yalnızca bu bilgisayarın klasörlerinde çalışır.", +"collection.server_folders_hint": "Kayıtlı bir sunucudaki (SMB, FTP, SFTP/WebDAV) klasör yerinde okunur: bu bilgisayara hiçbir şey kopyalanmaz ve oyunları doğrudan sunucudan yüklenir. Çöp Sepeti'ne taşı ve Düzenle yalnızca bu bilgisayarın klasörlerinde çalışır.", bottleneck_source_v2: "kaynağın okunması", android_local_network_allow: "İzin ver", android_local_network_body: "İzin verene kadar Android, uygulamanın PS5'ine ulaşmasını engeller.", @@ -3584,7 +3589,7 @@ sendpayload_probe_no_signature: "PS5Upload imzası bulunamadı — yalnızca bu sendpayload_probe_ok: "Yük dosyası sorunsuz görünüyor.", sendpayload_probe_read_error: "Bu dosya okunamadı ({error}). Hâlâ var olduğunu ve açılabildiğini kontrol edin, sonra yeniden seçin.", sendpayload_probe_too_small: "Bu dosya boş, gönderilecek bir şey yok.", -servers_subtitle: "NAS'larınız ve sunucularınız (SMB, FTP, FTPS, SFTP). Bir oyun veya dosya için göz attığınız her yerde, Gözat'ın yanındaki ▾ ile bunlardan seçin.", +servers_subtitle: "NAS'larınız ve sunucularınız (SMB, FTP, FTPS, SFTP/WebDAV). Bir oyun veya dosya için göz attığınız her yerde, Gözat'ın yanındaki ▾ ile bunlardan seçin.", settings_artwork_cache_explain_v2: "Oyun kapakları motorun çalıştığı yerde (bu bilgisayarda veya web sürümü için sunucuda) tutulur, böylece her seferinde PS5'inizden okunmak yerine anında yüklenir. Otomatik olarak yenilenir ve bir konsolda oyun kurduğunuzda veya kaldırdığınızda o konsol için silinir.", settings_backup_downloaded: "{name} indirildi", settings_backup_hint_v2: "Temayı, PS5 listesini, bildirim geçmişini, etkinlik kaydını ve diğer tercihleri tek bir JSON dosyasında toplayın; uygulamanın depolamasını sildikten sonra veya başka bir makinede geri yüklemek için. Konsol eşleştirmeleri, Koleksiyon, kayıtlı sunucuların parolaları ve geri taşıma kütüphaneleri dahil değildir.", diff --git a/client/src/i18n/locales/vi.ts b/client/src/i18n/locales/vi.ts index 2b9a2ace..cb9039e6 100644 --- a/client/src/i18n/locales/vi.ts +++ b/client/src/i18n/locales/vi.ts @@ -2609,7 +2609,7 @@ helper_mismatch_go: "Tải lại trợ giúp", "conn_edit_title": "Sửa {name}", "conn_add_title": "Thêm máy chủ", "conn_empty_title": "Chưa có máy chủ", -"conn_empty_body": "Thêm NAS, thư mục chia sẻ trên PC hoặc máy chủ FTP/SFTP, rồi chọn trò chơi ngay từ đó.", +"conn_empty_body": "Thêm NAS, thư mục chia sẻ trên PC hoặc máy chủ FTP/SFTP/WebDAV, rồi chọn trò chơi ngay từ đó.", "conn_browse": "Duyệt", "conn_test": "Kiểm tra", "conn_edit": "Sửa", @@ -2617,6 +2617,11 @@ helper_mismatch_go: "Tải lại trợ giúp", "conn_anonymous": "Ẩn danh (không người dùng hay mật khẩu)", "conn_protocol": "Giao thức", "conn_ftp_unencrypted": "Không mã hóa — dùng FTPS hoặc SFTP nếu máy chủ hỗ trợ.", +conn_webdav_unencrypted: "Không mã hóa — hãy dùng WebDAV (HTTPS) nếu máy chủ hỗ trợ.", +conn_webdav_root_hint: "Nhập thư mục gốc WebDAV của máy chủ vào Thư mục bắt đầu: /remote.php/dav/files/USER với Nextcloud, /dav với Alist, / với rclone serve webdav. Đăng nhập dùng xác thực Basic.", +conn_insecure_tls: "Chấp nhận chứng chỉ tự ký", +conn_insecure_tls_hint: "Chỉ dành cho NAS trong mạng của bạn có chứng chỉ tự ký. Bất kỳ ai giữa máy tính này và máy chủ đều có thể đọc kết nối.", +conn_no_signin: "Không đăng nhập (chia sẻ công khai)", "conn_name": "Tên", "conn_host": "Địa chỉ máy chủ", "conn_port": "Cổng", @@ -2638,7 +2643,7 @@ helper_mismatch_go: "Tải lại trợ giúp", "conn_accept_key": "Chấp nhận", "conn_save": "Lưu", "v5_home_servers": "Máy chủ", -"v5_home_servers_empty": "Chọn trò chơi ngay từ NAS hoặc thư mục chia sẻ của bạn, qua SMB, FTP hoặc SFTP.", +"v5_home_servers_empty": "Chọn trò chơi ngay từ NAS hoặc thư mục chia sẻ của bạn, qua SMB, FTP hoặc SFTP/WebDAV.", "v5_home_servers_connect": "Kết nối NAS hoặc máy chủ", "fpkg.stage.copy": "Sao chép từ máy chủ", "fpkg.stage.park": "Tạm cất bản dump", @@ -3215,9 +3220,9 @@ upload_as_image_open_convert: "Mở Chuyển đổi", upload_as_image_queued: "Tệp ảnh đã tạo xong và ở trong hàng chờ tải lên.", upload_as_image_title: "Hoặc gửi dưới dạng một tệp ảnh trò chơi", "collection.add_server_folder": "Thêm thư mục trên {name}", -"collection.no_servers_hint": "Trò chơi trên NAS? Lưu nó trong Kết nối (SMB, FTP hoặc SFTP), rồi có thể thêm thư mục của nó vào đây mà không cần gắn.", +"collection.no_servers_hint": "Trò chơi trên NAS? Lưu nó trong Kết nối (SMB, FTP hoặc SFTP/WebDAV), rồi có thể thêm thư mục của nó vào đây mà không cần gắn.", "collection.pick_server_folder": "Thư mục trên {name} chứa trò chơi của bạn", -"collection.server_folders_hint": "Thư mục trên máy chủ đã lưu (SMB, FTP, SFTP) được đọc tại chỗ: không có gì được sao chép về máy tính này, và trò chơi được cài thẳng từ máy chủ. Chuyển vào Thùng rác và Sắp xếp chỉ áp dụng cho thư mục trên máy tính này.", +"collection.server_folders_hint": "Thư mục trên máy chủ đã lưu (SMB, FTP, SFTP/WebDAV) được đọc tại chỗ: không có gì được sao chép về máy tính này, và trò chơi được cài thẳng từ máy chủ. Chuyển vào Thùng rác và Sắp xếp chỉ áp dụng cho thư mục trên máy tính này.", bottleneck_source_v2: "đọc nguồn", android_local_network_allow: "Cho phép", android_local_network_body: "Android chặn ứng dụng kết nối tới PS5 cho tới khi bạn cho phép.", @@ -3579,7 +3584,7 @@ sendpayload_probe_no_signature: "Không tìm thấy chữ ký PS5Upload — ch sendpayload_probe_ok: "Tệp payload có vẻ ổn.", sendpayload_probe_read_error: "Không đọc được tệp này ({error}). Kiểm tra xem tệp còn tồn tại và mở được không, rồi chọn lại.", sendpayload_probe_too_small: "Tệp này trống nên không có gì để gửi.", -servers_subtitle: "NAS và máy chủ của bạn (SMB, FTP, FTPS, SFTP). Ở bất cứ đâu bạn duyệt tìm trò chơi hay tệp, hãy chọn từ chúng bằng nút ▾ cạnh Duyệt.", +servers_subtitle: "NAS và máy chủ của bạn (SMB, FTP, FTPS, SFTP/WebDAV). Ở bất cứ đâu bạn duyệt tìm trò chơi hay tệp, hãy chọn từ chúng bằng nút ▾ cạnh Duyệt.", settings_artwork_cache_explain_v2: "Ảnh bìa trò chơi được lưu ở nơi engine chạy (máy tính này, hoặc máy chủ đối với bản web) để tải tức thì thay vì đọc từ PS5 mỗi lần. Chúng được làm mới tự động, và bị xóa cho một máy khi bạn cài hoặc gỡ một trò chơi trên máy đó.", settings_backup_downloaded: "Đã tải xuống {name}", settings_backup_hint_v2: "Gom giao diện, danh sách PS5, lịch sử thông báo, nhật ký hoạt động và các tùy chọn khác vào một tệp JSON, để khôi phục sau khi xóa bộ nhớ ứng dụng hoặc trên máy khác. Không bao gồm ghép đôi máy, Bộ sưu tập, mật khẩu máy chủ đã lưu và thư viện backport.", diff --git a/client/src/i18n/locales/zh-CN.ts b/client/src/i18n/locales/zh-CN.ts index b7225400..ebe01de5 100644 --- a/client/src/i18n/locales/zh-CN.ts +++ b/client/src/i18n/locales/zh-CN.ts @@ -2563,7 +2563,7 @@ helper_mismatch_go: "重新加载助手", "conn_edit_title": "编辑 {name}", "conn_add_title": "添加服务器", "conn_empty_title": "还没有服务器", -"conn_empty_body": "添加你的 NAS、电脑上的共享文件夹或 FTP/SFTP 服务器,即可直接从中选择游戏。", +"conn_empty_body": "添加你的 NAS、电脑上的共享文件夹或 FTP/SFTP/WebDAV 服务器,即可直接从中选择游戏。", "conn_browse": "浏览", "conn_test": "测试", "conn_edit": "编辑", @@ -2571,6 +2571,11 @@ helper_mismatch_go: "重新加载助手", "conn_anonymous": "匿名(无用户名和密码)", "conn_protocol": "协议", "conn_ftp_unencrypted": "未加密——服务器支持的话请使用 FTPS 或 SFTP。", +conn_webdav_unencrypted: "未加密 — 服务器支持时请使用 WebDAV (HTTPS)。", +conn_webdav_root_hint: "把服务器的 WebDAV 根目录填到“起始文件夹”:Nextcloud 为 /remote.php/dav/files/用户名,Alist 为 /dav,rclone serve webdav 为 /。登录使用 Basic 认证。", +conn_insecure_tls: "接受自签名证书", +conn_insecure_tls_hint: "仅用于自己网络中使用自签名证书的 NAS。此时本机与服务器之间的任何人都可能读取连接内容。", +conn_no_signin: "不登录(公开共享)", "conn_name": "名称", "conn_host": "服务器地址", "conn_port": "端口", @@ -2592,7 +2597,7 @@ helper_mismatch_go: "重新加载助手", "conn_accept_key": "接受", "conn_save": "保存", "v5_home_servers": "服务器", -"v5_home_servers_empty": "通过 SMB、FTP 或 SFTP 直接从你的 NAS 或共享文件夹选择游戏。", +"v5_home_servers_empty": "通过 SMB、FTP 或 SFTP/WebDAV 直接从你的 NAS 或共享文件夹选择游戏。", "v5_home_servers_connect": "连接 NAS 或服务器", "fpkg.stage.copy": "从服务器复制", "fpkg.stage.park": "暂存转储", @@ -3169,9 +3174,9 @@ upload_as_image_open_convert: "打开转换", upload_as_image_queued: "镜像已制作完成并在上传队列中。", upload_as_image_title: "或作为一个游戏镜像发送", "collection.add_server_folder": "添加 {name} 上的文件夹", -"collection.no_servers_hint": "游戏在 NAS 上?在“连接”中保存它(SMB、FTP 或 SFTP),就能不挂载直接在这里添加它的文件夹。", +"collection.no_servers_hint": "游戏在 NAS 上?在“连接”中保存它(SMB、FTP 或 SFTP/WebDAV),就能不挂载直接在这里添加它的文件夹。", "collection.pick_server_folder": "{name} 上存放游戏的文件夹", -"collection.server_folders_hint": "已保存服务器(SMB、FTP、SFTP)上的文件夹直接就地读取:不会复制到这台电脑,其中的游戏直接从服务器安装。“移到废纸篓”和“整理”只适用于这台电脑上的文件夹。", +"collection.server_folders_hint": "已保存服务器(SMB、FTP、SFTP/WebDAV)上的文件夹直接就地读取:不会复制到这台电脑,其中的游戏直接从服务器安装。“移到废纸篓”和“整理”只适用于这台电脑上的文件夹。", bottleneck_source_v2: "读取源文件", android_local_network_allow: "允许", android_local_network_body: "在你允许之前,Android 会阻止此应用连接你的 PS5。", @@ -3533,7 +3538,7 @@ sendpayload_probe_no_signature: "未找到 PS5Upload 签名——仅在你信任 sendpayload_probe_ok: "Payload 文件看起来没问题。", sendpayload_probe_read_error: "无法读取此文件({error})。请确认它仍然存在且可以打开,然后重新选择。", sendpayload_probe_too_small: "此文件为空,没有可发送的内容。", -servers_subtitle: "你的 NAS 和服务器(SMB、FTP、FTPS、SFTP)。在任何浏览游戏或文件的地方,都可以通过“浏览”旁的 ▾ 从中选择。", +servers_subtitle: "你的 NAS 和服务器(SMB、FTP、FTPS、SFTP/WebDAV)。在任何浏览游戏或文件的地方,都可以通过“浏览”旁的 ▾ 从中选择。", settings_artwork_cache_explain_v2: "游戏封面保存在引擎运行的地方(这台电脑,或网页版的服务器),因此可以立即加载,而不必每次都从 PS5 读取。它们会自动刷新,当你在某台主机上安装或卸载游戏时,该主机的封面会被移除。", settings_backup_downloaded: "已下载 {name}", settings_backup_hint_v2: "将主题、PS5 列表、通知历史、活动日志和其他偏好设置打包为一个 JSON 文件,以便在清除应用存储后或在另一台机器上恢复。不包括主机配对、收藏、已保存服务器的密码和降级移植库。", diff --git a/client/src/i18n/locales/zh-TW.ts b/client/src/i18n/locales/zh-TW.ts index 4b9357f9..fe5a2244 100644 --- a/client/src/i18n/locales/zh-TW.ts +++ b/client/src/i18n/locales/zh-TW.ts @@ -2557,7 +2557,7 @@ helper_mismatch_go: "重新載入小幫手", "conn_edit_title": "編輯 {name}", "conn_add_title": "新增伺服器", "conn_empty_title": "還沒有伺服器", -"conn_empty_body": "新增您的 NAS、電腦上的共用資料夾或 FTP/SFTP 伺服器,即可直接從中選擇遊戲。", +"conn_empty_body": "新增您的 NAS、電腦上的共用資料夾或 FTP/SFTP/WebDAV 伺服器,即可直接從中選擇遊戲。", "conn_browse": "瀏覽", "conn_test": "測試", "conn_edit": "編輯", @@ -2565,6 +2565,11 @@ helper_mismatch_go: "重新載入小幫手", "conn_anonymous": "匿名(無使用者名稱與密碼)", "conn_protocol": "通訊協定", "conn_ftp_unencrypted": "未加密——伺服器支援的話請使用 FTPS 或 SFTP。", +conn_webdav_unencrypted: "未加密 — 伺服器支援時請使用 WebDAV (HTTPS)。", +conn_webdav_root_hint: "把伺服器的 WebDAV 根目錄填到「起始資料夾」:Nextcloud 為 /remote.php/dav/files/使用者,Alist 為 /dav,rclone serve webdav 為 /。登入使用 Basic 驗證。", +conn_insecure_tls: "接受自我簽署憑證", +conn_insecure_tls_hint: "僅用於自己網路中使用自我簽署憑證的 NAS。此時本機與伺服器之間的任何人都可能讀取連線內容。", +conn_no_signin: "不登入(公開共享)", "conn_name": "名稱", "conn_host": "伺服器位址", "conn_port": "連接埠", @@ -2586,7 +2591,7 @@ helper_mismatch_go: "重新載入小幫手", "conn_accept_key": "接受", "conn_save": "儲存", "v5_home_servers": "伺服器", -"v5_home_servers_empty": "透過 SMB、FTP 或 SFTP 直接從您的 NAS 或共用資料夾選擇遊戲。", +"v5_home_servers_empty": "透過 SMB、FTP 或 SFTP/WebDAV 直接從您的 NAS 或共用資料夾選擇遊戲。", "v5_home_servers_connect": "連線 NAS 或伺服器", "fpkg.stage.copy": "從伺服器複製", "fpkg.stage.park": "暫存傾印", @@ -3163,9 +3168,9 @@ upload_as_image_open_convert: "開啟轉換", upload_as_image_queued: "映像檔已製作完成並在上傳佇列中。", upload_as_image_title: "或作為一個遊戲映像檔傳送", "collection.add_server_folder": "新增 {name} 上的資料夾", -"collection.no_servers_hint": "遊戲在 NAS 上?在「連線」中儲存它(SMB、FTP 或 SFTP),就能不掛載直接在這裡新增它的資料夾。", +"collection.no_servers_hint": "遊戲在 NAS 上?在「連線」中儲存它(SMB、FTP 或 SFTP/WebDAV),就能不掛載直接在這裡新增它的資料夾。", "collection.pick_server_folder": "{name} 上存放遊戲的資料夾", -"collection.server_folders_hint": "已儲存伺服器(SMB、FTP、SFTP)上的資料夾直接就地讀取:不會複製到這台電腦,其中的遊戲直接從伺服器安裝。「移到垃圾桶」和「整理」只適用於這台電腦上的資料夾。", +"collection.server_folders_hint": "已儲存伺服器(SMB、FTP、SFTP/WebDAV)上的資料夾直接就地讀取:不會複製到這台電腦,其中的遊戲直接從伺服器安裝。「移到垃圾桶」和「整理」只適用於這台電腦上的資料夾。", bottleneck_source_v2: "讀取來源", android_local_network_allow: "允許", android_local_network_body: "在你允許之前,Android 會阻止此 App 連線到你的 PS5。", @@ -3527,7 +3532,7 @@ sendpayload_probe_no_signature: "找不到 PS5Upload 簽章——僅在你信任 sendpayload_probe_ok: "Payload 檔案看起來沒問題。", sendpayload_probe_read_error: "無法讀取此檔案({error})。請確認它仍然存在且可以開啟,然後重新選擇。", sendpayload_probe_too_small: "此檔案是空的,沒有可傳送的內容。", -servers_subtitle: "你的 NAS 和伺服器(SMB、FTP、FTPS、SFTP)。在任何瀏覽遊戲或檔案的地方,都可以透過「瀏覽」旁的 ▾ 從中選擇。", +servers_subtitle: "你的 NAS 和伺服器(SMB、FTP、FTPS、SFTP/WebDAV)。在任何瀏覽遊戲或檔案的地方,都可以透過「瀏覽」旁的 ▾ 從中選擇。", settings_artwork_cache_explain_v2: "遊戲封面保存在引擎執行的地方(這台電腦,或網頁版的伺服器),因此可以立即載入,而不必每次都從 PS5 讀取。它們會自動更新,當你在某台主機上安裝或解除安裝遊戲時,該主機的封面會被移除。", settings_backup_downloaded: "已下載 {name}", settings_backup_hint_v2: "將主題、PS5 清單、通知歷史、活動日誌和其他偏好設定打包為一個 JSON 檔案,以便在清除應用程式儲存空間後或在另一台電腦上還原。不包括主機配對、收藏、已儲存伺服器的密碼和降版移植函式庫。", diff --git a/client/src/screens/Connections/ConnectionForm.test.tsx b/client/src/screens/Connections/ConnectionForm.test.tsx index 45289689..c0dfd3a3 100644 --- a/client/src/screens/Connections/ConnectionForm.test.tsx +++ b/client/src/screens/Connections/ConnectionForm.test.tsx @@ -14,6 +14,7 @@ import { ConnectionFormView, emptyForm, switchProtocol, + toRequest, type ConnectionFormProps, type FormValue, } from "./ConnectionForm"; @@ -61,6 +62,19 @@ describe("connection form", () => { expect(html({ protocol: "ftps", port: 21 })).not.toContain("Unencrypted"); }); + it("offers WebDAV with its ports, its root hint and the self-signed switch", () => { + const smb = { ...emptyForm(), protocol: "smb" as const, port: 445 }; + expect(switchProtocol(smb, "webdav").port).toBe(80); + expect(switchProtocol(smb, "webdavs").port).toBe(443); + expect(html({ protocol: "webdav", port: 80 })).toContain("Unencrypted"); + expect(html({ protocol: "webdav", port: 80 })).toContain("remote.php/dav"); + expect(html({ protocol: "webdavs", port: 443 })).toContain("self-signed"); + expect(html({ protocol: "webdav", port: 80 })).not.toContain("self-signed"); + const req = toRequest({ ...emptyForm(), protocol: "webdavs", port: 443, insecureTls: true, name: "n", host: "h" }); + expect(req.connection.insecure_tls).toBe(true); + expect(toRequest({ ...emptyForm(), protocol: "webdav", insecureTls: true, name: "n", host: "h" }).connection.insecure_tls).toBe(false); + }); + it("asks for the share only on SMB and hides credentials for guests", () => { expect(html({ protocol: "smb" })).toContain("Share"); expect(html({ protocol: "sftp", port: 22 })).not.toContain(">Share<"); diff --git a/client/src/screens/Connections/ConnectionForm.tsx b/client/src/screens/Connections/ConnectionForm.tsx index edb21443..856e8d64 100644 --- a/client/src/screens/Connections/ConnectionForm.tsx +++ b/client/src/screens/Connections/ConnectionForm.tsx @@ -20,9 +20,23 @@ export interface FormValue { keyName: string; keyPassphrase: string; startPath: string; + /** WebDAV over HTTPS: accept a self-signed certificate. */ + insecureTls: boolean; } -export const DEFAULT_PORT: Record = { smb: 445, ftp: 21, ftps: 21, sftp: 22 }; +export const DEFAULT_PORT: Record = { + smb: 445, + ftp: 21, + ftps: 21, + sftp: 22, + webdav: 80, + webdavs: 443, +}; + +/** Both WebDAV flavours. */ +export function isWebdav(p: Protocol): boolean { + return p === "webdav" || p === "webdavs"; +} export function emptyForm(): FormValue { return { @@ -39,6 +53,7 @@ export function emptyForm(): FormValue { keyName: "", keyPassphrase: "", startPath: "", + insecureTls: false, }; } @@ -58,6 +73,7 @@ export function toRequest(v: FormValue): { connection: ConnectionInput; secret: share: v.protocol === "smb" ? v.share.trim() : "", user: v.guest ? "" : v.user.trim(), start_path: v.startPath.trim(), + insecure_tls: v.protocol === "webdavs" && v.insecureTls, }; const secret: SecretInput = {}; if (!v.guest) { @@ -95,7 +111,9 @@ export function ConnectionFormView(p: ConnectionFormProps) { const guestLabel = v.protocol === "smb" ? tr("conn_guest", undefined, "Guest (no user or password)") - : tr("conn_anonymous", undefined, "Anonymous (no user or password)"); + : isWebdav(v.protocol) + ? tr("conn_no_signin", undefined, "No sign-in (public share)") + : tr("conn_anonymous", undefined, "Anonymous (no user or password)"); const canTest = !!v.name.trim() && !!v.host.trim() && (v.protocol !== "smb" || !!v.share.trim()); return (
@@ -106,6 +124,8 @@ export function ConnectionFormView(p: ConnectionFormProps) { { value: "ftp", label: "FTP" }, { value: "ftps", label: "FTPS" }, { value: "sftp", label: "SFTP" }, + { value: "webdav", label: "WebDAV" }, + { value: "webdavs", label: "WebDAV (HTTPS)" }, ]} value={v.protocol} onChange={(pr) => p.onChange(switchProtocol(v, pr as Protocol))} @@ -120,6 +140,25 @@ export function ConnectionFormView(p: ConnectionFormProps) { )}

)} + {v.protocol === "webdav" && ( +

+ + {tr( + "conn_webdav_unencrypted", + undefined, + "Unencrypted — use WebDAV (HTTPS) where the server supports it.", + )} +

+ )} + {isWebdav(v.protocol) && ( +

+ {tr( + "conn_webdav_root_hint", + undefined, + "Put the server's WebDAV root in Start folder: /remote.php/dav/files/USER for Nextcloud, /dav for Alist, / for rclone serve webdav. Sign-in is Basic authentication.", + )} +

+ )}
set({ startPath: e.target.value })} />
+ {v.protocol === "webdavs" && ( + set({ insecureTls })} + label={tr("conn_insecure_tls", undefined, "Accept a self-signed certificate")} + hint={tr( + "conn_insecure_tls_hint", + undefined, + "Only for a NAS on your own network whose certificate is self-signed. Anyone between this computer and the server could then read the connection.", + )} + /> + )} + {v.protocol !== "sftp" && ( set({ guest })} label={guestLabel} /> )} diff --git a/client/src/screens/Connections/index.tsx b/client/src/screens/Connections/index.tsx index d2db88fa..17e18abe 100644 --- a/client/src/screens/Connections/index.tsx +++ b/client/src/screens/Connections/index.tsx @@ -24,6 +24,8 @@ const PROTOCOL: Record = { ftp: "FTP", ftps: "FTPS", sftp: "SFTP", + webdav: "WebDAV", + webdavs: "WebDAV (HTTPS)", }; function formOf(c: Connection): FormValue { @@ -37,6 +39,7 @@ function formOf(c: Connection): FormValue { guest: !c.user, user: c.user, startPath: c.start_path, + insecureTls: !!c.insecure_tls, }; } diff --git a/client/src/screens/Home/ServersCard.tsx b/client/src/screens/Home/ServersCard.tsx index b6a433dd..e24b6d23 100644 --- a/client/src/screens/Home/ServersCard.tsx +++ b/client/src/screens/Home/ServersCard.tsx @@ -15,6 +15,8 @@ const PROTOCOL: Record = { ftp: "FTP", ftps: "FTPS", sftp: "SFTP", + webdav: "WebDAV", + webdavs: "WebDAV (HTTPS)", }; export function ServersCardView(props: { diff --git a/engine/crates/ps5upload-engine/src/remote/api.rs b/engine/crates/ps5upload-engine/src/remote/api.rs index cfd955a5..a2f6b6b0 100644 --- a/engine/crates/ps5upload-engine/src/remote/api.rs +++ b/engine/crates/ps5upload-engine/src/remote/api.rs @@ -28,6 +28,9 @@ pub struct ConnectionInput { pub user: String, #[serde(default)] pub start_path: String, + /// WebDAV over HTTPS: accept a self-signed certificate. + #[serde(default)] + pub insecure_tls: bool, } #[derive(Deserialize)] @@ -81,6 +84,7 @@ fn parse_body(body: ConnectionBody) -> Result<(Connection, Option), Box< .filter(|p| !p.is_empty()) .map(|password| Secret::Password { password }) }; + let insecure_tls = c.protocol == Protocol::Webdavs && c.insecure_tls; Ok(( Connection { id: String::new(), @@ -92,6 +96,7 @@ fn parse_body(body: ConnectionBody) -> Result<(Connection, Option), Box< user, start_path: c.start_path.trim().to_string(), host_key: None, + insecure_tls, }, secret, )) diff --git a/engine/crates/ps5upload-engine/src/remote/hints.rs b/engine/crates/ps5upload-engine/src/remote/hints.rs index 7b71900f..049f2119 100644 --- a/engine/crates/ps5upload-engine/src/remote/hints.rs +++ b/engine/crates/ps5upload-engine/src/remote/hints.rs @@ -19,7 +19,11 @@ pub fn hint_for(error: &str) -> Option<&'static str> { } else if has(&["signing"]) { Some("The server requires SMB signing, which a guest cannot use. Sign in with a user and password.") } else if has(&["certificate", "tls", "handshake"]) { - Some("The server's TLS certificate was not accepted.") + Some("The server's TLS certificate was not accepted. For a NAS with a self-signed certificate over WebDAV, turn on \"Accept a self-signed certificate\".") + } else if has(&["propfind", "not a webdav"]) { + Some("Put the server's WebDAV root in Start folder, e.g. /remote.php/dav/files/USER for Nextcloud or /dav for Alist, and check the port.") + } else if has(&["only basic authentication"]) { + Some("The server wants a sign-in method other than Basic. Turn Basic authentication on for WebDAV on the server, or use another protocol.") } else if has(&[ "refused", "timed out", diff --git a/engine/crates/ps5upload-engine/src/remote/mod.rs b/engine/crates/ps5upload-engine/src/remote/mod.rs index a0d7f91d..25f3587b 100644 --- a/engine/crates/ps5upload-engine/src/remote/mod.rs +++ b/engine/crates/ps5upload-engine/src/remote/mod.rs @@ -1,4 +1,4 @@ -//! Files on the user's own servers (SMB, FTP, FTPS, SFTP), read through one interface. +//! Files on the user's own servers (SMB, FTP, FTPS, SFTP, WebDAV), read through one interface. //! //! A pick from a server is a `remote:///` string (see [`path`]). The engine //! resolves it against the saved connections and reads it through [`RemoteFs`]: listing for the @@ -19,6 +19,7 @@ pub mod sftp_fs; pub mod smb_fs; pub mod source_fs; pub mod store; +pub mod webdav_fs; #[cfg(test)] use std::collections::BTreeMap; diff --git a/engine/crates/ps5upload-engine/src/remote/pool.rs b/engine/crates/ps5upload-engine/src/remote/pool.rs index e046b7be..6adef601 100644 --- a/engine/crates/ps5upload-engine/src/remote/pool.rs +++ b/engine/crates/ps5upload-engine/src/remote/pool.rs @@ -66,6 +66,9 @@ impl Connector for RealConnector { Protocol::Sftp => Ok(Arc::new( super::sftp_fs::SftpFs::connect(conn, secret).await?, )), + Protocol::Webdav | Protocol::Webdavs => Ok(Arc::new( + super::webdav_fs::WebdavFs::connect(conn, secret).await?, + )), } } } diff --git a/engine/crates/ps5upload-engine/src/remote/store.rs b/engine/crates/ps5upload-engine/src/remote/store.rs index bab8a9ef..73d530eb 100644 --- a/engine/crates/ps5upload-engine/src/remote/store.rs +++ b/engine/crates/ps5upload-engine/src/remote/store.rs @@ -14,6 +14,10 @@ pub enum Protocol { Ftp, Ftps, Sftp, + /// WebDAV over plain HTTP. + Webdav, + /// WebDAV over HTTPS. + Webdavs, } pub fn default_port(p: &Protocol) -> u16 { @@ -21,6 +25,8 @@ pub fn default_port(p: &Protocol) -> u16 { Protocol::Smb => 445, Protocol::Ftp | Protocol::Ftps => 21, Protocol::Sftp => 22, + Protocol::Webdav => 80, + Protocol::Webdavs => 443, } } @@ -43,6 +49,10 @@ pub struct Connection { /// SFTP: the host-key fingerprint the user accepted ("SHA256:…"). #[serde(default)] pub host_key: Option, + /// WebDAV over HTTPS: accept the server's certificate without verifying it (a NAS with a + /// self-signed certificate). Never a default. + #[serde(default)] + pub insecure_tls: bool, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -370,6 +380,7 @@ pub(crate) fn conn(name: &str, protocol: Protocol) -> Connection { user: "me".into(), start_path: String::new(), host_key: None, + insecure_tls: false, } } diff --git a/engine/crates/ps5upload-engine/src/remote/webdav_fs.rs b/engine/crates/ps5upload-engine/src/remote/webdav_fs.rs new file mode 100644 index 00000000..dda316d3 --- /dev/null +++ b/engine/crates/ps5upload-engine/src/remote/webdav_fs.rs @@ -0,0 +1,719 @@ +//! WebDAV (RFC 4918) over plain HTTP or HTTPS, read-only: PROPFIND for listings and stat, +//! ranged GET for reads. Basic auth only, which is what NAS boxes, Nextcloud, Alist and +//! rclone serve; Digest-only servers answer 401 and the hint says so. +//! +//! Paths are absolute on the server (`/remote.php/dav/files/me/games`): the DAV root goes +//! in the connection's start folder, and the browser walks down from there. Nothing here +//! writes. + +use std::sync::Arc; +use std::time::Duration; + +use base64::Engine as _; + +use super::store::{Connection, Protocol, Secret}; +use super::{Entry, Page, RemoteError, RemoteFile, RemoteFs, PAGE}; + +/// One request, including a ranged read of a few MiB from a slow NAS. +const REQUEST_TIMEOUT: Duration = Duration::from_secs(120); +/// A listing of a folder with thousands of entries is still well under this. +const PROPFIND_MAX: u64 = 64 * 1024 * 1024; + +pub struct WebdavFs { + agent: ureq::Agent, + /// `https://nas:5006`, no trailing slash. + base: String, + host: String, + /// `Basic …`, when the connection has a user. + auth: Option, +} + +impl WebdavFs { + pub async fn connect(conn: &Connection, secret: &Secret) -> Result { + let scheme = if conn.protocol == Protocol::Webdavs { + "https" + } else { + "http" + }; + let host = conn.host.trim().trim_matches('/'); + let hostport = if host.contains(':') && !host.starts_with('[') { + format!("[{host}]:{}", conn.port) + } else { + format!("{host}:{}", conn.port) + }; + let auth = match (conn.user.trim(), secret) { + ("", _) => None, + (user, Secret::Password { password }) => Some(format!( + "Basic {}", + base64::engine::general_purpose::STANDARD.encode(format!("{user}:{password}")) + )), + (user, _) => Some(format!( + "Basic {}", + base64::engine::general_purpose::STANDARD.encode(format!("{user}:")) + )), + }; + let config = ureq::Agent::config_builder() + .tls_config( + ureq::tls::TlsConfig::builder() + .disable_verification(conn.insecure_tls) + .build(), + ) + .http_status_as_error(false) + // PROPFIND is not in ureq's method list. + .allow_non_standard_methods(true) + .timeout_global(Some(REQUEST_TIMEOUT)) + .max_idle_connections_per_host(8) + .build(); + let fs = Self { + agent: ureq::Agent::new_with_config(config), + base: format!("{scheme}://{hostport}"), + host: hostport, + auth, + }; + // Signing in = one PROPFIND of the start folder (or the root). + let start = if conn.start_path.trim().is_empty() { + "/".to_string() + } else { + conn.start_path.trim().to_string() + }; + fs.propfind(&start, 0).await?; + Ok(fs) + } + + fn url(&self, path: &str) -> String { + format!("{}{}", self.base, encode_path(path)) + } + + /// PROPFIND `path` with `depth` 0 or 1, parsed. + async fn propfind(&self, path: &str, depth: u8) -> Result, RemoteError> { + let agent = self.agent.clone(); + let url = self.url(path); + let auth = self.auth.clone(); + let host = self.host.clone(); + tokio::task::spawn_blocking(move || { + let mut req = ureq::http::Request::builder() + .method("PROPFIND") + .uri(&url) + .header("Depth", depth.to_string()) + .header("Content-Type", "application/xml; charset=utf-8") + .header("User-Agent", "ps5upload"); + if let Some(a) = &auth { + req = req.header("Authorization", a); + } + let req = req + .body(PROPFIND_BODY) + .map_err(|e| RemoteError::Io(format!("{host}: {e}")))?; + let mut resp = agent.run(req).map_err(|e| map_err(&host, e))?; + let status = resp.status().as_u16(); + match status { + 207 | 200 => {} + 401 | 403 => return Err(RemoteError::Auth(auth_reason(status))), + 404 => return Err(RemoteError::NotFound(format!("{url} not found"))), + 405 | 501 => { + return Err(RemoteError::Io(format!( + "{host} does not answer PROPFIND (HTTP {status}): not a WebDAV server, or the start folder is not its DAV root" + ))) + } + _ => return Err(RemoteError::Io(format!("{host}: PROPFIND returned HTTP {status}"))), + } + let xml = resp + .body_mut() + .with_config() + .limit(PROPFIND_MAX) + .read_to_string() + .map_err(|e| RemoteError::Io(format!("{host}: {e}")))?; + Ok(parse_multistatus(&xml)) + }) + .await + .map_err(|e| RemoteError::Io(format!("{e}")))? + } + + /// The entries directly under `path`, excluding `path` itself. + async fn entries(&self, path: &str) -> Result, RemoteError> { + let me = normal(path); + let mut out: Vec = self + .propfind(path, 1) + .await? + .into_iter() + .filter(|i| normal(&i.path) != me) + .map(|i| i.entry()) + .collect(); + out.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(out) + } +} + +const PROPFIND_BODY: &str = r#" +"#; + +fn auth_reason(status: u16) -> String { + if status == 403 { + "the server refused this user (HTTP 403)".into() + } else { + "the server rejected the user and password (HTTP 401). Only Basic authentication is supported".into() + } +} + +fn map_err(host: &str, e: ureq::Error) -> RemoteError { + let s = e.to_string(); + let l = s.to_ascii_lowercase(); + if l.contains("certificate") || l.contains("tls") || l.contains("handshake") { + RemoteError::Io(format!("{host}: {s}")) + } else if l.contains("timed out") || l.contains("timeout") { + RemoteError::Io(format!("{host}: timed out")) + } else if l.contains("connect") + || l.contains("refused") + || l.contains("resolve") + || l.contains("dns") + || l.contains("unreachable") + { + RemoteError::Unreachable(format!("{host}: {s}")) + } else { + RemoteError::Io(format!("{host}: {s}")) + } +} + +/// `/a b/c` → `/a%20b/c`: path characters that are not safe in a URL are percent-encoded. +pub(crate) fn encode_path(path: &str) -> String { + let p = if path.starts_with('/') { + path.to_string() + } else { + format!("/{path}") + }; + let mut out = String::with_capacity(p.len() + 8); + for b in p.bytes() { + match b { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' => { + out.push(b as char) + } + _ => out.push_str(&format!("%{b:02X}")), + } + } + out +} + +pub(crate) fn decode_percent(s: &str) -> String { + let bytes = s.as_bytes(); + let mut out = Vec::with_capacity(bytes.len()); + let mut i = 0; + while i < bytes.len() { + if bytes[i] == b'%' && i + 2 < bytes.len() + 0 && i + 2 <= bytes.len() - 1 { + if let Ok(v) = u8::from_str_radix(&s[i + 1..i + 3], 16) { + out.push(v); + i += 3; + continue; + } + } + out.push(bytes[i]); + i += 1; + } + String::from_utf8_lossy(&out).into_owned() +} + +/// A path with one leading slash and no trailing slash ("" for the root). +pub(crate) fn normal(p: &str) -> String { + let t = p.trim().trim_end_matches('/'); + if t.is_empty() { + String::new() + } else if t.starts_with('/') { + t.to_string() + } else { + format!("/{t}") + } +} + +/// One `` of a multistatus body. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct DavItem { + /// Decoded, normalised server path. + pub path: String, + pub is_dir: bool, + pub size: u64, + pub mtime: Option, +} + +impl DavItem { + fn entry(&self) -> Entry { + Entry { + name: self.path.rsplit('/').next().unwrap_or("").to_string(), + is_dir: self.is_dir, + size: if self.is_dir { 0 } else { self.size }, + mtime: self.mtime, + } + } +} + +/// The text between `<[prefix:]name ...>` and ``, prefix-agnostic. An +/// empty element (``) yields `Some("")`. +fn element<'a>(xml: &'a str, name: &str) -> Option<(&'a str, &'a str)> { + let lower = xml.to_ascii_lowercase(); + let name = name.to_ascii_lowercase(); + let mut from = 0; + while let Some(lt) = lower[from..].find('<') { + let start = from + lt; + let rest = &lower[start + 1..]; + // Skip closing tags, comments and processing instructions. + if rest.starts_with('/') || rest.starts_with('!') || rest.starts_with('?') { + from = start + 1; + continue; + } + let tag_end = rest + .find(|c: char| c == '>' || c.is_whitespace() || c == '/') + .unwrap_or(rest.len()); + let tag = &rest[..tag_end]; + let local = tag.rsplit(':').next().unwrap_or(tag); + if local == name { + let gt = rest.find('>')?; + let self_closing = rest[..gt].ends_with('/'); + let body_start = start + 1 + gt + 1; + if self_closing { + return Some((&xml[body_start..body_start], &xml[body_start..])); + } + // The matching close tag: the first `` after the body (no nesting of + // same-named elements in a PROPFIND body). + let close = find_close(&lower[body_start..], &name)?; + let body_end = body_start + close; + let after = lower[body_end..].find('>').map(|g| body_end + g + 1)?; + return Some((&xml[body_start..body_end], &xml[after..])); + } + from = start + 1; + } + None +} + +fn find_close(lower: &str, name: &str) -> Option { + let mut from = 0; + while let Some(lt) = lower[from..].find("').unwrap_or(rest.len()); + let tag = rest[..end].trim(); + if tag.rsplit(':').next().unwrap_or(tag) == name { + return Some(at); + } + from = at + 2; + } + None +} + +fn unescape(s: &str) -> String { + s.replace("<", "<") + .replace(">", ">") + .replace(""", "\"") + .replace("'", "'") + .replace("'", "'") + .replace("&", "&") +} + +/// Every `` in a 207 body. Tolerant of any namespace prefix and of servers that +/// put an absolute URL in `href`. +pub(crate) fn parse_multistatus(xml: &str) -> Vec { + let mut out = Vec::new(); + let mut rest = xml; + while let Some((body, after)) = element(rest, "response") { + rest = after; + let Some((href, _)) = element(body, "href") else { + continue; + }; + let href = unescape(href.trim()); + // `http://host/a/b` → `/a/b`; `/a/b` stays. + let path_part = match href.find("://") { + Some(i) => href[i + 3..] + .find('/') + .map(|j| &href[i + 3 + j..]) + .unwrap_or("/"), + None => href.as_str(), + }; + let path = normal(&decode_percent(path_part)); + let is_dir = element(body, "resourcetype") + .map(|(rt, _)| element(rt, "collection").is_some()) + .unwrap_or(false) + || href.ends_with('/'); + let size = element(body, "getcontentlength") + .and_then(|(s, _)| s.trim().parse::().ok()) + .unwrap_or(0); + let mtime = element(body, "getlastmodified").and_then(|(s, _)| parse_http_date(s.trim())); + out.push(DavItem { + path, + is_dir, + size, + mtime, + }); + } + out +} + +/// `Sat, 11 Oct 2026 09:00:00 GMT` → Unix seconds. Only the RFC 1123 form servers send. +pub(crate) fn parse_http_date(s: &str) -> Option { + let parts: Vec<&str> = s.split_whitespace().collect(); + if parts.len() < 5 { + return None; + } + let (day, mon, year, hms) = (parts[1], parts[2], parts[3], parts[4]); + let day: i64 = day.parse().ok()?; + let year: i64 = year.parse().ok()?; + let month = [ + "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec", + ] + .iter() + .position(|m| m.eq_ignore_ascii_case(mon))? as i64 + + 1; + let mut t = hms.split(':'); + let h: i64 = t.next()?.parse().ok()?; + let m: i64 = t.next()?.parse().ok()?; + let sec: i64 = t.next()?.parse().ok()?; + // Days from civil (Howard Hinnant), proleptic Gregorian. + let (y, mo) = if month <= 2 { + (year - 1, month + 9) + } else { + (year, month - 3) + }; + let era = y.div_euclid(400); + let yoe = y - era * 400; + let doy = (153 * mo + 2) / 5 + day - 1; + let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + let days = era * 146_097 + doe - 719_468; + Some(days * 86_400 + h * 3600 + m * 60 + sec) +} + +struct WebdavFile { + fs: Arc, + path: String, + size: u64, +} + +/// What a file handle needs from the session (cloned out so handles outlive the listing). +struct WebdavInner { + agent: ureq::Agent, + base: String, + host: String, + auth: Option, +} + +#[async_trait::async_trait] +impl RemoteFile for WebdavFile { + fn size(&self) -> u64 { + self.size + } + + async fn read_at(&self, offset: u64, len: u64) -> Result, RemoteError> { + if offset >= self.size || len == 0 { + return Ok(Vec::new()); + } + let len = len.min(self.size - offset); + let end = offset + len - 1; + let inner = Arc::clone(&self.fs); + let url = format!("{}{}", inner.base, encode_path(&self.path)); + tokio::task::spawn_blocking(move || { + let mut req = inner + .agent + .get(&url) + .header("User-Agent", "ps5upload") + .header("Range", format!("bytes={offset}-{end}")); + if let Some(a) = &inner.auth { + req = req.header("Authorization", a); + } + let mut resp = req.call().map_err(|e| map_err(&inner.host, e))?; + match resp.status().as_u16() { + 206 => {} + // The server ignored the range: usable only from the start, and only up to len. + 200 if offset == 0 => {} + 200 => { + return Err(RemoteError::Io(format!( + "{} does not support ranged reads (HTTP 200 for a Range request)", + inner.host + ))) + } + 401 | 403 => return Err(RemoteError::Auth(auth_reason(resp.status().as_u16()))), + 404 => return Err(RemoteError::NotFound(format!("{url} not found"))), + s => { + return Err(RemoteError::Io(format!( + "{}: GET returned HTTP {s}", + inner.host + ))) + } + } + // Read exactly the bytes asked for; a 200 body (whole file) is cut at `len`. + use std::io::Read; + let mut buf = Vec::with_capacity(len as usize); + resp.body_mut() + .as_reader() + .take(len) + .read_to_end(&mut buf) + .map_err(|e| RemoteError::Io(format!("{}: {e}", inner.host)))?; + Ok(buf) + }) + .await + .map_err(|e| RemoteError::Io(format!("{e}")))? + } +} + +#[async_trait::async_trait] +impl RemoteFs for WebdavFs { + async fn list(&self, path: &str, cursor: Option) -> Result { + let all = self.entries(path).await?; + let from = cursor + .and_then(|c| c.parse::().ok()) + .unwrap_or(0) + .min(all.len()); + let to = (from + PAGE).min(all.len()); + Ok(Page { + entries: all[from..to].to_vec(), + next_cursor: (to < all.len()).then(|| to.to_string()), + }) + } + + async fn stat(&self, path: &str) -> Result { + let me = normal(path); + let items = self.propfind(path, 0).await?; + let item = items + .into_iter() + .find(|i| normal(&i.path) == me) + .or_else(|| None) + .ok_or_else(|| RemoteError::NotFound(format!("{path} not found")))?; + Ok(Entry { + name: if me.is_empty() { + String::new() + } else { + item.entry().name + }, + ..item.entry() + }) + } + + async fn open(&self, path: &str) -> Result, RemoteError> { + let e = self.stat(path).await?; + if e.is_dir { + return Err(RemoteError::NotFound(format!("{path} is a folder"))); + } + Ok(Arc::new(WebdavFile { + fs: Arc::new(WebdavInner { + agent: self.agent.clone(), + base: self.base.clone(), + host: self.host.clone(), + auth: self.auth.clone(), + }), + path: normal(path), + size: e.size, + })) + } + + async fn walk(&self, path: &str, limit: usize) -> Result, RemoteError> { + let mut out = Vec::new(); + let mut stack = vec![String::new()]; + while let Some(rel) = stack.pop() { + let dir = if rel.is_empty() { + path.to_string() + } else { + format!("{}/{rel}", path.trim_end_matches('/')) + }; + for e in self.entries(&dir).await? { + let child = if rel.is_empty() { + e.name.clone() + } else { + format!("{rel}/{}", e.name) + }; + if e.is_dir { + stack.push(child); + } else { + out.push((child, e)); + if out.len() >= limit { + return Ok(out); + } + } + } + } + Ok(out) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const NEXTCLOUD: &str = r#" + + + /remote.php/dav/files/me/games/ + Sat, 11 Oct 2026 09:00:00 GMTHTTP/1.1 200 OK + HTTP/1.1 404 Not Found + + + /remote.php/dav/files/me/games/Jak%20X/ + HTTP/1.1 200 OK + + + http://nas/remote.php/dav/files/me/games/game%20&%20more.pkg + 12345Thu, 01 Jan 1970 00:00:10 GMTHTTP/1.1 200 OK + +"#; + + #[test] + fn parses_a_nextcloud_listing_with_any_prefix_and_absolute_hrefs() { + let items = parse_multistatus(NEXTCLOUD); + assert_eq!(items.len(), 3); + assert_eq!(items[0].path, "/remote.php/dav/files/me/games"); + assert!(items[0].is_dir); + assert_eq!( + items[0].mtime, + parse_http_date("Sat, 11 Oct 2026 09:00:00 GMT") + ); + assert_eq!(items[1].path, "/remote.php/dav/files/me/games/Jak X"); + assert!(items[1].is_dir); + assert_eq!( + items[2].path, + "/remote.php/dav/files/me/games/game & more.pkg" + ); + assert!(!items[2].is_dir); + assert_eq!(items[2].size, 12345); + assert_eq!(items[2].mtime, Some(10)); + // No prefix at all, uppercase tags, and a self-closing resourcetype. + let plain = r#"/x/y.bin7"#; + let p = parse_multistatus(plain); + assert_eq!(p[0].path, "/x/y.bin"); + assert!(!p[0].is_dir); + assert_eq!(p[0].size, 7); + } + + #[test] + fn http_dates_become_unix_seconds() { + assert_eq!(parse_http_date("Thu, 01 Jan 1970 00:00:00 GMT"), Some(0)); + assert_eq!( + parse_http_date("Sat, 11 Oct 2026 09:00:00 GMT"), + Some(1_791_709_200) + ); + assert_eq!(parse_http_date("nonsense"), None); + } + + #[test] + fn paths_round_trip_through_the_url() { + assert_eq!( + encode_path("/games/Jak X/a&b.pkg"), + "/games/Jak%20X/a%26b.pkg" + ); + assert_eq!( + decode_percent("/games/Jak%20X/a%26b.pkg"), + "/games/Jak X/a&b.pkg" + ); + assert_eq!(decode_percent("100%"), "100%"); + assert_eq!(normal("games/"), "/games"); + assert_eq!(normal("/"), ""); + } + + /// A loopback server that speaks just enough WebDAV: PROPFIND of `/d/` lists two files, + /// GET honours Range. Proves the request shape and the read path end to end. + #[tokio::test] + async fn lists_and_reads_from_a_loopback_server() { + use std::io::{Read, Write}; + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + let data: Vec = (0..1000u32).map(|i| (i % 251) as u8).collect(); + let served = data.clone(); + std::thread::spawn(move || { + for stream in listener.incoming() { + let mut s = stream.unwrap(); + let mut buf = vec![0u8; 8192]; + let mut got = 0; + let (head_len, body_len) = loop { + let n = s.read(&mut buf[got..]).unwrap(); + if n == 0 { + break (got, 0); + } + got += n; + let text = String::from_utf8_lossy(&buf[..got]).to_ascii_lowercase(); + if let Some(i) = text.find("\r\n\r\n") { + let cl = text + .lines() + .find_map(|l| l.strip_prefix("content-length: ")) + .and_then(|v| v.trim().parse::().ok()) + .unwrap_or(0); + break (i + 4, cl); + } + }; + while got < head_len + body_len { + let n = s.read(&mut buf[got..]).unwrap(); + if n == 0 { + break; + } + got += n; + } + // Header names arrive lowercased from the http crate; match case-insensitively. + let text = String::from_utf8_lossy(&buf[..head_len]).to_ascii_lowercase(); + let line = text.lines().next().unwrap_or("").to_string(); + let authed = text.contains("authorization: basic dTpw".to_ascii_lowercase().as_str()); // u:p + let resp: Vec = if !authed { + b"HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + .to_vec() + } else if line.starts_with("propfind /d") { + let body = r#"/d//d/a%20b.bin1000/d/sub/"#; + format!("HTTP/1.1 207 Multi-Status\r\nContent-Type: application/xml\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).into_bytes() + } else if line.starts_with("get /d/a%20b.bin") { + let range = text + .lines() + .find_map(|l| l.strip_prefix("range: bytes=")) + .map(|r| { + let (a, b) = r.trim().split_once('-').unwrap(); + (a.parse::().unwrap(), b.parse::().unwrap()) + }); + match range { + Some((a, b)) => { + let slice = &served[a..=b.min(served.len() - 1)]; + let mut r = format!("HTTP/1.1 206 Partial Content\r\nContent-Range: bytes {a}-{}/{}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", a + slice.len() - 1, served.len(), slice.len()).into_bytes(); + r.extend_from_slice(slice); + r + } + None => b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n".to_vec(), + } + } else { + b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + .to_vec() + }; + s.write_all(&resp).unwrap(); + } + }); + let conn = Connection { + id: "t".into(), + name: "t".into(), + protocol: Protocol::Webdav, + host: "127.0.0.1".into(), + port, + share: String::new(), + user: "u".into(), + start_path: "/d".into(), + host_key: None, + insecure_tls: false, + }; + // Wrong password: a clear sign-in error, not a listing. + let bad = WebdavFs::connect( + &conn, + &Secret::Password { + password: "x".into(), + }, + ) + .await + .err(); + assert!(matches!(bad, Some(RemoteError::Auth(_))), "{bad:?}"); + let fs = WebdavFs::connect( + &conn, + &Secret::Password { + password: "p".into(), + }, + ) + .await + .unwrap(); + let page = fs.list("/d", None).await.unwrap(); + let names: Vec<_> = page + .entries + .iter() + .map(|e| (e.name.as_str(), e.is_dir)) + .collect(); + assert_eq!(names, vec![("a b.bin", false), ("sub", true)]); + let f = fs.open("/d/a b.bin").await.unwrap(); + assert_eq!(f.size(), 1000); + assert_eq!(f.read_at(10, 5).await.unwrap(), data[10..15]); + assert_eq!(f.read_at(995, 50).await.unwrap(), data[995..]); + assert!(f.read_at(1000, 5).await.unwrap().is_empty()); + let walked = fs.walk("/d", 10).await.unwrap(); + assert!(walked.iter().any(|(p, _)| p == "a b.bin")); + } +}