mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 01:27:11 +00:00
feat(remote): enhance file transfer functionality and UI improvements
- Added a drag-and-drop upload feature in the Web Remote interface, allowing users to easily upload files to the currently open folder on the remote side. - Implemented file transfer features including zstd compression, overwrite confirmation dialogs, and a context menu for file management. - Updated various language files to reflect new UI elements and messages related to file transfer operations. - Enhanced CSS for improved layout and user experience in the file transfer interface.
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Web Remote — File Transfer drop zone:** left panel shows a drag-and-drop upload box (click to pick files, multi-file supported). Uploads go to the folder currently open on the remote side; progress appears in the Transfers column.
|
||||
- **Web Remote — RustDesk file transfer parity:** 128 KB blocks, zstd upload compression, resume via `offset_blk` / `transferred_size`, overwrite confirmation dialog (Skip / Overwrite / apply to all), remote context menu (rename, delete, new folder), hidden-files toggle, and Resume in the transfer queue after errors.
|
||||
|
||||
### Changed
|
||||
- **Docker — official single container (default):** `install.sh` and quick-start docs now deploy one pre-built image (`ghcr.io/unitronix/betterdesk`) via `docker-compose.quick.single.yml`. Updates are a single `docker compose pull && up -d`. Legacy two-container layout remains available with `install.sh --split` or `docker-compose.quick.yml`. Panel update instructions respect `BETTERDESK_DOCKER_LAYOUT` (`single` vs `split`). Migrating from split to single reuses the same named volumes; RustDesk client API URL changes from port `21114` to `21121`.
|
||||
|
||||
|
||||
+17
-2
@@ -1261,7 +1261,7 @@
|
||||
"file_status_downloaded": "تم التنزيل",
|
||||
"file_status_uploaded": "تم الرفع",
|
||||
"file_drop_upload_hint": "أفلت للرفع إلى: {path}",
|
||||
"file_local_upload_hint": "اسحب الملفات إلى اللوحة البعيدة أو استخدم الرفع. تُحفظ التنزيلات في مجلد المتصفح الافتراضي.",
|
||||
"file_local_upload_hint": "أسقط الملفات في المربع على اليسار أو انقر للرفع. الوجهة هي المجلد المفتوح على الجانب البعيد.",
|
||||
"file_transfer_stalled": "الجهاز البعيد لم يبدأ النقل",
|
||||
"file_no_local_tree": "اختر مجلدًا لتصفح الملفات المحلية (اختياري)",
|
||||
"reset_keyboard": "إعادة تعيين حالة لوحة المفاتيح",
|
||||
@@ -1269,7 +1269,22 @@
|
||||
"keyboard_mode": "وضع لوحة المفاتيح",
|
||||
"keyboard_mode_auto": "تلقائي (Map، مثل RustDesk)",
|
||||
"keyboard_mode_legacy": "Legacy (حرف)",
|
||||
"keyboard_mode_map": "Map (رمز مسح فعلي)"
|
||||
"keyboard_mode_map": "Map (رمز مسح فعلي)",
|
||||
"file_dropzone_title": "أسقط الملفات هنا",
|
||||
"file_dropzone_subtitle": "أو انقر لاختيار الملفات",
|
||||
"file_dropzone_multiple": "يدعم عدة ملفات",
|
||||
"file_dropzone_waiting": "في انتظار الاتصال البعيد…",
|
||||
"file_overwrite_title": "الملف موجود بالفعل",
|
||||
"file_overwrite_message": "يوجد ملف باسم «{name}» على الجهاز البعيد. هل تريد استبداله؟",
|
||||
"file_overwrite_skip": "تخطي",
|
||||
"file_overwrite_skip_all": "تخطي الكل",
|
||||
"file_overwrite_confirm": "استبدال",
|
||||
"file_overwrite_all": "استبدال الكل",
|
||||
"file_resume": "استئناف",
|
||||
"file_open": "فتح",
|
||||
"file_up": "أعلى",
|
||||
"file_home": "الرئيسية",
|
||||
"file_hide_hidden": "إخفاء الملفات المخفية"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "هوية العلامة التجارية",
|
||||
|
||||
+17
-2
@@ -1260,9 +1260,24 @@
|
||||
"file_status_downloaded": "Staženo",
|
||||
"file_status_uploaded": "Nahráno",
|
||||
"file_drop_upload_hint": "Přetáhněte pro nahrání do: {path}",
|
||||
"file_local_upload_hint": "Přetáhněte soubory na vzdálený panel nebo použijte Nahrát. Stažené soubory se uloží do výchozí složky prohlížeče.",
|
||||
"file_local_upload_hint": "Přetáhněte soubory do pole vlevo nebo klikněte pro nahrání. Cílem je složka otevřená na vzdálené straně.",
|
||||
"file_transfer_stalled": "Vzdálené zařízení nezahájilo přenos",
|
||||
"file_no_local_tree": "Vyberte složku pro procházení místních souborů (volitelné)"
|
||||
"file_no_local_tree": "Vyberte složku pro procházení místních souborů (volitelné)",
|
||||
"file_dropzone_title": "Přetáhněte soubory sem",
|
||||
"file_dropzone_subtitle": "nebo klikněte pro výběr souborů",
|
||||
"file_dropzone_multiple": "Podporováno více souborů",
|
||||
"file_dropzone_waiting": "Čekání na vzdálené připojení…",
|
||||
"file_overwrite_title": "Soubor již existuje",
|
||||
"file_overwrite_message": "Soubor „{name}“ již na vzdáleném počítači existuje. Přepsat?",
|
||||
"file_overwrite_skip": "Přeskočit",
|
||||
"file_overwrite_skip_all": "Přeskočit vše",
|
||||
"file_overwrite_confirm": "Přepsat",
|
||||
"file_overwrite_all": "Přepsat vše",
|
||||
"file_resume": "Obnovit",
|
||||
"file_open": "Otevřít",
|
||||
"file_up": "Nahoru",
|
||||
"file_home": "Domů",
|
||||
"file_hide_hidden": "Skrýt skryté soubory"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Identita značky",
|
||||
|
||||
+17
-2
@@ -1266,9 +1266,24 @@
|
||||
"file_status_downloaded": "Downloadet",
|
||||
"file_status_uploaded": "Uploadet",
|
||||
"file_drop_upload_hint": "Slip for at uploade til: {path}",
|
||||
"file_local_upload_hint": "Træk filer til det fjernpanel eller brug Upload. Downloads gemmes i browserens standardmappe.",
|
||||
"file_local_upload_hint": "Slip filer i feltet til venstre eller klik for at uploade. Destinationen er mappen åben på remote-siden.",
|
||||
"file_transfer_stalled": "Fjernenhed startede ikke overførslen",
|
||||
"file_no_local_tree": "Vælg en mappe for at gennemse lokale filer (valgfrit)"
|
||||
"file_no_local_tree": "Vælg en mappe for at gennemse lokale filer (valgfrit)",
|
||||
"file_dropzone_title": "Slip filer her",
|
||||
"file_dropzone_subtitle": "eller klik for at vælge filer",
|
||||
"file_dropzone_multiple": "Flere filer understøttes",
|
||||
"file_dropzone_waiting": "Venter på remote-forbindelse…",
|
||||
"file_overwrite_title": "Filen findes allerede",
|
||||
"file_overwrite_message": "En fil ved navn «{name}» findes allerede på remote-computeren. Overskrive?",
|
||||
"file_overwrite_skip": "Spring over",
|
||||
"file_overwrite_skip_all": "Spring alle over",
|
||||
"file_overwrite_confirm": "Overskriv",
|
||||
"file_overwrite_all": "Overskriv alle",
|
||||
"file_resume": "Genoptag",
|
||||
"file_open": "Åbn",
|
||||
"file_up": "Op",
|
||||
"file_home": "Hjem",
|
||||
"file_hide_hidden": "Skjul skjulte filer"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Brand Identitet",
|
||||
|
||||
+17
-2
@@ -1260,9 +1260,24 @@
|
||||
"file_status_downloaded": "Heruntergeladen",
|
||||
"file_status_uploaded": "Hochgeladen",
|
||||
"file_drop_upload_hint": "Zum Hochladen hier ablegen: {path}",
|
||||
"file_local_upload_hint": "Dateien auf das Remote-Panel ziehen oder Hochladen verwenden. Downloads landen im Standardordner des Browsers.",
|
||||
"file_local_upload_hint": "Dateien links ablegen oder klicken zum Hochladen. Ziel ist der auf der Remote-Seite geöffnete Ordner.",
|
||||
"file_transfer_stalled": "Remote-Gerät hat Transfer nicht gestartet",
|
||||
"file_no_local_tree": "Ordner wählen, um lokale Dateien zu durchsuchen (optional)"
|
||||
"file_no_local_tree": "Ordner wählen, um lokale Dateien zu durchsuchen (optional)",
|
||||
"file_dropzone_title": "Dateien hier ablegen",
|
||||
"file_dropzone_subtitle": "oder klicken zum Auswählen",
|
||||
"file_dropzone_multiple": "Mehrere Dateien unterstützt",
|
||||
"file_dropzone_waiting": "Warte auf Remote-Verbindung…",
|
||||
"file_overwrite_title": "Datei existiert bereits",
|
||||
"file_overwrite_message": "Die Datei „{name}“ existiert bereits auf dem Remote-Computer. Überschreiben?",
|
||||
"file_overwrite_skip": "Überspringen",
|
||||
"file_overwrite_skip_all": "Alle überspringen",
|
||||
"file_overwrite_confirm": "Überschreiben",
|
||||
"file_overwrite_all": "Alle überschreiben",
|
||||
"file_resume": "Fortsetzen",
|
||||
"file_open": "Öffnen",
|
||||
"file_up": "Nach oben",
|
||||
"file_home": "Start",
|
||||
"file_hide_hidden": "Versteckte Dateien ausblenden"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Markenidentität",
|
||||
|
||||
+17
-2
@@ -1281,9 +1281,24 @@
|
||||
"file_status_downloaded": "Downloaded",
|
||||
"file_status_uploaded": "Uploaded",
|
||||
"file_drop_upload_hint": "Drop to upload to: {path}",
|
||||
"file_local_upload_hint": "Drag files onto the remote panel or use Upload. Downloads save to your browser's default folder.",
|
||||
"file_local_upload_hint": "Drop files in the box on the left or click it to upload. Destination is the folder open on the remote side.",
|
||||
"file_transfer_stalled": "Remote did not start transfer",
|
||||
"file_no_local_tree": "Choose a folder to browse local files (optional)"
|
||||
"file_no_local_tree": "Choose a folder to browse local files (optional)",
|
||||
"file_dropzone_title": "Drop files here",
|
||||
"file_dropzone_subtitle": "or click to choose files",
|
||||
"file_dropzone_multiple": "Multiple files supported",
|
||||
"file_dropzone_waiting": "Waiting for remote connection…",
|
||||
"file_overwrite_title": "File already exists",
|
||||
"file_overwrite_message": "A file named \"{name}\" already exists on the remote computer. Overwrite it?",
|
||||
"file_overwrite_skip": "Skip",
|
||||
"file_overwrite_skip_all": "Skip all",
|
||||
"file_overwrite_confirm": "Overwrite",
|
||||
"file_overwrite_all": "Overwrite all",
|
||||
"file_resume": "Resume",
|
||||
"file_open": "Open",
|
||||
"file_up": "Up",
|
||||
"file_home": "Home",
|
||||
"file_hide_hidden": "Hide hidden files"
|
||||
},
|
||||
"remote_dashboard": {
|
||||
"title": "Remote Desktop Client",
|
||||
|
||||
+17
-2
@@ -1260,9 +1260,24 @@
|
||||
"file_status_downloaded": "Descargado",
|
||||
"file_status_uploaded": "Subido",
|
||||
"file_drop_upload_hint": "Suelta para subir a: {path}",
|
||||
"file_local_upload_hint": "Arrastra archivos al panel remoto o usa Subir. Las descargas se guardan en la carpeta predeterminada del navegador.",
|
||||
"file_local_upload_hint": "Suelta archivos en el recuadro izquierdo o haz clic para subir. El destino es la carpeta abierta en el equipo remoto.",
|
||||
"file_transfer_stalled": "El dispositivo remoto no inició la transferencia",
|
||||
"file_no_local_tree": "Elige una carpeta para explorar archivos locales (opcional)"
|
||||
"file_no_local_tree": "Elige una carpeta para explorar archivos locales (opcional)",
|
||||
"file_dropzone_title": "Suelta archivos aquí",
|
||||
"file_dropzone_subtitle": "o haz clic para elegir archivos",
|
||||
"file_dropzone_multiple": "Varios archivos admitidos",
|
||||
"file_dropzone_waiting": "Esperando conexión remota…",
|
||||
"file_overwrite_title": "El archivo ya existe",
|
||||
"file_overwrite_message": "Ya existe un archivo llamado «{name}» en el equipo remoto. ¿Sobrescribir?",
|
||||
"file_overwrite_skip": "Omitir",
|
||||
"file_overwrite_skip_all": "Omitir todos",
|
||||
"file_overwrite_confirm": "Sobrescribir",
|
||||
"file_overwrite_all": "Sobrescribir todos",
|
||||
"file_resume": "Reanudar",
|
||||
"file_open": "Abrir",
|
||||
"file_up": "Subir",
|
||||
"file_home": "Inicio",
|
||||
"file_hide_hidden": "Ocultar archivos ocultos"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Identidad de marca",
|
||||
|
||||
+17
-2
@@ -1266,9 +1266,24 @@
|
||||
"file_status_downloaded": "Ladattu",
|
||||
"file_status_uploaded": "Lähetetty",
|
||||
"file_drop_upload_hint": "Pudota ladataksesi kohteeseen: {path}",
|
||||
"file_local_upload_hint": "Vedä tiedostot etäpaneeliin tai käytä Lataa. Lataukset tallentuvat selaimen oletuskansioon.",
|
||||
"file_local_upload_hint": "Pudota tiedostot vasemmalla olevaan laatikkoon tai napsauta ladataksesi. Kohde on etäpuolella avoinna oleva kansio.",
|
||||
"file_transfer_stalled": "Etälaite ei aloittanut siirtoa",
|
||||
"file_no_local_tree": "Valitse kansio paikallisten tiedostojen selaamiseen (valinnainen)"
|
||||
"file_no_local_tree": "Valitse kansio paikallisten tiedostojen selaamiseen (valinnainen)",
|
||||
"file_dropzone_title": "Pudota tiedostot tähän",
|
||||
"file_dropzone_subtitle": "tai napsauta valitaksesi tiedostot",
|
||||
"file_dropzone_multiple": "Useita tiedostoja tuettu",
|
||||
"file_dropzone_waiting": "Odotetaan etäyhteyttä…",
|
||||
"file_overwrite_title": "Tiedosto on jo olemassa",
|
||||
"file_overwrite_message": "Tiedosto «{name}» on jo etätietokoneella. Korvataanko?",
|
||||
"file_overwrite_skip": "Ohita",
|
||||
"file_overwrite_skip_all": "Ohita kaikki",
|
||||
"file_overwrite_confirm": "Korvaa",
|
||||
"file_overwrite_all": "Korvaa kaikki",
|
||||
"file_resume": "Jatka",
|
||||
"file_open": "Avaa",
|
||||
"file_up": "Ylös",
|
||||
"file_home": "Alku",
|
||||
"file_hide_hidden": "Piilota piilotetut tiedostot"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Brändin identiteetti",
|
||||
|
||||
+17
-2
@@ -1277,9 +1277,24 @@
|
||||
"file_status_downloaded": "Téléchargé",
|
||||
"file_status_uploaded": "Envoyé",
|
||||
"file_drop_upload_hint": "Déposez pour envoyer vers : {path}",
|
||||
"file_local_upload_hint": "Glissez des fichiers sur le panneau distant ou utilisez Envoyer. Les téléchargements vont dans le dossier par défaut du navigateur.",
|
||||
"file_local_upload_hint": "Déposez les fichiers dans la zone à gauche ou cliquez pour téléverser. La destination est le dossier ouvert côté distant.",
|
||||
"file_transfer_stalled": "L'appareil distant n'a pas démarré le transfert",
|
||||
"file_no_local_tree": "Choisissez un dossier pour parcourir les fichiers locaux (facultatif)"
|
||||
"file_no_local_tree": "Choisissez un dossier pour parcourir les fichiers locaux (facultatif)",
|
||||
"file_dropzone_title": "Déposez les fichiers ici",
|
||||
"file_dropzone_subtitle": "ou cliquez pour choisir des fichiers",
|
||||
"file_dropzone_multiple": "Plusieurs fichiers pris en charge",
|
||||
"file_dropzone_waiting": "En attente de la connexion distante…",
|
||||
"file_overwrite_title": "Le fichier existe déjà",
|
||||
"file_overwrite_message": "Un fichier nommé « {name} » existe déjà sur l’ordinateur distant. Remplacer ?",
|
||||
"file_overwrite_skip": "Ignorer",
|
||||
"file_overwrite_skip_all": "Tout ignorer",
|
||||
"file_overwrite_confirm": "Remplacer",
|
||||
"file_overwrite_all": "Tout remplacer",
|
||||
"file_resume": "Reprendre",
|
||||
"file_open": "Ouvrir",
|
||||
"file_up": "Monter",
|
||||
"file_home": "Accueil",
|
||||
"file_hide_hidden": "Masquer les fichiers cachés"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Identité de marque",
|
||||
|
||||
+17
-2
@@ -1266,9 +1266,24 @@
|
||||
"file_status_downloaded": "डाउनलोड हो गया",
|
||||
"file_status_uploaded": "अपलोड हो गया",
|
||||
"file_drop_upload_hint": "अपलोड करने के लिए यहाँ छोड़ें: {path}",
|
||||
"file_local_upload_hint": "फ़ाइलों को रिमोट पैनल पर खींचें या अपलोड का उपयोग करें। डाउनलोड आपके ब्राउज़र के डिफ़ॉल्ट फ़ोल्डर में सहेजे जाते हैं।",
|
||||
"file_local_upload_hint": "बाएँ बॉक्स में फ़ाइलें छोड़ें या अपलोड के लिए क्लिक करें। गंतव्य रिमोट पर खुला फ़ोल्डर है।",
|
||||
"file_transfer_stalled": "रिमोट ने स्थानांतरण शुरू नहीं किया",
|
||||
"file_no_local_tree": "स्थानीय फ़ाइलें ब्राउज़ करने के लिए फ़ोल्डर चुनें (वैकल्पिक)"
|
||||
"file_no_local_tree": "स्थानीय फ़ाइलें ब्राउज़ करने के लिए फ़ोल्डर चुनें (वैकल्पिक)",
|
||||
"file_dropzone_title": "फ़ाइलें यहाँ छोड़ें",
|
||||
"file_dropzone_subtitle": "या फ़ाइलें चुनने के लिए क्लिक करें",
|
||||
"file_dropzone_multiple": "कई फ़ाइलें समर्थित",
|
||||
"file_dropzone_waiting": "रिमोट कनेक्शन की प्रतीक्षा…",
|
||||
"file_overwrite_title": "फ़ाइल पहले से मौजूद है",
|
||||
"file_overwrite_message": "«{name}» नाम की फ़ाइल रिमोट कंप्यूटर पर पहले से है। ओवरराइट करें?",
|
||||
"file_overwrite_skip": "छोड़ें",
|
||||
"file_overwrite_skip_all": "सभी छोड़ें",
|
||||
"file_overwrite_confirm": "ओवरराइट",
|
||||
"file_overwrite_all": "सभी ओवरराइट",
|
||||
"file_resume": "फिर से शुरू",
|
||||
"file_open": "खोलें",
|
||||
"file_up": "ऊपर",
|
||||
"file_home": "होम",
|
||||
"file_hide_hidden": "छिपी फ़ाइलें छुपाएँ"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "ब्रांड पहचान",
|
||||
|
||||
+17
-2
@@ -1266,9 +1266,24 @@
|
||||
"file_status_downloaded": "Letöltve",
|
||||
"file_status_uploaded": "Feltöltve",
|
||||
"file_drop_upload_hint": "Engedje el a feltöltéshez ide: {path}",
|
||||
"file_local_upload_hint": "Húzza a fájlokat a távoli panelre, vagy használja a Feltöltést. A letöltések a böngésző alapértelmezett mappájába kerülnek.",
|
||||
"file_local_upload_hint": "Húzza ide a fájlokat a bal oldali mezőbe, vagy kattintson a feltöltéshez. A cél a távoli oldalon megnyitott mappa.",
|
||||
"file_transfer_stalled": "A távoli eszköz nem indította el az átvitelt",
|
||||
"file_no_local_tree": "Válasszon mappát a helyi fájlok böngészéséhez (opcionális)"
|
||||
"file_no_local_tree": "Válasszon mappát a helyi fájlok böngészéséhez (opcionális)",
|
||||
"file_dropzone_title": "Húzza ide a fájlokat",
|
||||
"file_dropzone_subtitle": "vagy kattintson a fájlok kiválasztásához",
|
||||
"file_dropzone_multiple": "Több fájl támogatott",
|
||||
"file_dropzone_waiting": "Várakozás a távoli kapcsolatra…",
|
||||
"file_overwrite_title": "A fájl már létezik",
|
||||
"file_overwrite_message": "A(z) «{name}» nevű fájl már létezik a távoli gépen. Felülírja?",
|
||||
"file_overwrite_skip": "Kihagyás",
|
||||
"file_overwrite_skip_all": "Összes kihagyása",
|
||||
"file_overwrite_confirm": "Felülírás",
|
||||
"file_overwrite_all": "Összes felülírása",
|
||||
"file_resume": "Folytatás",
|
||||
"file_open": "Megnyitás",
|
||||
"file_up": "Fel",
|
||||
"file_home": "Kezdőlap",
|
||||
"file_hide_hidden": "Rejtett fájlok elrejtése"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Márkaidentitás",
|
||||
|
||||
+17
-2
@@ -1266,9 +1266,24 @@
|
||||
"file_status_downloaded": "Diunduh",
|
||||
"file_status_uploaded": "Diunggah",
|
||||
"file_drop_upload_hint": "Lepaskan untuk mengunggah ke: {path}",
|
||||
"file_local_upload_hint": "Seret file ke panel remote atau gunakan Unggah. Unduhan disimpan ke folder default browser.",
|
||||
"file_local_upload_hint": "Letakkan file di kotak kiri atau klik untuk mengunggah. Tujuan adalah folder yang terbuka di sisi remote.",
|
||||
"file_transfer_stalled": "Perangkat remote tidak memulai transfer",
|
||||
"file_no_local_tree": "Pilih folder untuk menelusuri file lokal (opsional)"
|
||||
"file_no_local_tree": "Pilih folder untuk menelusuri file lokal (opsional)",
|
||||
"file_dropzone_title": "Letakkan file di sini",
|
||||
"file_dropzone_subtitle": "atau klik untuk memilih file",
|
||||
"file_dropzone_multiple": "Beberapa file didukung",
|
||||
"file_dropzone_waiting": "Menunggu koneksi remote…",
|
||||
"file_overwrite_title": "File sudah ada",
|
||||
"file_overwrite_message": "File bernama «{name}» sudah ada di komputer remote. Timpa?",
|
||||
"file_overwrite_skip": "Lewati",
|
||||
"file_overwrite_skip_all": "Lewati semua",
|
||||
"file_overwrite_confirm": "Timpa",
|
||||
"file_overwrite_all": "Timpa semua",
|
||||
"file_resume": "Lanjutkan",
|
||||
"file_open": "Buka",
|
||||
"file_up": "Naik",
|
||||
"file_home": "Beranda",
|
||||
"file_hide_hidden": "Sembunyikan file tersembunyi"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Identitas Merek",
|
||||
|
||||
+17
-2
@@ -1260,9 +1260,24 @@
|
||||
"file_status_downloaded": "Scaricato",
|
||||
"file_status_uploaded": "Caricato",
|
||||
"file_drop_upload_hint": "Rilascia per caricare in: {path}",
|
||||
"file_local_upload_hint": "Trascina i file sul pannello remoto o usa Carica. I download vanno nella cartella predefinita del browser.",
|
||||
"file_local_upload_hint": "Trascina i file nel riquadro a sinistra o clicca per caricare. La destinazione è la cartella aperta sul lato remoto.",
|
||||
"file_transfer_stalled": "Il dispositivo remoto non ha avviato il trasferimento",
|
||||
"file_no_local_tree": "Scegli una cartella per sfogliare i file locali (facoltativo)"
|
||||
"file_no_local_tree": "Scegli una cartella per sfogliare i file locali (facoltativo)",
|
||||
"file_dropzone_title": "Trascina i file qui",
|
||||
"file_dropzone_subtitle": "o clicca per scegliere i file",
|
||||
"file_dropzone_multiple": "Più file supportati",
|
||||
"file_dropzone_waiting": "In attesa della connessione remota…",
|
||||
"file_overwrite_title": "Il file esiste già",
|
||||
"file_overwrite_message": "Un file denominato «{name}» esiste già sul computer remoto. Sovrascrivere?",
|
||||
"file_overwrite_skip": "Salta",
|
||||
"file_overwrite_skip_all": "Salta tutti",
|
||||
"file_overwrite_confirm": "Sovrascrivi",
|
||||
"file_overwrite_all": "Sovrascrivi tutti",
|
||||
"file_resume": "Riprendi",
|
||||
"file_open": "Apri",
|
||||
"file_up": "Su",
|
||||
"file_home": "Home",
|
||||
"file_hide_hidden": "Nascondi file nascosti"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Identità del marchio",
|
||||
|
||||
+17
-2
@@ -1260,9 +1260,24 @@
|
||||
"file_status_downloaded": "ダウンロード完了",
|
||||
"file_status_uploaded": "アップロード完了",
|
||||
"file_drop_upload_hint": "ドロップしてアップロード先: {path}",
|
||||
"file_local_upload_hint": "ファイルをリモートパネルにドラッグするか、アップロードを使用してください。ダウンロードはブラウザの既定フォルダーに保存されます。",
|
||||
"file_local_upload_hint": "左のボックスにファイルをドロップするか、クリックしてアップロードします。送信先はリモート側で開いているフォルダーです。",
|
||||
"file_transfer_stalled": "リモートが転送を開始しませんでした",
|
||||
"file_no_local_tree": "ローカルファイルを参照するフォルダーを選択(任意)"
|
||||
"file_no_local_tree": "ローカルファイルを参照するフォルダーを選択(任意)",
|
||||
"file_dropzone_title": "ここにファイルをドロップ",
|
||||
"file_dropzone_subtitle": "またはクリックしてファイルを選択",
|
||||
"file_dropzone_multiple": "複数ファイルに対応",
|
||||
"file_dropzone_waiting": "リモート接続を待機中…",
|
||||
"file_overwrite_title": "ファイルは既に存在します",
|
||||
"file_overwrite_message": "リモートに「{name}」というファイルが既にあります。上書きしますか?",
|
||||
"file_overwrite_skip": "スキップ",
|
||||
"file_overwrite_skip_all": "すべてスキップ",
|
||||
"file_overwrite_confirm": "上書き",
|
||||
"file_overwrite_all": "すべて上書き",
|
||||
"file_resume": "再開",
|
||||
"file_open": "開く",
|
||||
"file_up": "上へ",
|
||||
"file_home": "ホーム",
|
||||
"file_hide_hidden": "隠しファイルを非表示"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "ブランドアイデンティティ",
|
||||
|
||||
+17
-2
@@ -1260,9 +1260,24 @@
|
||||
"file_status_downloaded": "다운로드됨",
|
||||
"file_status_uploaded": "업로드됨",
|
||||
"file_drop_upload_hint": "업로드 대상에 놓기: {path}",
|
||||
"file_local_upload_hint": "파일을 원격 패널에 끌어다 놓거나 업로드를 사용하세요. 다운로드는 브라우저 기본 폴더에 저장됩니다.",
|
||||
"file_local_upload_hint": "왼쪽 상자에 파일을 놓거나 클릭하여 업로드하세요. 대상은 원격 쪽에서 열린 폴더입니다.",
|
||||
"file_transfer_stalled": "원격 장치가 전송을 시작하지 않았습니다",
|
||||
"file_no_local_tree": "로컬 파일을 탐색하려면 폴더 선택(선택 사항)"
|
||||
"file_no_local_tree": "로컬 파일을 탐색하려면 폴더 선택(선택 사항)",
|
||||
"file_dropzone_title": "여기에 파일 놓기",
|
||||
"file_dropzone_subtitle": "또는 클릭하여 파일 선택",
|
||||
"file_dropzone_multiple": "여러 파일 지원",
|
||||
"file_dropzone_waiting": "원격 연결 대기 중…",
|
||||
"file_overwrite_title": "파일이 이미 있습니다",
|
||||
"file_overwrite_message": "원격 컴퓨터에 «{name}» 파일이 이미 있습니다. 덮어쓸까요?",
|
||||
"file_overwrite_skip": "건너뛰기",
|
||||
"file_overwrite_skip_all": "모두 건너뛰기",
|
||||
"file_overwrite_confirm": "덮어쓰기",
|
||||
"file_overwrite_all": "모두 덮어쓰기",
|
||||
"file_resume": "재개",
|
||||
"file_open": "열기",
|
||||
"file_up": "위로",
|
||||
"file_home": "홈",
|
||||
"file_hide_hidden": "숨김 파일 숨기기"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "브랜드 아이덴티티",
|
||||
|
||||
+17
-2
@@ -1266,9 +1266,24 @@
|
||||
"file_status_downloaded": "Lastet ned",
|
||||
"file_status_uploaded": "Lastet opp",
|
||||
"file_drop_upload_hint": "Slipp for å laste opp til: {path}",
|
||||
"file_local_upload_hint": "Dra filer til fjernpanelet eller bruk Last opp. Nedlastinger lagres i nettleserens standardmappe.",
|
||||
"file_local_upload_hint": "Slipp filer i boksen til venstre eller klikk for å laste opp. Målet er mappen som er åpen på fjernsiden.",
|
||||
"file_transfer_stalled": "Fjernenheten startet ikke overføringen",
|
||||
"file_no_local_tree": "Velg en mappe for å bla i lokale filer (valgfritt)"
|
||||
"file_no_local_tree": "Velg en mappe for å bla i lokale filer (valgfritt)",
|
||||
"file_dropzone_title": "Slipp filer her",
|
||||
"file_dropzone_subtitle": "eller klikk for å velge filer",
|
||||
"file_dropzone_multiple": "Flere filer støttes",
|
||||
"file_dropzone_waiting": "Venter på fjernforbindelse…",
|
||||
"file_overwrite_title": "Filen finnes allerede",
|
||||
"file_overwrite_message": "En fil med navnet «{name}» finnes allerede på fjernmaskinen. Overskrive?",
|
||||
"file_overwrite_skip": "Hopp over",
|
||||
"file_overwrite_skip_all": "Hopp over alle",
|
||||
"file_overwrite_confirm": "Overskriv",
|
||||
"file_overwrite_all": "Overskriv alle",
|
||||
"file_resume": "Gjenoppta",
|
||||
"file_open": "Åpne",
|
||||
"file_up": "Opp",
|
||||
"file_home": "Hjem",
|
||||
"file_hide_hidden": "Skjul skjulte filer"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Merkeidentitet",
|
||||
|
||||
+17
-2
@@ -1260,9 +1260,24 @@
|
||||
"file_status_downloaded": "Gedownload",
|
||||
"file_status_uploaded": "Geüpload",
|
||||
"file_drop_upload_hint": "Laat los om te uploaden naar: {path}",
|
||||
"file_local_upload_hint": "Sleep bestanden naar het externe paneel of gebruik Uploaden. Downloads worden opgeslagen in de standaardmap van de browser.",
|
||||
"file_local_upload_hint": "Sleep bestanden in het vak links of klik om te uploaden. Bestemming is de map open aan de remote-kant.",
|
||||
"file_transfer_stalled": "Extern apparaat startte overdracht niet",
|
||||
"file_no_local_tree": "Kies een map om lokale bestanden te bladeren (optioneel)"
|
||||
"file_no_local_tree": "Kies een map om lokale bestanden te bladeren (optioneel)",
|
||||
"file_dropzone_title": "Sleep bestanden hierheen",
|
||||
"file_dropzone_subtitle": "of klik om bestanden te kiezen",
|
||||
"file_dropzone_multiple": "Meerdere bestanden ondersteund",
|
||||
"file_dropzone_waiting": "Wachten op remote-verbinding…",
|
||||
"file_overwrite_title": "Bestand bestaat al",
|
||||
"file_overwrite_message": "Er bestaat al een bestand «{name}» op de remote-computer. Overschrijven?",
|
||||
"file_overwrite_skip": "Overslaan",
|
||||
"file_overwrite_skip_all": "Alles overslaan",
|
||||
"file_overwrite_confirm": "Overschrijven",
|
||||
"file_overwrite_all": "Alles overschrijven",
|
||||
"file_resume": "Hervatten",
|
||||
"file_open": "Openen",
|
||||
"file_up": "Omhoog",
|
||||
"file_home": "Home",
|
||||
"file_hide_hidden": "Verborgen bestanden verbergen"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Merkidentiteit",
|
||||
|
||||
+17
-2
@@ -1273,9 +1273,24 @@
|
||||
"file_status_downloaded": "Pobrano",
|
||||
"file_status_uploaded": "Wysłano",
|
||||
"file_drop_upload_hint": "Upuść, aby przesłać do: {path}",
|
||||
"file_local_upload_hint": "Przeciągnij pliki na panel zdalny lub użyj Prześlij. Pobrania trafiają do domyślnego folderu przeglądarki.",
|
||||
"file_local_upload_hint": "Upuść pliki w polu po lewej lub kliknij je, aby przesłać. Folder docelowy to ten otwarty po stronie zdalnej.",
|
||||
"file_transfer_stalled": "Urządzenie zdalne nie rozpoczęło transferu",
|
||||
"file_no_local_tree": "Wybierz folder, aby przeglądać pliki lokalne (opcjonalnie)"
|
||||
"file_no_local_tree": "Wybierz folder, aby przeglądać pliki lokalne (opcjonalnie)",
|
||||
"file_dropzone_title": "Upuść pliki tutaj",
|
||||
"file_dropzone_subtitle": "lub kliknij, aby wybrać pliki",
|
||||
"file_dropzone_multiple": "Obsługiwane wiele plików",
|
||||
"file_dropzone_waiting": "Oczekiwanie na połączenie ze zdalnym komputerem…",
|
||||
"file_overwrite_title": "Plik już istnieje",
|
||||
"file_overwrite_message": "Plik „{name}” już istnieje na komputerze zdalnym. Nadpisać?",
|
||||
"file_overwrite_skip": "Pomiń",
|
||||
"file_overwrite_skip_all": "Pomiń wszystkie",
|
||||
"file_overwrite_confirm": "Nadpisz",
|
||||
"file_overwrite_all": "Nadpisz wszystkie",
|
||||
"file_resume": "Wznów",
|
||||
"file_open": "Otwórz",
|
||||
"file_up": "W górę",
|
||||
"file_home": "Początek",
|
||||
"file_hide_hidden": "Ukryj ukryte pliki"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Identyfikacja marki",
|
||||
|
||||
+17
-2
@@ -1260,9 +1260,24 @@
|
||||
"file_status_downloaded": "Transferido",
|
||||
"file_status_uploaded": "Enviado",
|
||||
"file_drop_upload_hint": "Largue para enviar para: {path}",
|
||||
"file_local_upload_hint": "Arraste ficheiros para o painel remoto ou use Enviar. As transferências são guardadas na pasta predefinida do navegador.",
|
||||
"file_local_upload_hint": "Solte ficheiros na caixa à esquerda ou clique para enviar. O destino é a pasta aberta no lado remoto.",
|
||||
"file_transfer_stalled": "O dispositivo remoto não iniciou a transferência",
|
||||
"file_no_local_tree": "Escolha uma pasta para procurar ficheiros locais (opcional)"
|
||||
"file_no_local_tree": "Escolha uma pasta para procurar ficheiros locais (opcional)",
|
||||
"file_dropzone_title": "Solte ficheiros aqui",
|
||||
"file_dropzone_subtitle": "ou clique para escolher ficheiros",
|
||||
"file_dropzone_multiple": "Vários ficheiros suportados",
|
||||
"file_dropzone_waiting": "A aguardar ligação remota…",
|
||||
"file_overwrite_title": "O ficheiro já existe",
|
||||
"file_overwrite_message": "Já existe um ficheiro chamado «{name}» no computador remoto. Substituir?",
|
||||
"file_overwrite_skip": "Ignorar",
|
||||
"file_overwrite_skip_all": "Ignorar todos",
|
||||
"file_overwrite_confirm": "Substituir",
|
||||
"file_overwrite_all": "Substituir todos",
|
||||
"file_resume": "Retomar",
|
||||
"file_open": "Abrir",
|
||||
"file_up": "Acima",
|
||||
"file_home": "Início",
|
||||
"file_hide_hidden": "Ocultar ficheiros ocultos"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Identidade da marca",
|
||||
|
||||
+17
-2
@@ -1266,9 +1266,24 @@
|
||||
"file_status_downloaded": "Descărcat",
|
||||
"file_status_uploaded": "Încărcat",
|
||||
"file_drop_upload_hint": "Plasați pentru a încărca în: {path}",
|
||||
"file_local_upload_hint": "Trageți fișiere pe panoul la distanță sau folosiți Încărcare. Descărcările se salvează în folderul implicit al browserului.",
|
||||
"file_local_upload_hint": "Trageți fișierele în caseta din stânga sau faceți clic pentru încărcare. Destinația este folderul deschis pe partea remote.",
|
||||
"file_transfer_stalled": "Dispozitivul la distanță nu a pornit transferul",
|
||||
"file_no_local_tree": "Alegeți un folder pentru a răsfoi fișierele locale (opțional)"
|
||||
"file_no_local_tree": "Alegeți un folder pentru a răsfoi fișierele locale (opțional)",
|
||||
"file_dropzone_title": "Trageți fișierele aici",
|
||||
"file_dropzone_subtitle": "sau faceți clic pentru a alege fișiere",
|
||||
"file_dropzone_multiple": "Mai multe fișiere acceptate",
|
||||
"file_dropzone_waiting": "Se așteaptă conexiunea remote…",
|
||||
"file_overwrite_title": "Fișierul există deja",
|
||||
"file_overwrite_message": "Un fișier numit «{name}» există deja pe calculatorul remote. Suprascrieți?",
|
||||
"file_overwrite_skip": "Omite",
|
||||
"file_overwrite_skip_all": "Omite toate",
|
||||
"file_overwrite_confirm": "Suprascrie",
|
||||
"file_overwrite_all": "Suprascrie toate",
|
||||
"file_resume": "Reia",
|
||||
"file_open": "Deschide",
|
||||
"file_up": "Sus",
|
||||
"file_home": "Acasă",
|
||||
"file_hide_hidden": "Ascunde fișierele ascunse"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Identitatea de marcă",
|
||||
|
||||
+17
-2
@@ -1266,9 +1266,24 @@
|
||||
"file_status_downloaded": "Nedladdad",
|
||||
"file_status_uploaded": "Uppladdad",
|
||||
"file_drop_upload_hint": "Släpp för att ladda upp till: {path}",
|
||||
"file_local_upload_hint": "Dra filer till fjärrpanelen eller använd Ladda upp. Nedladdningar sparas i webbläsarens standardmapp.",
|
||||
"file_local_upload_hint": "Släpp filer i rutan till vänster eller klicka för att ladda upp. Målet är mappen som är öppen på fjärrsidan.",
|
||||
"file_transfer_stalled": "Fjärrenheten startade inte överföringen",
|
||||
"file_no_local_tree": "Välj en mapp för att bläddra lokala filer (valfritt)"
|
||||
"file_no_local_tree": "Välj en mapp för att bläddra lokala filer (valfritt)",
|
||||
"file_dropzone_title": "Släpp filer här",
|
||||
"file_dropzone_subtitle": "eller klicka för att välja filer",
|
||||
"file_dropzone_multiple": "Flera filer stöds",
|
||||
"file_dropzone_waiting": "Väntar på fjärranslutning…",
|
||||
"file_overwrite_title": "Filen finns redan",
|
||||
"file_overwrite_message": "En fil med namnet «{name}» finns redan på fjärrdatorn. Skriva över?",
|
||||
"file_overwrite_skip": "Hoppa över",
|
||||
"file_overwrite_skip_all": "Hoppa över alla",
|
||||
"file_overwrite_confirm": "Skriv över",
|
||||
"file_overwrite_all": "Skriv över alla",
|
||||
"file_resume": "Återuppta",
|
||||
"file_open": "Öppna",
|
||||
"file_up": "Upp",
|
||||
"file_home": "Hem",
|
||||
"file_hide_hidden": "Dölj dolda filer"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Varumärkesidentitet",
|
||||
|
||||
+17
-2
@@ -1266,9 +1266,24 @@
|
||||
"file_status_downloaded": "ดาวน์โหลดแล้ว",
|
||||
"file_status_uploaded": "อัปโหลดแล้ว",
|
||||
"file_drop_upload_hint": "วางเพื่ออัปโหลดไปที่: {path}",
|
||||
"file_local_upload_hint": "ลากไฟล์ไปที่แผงระยะไกลหรือใช้การอัปโหลด การดาวน์โหลดจะบันทึกไปยังโฟลเดอร์เริ่มต้นของเบราว์เซอร์",
|
||||
"file_local_upload_hint": "วางไฟล์ในกล่องทางซ้ายหรือคลิกเพื่ออัปโหลด ปลายทางคือโฟลเดอร์ที่เปิดอยู่ฝั่ง remote",
|
||||
"file_transfer_stalled": "อุปกรณ์ระยะไกลไม่เริ่มการถ่ายโอน",
|
||||
"file_no_local_tree": "เลือกโฟลเดอร์เพื่อเรียกดูไฟล์ในเครื่อง (ไม่บังคับ)"
|
||||
"file_no_local_tree": "เลือกโฟลเดอร์เพื่อเรียกดูไฟล์ในเครื่อง (ไม่บังคับ)",
|
||||
"file_dropzone_title": "วางไฟล์ที่นี่",
|
||||
"file_dropzone_subtitle": "หรือคลิกเพื่อเลือกไฟล์",
|
||||
"file_dropzone_multiple": "รองรับหลายไฟล์",
|
||||
"file_dropzone_waiting": "กำลังรอการเชื่อมต่อ remote…",
|
||||
"file_overwrite_title": "มีไฟล์นี้อยู่แล้ว",
|
||||
"file_overwrite_message": "มีไฟล์ «{name}» อยู่บนคอมพิวเตอร์ remote แล้ว เขียนทับหรือไม่?",
|
||||
"file_overwrite_skip": "ข้าม",
|
||||
"file_overwrite_skip_all": "ข้ามทั้งหมด",
|
||||
"file_overwrite_confirm": "เขียนทับ",
|
||||
"file_overwrite_all": "เขียนทับทั้งหมด",
|
||||
"file_resume": "ดำเนินต่อ",
|
||||
"file_open": "เปิด",
|
||||
"file_up": "ขึ้น",
|
||||
"file_home": "หน้าแรก",
|
||||
"file_hide_hidden": "ซ่อนไฟล์ที่ซ่อนอยู่"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "เอกลักษณ์ของแบรนด์",
|
||||
|
||||
+17
-2
@@ -1266,9 +1266,24 @@
|
||||
"file_status_downloaded": "İndirildi",
|
||||
"file_status_uploaded": "Yüklendi",
|
||||
"file_drop_upload_hint": "Yüklemek için bırakın: {path}",
|
||||
"file_local_upload_hint": "Dosyaları uzak panele sürükleyin veya Yükle'yi kullanın. İndirmeler tarayıcının varsayılan klasörüne kaydedilir.",
|
||||
"file_local_upload_hint": "Dosyaları soldaki kutuya bırakın veya yüklemek için tıklayın. Hedef, uzak tarafta açık klasördür.",
|
||||
"file_transfer_stalled": "Uzak cihaz aktarımı başlatmadı",
|
||||
"file_no_local_tree": "Yerel dosyalara göz atmak için klasör seçin (isteğe bağlı)"
|
||||
"file_no_local_tree": "Yerel dosyalara göz atmak için klasör seçin (isteğe bağlı)",
|
||||
"file_dropzone_title": "Dosyaları buraya bırakın",
|
||||
"file_dropzone_subtitle": "veya dosya seçmek için tıklayın",
|
||||
"file_dropzone_multiple": "Birden fazla dosya desteklenir",
|
||||
"file_dropzone_waiting": "Uzak bağlantı bekleniyor…",
|
||||
"file_overwrite_title": "Dosya zaten var",
|
||||
"file_overwrite_message": "«{name}» adlı dosya uzak bilgisayarda zaten mevcut. Üzerine yazılsın mı?",
|
||||
"file_overwrite_skip": "Atla",
|
||||
"file_overwrite_skip_all": "Tümünü atla",
|
||||
"file_overwrite_confirm": "Üzerine yaz",
|
||||
"file_overwrite_all": "Tümünün üzerine yaz",
|
||||
"file_resume": "Sürdür",
|
||||
"file_open": "Aç",
|
||||
"file_up": "Yukarı",
|
||||
"file_home": "Ana",
|
||||
"file_hide_hidden": "Gizli dosyaları gizle"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Marka Kimliği",
|
||||
|
||||
+17
-2
@@ -1266,9 +1266,24 @@
|
||||
"file_status_downloaded": "Завантажено",
|
||||
"file_status_uploaded": "Завантажено",
|
||||
"file_drop_upload_hint": "Перетягніть для завантаження в: {path}",
|
||||
"file_local_upload_hint": "Перетягніть файли на віддалену панель або скористайтеся завантаженням. Завантаження зберігаються в папці браузера за замовчуванням.",
|
||||
"file_local_upload_hint": "Перетягніть файли в поле зліва або натисніть для завантаження. Призначення — папка, відкрита на віддаленій стороні.",
|
||||
"file_transfer_stalled": "Віддалений пристрій не розпочав передачу",
|
||||
"file_no_local_tree": "Оберіть папку для перегляду локальних файлів (необов'язково)"
|
||||
"file_no_local_tree": "Оберіть папку для перегляду локальних файлів (необов'язково)",
|
||||
"file_dropzone_title": "Перетягніть файли сюди",
|
||||
"file_dropzone_subtitle": "або натисніть, щоб вибрати файли",
|
||||
"file_dropzone_multiple": "Підтримується кілька файлів",
|
||||
"file_dropzone_waiting": "Очікування віддаленого з’єднання…",
|
||||
"file_overwrite_title": "Файл уже існує",
|
||||
"file_overwrite_message": "Файл «{name}» уже існує на віддаленому комп’ютері. Перезаписати?",
|
||||
"file_overwrite_skip": "Пропустити",
|
||||
"file_overwrite_skip_all": "Пропустити всі",
|
||||
"file_overwrite_confirm": "Перезаписати",
|
||||
"file_overwrite_all": "Перезаписати всі",
|
||||
"file_resume": "Відновити",
|
||||
"file_open": "Відкрити",
|
||||
"file_up": "Вгору",
|
||||
"file_home": "Початок",
|
||||
"file_hide_hidden": "Приховати приховані файли"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Ідентичність бренду",
|
||||
|
||||
+17
-2
@@ -1266,9 +1266,24 @@
|
||||
"file_status_downloaded": "Đã tải xuống",
|
||||
"file_status_uploaded": "Đã tải lên",
|
||||
"file_drop_upload_hint": "Thả để tải lên: {path}",
|
||||
"file_local_upload_hint": "Kéo tệp vào bảng từ xa hoặc dùng Tải lên. Tệp tải xuống lưu vào thư mục mặc định của trình duyệt.",
|
||||
"file_local_upload_hint": "Thả tệp vào hộp bên trái hoặc nhấp để tải lên. Đích là thư mục đang mở ở phía remote.",
|
||||
"file_transfer_stalled": "Thiết bị từ xa không bắt đầu truyền",
|
||||
"file_no_local_tree": "Chọn thư mục để duyệt tệp cục bộ (tùy chọn)"
|
||||
"file_no_local_tree": "Chọn thư mục để duyệt tệp cục bộ (tùy chọn)",
|
||||
"file_dropzone_title": "Thả tệp vào đây",
|
||||
"file_dropzone_subtitle": "hoặc nhấp để chọn tệp",
|
||||
"file_dropzone_multiple": "Hỗ trợ nhiều tệp",
|
||||
"file_dropzone_waiting": "Đang chờ kết nối remote…",
|
||||
"file_overwrite_title": "Tệp đã tồn tại",
|
||||
"file_overwrite_message": "Tệp «{name}» đã có trên máy remote. Ghi đè?",
|
||||
"file_overwrite_skip": "Bỏ qua",
|
||||
"file_overwrite_skip_all": "Bỏ qua tất cả",
|
||||
"file_overwrite_confirm": "Ghi đè",
|
||||
"file_overwrite_all": "Ghi đè tất cả",
|
||||
"file_resume": "Tiếp tục",
|
||||
"file_open": "Mở",
|
||||
"file_up": "Lên",
|
||||
"file_home": "Trang chủ",
|
||||
"file_hide_hidden": "Ẩn tệp ẩn"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "Nhận diện thương hiệu",
|
||||
|
||||
@@ -1277,9 +1277,24 @@
|
||||
"file_status_downloaded": "已下載",
|
||||
"file_status_uploaded": "已上傳",
|
||||
"file_drop_upload_hint": "拖放到此處上傳到:{path}",
|
||||
"file_local_upload_hint": "將檔案拖到遠端面板或使用上傳。下載會儲存到瀏覽器的預設資料夾。",
|
||||
"file_local_upload_hint": "將檔案拖到左側框中或點擊上傳。目的地為遠端側目前開啟的資料夾。",
|
||||
"file_transfer_stalled": "遠端裝置未開始傳輸",
|
||||
"file_no_local_tree": "選擇資料夾以瀏覽本機檔案(選用)"
|
||||
"file_no_local_tree": "選擇資料夾以瀏覽本機檔案(選用)",
|
||||
"file_dropzone_title": "將檔案拖放到此處",
|
||||
"file_dropzone_subtitle": "或點擊選擇檔案",
|
||||
"file_dropzone_multiple": "支援多個檔案",
|
||||
"file_dropzone_waiting": "正在等待遠端連線…",
|
||||
"file_overwrite_title": "檔案已存在",
|
||||
"file_overwrite_message": "遠端電腦上已存在名為「{name}」的檔案。是否覆寫?",
|
||||
"file_overwrite_skip": "略過",
|
||||
"file_overwrite_skip_all": "全部略過",
|
||||
"file_overwrite_confirm": "覆寫",
|
||||
"file_overwrite_all": "全部覆寫",
|
||||
"file_resume": "繼續",
|
||||
"file_open": "開啟",
|
||||
"file_up": "上層",
|
||||
"file_home": "首頁",
|
||||
"file_hide_hidden": "隱藏隱藏檔案"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "品牌標識",
|
||||
|
||||
+17
-2
@@ -1260,9 +1260,24 @@
|
||||
"file_status_downloaded": "已下载",
|
||||
"file_status_uploaded": "已上传",
|
||||
"file_drop_upload_hint": "拖放到此处上传到:{path}",
|
||||
"file_local_upload_hint": "将文件拖到远程面板或使用上传。下载会保存到浏览器的默认文件夹。",
|
||||
"file_local_upload_hint": "将文件拖到左侧框中或点击上传。目标为远程侧当前打开的文件夹。",
|
||||
"file_transfer_stalled": "远程设备未开始传输",
|
||||
"file_no_local_tree": "选择文件夹以浏览本地文件(可选)"
|
||||
"file_no_local_tree": "选择文件夹以浏览本地文件(可选)",
|
||||
"file_dropzone_title": "将文件拖放到此处",
|
||||
"file_dropzone_subtitle": "或点击选择文件",
|
||||
"file_dropzone_multiple": "支持多个文件",
|
||||
"file_dropzone_waiting": "正在等待远程连接…",
|
||||
"file_overwrite_title": "文件已存在",
|
||||
"file_overwrite_message": "远程计算机上已存在名为「{name}」的文件。是否覆盖?",
|
||||
"file_overwrite_skip": "跳过",
|
||||
"file_overwrite_skip_all": "全部跳过",
|
||||
"file_overwrite_confirm": "覆盖",
|
||||
"file_overwrite_all": "全部覆盖",
|
||||
"file_resume": "继续",
|
||||
"file_open": "打开",
|
||||
"file_up": "上级",
|
||||
"file_home": "主页",
|
||||
"file_hide_hidden": "隐藏隐藏文件"
|
||||
},
|
||||
"branding": {
|
||||
"identity_title": "品牌标识",
|
||||
|
||||
@@ -1449,6 +1449,7 @@
|
||||
flex-direction: column;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ft-modal-header {
|
||||
@@ -1661,14 +1662,191 @@
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.ft-local-hint {
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
.ft-local-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ft-local-dropzone {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin: 12px;
|
||||
min-height: 180px;
|
||||
padding: 24px 16px;
|
||||
border: 2px dashed var(--border-primary, #30363d);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ft-local-dropzone.compact {
|
||||
flex: 0 0 auto;
|
||||
min-height: 0;
|
||||
padding: 10px 12px;
|
||||
margin: 8px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.ft-local-dropzone.compact .ft-dropzone-icon {
|
||||
font-size: 24px !important;
|
||||
}
|
||||
|
||||
.ft-local-dropzone.compact .ft-dropzone-subtitle,
|
||||
.ft-local-dropzone.compact .ft-dropzone-multiple {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ft-local-dropzone:hover:not(.ft-dropzone-disabled) {
|
||||
border-color: var(--accent, #3884ff);
|
||||
background: rgba(56, 132, 255, 0.04);
|
||||
}
|
||||
|
||||
.ft-local-dropzone.ft-dropzone-active {
|
||||
border-color: var(--accent, #3884ff);
|
||||
background: rgba(56, 132, 255, 0.08);
|
||||
}
|
||||
|
||||
.ft-local-dropzone.ft-dropzone-disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ft-dropzone-icon {
|
||||
font-size: 40px !important;
|
||||
color: var(--accent, #3884ff);
|
||||
}
|
||||
|
||||
.ft-dropzone-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #e6edf3);
|
||||
}
|
||||
|
||||
.ft-dropzone-subtitle,
|
||||
.ft-dropzone-multiple {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #8b949e);
|
||||
}
|
||||
|
||||
.ft-local-hint .ft-hint-text {
|
||||
display: inline;
|
||||
.ft-dropzone-target {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #8b949e);
|
||||
margin-top: 4px;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.ft-btn.active {
|
||||
color: var(--accent, #3884ff);
|
||||
}
|
||||
|
||||
.ft-overwrite-dialog {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.ft-overwrite-dialog[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ft-overwrite-panel {
|
||||
background: var(--bg-secondary, #161b22);
|
||||
border: 1px solid var(--border-primary, #30363d);
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
max-width: 420px;
|
||||
width: 90%;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.ft-overwrite-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ft-overwrite-message {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #8b949e);
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ft-overwrite-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.ft-context-menu {
|
||||
position: fixed;
|
||||
z-index: 300;
|
||||
min-width: 160px;
|
||||
background: var(--bg-secondary, #161b22);
|
||||
border: 1px solid var(--border-primary, #30363d);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.ft-context-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-primary, #e6edf3);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ft-context-item:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.ft-context-item .material-icons {
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary, #8b949e);
|
||||
}
|
||||
|
||||
.ft-transfer-resume {
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--accent, #3884ff);
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ft-transfer-resume:hover {
|
||||
background: rgba(56, 132, 255, 0.12);
|
||||
}
|
||||
|
||||
.ft-transfer-resume .material-icons {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.ft-btn:disabled {
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
this._selectedRemote = null;
|
||||
this._transferMeta = new Map();
|
||||
this._wiredSessionId = null;
|
||||
this._remoteReady = false;
|
||||
this._contextMenu = null;
|
||||
}
|
||||
|
||||
FileTransferModal.prototype._ensureDom = function () {
|
||||
@@ -55,19 +57,29 @@
|
||||
'<div class="ft-pane ft-pane-local">' +
|
||||
'<div class="ft-pane-head"><span class="material-icons">computer</span><span class="ft-pane-label"></span></div>' +
|
||||
'<div class="ft-pane-toolbar">' +
|
||||
'<button type="button" class="ft-btn ft-local-pick"><span class="material-icons">folder_open</span></button>' +
|
||||
'<button type="button" class="ft-btn ft-local-pick" title=""><span class="material-icons">folder_open</span></button>' +
|
||||
'<button type="button" class="ft-btn ft-local-up"><span class="material-icons">arrow_upward</span></button>' +
|
||||
'<button type="button" class="ft-btn ft-local-home"><span class="material-icons">home</span></button>' +
|
||||
'<span class="ft-path ft-local-path"></span>' +
|
||||
'<button type="button" class="ft-btn ft-btn-action ft-local-send"></button>' +
|
||||
'</div>' +
|
||||
'<div class="ft-local-hint"></div>' +
|
||||
'<div class="ft-list ft-local-list"></div>' +
|
||||
'<div class="ft-local-body">' +
|
||||
'<div class="ft-local-dropzone ft-dropzone-disabled" tabindex="0" role="button">' +
|
||||
'<span class="material-icons ft-dropzone-icon">upload_file</span>' +
|
||||
'<span class="ft-dropzone-title"></span>' +
|
||||
'<span class="ft-dropzone-subtitle"></span>' +
|
||||
'<span class="ft-dropzone-multiple"></span>' +
|
||||
'<span class="ft-dropzone-target"></span>' +
|
||||
'</div>' +
|
||||
'<div class="ft-list ft-local-list" hidden></div>' +
|
||||
'</div>' +
|
||||
'<input type="file" class="ft-local-file-input" multiple hidden />' +
|
||||
'</div>' +
|
||||
'<div class="ft-pane ft-pane-remote">' +
|
||||
'<div class="ft-pane-head"><span class="material-icons">desktop_windows</span><span class="ft-pane-label"></span></div>' +
|
||||
'<div class="ft-pane-toolbar">' +
|
||||
'<button type="button" class="ft-btn ft-remote-new-folder" title=""><span class="material-icons">create_new_folder</span></button>' +
|
||||
'<button type="button" class="ft-btn ft-remote-hidden" title=""><span class="material-icons">visibility_off</span></button>' +
|
||||
'<button type="button" class="ft-btn ft-remote-up"><span class="material-icons">arrow_upward</span></button>' +
|
||||
'<button type="button" class="ft-btn ft-remote-home"><span class="material-icons">home</span></button>' +
|
||||
'<span class="ft-path ft-remote-path"></span>' +
|
||||
@@ -85,6 +97,18 @@
|
||||
'<div class="ft-queue-empty"><span class="material-icons">sync_alt</span><span></span></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="ft-overwrite-dialog" hidden role="alertdialog" aria-modal="true">' +
|
||||
'<div class="ft-overwrite-panel">' +
|
||||
'<div class="ft-overwrite-title"></div>' +
|
||||
'<div class="ft-overwrite-message"></div>' +
|
||||
'<div class="ft-overwrite-actions">' +
|
||||
'<button type="button" class="btn btn-sm btn-secondary ft-overwrite-skip"></button>' +
|
||||
'<button type="button" class="btn btn-sm btn-secondary ft-overwrite-skip-all"></button>' +
|
||||
'<button type="button" class="btn btn-sm btn-primary ft-overwrite-confirm"></button>' +
|
||||
'<button type="button" class="btn btn-sm btn-primary ft-overwrite-all"></button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(el);
|
||||
this._el = el;
|
||||
@@ -94,6 +118,12 @@
|
||||
el.querySelector('.ft-local-send').innerHTML = t('remote.file_send', 'Send') + ' <span class="material-icons">chevron_right</span>';
|
||||
el.querySelector('.ft-remote-download').innerHTML = '<span class="material-icons">chevron_left</span> ' + t('remote.file_download', 'Download');
|
||||
el.querySelector('.ft-queue-empty span:last-child').textContent = t('remote.file_queue_empty', 'No transfers in progress');
|
||||
el.querySelector('.ft-local-pick').title = t('remote.file_pick_folder', 'Choose folder');
|
||||
el.querySelector('.ft-remote-new-folder').title = t('remote.file_new_folder', 'New Folder');
|
||||
el.querySelector('.ft-remote-hidden').title = t('remote.file_show_hidden', 'Show hidden files');
|
||||
|
||||
this._initDropzoneLabels();
|
||||
this._initOverwriteDialogLabels();
|
||||
|
||||
var self = this;
|
||||
el.querySelector('.ft-modal-close').addEventListener('click', function () { self.close(); });
|
||||
@@ -105,12 +135,45 @@
|
||||
el.querySelector('.ft-remote-up').addEventListener('click', function () { self._remoteGoUp(); });
|
||||
el.querySelector('.ft-remote-home').addEventListener('click', function () { self._remoteGoHome(); });
|
||||
el.querySelector('.ft-remote-download').addEventListener('click', function () { self._downloadSelectedRemote(); });
|
||||
el.querySelector('.ft-remote-new-folder').addEventListener('click', function () { self._remoteNewFolder(); });
|
||||
el.querySelector('.ft-remote-hidden').addEventListener('click', function () { self._toggleRemoteHidden(); });
|
||||
el.querySelector('.ft-local-file-input').addEventListener('change', function (e) {
|
||||
self._uploadFiles(e.target.files);
|
||||
e.target.value = '';
|
||||
});
|
||||
|
||||
var dropzone = el.querySelector('.ft-local-dropzone');
|
||||
dropzone.addEventListener('click', function () {
|
||||
if (dropzone.classList.contains('ft-dropzone-disabled')) return;
|
||||
self._el.querySelector('.ft-local-file-input').click();
|
||||
});
|
||||
dropzone.addEventListener('keydown', function (e) {
|
||||
if (dropzone.classList.contains('ft-dropzone-disabled')) return;
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
self._el.querySelector('.ft-local-file-input').click();
|
||||
}
|
||||
});
|
||||
dropzone.addEventListener('dragover', function (e) {
|
||||
if (dropzone.classList.contains('ft-dropzone-disabled')) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
dropzone.classList.add('ft-dropzone-active');
|
||||
});
|
||||
dropzone.addEventListener('dragleave', function (e) {
|
||||
if (dropzone.contains(e.relatedTarget)) return;
|
||||
dropzone.classList.remove('ft-dropzone-active');
|
||||
});
|
||||
dropzone.addEventListener('drop', function (e) {
|
||||
if (dropzone.classList.contains('ft-dropzone-disabled')) return;
|
||||
e.preventDefault();
|
||||
dropzone.classList.remove('ft-dropzone-active');
|
||||
if (e.dataTransfer && e.dataTransfer.files.length) self._uploadFiles(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
var remotePane = el.querySelector('.ft-pane-remote');
|
||||
remotePane.addEventListener('dragover', function (e) {
|
||||
if (!self._remoteReady) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
remotePane.classList.add('ft-drag-over');
|
||||
@@ -122,11 +185,66 @@
|
||||
self._hideDragOverlay();
|
||||
});
|
||||
remotePane.addEventListener('drop', function (e) {
|
||||
if (!self._remoteReady) return;
|
||||
e.preventDefault();
|
||||
remotePane.classList.remove('ft-drag-over');
|
||||
self._hideDragOverlay();
|
||||
if (e.dataTransfer && e.dataTransfer.files.length) self._uploadFiles(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
document.addEventListener('click', function () { self._hideContextMenu(); });
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._initDropzoneLabels = function () {
|
||||
if (!this._el) return;
|
||||
var dz = this._el.querySelector('.ft-local-dropzone');
|
||||
if (!dz) return;
|
||||
dz.querySelector('.ft-dropzone-title').textContent = t('remote.file_dropzone_title', 'Drop files here');
|
||||
dz.querySelector('.ft-dropzone-subtitle').textContent = t('remote.file_dropzone_subtitle', 'or click to choose files');
|
||||
dz.querySelector('.ft-dropzone-multiple').textContent = t('remote.file_dropzone_multiple', 'Multiple files supported');
|
||||
dz.setAttribute('aria-label', t('remote.file_dropzone_title', 'Drop files here'));
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._initOverwriteDialogLabels = function () {
|
||||
if (!this._el) return;
|
||||
this._el.querySelector('.ft-overwrite-title').textContent = t('remote.file_overwrite_title', 'File already exists');
|
||||
this._el.querySelector('.ft-overwrite-skip').textContent = t('remote.file_overwrite_skip', 'Skip');
|
||||
this._el.querySelector('.ft-overwrite-skip-all').textContent = t('remote.file_overwrite_skip_all', 'Skip all');
|
||||
this._el.querySelector('.ft-overwrite-confirm').textContent = t('remote.file_overwrite_confirm', 'Overwrite');
|
||||
this._el.querySelector('.ft-overwrite-all').textContent = t('remote.file_overwrite_all', 'Overwrite all');
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._updateDropzoneTarget = function () {
|
||||
if (!this._el) return;
|
||||
var target = this._el.querySelector('.ft-dropzone-target');
|
||||
if (!target) return;
|
||||
var ft = this._session && this._session.client && this._session.client.fileTransfer;
|
||||
var path = (ft && ft.currentPath) ? ft.currentPath : t('remote.file_remote', 'Remote');
|
||||
var hint = t('remote.file_drop_upload_hint', 'Drop to upload to: {path}');
|
||||
target.textContent = hint.replace('{path}', path);
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._setDropzoneEnabled = function (enabled) {
|
||||
if (!this._el) return;
|
||||
this._remoteReady = !!enabled;
|
||||
var dz = this._el.querySelector('.ft-local-dropzone');
|
||||
if (!dz) return;
|
||||
dz.classList.toggle('ft-dropzone-disabled', !enabled);
|
||||
if (!enabled) {
|
||||
var wait = t('remote.file_dropzone_waiting', 'Waiting for remote connection…');
|
||||
dz.querySelector('.ft-dropzone-target').textContent = wait;
|
||||
} else {
|
||||
this._updateDropzoneTarget();
|
||||
}
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._syncLocalLayout = function () {
|
||||
if (!this._el) return;
|
||||
var hasRoot = this._local.hasRoot;
|
||||
var dz = this._el.querySelector('.ft-local-dropzone');
|
||||
var list = this._el.querySelector('.ft-local-list');
|
||||
if (dz) dz.classList.toggle('compact', hasRoot);
|
||||
if (list) list.hidden = !hasRoot;
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._syncSaveDownloadHook = function () {
|
||||
@@ -163,6 +281,7 @@
|
||||
var noTreeTitle = t('remote.file_no_local_tree', 'Choose a folder to browse local files (optional)');
|
||||
var upBtn = this._el.querySelector('.ft-local-up');
|
||||
var homeBtn = this._el.querySelector('.ft-local-home');
|
||||
var sendBtn = this._el.querySelector('.ft-local-send');
|
||||
if (upBtn) {
|
||||
if (!upBtn.dataset.defaultTitle) upBtn.title = t('remote.file_up', 'Up');
|
||||
upBtn.dataset.defaultTitle = upBtn.title;
|
||||
@@ -175,6 +294,24 @@
|
||||
homeBtn.disabled = !hasRoot;
|
||||
homeBtn.title = hasRoot ? homeBtn.dataset.defaultTitle : noTreeTitle;
|
||||
}
|
||||
if (sendBtn) sendBtn.style.display = hasRoot ? '' : 'none';
|
||||
this._syncLocalLayout();
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._updateRemoteHiddenButton = function () {
|
||||
var btn = this._el && this._el.querySelector('.ft-remote-hidden');
|
||||
var ft = this._session && this._session.client && this._session.client.fileTransfer;
|
||||
if (!btn || !ft) return;
|
||||
var icon = btn.querySelector('.material-icons');
|
||||
if (ft.showHidden) {
|
||||
icon.textContent = 'visibility';
|
||||
btn.title = t('remote.file_hide_hidden', 'Hide hidden files');
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
icon.textContent = 'visibility_off';
|
||||
btn.title = t('remote.file_show_hidden', 'Show hidden files');
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
};
|
||||
|
||||
FileTransferModal.prototype.open = function (session) {
|
||||
@@ -182,15 +319,18 @@
|
||||
this._session = session;
|
||||
if (!session.client || !session.client.fileTransfer) return;
|
||||
this._transferMeta.clear();
|
||||
this._remoteReady = false;
|
||||
this._el.querySelector('.ft-modal-title').textContent =
|
||||
t('remote.file_transfer', 'File Transfer') + ' — ' + (session.deviceName || session.deviceId);
|
||||
this._updateLocalHint();
|
||||
this._initDropzoneLabels();
|
||||
this._setDropzoneEnabled(false);
|
||||
this._updateLocalToolbarState();
|
||||
this._renderLocalList();
|
||||
this._remoteEntries = [];
|
||||
this._selectedRemote = null;
|
||||
this._el.querySelector('.ft-remote-path').textContent = '';
|
||||
this._renderRemoteLoading(t('remote.file_connecting', 'Connecting file transfer session…'));
|
||||
this._updateRemoteHiddenButton();
|
||||
this._el.style.display = 'flex';
|
||||
document.getElementById('btn-file-transfer')?.classList.add('active');
|
||||
if (this._wiredSessionId !== session.deviceId) {
|
||||
@@ -205,6 +345,8 @@
|
||||
FileTransferModal.prototype.close = function () {
|
||||
if (this._el) this._el.style.display = 'none';
|
||||
this._hideDragOverlay();
|
||||
this._hideContextMenu();
|
||||
this._hideOverwriteDialog();
|
||||
document.getElementById('btn-file-transfer')?.classList.remove('active');
|
||||
if (this._session && this._session.client && this._session.client.fileTransfer) {
|
||||
this._session.client.fileTransfer._saveDownload = null;
|
||||
@@ -222,56 +364,43 @@
|
||||
if (!self._remoteEntries.length) {
|
||||
self._renderRemoteLoading(t('remote.file_loading', 'Loading…'));
|
||||
}
|
||||
self._setDropzoneEnabled(false);
|
||||
});
|
||||
client.on('file_connecting', function () {
|
||||
if (!self._remoteEntries.length) {
|
||||
self._renderRemoteLoading(t('remote.file_connecting', 'Connecting file transfer session…'));
|
||||
}
|
||||
self._setDropzoneEnabled(false);
|
||||
});
|
||||
client.on('file_connect_error', function (data) {
|
||||
self._renderRemoteError(data.error || t('remote.file_connect_failed', 'Could not open file transfer session'));
|
||||
self._setDropzoneEnabled(false);
|
||||
});
|
||||
client.on('file_dir', function (data) {
|
||||
self._remoteEntries = data.entries || [];
|
||||
self._selectedRemote = null;
|
||||
self._el.querySelector('.ft-remote-path').textContent = data.path || '/';
|
||||
self._renderRemoteList();
|
||||
self._setDropzoneEnabled(true);
|
||||
self._updateDropzoneTarget();
|
||||
self._updateRemoteHiddenButton();
|
||||
});
|
||||
client.on('file_browse_timeout', function () {
|
||||
self._renderRemoteError(t('remote.file_timeout', 'Remote device did not respond.'));
|
||||
self._setDropzoneEnabled(false);
|
||||
});
|
||||
client.on('file_transfer_start', function (data) { self._addTransfer(data); });
|
||||
client.on('file_transfer_progress', function (data) { self._updateTransfer(data); });
|
||||
client.on('file_transfer_complete', function (data) { self._completeTransfer(data); });
|
||||
client.on('file_transfer_error', function (data) { self._errorTransfer(data); });
|
||||
client.on('file_transfer_cancelled', function (data) { self._removeTransfer(data.id); });
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._updateLocalHint = function () {
|
||||
var self = this;
|
||||
var hint = this._el.querySelector('.ft-local-hint');
|
||||
if (this._local.hasRoot) {
|
||||
hint.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (LocalFiles.isSupported()) {
|
||||
hint.innerHTML = '<button type="button" class="btn btn-sm btn-secondary ft-local-pick-inline">' +
|
||||
escapeHtml(t('remote.file_pick_folder', 'Choose folder')) + '</button>' +
|
||||
'<span class="ft-hint-text"> ' +
|
||||
escapeHtml(t('remote.file_local_upload_hint',
|
||||
'Drag files onto the remote panel or use Upload. Downloads save to your browser\'s default folder.')) +
|
||||
'</span>';
|
||||
hint.querySelector('.ft-local-pick-inline').addEventListener('click', function () { self._pickLocalFolder(); });
|
||||
} else {
|
||||
hint.innerHTML = '<span class="ft-hint-text">' +
|
||||
escapeHtml(t('remote.file_local_upload_hint',
|
||||
'Drag files onto the remote panel or use Upload. Downloads save to your browser\'s default folder.')) +
|
||||
'</span> <button type="button" class="btn btn-sm btn-secondary ft-local-upload-fallback">' +
|
||||
escapeHtml(t('remote.file_upload', 'Upload')) + '</button>';
|
||||
hint.querySelector('.ft-local-upload-fallback').addEventListener('click', function () {
|
||||
self._el.querySelector('.ft-local-file-input').click();
|
||||
});
|
||||
}
|
||||
client.on('file_transfer_overwrite_prompt', function (data) { self._showOverwriteDialog(data); });
|
||||
client.on('file_action', function () {
|
||||
setTimeout(function () {
|
||||
var ft = self._session && self._session.client && self._session.client.fileTransfer;
|
||||
if (ft) ft.browseDir(ft.currentPath || '');
|
||||
}, 400);
|
||||
});
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._pickLocalFolder = async function () {
|
||||
@@ -281,7 +410,6 @@
|
||||
this._localEntries = await this._local.listCurrent();
|
||||
this._selectedLocal = null;
|
||||
this._renderLocalList();
|
||||
this._el.querySelector('.ft-local-hint').innerHTML = '';
|
||||
this._syncSaveDownloadHook();
|
||||
this._updateLocalToolbarState();
|
||||
} catch (e) {
|
||||
@@ -309,13 +437,11 @@
|
||||
|
||||
FileTransferModal.prototype._renderLocalList = function () {
|
||||
var list = this._el.querySelector('.ft-local-list');
|
||||
list.innerHTML = '';
|
||||
if (!this._local.hasRoot) {
|
||||
list.innerHTML = '<div class="ft-empty">' + escapeHtml(
|
||||
t('remote.file_local_upload_hint', 'Drag files onto the remote panel or use Upload.')
|
||||
) + '</div>';
|
||||
list.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = '';
|
||||
if (!this._localEntries.length) {
|
||||
list.innerHTML = '<div class="ft-empty">' + escapeHtml(t('remote.file_empty', 'No files')) + '</div>';
|
||||
return;
|
||||
@@ -368,6 +494,13 @@
|
||||
});
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._buildRemoteFullPath = function (entry) {
|
||||
var ft = this._session.client.fileTransfer;
|
||||
var sep = (ft.currentPath || '').includes('\\') ? '\\' : '/';
|
||||
var base = ft.currentPath || '';
|
||||
return base ? (base.replace(/[\\/]+$/, '') + sep + entry.name) : entry.name;
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._renderRemoteList = function () {
|
||||
var list = this._el.querySelector('.ft-remote-list');
|
||||
list.innerHTML = '';
|
||||
@@ -399,6 +532,10 @@
|
||||
row.addEventListener('dblclick', function () {
|
||||
if (!entry.isDir) self._downloadRemoteEntry(entry);
|
||||
});
|
||||
row.addEventListener('contextmenu', function (e) {
|
||||
e.preventDefault();
|
||||
self._showRemoteContextMenu(e, entry);
|
||||
});
|
||||
list.appendChild(row);
|
||||
});
|
||||
};
|
||||
@@ -411,6 +548,121 @@
|
||||
this._session.client?.fileTransfer?.browseDir('');
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._remoteNewFolder = function () {
|
||||
var ft = this._session.client?.fileTransfer;
|
||||
if (!ft) return;
|
||||
var name = window.prompt(t('remote.file_new_folder_prompt', 'Enter folder name:'));
|
||||
if (!name || !name.trim()) return;
|
||||
var sep = (ft.currentPath || '').includes('\\') ? '\\' : '/';
|
||||
var base = ft.currentPath || '';
|
||||
var path = base ? (base.replace(/[\\/]+$/, '') + sep + name.trim()) : name.trim();
|
||||
ft.createDir(path);
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._toggleRemoteHidden = function () {
|
||||
var ft = this._session.client?.fileTransfer;
|
||||
if (!ft) return;
|
||||
ft.setShowHidden(!ft.showHidden);
|
||||
this._updateRemoteHiddenButton();
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._hideContextMenu = function () {
|
||||
if (this._contextMenu) {
|
||||
this._contextMenu.remove();
|
||||
this._contextMenu = null;
|
||||
}
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._showRemoteContextMenu = function (e, entry) {
|
||||
var self = this;
|
||||
this._hideContextMenu();
|
||||
var menu = document.createElement('div');
|
||||
menu.className = 'ft-context-menu';
|
||||
menu.style.left = e.clientX + 'px';
|
||||
menu.style.top = e.clientY + 'px';
|
||||
|
||||
function addItem(label, icon, action) {
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'ft-context-item';
|
||||
btn.innerHTML = '<span class="material-icons">' + icon + '</span><span>' + escapeHtml(label) + '</span>';
|
||||
btn.addEventListener('click', function () {
|
||||
self._hideContextMenu();
|
||||
action();
|
||||
});
|
||||
menu.appendChild(btn);
|
||||
}
|
||||
|
||||
if (entry.isDir) {
|
||||
addItem(t('remote.file_open', 'Open'), 'folder_open', function () {
|
||||
var ft = self._session.client.fileTransfer;
|
||||
var sep = (ft.currentPath || '').includes('\\') ? '\\' : '/';
|
||||
var base = ft.currentPath || '';
|
||||
var next = base ? (base.replace(/[\\/]+$/, '') + sep + entry.name) : entry.name;
|
||||
ft.browseDir(next);
|
||||
});
|
||||
} else {
|
||||
addItem(t('remote.file_download', 'Download'), 'download', function () {
|
||||
self._downloadRemoteEntry(entry);
|
||||
});
|
||||
}
|
||||
addItem(t('remote.file_rename', 'Rename'), 'drive_file_rename_outline', function () {
|
||||
self._remoteRename(entry);
|
||||
});
|
||||
addItem(t('actions.delete', 'Delete'), 'delete_outline', function () {
|
||||
self._remoteDelete(entry);
|
||||
});
|
||||
|
||||
document.body.appendChild(menu);
|
||||
this._contextMenu = menu;
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._remoteRename = function (entry) {
|
||||
var ft = this._session.client?.fileTransfer;
|
||||
if (!ft) return;
|
||||
var newName = window.prompt(t('remote.file_rename_prompt', 'Enter new name:'), entry.name);
|
||||
if (!newName || !newName.trim() || newName.trim() === entry.name) return;
|
||||
ft.rename(this._buildRemoteFullPath(entry), newName.trim());
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._remoteDelete = function (entry) {
|
||||
var ft = this._session.client?.fileTransfer;
|
||||
if (!ft) return;
|
||||
if (!window.confirm(t('remote.file_delete_confirm', 'Are you sure you want to delete this item?'))) return;
|
||||
var path = this._buildRemoteFullPath(entry);
|
||||
if (entry.isDir) ft.removeDir(path, true);
|
||||
else ft.removeFile(path);
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._showOverwriteDialog = function (data) {
|
||||
var dlg = this._el.querySelector('.ft-overwrite-dialog');
|
||||
if (!dlg) return;
|
||||
var msg = t('remote.file_overwrite_message', 'A file named "{name}" already exists on the remote computer. Overwrite it?');
|
||||
dlg.querySelector('.ft-overwrite-message').textContent = msg.replace('{name}', data.fileName || 'file');
|
||||
dlg.hidden = false;
|
||||
var self = this;
|
||||
var ft = this._session.client?.fileTransfer;
|
||||
var id = data.id;
|
||||
var bind = function (sel, skip, all) {
|
||||
var btn = dlg.querySelector(sel);
|
||||
var clone = btn.cloneNode(true);
|
||||
btn.parentNode.replaceChild(clone, btn);
|
||||
clone.addEventListener('click', function () {
|
||||
self._hideOverwriteDialog();
|
||||
if (ft) ft.confirmOverwrite(id, skip, all);
|
||||
});
|
||||
};
|
||||
bind('.ft-overwrite-skip', true, false);
|
||||
bind('.ft-overwrite-skip-all', true, true);
|
||||
bind('.ft-overwrite-confirm', false, false);
|
||||
bind('.ft-overwrite-all', false, true);
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._hideOverwriteDialog = function () {
|
||||
var dlg = this._el && this._el.querySelector('.ft-overwrite-dialog');
|
||||
if (dlg) dlg.hidden = true;
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._sendSelectedLocal = function () {
|
||||
if (this._selectedLocal && !this._selectedLocal.isDir) this._uploadLocalEntry(this._selectedLocal);
|
||||
};
|
||||
@@ -424,7 +676,7 @@
|
||||
|
||||
FileTransferModal.prototype._uploadFiles = function (fileList) {
|
||||
var ft = this._session.client?.fileTransfer;
|
||||
if (!ft || !fileList) return;
|
||||
if (!ft || !fileList || !this._remoteReady) return;
|
||||
for (var i = 0; i < fileList.length; i++) ft.uploadFile(fileList[i], ft.currentPath || '');
|
||||
};
|
||||
|
||||
@@ -449,7 +701,8 @@
|
||||
percent: 0,
|
||||
transferred: 0,
|
||||
total: data.fileSize || 0,
|
||||
error: null
|
||||
error: null,
|
||||
resumable: false
|
||||
});
|
||||
}
|
||||
return this._transferMeta.get(id);
|
||||
@@ -478,12 +731,14 @@
|
||||
|
||||
var icon = meta.type === 'upload' ? 'upload' : 'download';
|
||||
if (meta.phase === 'pending') icon = 'hourglass_empty';
|
||||
if (meta.error && meta.resumable) icon = 'replay';
|
||||
|
||||
var indeterminate = meta.phase === 'pending';
|
||||
var fillWidth = meta.error ? 0 : Math.min(100, meta.percent || 0);
|
||||
var fillClass = 'ft-transfer-fill' + (indeterminate ? ' indeterminate' : '');
|
||||
var fillStyle = indeterminate ? '' : ' style="width:' + fillWidth + '%"';
|
||||
var canCancel = !meta.error && meta.phase !== 'done' && meta.phase !== 'saving';
|
||||
var canResume = meta.error && meta.resumable;
|
||||
|
||||
return '<div class="' + rowClass.join(' ') + '" data-id="' + meta.id + '">' +
|
||||
'<span class="material-icons ft-transfer-icon">' + icon + '</span>' +
|
||||
@@ -492,6 +747,8 @@
|
||||
'<div class="ft-transfer-bar"><div class="' + fillClass + '"' + fillStyle + '></div></div>' +
|
||||
'<div class="ft-transfer-status">' + escapeHtml(this._transferStatusText(meta)) + '</div>' +
|
||||
'</div>' +
|
||||
(canResume ? '<button type="button" class="ft-transfer-resume" title="' + escapeHtml(t('remote.file_resume', 'Resume')) + '">' +
|
||||
'<span class="material-icons">replay</span></button>' : '') +
|
||||
(canCancel ? '<button type="button" class="ft-transfer-cancel" title="' + escapeHtml(t('common.cancel', 'Cancel')) + '">' +
|
||||
'<span class="material-icons">close</span></button>' : '') +
|
||||
'</div>';
|
||||
@@ -517,6 +774,23 @@
|
||||
if (id && ft) ft.cancelTransfer(id);
|
||||
});
|
||||
});
|
||||
list.querySelectorAll('.ft-transfer-resume').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var row = btn.closest('.ft-transfer-row');
|
||||
var id = Number(row && row.dataset.id);
|
||||
var ft = self._session.client?.fileTransfer;
|
||||
if (id && ft) {
|
||||
var meta = self._transferMeta.get(id);
|
||||
if (meta) {
|
||||
meta.error = null;
|
||||
meta.phase = 'pending';
|
||||
meta.resumable = false;
|
||||
}
|
||||
ft.resumeTransfer(id);
|
||||
self._paintTransferQueue();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
FileTransferModal.prototype._addTransfer = function (data) {
|
||||
@@ -527,6 +801,7 @@
|
||||
meta.phase = 'pending';
|
||||
meta.percent = 0;
|
||||
meta.error = null;
|
||||
meta.resumable = false;
|
||||
this._paintTransferQueue();
|
||||
};
|
||||
|
||||
@@ -549,6 +824,8 @@
|
||||
meta.phase = 'done';
|
||||
meta.percent = 100;
|
||||
meta.transferred = meta.total || meta.transferred;
|
||||
meta.error = null;
|
||||
meta.resumable = false;
|
||||
this._paintTransferQueue();
|
||||
};
|
||||
|
||||
@@ -560,6 +837,7 @@
|
||||
}
|
||||
meta.error = errMsg;
|
||||
meta.phase = 'error';
|
||||
meta.resumable = !!data.resumable;
|
||||
this._paintTransferQueue();
|
||||
};
|
||||
|
||||
|
||||
@@ -56,9 +56,41 @@ class RDFileTransfer {
|
||||
FILE_LINK: 5
|
||||
};
|
||||
|
||||
// Block size for uploads (64KB, matching RustDesk default)
|
||||
this.BLOCK_SIZE = 65536;
|
||||
// Block size for uploads (128KB, matching hbb_common BUF_SIZE)
|
||||
this.BLOCK_SIZE = 131072;
|
||||
this.TRANSFER_STALL_MS = 15000;
|
||||
|
||||
/** @type {'overwrite'|'skip'|null} Session-wide overwrite strategy */
|
||||
this._overwriteStrategy = null;
|
||||
|
||||
/** @type {Map<number, Object>} Pending overwrite prompts */
|
||||
this._pendingOverwrite = new Map();
|
||||
}
|
||||
|
||||
/** Extensions that skip zstd compression (RustDesk is_compressed_file parity) */
|
||||
static get COMPRESSED_EXTENSIONS() {
|
||||
return new Set(['xz', 'gz', 'zip', '7z', 'rar', 'bz2', 'tgz', 'png', 'jpg', 'jpeg', 'webp', 'gif', 'mp4', 'mp3', 'avi', 'mkv']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fileName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isPreCompressedFileName(fileName) {
|
||||
const ext = String(fileName || '').split('.').pop().toLowerCase();
|
||||
return RDFileTransfer.COMPRESSED_EXTENSIONS.has(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} transferredSize
|
||||
* @param {number} blockSize
|
||||
* @returns {number}
|
||||
*/
|
||||
static computeOffsetBlk(transferredSize, blockSize) {
|
||||
const size = Number(transferredSize || 0);
|
||||
const blk = Number(blockSize || 131072);
|
||||
if (!size || !blk) return 0;
|
||||
return Math.floor(size / blk);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,16 +117,26 @@ class RDFileTransfer {
|
||||
return dir + (dir.endsWith(sep) ? '' : sep) + (fileName || '');
|
||||
}
|
||||
|
||||
_failTransfer(id, message) {
|
||||
_failTransfer(id, message, opts) {
|
||||
const options = opts || {};
|
||||
const transfer = this._transfers.get(id);
|
||||
if (!transfer) return;
|
||||
this._clearTransferTimeout(id);
|
||||
const fileName = transfer.fileName;
|
||||
this._transfers.delete(id);
|
||||
const resumable = !!options.resumable;
|
||||
if (resumable) {
|
||||
transfer.status = 'error';
|
||||
transfer.errorMessage = message || 'Transfer failed';
|
||||
transfer.resumable = true;
|
||||
} else {
|
||||
this._transfers.delete(id);
|
||||
}
|
||||
this._emit('file_transfer_error', {
|
||||
id: id,
|
||||
fileName: fileName,
|
||||
error: message || 'Transfer failed'
|
||||
error: message || 'Transfer failed',
|
||||
resumable: resumable,
|
||||
type: transfer.type
|
||||
});
|
||||
}
|
||||
|
||||
@@ -135,11 +177,17 @@ class RDFileTransfer {
|
||||
const t = self._transfers.get(id);
|
||||
if (!t || t.status !== 'pending') return;
|
||||
t.status = 'error';
|
||||
self._transfers.delete(id);
|
||||
t.resumable = (t.type === 'upload' && (t.sentBytes || 0) > 0)
|
||||
|| (t.type === 'download' && (t.receivedBytes || 0) > 0);
|
||||
if (!t.resumable) {
|
||||
self._transfers.delete(id);
|
||||
}
|
||||
self._emit('file_transfer_error', {
|
||||
id: id,
|
||||
fileName: t.fileName,
|
||||
error: 'Remote did not start transfer'
|
||||
error: 'Remote did not start transfer',
|
||||
resumable: t.resumable,
|
||||
type: t.type
|
||||
});
|
||||
}, this.TRANSFER_STALL_MS);
|
||||
}
|
||||
@@ -170,6 +218,84 @@ class RDFileTransfer {
|
||||
get enabled() { return this._enabled; }
|
||||
get currentPath() { return this._currentPath; }
|
||||
get entries() { return this._entries; }
|
||||
get showHidden() { return this._showHidden; }
|
||||
|
||||
/**
|
||||
* Respond to overwrite prompt from UI.
|
||||
* @param {number} id
|
||||
* @param {boolean} skip
|
||||
* @param {boolean} [applyToAll]
|
||||
*/
|
||||
confirmOverwrite(id, skip, applyToAll) {
|
||||
const pending = this._pendingOverwrite.get(id);
|
||||
if (!pending) return;
|
||||
this._pendingOverwrite.delete(id);
|
||||
if (applyToAll) {
|
||||
this._overwriteStrategy = skip ? 'skip' : 'overwrite';
|
||||
}
|
||||
pending.resolve(!!skip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume a failed transfer when checkpoint data is available.
|
||||
* @param {number} id
|
||||
*/
|
||||
resumeTransfer(id) {
|
||||
const transfer = this._transfers.get(id);
|
||||
if (!transfer || !transfer.resumable) return;
|
||||
|
||||
transfer.status = 'pending';
|
||||
transfer.errorMessage = null;
|
||||
transfer.resumable = false;
|
||||
const self = this;
|
||||
|
||||
this._emit('file_transfer_start', {
|
||||
id: id,
|
||||
type: transfer.type,
|
||||
fileName: transfer.fileName,
|
||||
fileSize: transfer.fileSize
|
||||
});
|
||||
|
||||
this._runWithConnection(function () {
|
||||
try {
|
||||
if (transfer.type === 'upload') {
|
||||
const modified = transfer.file && transfer.file.lastModified
|
||||
? Math.floor(transfer.file.lastModified / 1000) : 0;
|
||||
const startOffset = transfer.sentBytes || 0;
|
||||
const isResume = startOffset > 0;
|
||||
if (!isResume) {
|
||||
const file = transfer.file;
|
||||
const files = [{
|
||||
entryType: self.FILE_TYPE.FILE,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
modifiedTime: modified
|
||||
}];
|
||||
self._sendMessageSafe(self._proto.buildFileReceiveRequest(
|
||||
id, transfer.remotePath, files, transfer.fileNum || 0, Number(file.size)
|
||||
));
|
||||
}
|
||||
self._sendMessageSafe(self._proto.buildFileDigest(
|
||||
id, transfer.fileNum || 0, transfer.fileSize, modified,
|
||||
{ isUpload: true, isResume: isResume, transferredSize: startOffset }
|
||||
));
|
||||
self._armTransferTimeout(id);
|
||||
} else if (transfer.type === 'download') {
|
||||
const fullPath = RDFileTransfer.buildRemoteFilePath(
|
||||
transfer.remotePath, transfer.fileName
|
||||
);
|
||||
self._sendMessageSafe(self._proto.buildFileSendRequest(
|
||||
id, fullPath, self._showHidden, transfer.fileNum || 0
|
||||
));
|
||||
self._armTransferTimeout(id);
|
||||
}
|
||||
} catch (err) {
|
||||
self._failTransfer(id, err.message || String(err));
|
||||
}
|
||||
}).catch(function (err) {
|
||||
self._failTransfer(id, err.message || 'Could not connect file transfer session');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Browse a directory on the remote machine
|
||||
@@ -493,9 +619,20 @@ class RDFileTransfer {
|
||||
|
||||
const fileNum = transfer.fileNum;
|
||||
const isIdentical = !!(digest.isIdentical || digest.is_identical);
|
||||
const transferredSize = Number(
|
||||
digest.transferredSize != null ? digest.transferredSize : (digest.transferred_size || 0)
|
||||
);
|
||||
const isResume = !!(digest.isResume || digest.is_resume);
|
||||
|
||||
if (transfer.type === 'download') {
|
||||
this._sendMessageSafe(this._proto.buildFileSendConfirm(id, fileNum, false, 0));
|
||||
let offsetBlk = 0;
|
||||
if (isResume && transferredSize > 0) {
|
||||
offsetBlk = RDFileTransfer.computeOffsetBlk(transferredSize, this.BLOCK_SIZE);
|
||||
transfer.receivedBytes = transferredSize;
|
||||
} else if (transfer.receivedBytes > 0) {
|
||||
offsetBlk = RDFileTransfer.computeOffsetBlk(transfer.receivedBytes, this.BLOCK_SIZE);
|
||||
}
|
||||
this._sendMessageSafe(this._proto.buildFileSendConfirm(id, fileNum, false, offsetBlk));
|
||||
} else if (transfer.type === 'upload') {
|
||||
// Peer-initiated digest (overwrite check) — operator already sent initial digest.
|
||||
if (!(digest.isUpload || digest.is_upload)) return;
|
||||
@@ -511,16 +648,57 @@ class RDFileTransfer {
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._sendMessageSafe(this._proto.buildFileSendConfirm(id, fileNum, false, 0));
|
||||
this._sendUploadBlocks(transfer);
|
||||
const self = this;
|
||||
const proceedUpload = function (skip) {
|
||||
if (skip) {
|
||||
self._sendMessageSafe(self._proto.buildFileSendConfirm(id, fileNum, true, 0));
|
||||
self._transfers.delete(id);
|
||||
self._emit('file_transfer_complete', {
|
||||
id: id,
|
||||
fileName: transfer.fileName,
|
||||
fileSize: transfer.fileSize,
|
||||
type: 'upload',
|
||||
elapsed: (Date.now() - transfer.startTime) / 1000,
|
||||
skipped: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
self._sendMessageSafe(self._proto.buildFileSendConfirm(id, fileNum, false, 0));
|
||||
self._sendUploadBlocks(transfer, transfer.sentBytes || 0);
|
||||
};
|
||||
|
||||
if (this._overwriteStrategy === 'skip') {
|
||||
proceedUpload(true);
|
||||
return;
|
||||
}
|
||||
if (this._overwriteStrategy === 'overwrite') {
|
||||
proceedUpload(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this._pendingOverwrite.set(id, {
|
||||
resolve: proceedUpload
|
||||
});
|
||||
this._emit('file_transfer_overwrite_prompt', {
|
||||
id: id,
|
||||
fileName: transfer.fileName,
|
||||
fileSize: transfer.fileSize,
|
||||
remotePath: transfer.remotePath
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const startTransferred = transfer.type === 'download'
|
||||
? (transfer.receivedBytes || transferredSize || 0)
|
||||
: (transfer.sentBytes || 0);
|
||||
this._emit('file_transfer_progress', {
|
||||
id: id,
|
||||
fileName: transfer.fileName,
|
||||
fileSize: transfer.fileSize,
|
||||
transferred: 0,
|
||||
percent: 0,
|
||||
transferred: startTransferred,
|
||||
percent: transfer.fileSize > 0
|
||||
? Math.min(100, Math.round((startTransferred / transfer.fileSize) * 100))
|
||||
: 0,
|
||||
type: transfer.type,
|
||||
phase: 'transferring'
|
||||
});
|
||||
@@ -550,17 +728,26 @@ class RDFileTransfer {
|
||||
return;
|
||||
}
|
||||
|
||||
const offsetBlk = Number(
|
||||
confirm.offsetBlk != null ? confirm.offsetBlk : (confirm.offset_blk || 0)
|
||||
);
|
||||
const startOffset = offsetBlk > 0
|
||||
? offsetBlk * this.BLOCK_SIZE
|
||||
: (transfer.sentBytes || 0);
|
||||
|
||||
transfer.status = 'transferring';
|
||||
this._emit('file_transfer_progress', {
|
||||
id: id,
|
||||
fileName: transfer.fileName,
|
||||
fileSize: transfer.fileSize,
|
||||
transferred: 0,
|
||||
percent: 0,
|
||||
transferred: startOffset,
|
||||
percent: transfer.fileSize > 0
|
||||
? Math.min(100, Math.round((startOffset / transfer.fileSize) * 100))
|
||||
: 0,
|
||||
type: 'upload',
|
||||
phase: 'transferring'
|
||||
});
|
||||
this._sendUploadBlocks(transfer);
|
||||
this._sendUploadBlocks(transfer, startOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -664,37 +851,52 @@ class RDFileTransfer {
|
||||
if (transfer) {
|
||||
this._clearTransferTimeout(id);
|
||||
transfer.status = 'error';
|
||||
this._transfers.delete(id);
|
||||
transfer.resumable = (transfer.receivedBytes || transfer.sentBytes || 0) > 0;
|
||||
if (!transfer.resumable) {
|
||||
this._transfers.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
this._emit('file_transfer_error', {
|
||||
id: id,
|
||||
fileName: fileName,
|
||||
error: error.error || 'Unknown error'
|
||||
error: error.error || 'Unknown error',
|
||||
resumable: transfer ? transfer.resumable : false,
|
||||
type: transfer ? transfer.type : null
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Upload block streaming ----
|
||||
|
||||
async _tryCompressBlock(data, fileName) {
|
||||
if (RDFileTransfer.isPreCompressedFileName(fileName)) {
|
||||
return { data: data, compressed: false };
|
||||
}
|
||||
const result = await RDCompress.compressZstd(data);
|
||||
return { data: result.content, compressed: result.compress };
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream file blocks for upload
|
||||
* @param {Object} transfer
|
||||
* @param {number} [startOffset=0]
|
||||
*/
|
||||
async _sendUploadBlocks(transfer) {
|
||||
async _sendUploadBlocks(transfer, startOffset) {
|
||||
const file = transfer.file;
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
let offset = 0;
|
||||
let blkId = 0;
|
||||
let offset = Math.max(0, Number(startOffset || 0));
|
||||
let blkId = RDFileTransfer.computeOffsetBlk(offset, this.BLOCK_SIZE);
|
||||
|
||||
while (offset < file.size && transfer.status === 'transferring') {
|
||||
const end = Math.min(offset + this.BLOCK_SIZE, file.size);
|
||||
const slice = file.slice(offset, end);
|
||||
const data = new Uint8Array(await slice.arrayBuffer());
|
||||
const raw = new Uint8Array(await slice.arrayBuffer());
|
||||
const packed = await this._tryCompressBlock(raw, file.name);
|
||||
|
||||
this._sendMessageSafe(this._proto.buildFileBlock(
|
||||
transfer.id, transfer.fileNum, data, false, blkId
|
||||
transfer.id, transfer.fileNum, packed.data, packed.compressed, blkId
|
||||
));
|
||||
|
||||
transfer.sentBytes = end;
|
||||
@@ -723,12 +925,17 @@ class RDFileTransfer {
|
||||
}
|
||||
} catch (err) {
|
||||
transfer.status = 'error';
|
||||
transfer.resumable = (transfer.sentBytes || 0) > 0 && (transfer.sentBytes || 0) < file.size;
|
||||
this._emit('file_transfer_error', {
|
||||
id: transfer.id,
|
||||
fileName: transfer.fileName,
|
||||
error: err.message || 'Upload failed'
|
||||
error: err.message || 'Upload failed',
|
||||
resumable: transfer.resumable,
|
||||
type: 'upload'
|
||||
});
|
||||
this._transfers.delete(transfer.id);
|
||||
if (!transfer.resumable) {
|
||||
this._transfers.delete(transfer.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -855,3 +1062,7 @@ class RDFileTransfer {
|
||||
return { active: active, count: active.length };
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports.RDFileTransfer = RDFileTransfer;
|
||||
}
|
||||
|
||||
@@ -631,6 +631,10 @@ class RDProtocol {
|
||||
};
|
||||
if (opts.isUpload) digest.isUpload = true;
|
||||
if (opts.isIdentical) digest.isIdentical = true;
|
||||
if (opts.isResume) digest.isResume = true;
|
||||
if (opts.transferredSize != null) {
|
||||
digest.transferredSize = Number(opts.transferredSize);
|
||||
}
|
||||
return { fileResponse: { digest: digest } };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
const { RDFileTransfer } = require('../public/js/rdclient/filetransfer.js');
|
||||
|
||||
describe('RDFileTransfer static helpers', () => {
|
||||
it('uses 128 KB block size (hbb_common parity)', () => {
|
||||
const ft = new RDFileTransfer({
|
||||
proto: {},
|
||||
sendMessage: () => {},
|
||||
emit: () => {}
|
||||
});
|
||||
expect(ft.BLOCK_SIZE).toBe(131072);
|
||||
});
|
||||
|
||||
it('computeOffsetBlk derives block index from transferred bytes', () => {
|
||||
expect(RDFileTransfer.computeOffsetBlk(0, 131072)).toBe(0);
|
||||
expect(RDFileTransfer.computeOffsetBlk(131072, 131072)).toBe(1);
|
||||
expect(RDFileTransfer.computeOffsetBlk(262144, 131072)).toBe(2);
|
||||
});
|
||||
|
||||
it('isPreCompressedFileName skips zstd for archives and media', () => {
|
||||
expect(RDFileTransfer.isPreCompressedFileName('photo.jpg')).toBe(true);
|
||||
expect(RDFileTransfer.isPreCompressedFileName('archive.zip')).toBe(true);
|
||||
expect(RDFileTransfer.isPreCompressedFileName('readme.txt')).toBe(false);
|
||||
expect(RDFileTransfer.isPreCompressedFileName('data.bin')).toBe(false);
|
||||
});
|
||||
|
||||
it('buildRemoteFilePath joins Windows and Unix paths', () => {
|
||||
expect(RDFileTransfer.buildRemoteFilePath('C:\\Users\\admin', 'test.txt'))
|
||||
.toBe('C:\\Users\\admin\\test.txt');
|
||||
expect(RDFileTransfer.buildRemoteFilePath('/home/user/', 'a.bin'))
|
||||
.toBe('/home/user/a.bin');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RDFileTransfer overwrite strategy', () => {
|
||||
it('confirmOverwrite resolves pending prompt and sets session strategy', () => {
|
||||
const ft = new RDFileTransfer({
|
||||
proto: {},
|
||||
sendMessage: () => {},
|
||||
emit: () => {}
|
||||
});
|
||||
let skipped = null;
|
||||
ft._pendingOverwrite.set(1, {
|
||||
resolve: (s) => { skipped = s; }
|
||||
});
|
||||
ft.confirmOverwrite(1, false, true);
|
||||
expect(skipped).toBe(false);
|
||||
expect(ft._overwriteStrategy).toBe('overwrite');
|
||||
expect(ft._pendingOverwrite.has(1)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -190,6 +190,32 @@ describe('RDClient file transfer message encoding', () => {
|
||||
expect(decoded.fileResponse.digest.fileSize).toBe('12345');
|
||||
expect(decoded.fileResponse.digest.lastModified).toBe('1700000000');
|
||||
});
|
||||
|
||||
it('encodes FileResponse.digest with resume fields', () => {
|
||||
const msg = Message.create({
|
||||
fileResponse: {
|
||||
digest: {
|
||||
id: 9,
|
||||
fileNum: 0,
|
||||
fileSize: 999999,
|
||||
lastModified: 1700000001,
|
||||
isResume: true,
|
||||
transferredSize: 131072
|
||||
}
|
||||
}
|
||||
});
|
||||
const decoded = Message.decode(Message.encode(msg).finish()).toJSON();
|
||||
expect(decoded.fileResponse.digest.isResume).toBe(true);
|
||||
expect(decoded.fileResponse.digest.transferredSize).toBe('131072');
|
||||
});
|
||||
|
||||
it('encodes FileAction.send_confirm with offset_blk for resume', () => {
|
||||
const action = FileAction.create({
|
||||
sendConfirm: { id: 2, fileNum: 0, offsetBlk: 4 }
|
||||
});
|
||||
const decoded = FileAction.decode(FileAction.encode(action).finish()).toJSON();
|
||||
expect(decoded.sendConfirm.offsetBlk).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RDClient KeyEvent protobuf encoding', () => {
|
||||
|
||||
Reference in New Issue
Block a user