From 413cc8fbdec6db5e76e6249fa1adfa8bbff3038b Mon Sep 17 00:00:00 2001 From: niklascfw Date: Fri, 7 Aug 2026 00:24:47 +0200 Subject: [PATCH] Add Finder-style New Folder with inline rename on the Switch. Create unique folders without a crashing prompt, then focus the name for editing; also fix folder association type and UTF-8 MTP string encoding. --- src/mtp/MTPOperations.cpp | 66 +++++++++++++--- src/mtp/MTPOperations.hpp | 3 + src/ui/App.hpp | 7 +- src/ui/App.mm | 91 +++++++++++++++++++++- src/ui/WebUI.mm | 28 ++++++- src/ui/webroot/app.css | 22 ++++++ src/ui/webroot/app.js | 126 ++++++++++++++++++++++++++++--- src/ui/webroot/index.html | 5 ++ src/ui/webroot/lang/de.json | 6 +- src/ui/webroot/lang/en.json | 6 +- src/ui/webroot/lang/es.json | 6 +- src/ui/webroot/lang/fr.json | 6 +- src/ui/webroot/lang/it.json | 6 +- src/ui/webroot/lang/ja.json | 6 +- src/ui/webroot/lang/nl.json | 6 +- src/ui/webroot/lang/pt.json | 6 +- src/ui/webroot/lang/ru.json | 6 +- src/ui/webroot/lang/zh-Hans.json | 6 +- 18 files changed, 371 insertions(+), 37 deletions(-) diff --git a/src/mtp/MTPOperations.cpp b/src/mtp/MTPOperations.cpp index 87bf38a..7b0e9d3 100644 --- a/src/mtp/MTPOperations.cpp +++ b/src/mtp/MTPOperations.cpp @@ -22,17 +22,46 @@ void MTPOperations::put_le64(std::vector& b, uint64_t v) { // UTF-8 → MTP UTF-16LE string: [1-byte len][len × 2-byte UTF-16LE chars] void MTPOperations::encode_mtp_string(std::vector& buf, const std::string& utf8) { if (utf8.empty()) { buf.push_back(0); return; } - // Simple ASCII-only encoding (sufficient for filenames in practice) - size_t len = std::min(utf8.size(), size_t(255)) + 1; // +1 for null terminator - buf.push_back(static_cast(len)); - for (size_t i = 0; i < len - 1; ++i) { - uint8_t c = static_cast(utf8[i]); - buf.push_back(c); - buf.push_back(0); + + std::vector units; + units.reserve(utf8.size() + 1); + size_t i = 0; + while (i < utf8.size() && units.size() < 254) { + const unsigned char c = static_cast(utf8[i]); + uint32_t cp = 0; + if (c < 0x80) { + cp = c; i += 1; + } else if ((c & 0xE0) == 0xC0 && i + 1 < utf8.size()) { + cp = (uint32_t(c & 0x1F) << 6) | (utf8[i + 1] & 0x3F); + i += 2; + } else if ((c & 0xF0) == 0xE0 && i + 2 < utf8.size()) { + cp = (uint32_t(c & 0x0F) << 12) + | (uint32_t(utf8[i + 1] & 0x3F) << 6) + | (utf8[i + 2] & 0x3F); + i += 3; + } else if ((c & 0xF8) == 0xF0 && i + 3 < utf8.size()) { + cp = (uint32_t(c & 0x07) << 18) + | (uint32_t(utf8[i + 1] & 0x3F) << 12) + | (uint32_t(utf8[i + 2] & 0x3F) << 6) + | (utf8[i + 3] & 0x3F); + i += 4; + } else { + cp = '?'; i += 1; + } + if (cp >= 0x10000) { + cp -= 0x10000; + units.push_back(static_cast(0xD800 + (cp >> 10))); + if (units.size() < 254) + units.push_back(static_cast(0xDC00 + (cp & 0x3FF))); + } else { + units.push_back(static_cast(cp)); + } } - // Null terminator - buf.push_back(0); - buf.push_back(0); + + const size_t len = units.size() + 1; // includes null terminator + buf.push_back(static_cast(len)); + for (uint16_t u : units) put_le16(buf, u); + put_le16(buf, 0); } // ─── Parsing helpers ────────────────────────────────────────────────────────── @@ -203,7 +232,8 @@ std::vector MTPOperations::build_object_info(uint32_t storage_id, put_le32(b, 0); // image_pix_h put_le32(b, 0); // image_bit_depth put_le32(b, parent_handle); - put_le16(b, 0); // association_type + // Generic Folder association (0x0001) required for directory objects. + put_le16(b, format == static_cast(ObjectFormat::Association) ? 0x0001 : 0); put_le32(b, 0); // association_desc put_le32(b, 0); // sequence_number encode_mtp_string(b, filename); @@ -330,4 +360,18 @@ uint32_t MTPOperations::create_directory(uint32_t storage_id, return resp.params.size() >= 3 ? resp.params[2] : 0; } +void MTPOperations::rename_object(uint32_t handle, const std::string& new_name) { + std::lock_guard lock(session_.mutex()); + std::array params{ handle, PROP_OBJECT_FILENAME }; + session_.send_command(OpCode::SetObjectPropValue, params); + + std::vector payload; + encode_mtp_string(payload, new_name); + session_.send_data(payload); + + auto resp = session_.receive_response(); + if (resp.code != ResponseCode::OK) + throw MTPException(resp.code, MTPException::code_name(resp.code)); +} + } // namespace mtp diff --git a/src/mtp/MTPOperations.hpp b/src/mtp/MTPOperations.hpp index 7b2928f..8a670b5 100644 --- a/src/mtp/MTPOperations.hpp +++ b/src/mtp/MTPOperations.hpp @@ -48,6 +48,9 @@ public: uint32_t parent_handle, const std::string& name); + // Rename an existing object via SetObjectPropValue(ObjectFileName). + void rename_object(uint32_t handle, const std::string& new_name); + private: MTPSession& session_; diff --git a/src/ui/App.hpp b/src/ui/App.hpp index b86ce8c..208f2ff 100644 --- a/src/ui/App.hpp +++ b/src/ui/App.hpp @@ -58,7 +58,12 @@ public: void do_cancel(const std::string& id); void do_cancel_all(); void do_delete(uint32_t handle); - void do_create_folder(const std::string& name); + // Create a uniquely named folder (default "New Folder") in the current remote dir. + // On success fills out_* and returns true. + bool do_create_folder(const std::string& preferred_name, + uint32_t& out_handle, uint32_t& out_storage_id, + std::string& out_name); + bool do_rename(uint32_t handle, const std::string& new_name); void reconnect(); // macOS sleep/wake — tear down stale libusb state and reconnect. diff --git a/src/ui/App.mm b/src/ui/App.mm index 172d404..9d95eed 100644 --- a/src/ui/App.mm +++ b/src/ui/App.mm @@ -10,6 +10,7 @@ #include #include #include +#include namespace fs = std::filesystem; namespace omniMTP { @@ -515,12 +516,94 @@ void App::do_delete(uint32_t h) { catch (...) {} refresh_remote(); } -void App::do_create_folder(const std::string& name) { - if (remote_nav_stack_.empty()) return; +bool App::do_create_folder(const std::string& preferred_name, + uint32_t& out_handle, uint32_t& out_storage_id, + std::string& out_name) { + out_handle = 0; + out_storage_id = 0; + out_name.clear(); + if (!connected_.load() || remote_nav_stack_.empty()) return false; + + auto sanitize = [](std::string s) { + for (char& c : s) { + if (c == '/' || c == '\\' || c == ':' || c == 0) c = '_'; + } + while (!s.empty() && (s.front() == ' ' || s.front() == '.')) + s.erase(s.begin()); + while (!s.empty() && s.back() == ' ') + s.pop_back(); + return s; + }; + + std::string base = sanitize(preferred_name); + if (base.empty()) base = "New Folder"; + + // Snapshot names currently visible so we can pick "New Folder 2", … + std::unordered_set used; + { + std::lock_guard lock(device_mtx_); + for (const auto& e : remote_entries_) + used.insert(e.name); + } + + std::string candidate = base; + if (used.count(candidate)) { + for (int i = 2; i < 10000; ++i) { + candidate = base + " " + std::to_string(i); + if (!used.count(candidate)) break; + } + } + const auto& top = remote_nav_stack_.back(); - try { std::lock_guard lock(device_mtx_); if (ops_) ops_->create_directory(top.storage_id, top.handle, name); } - catch (...) {} + uint32_t handle = 0; + try { + std::lock_guard lock(device_mtx_); + if (!ops_) return false; + handle = ops_->create_directory(top.storage_id, top.handle, candidate); + } catch (const std::exception& e) { + set_status("status.listing_error", {e.what()}); + return false; + } catch (...) { + set_status("status.listing_error", {"Failed to create folder"}); + return false; + } + if (handle == 0) { + set_status("status.listing_error", {"Failed to create folder"}); + return false; + } + + out_handle = handle; + out_storage_id = top.storage_id; + out_name = candidate; refresh_remote(); + return true; +} + +bool App::do_rename(uint32_t handle, const std::string& new_name) { + if (!connected_.load()) return false; + std::string clean = new_name; + for (char& c : clean) { + if (c == '/' || c == '\\' || c == ':' || c == 0) c = '_'; + } + while (!clean.empty() && (clean.front() == ' ' || clean.front() == '.')) + clean.erase(clean.begin()); + while (!clean.empty() && clean.back() == ' ') + clean.pop_back(); + if (clean.empty()) return false; + + try { + std::lock_guard lock(device_mtx_); + if (!ops_) return false; + ops_->rename_object(handle, clean); + } catch (const std::exception& e) { + set_status("status.listing_error", {e.what()}); + return false; + } catch (...) { + set_status("status.listing_error", {"Rename failed"}); + return false; + } + refresh_remote(); + return true; } void App::reconnect() { usb_recovery_pending_.store(true); diff --git a/src/ui/WebUI.mm b/src/ui/WebUI.mm index 5b9dedf..0811f7e 100644 --- a/src/ui/WebUI.mm +++ b/src/ui/WebUI.mm @@ -399,8 +399,34 @@ static NSString* OmniPreferredDownloadURL(NSDictionary* release) { if (handle) _app->do_delete(handle.unsignedIntValue); } else if ([action isEqualToString:@"create_folder"]) { + NSString* preferred = body[@"defaultName"]; + std::string preferredName = (preferred && preferred.length > 0) + ? preferred.UTF8String : "New Folder"; + uint32_t handle = 0, storageId = 0; + std::string createdName; + bool ok = _app->do_create_folder(preferredName, handle, storageId, createdName); + if (!ok) { + [self sendNullResult:rid]; + return; + } + NSDictionary* payload = @{ + @"handle": @(handle), + @"storageId": @(storageId), + @"name": [NSString stringWithUTF8String:createdName.c_str()], + }; + NSData* data = [NSJSONSerialization dataWithJSONObject:payload options:0 error:nil]; + NSString* json = data ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : @"null"; + [self sendRawResult:json responseId:rid]; + return; + + } else if ([action isEqualToString:@"rename_remote"]) { + NSNumber* handle = body[@"handle"]; NSString* name = body[@"name"]; - if (name && name.length > 0) _app->do_create_folder(name.UTF8String); + bool ok = NO; + if (handle && name) + ok = _app->do_rename(handle.unsignedIntValue, name.UTF8String); + [self sendRawResult:(ok ? @"true" : @"false") responseId:rid]; + return; } else if ([action isEqualToString:@"reconnect"]) { _app->reconnect(); diff --git a/src/ui/webroot/app.css b/src/ui/webroot/app.css index 9e244a6..191a1fa 100644 --- a/src/ui/webroot/app.css +++ b/src/ui/webroot/app.css @@ -361,6 +361,28 @@ body { align-items: center; gap: 7px; overflow: hidden; + min-width: 0; + flex: 1; +} +.fname-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.fname-edit { + flex: 1; + min-width: 80px; + margin: 0; + padding: 1px 6px; + border: 1px solid var(--accent); + border-radius: 4px; + background: #1e1e22; + color: var(--label); + font: inherit; + outline: none; + box-shadow: 0 0 0 2px rgba(10,132,255,0.25); + user-select: text !important; + -webkit-user-select: text !important; } .dot { width: 7px; diff --git a/src/ui/webroot/app.js b/src/ui/webroot/app.js index 64a1f02..cf5e7ae 100644 --- a/src/ui/webroot/app.js +++ b/src/ui/webroot/app.js @@ -127,6 +127,70 @@ async function confirmDialog(message) { return result === true || result === 'true'; } +let _pendingRenameHandle = null; +let _inlineRenameActive = false; + +async function createFolderInline() { + if (!S.connected || _inlineRenameActive) return; + const result = await call('create_folder', { + defaultName: t('prompt.new_folder_default'), + }); + if (!result || result.handle == null) return; + _pendingRenameHandle = String(result.handle); + _remoteFingerprint = ''; + renderRemoteFiles(); +} + +function startInlineRename(handleKey) { + const key = String(handleKey); + const row = document.querySelector(`#remote-files .frow[data-key="${CSS.escape(key)}"]`); + if (!row) { + _pendingRenameHandle = key; + return; + } + if (row.querySelector('.fname-edit')) return; + _pendingRenameHandle = null; + + const textEl = row.querySelector('.fname-text'); + if (!textEl) return; + const oldName = textEl.textContent; + const className = textEl.className; + + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'fname-edit'; + input.value = oldName; + input.setAttribute('spellcheck', 'false'); + textEl.replaceWith(input); + _inlineRenameActive = true; + + input.focus(); + input.select(); + + let finished = false; + const finish = async (commit) => { + if (finished) return; + finished = true; + _inlineRenameActive = false; + const newName = input.value.trim(); + if (commit && newName && newName !== oldName) { + await call('rename_remote', { handle: Number(key), name: newName }); + } + _remoteFingerprint = ''; + renderRemoteFiles(); + }; + + input.addEventListener('keydown', e => { + e.stopPropagation(); + if (e.key === 'Enter') { e.preventDefault(); finish(true); } + if (e.key === 'Escape') { e.preventDefault(); finish(false); } + }); + input.addEventListener('blur', () => finish(true)); + input.addEventListener('mousedown', e => e.stopPropagation()); + input.addEventListener('click', e => e.stopPropagation()); + input.addEventListener('dblclick', e => e.stopPropagation()); +} + // ── State ───────────────────────────────────────────────────────────────────── const S = { connected: false, deviceName: '', @@ -251,7 +315,7 @@ function buildFileRows(files, selSet, idKey) { return `
- ${esc(f.name)} + ${esc(f.name)}
${f.isDir ? '' : esc(f.sizeStr)} ${esc(f.date)} @@ -327,6 +391,9 @@ function renderRemoteFiles() { const el = document.getElementById('remote-files'); const empty = document.getElementById('remote-empty-state'); + // Don't clobber an in-progress inline rename. + if (_inlineRenameActive) return; + if (!S.connected) { if (empty) empty.style.display = ''; el.querySelectorAll('table,.loading,.empty-state:not(#remote-empty-state)') @@ -360,6 +427,11 @@ function renderRemoteFiles() { } else { applySelectionToDOM(el, S.selRemote); } + + if (_pendingRenameHandle) { + const handle = _pendingRenameHandle; + requestAnimationFrame(() => startInlineRename(handle)); + } } function attachRemoteEvents(el) { @@ -443,6 +515,7 @@ function renderRemoteToolbar() { crumb.querySelectorAll('.crumb-link').forEach(el => el.addEventListener('click', () => call('navigate_remote_to', {index: +el.dataset.idx}))); document.getElementById('btn-remote-up').disabled = stack.length <= 1; + document.getElementById('btn-new-folder').disabled = !S.connected; } function renderDeviceBar() { @@ -539,25 +612,53 @@ function renderStatusBar() { } let _ctxFile = null; +let _ctxMode = 'file'; // 'file' | 'blank' -function showRemoteCtxMenu(file, e) { - _ctxFile = file; - if (!S.selRemote.has(String(file.handle))) { - S.selRemote.clear(); - S.selRemote.add(String(file.handle)); - applySelectionToDOM(document.getElementById('remote-files'), S.selRemote); - } +function positionCtxMenu(e) { const m = document.getElementById('ctx-menu'); m.style.left = e.clientX + 'px'; m.style.top = e.clientY + 'px'; m.classList.remove('hidden'); } +function showRemoteCtxMenu(file, e) { + _ctxFile = file; + _ctxMode = 'file'; + if (!S.selRemote.has(String(file.handle))) { + S.selRemote.clear(); + S.selRemote.add(String(file.handle)); + applySelectionToDOM(document.getElementById('remote-files'), S.selRemote); + } + document.getElementById('ctx-new-folder').style.display = ''; + document.getElementById('ctx-sep-delete').style.display = ''; + document.getElementById('ctx-delete').style.display = ''; + positionCtxMenu(e); +} + +function showBlankCtxMenu(e) { + if (!S.connected) return; + _ctxFile = null; + _ctxMode = 'blank'; + document.getElementById('ctx-new-folder').style.display = ''; + document.getElementById('ctx-sep-delete').style.display = 'none'; + document.getElementById('ctx-delete').style.display = 'none'; + positionCtxMenu(e); +} + document.addEventListener('click', e => { if (e.target.closest('#ctx-menu')) return; document.getElementById('ctx-menu').classList.add('hidden'); }); -document.addEventListener('contextmenu', e => { if (!e.target.closest('.frow')) { e.preventDefault(); document.getElementById('ctx-menu').classList.add('hidden'); } }); +document.addEventListener('contextmenu', e => { + if (e.target.closest('.frow')) return; + if (e.target.closest('#remote-files') && S.connected) { + e.preventDefault(); + showBlankCtxMenu(e); + return; + } + e.preventDefault(); + document.getElementById('ctx-menu').classList.add('hidden'); +}); window.omniClearSelection = function() { const sel = window.getSelection(); @@ -588,6 +689,7 @@ document.addEventListener('selectstart', e => { document.addEventListener('mousedown', e => { if (e.button !== 0) return; + if (e.target.closest('.fname-edit')) return; const row = e.target.closest('#remote-files .frow'); if (!row) return; e.preventDefault(); @@ -605,6 +707,7 @@ document.addEventListener('DOMContentLoaded', async () => { document.getElementById('btn-remote-up') .addEventListener('click', () => call('navigate_remote_up')); document.getElementById('btn-remote-refresh').addEventListener('click', () => call('refresh_remote')); + document.getElementById('btn-new-folder') .addEventListener('click', () => createFolderInline()); document.getElementById('btn-scan') .addEventListener('click', () => call('reconnect')); document.getElementById('btn-open-picker') .addEventListener('click', () => call('open_file_picker')); document.getElementById('btn-cancel-all') .addEventListener('click', () => call('cancel_all')); @@ -613,12 +716,15 @@ document.addEventListener('DOMContentLoaded', async () => { const bindCtx = (id, fn) => { document.getElementById(id).addEventListener('click', e => { e.stopPropagation(); + document.getElementById('ctx-menu').classList.add('hidden'); fn(); }); }; + bindCtx('ctx-new-folder', () => createFolderInline()); + bindCtx('ctx-delete', async () => { - if (!_ctxFile) return; + if (!_ctxFile || _ctxMode !== 'file') return; const multi = S.selRemote.size > 1 && S.selRemote.has(String(_ctxFile.handle)); const message = multi ? t('confirm.delete_items', { n: S.selRemote.size }) diff --git a/src/ui/webroot/index.html b/src/ui/webroot/index.html index b2b757f..eb513d2 100644 --- a/src/ui/webroot/index.html +++ b/src/ui/webroot/index.html @@ -33,6 +33,9 @@ + @@ -69,6 +72,8 @@ diff --git a/src/ui/webroot/lang/de.json b/src/ui/webroot/lang/de.json index c23e3b6..35bd803 100644 --- a/src/ui/webroot/lang/de.json +++ b/src/ui/webroot/lang/de.json @@ -58,5 +58,9 @@ "update.up_to_date_body": "OmniMTP {version} ist die aktuelle Version.", "update.check_failed_title": "Update-Prüfung fehlgeschlagen", "update.check_failed_body": "Der Update-Server konnte nicht erreicht werden.", - "update.http_error": "Server antwortete mit HTTP {code}." + "update.http_error": "Server antwortete mit HTTP {code}.", + "btn.new_folder": "Neuer Ordner", + "ctx.new_folder": "Neuer Ordner", + "prompt.new_folder": "Ordnername:", + "prompt.new_folder_default": "Neuer Ordner" } diff --git a/src/ui/webroot/lang/en.json b/src/ui/webroot/lang/en.json index 4a9e51a..89c16d2 100644 --- a/src/ui/webroot/lang/en.json +++ b/src/ui/webroot/lang/en.json @@ -58,5 +58,9 @@ "update.up_to_date_body": "OmniMTP {version} is the latest release.", "update.check_failed_title": "Update Check Failed", "update.check_failed_body": "Could not reach the update server.", - "update.http_error": "Server returned HTTP {code}." + "update.http_error": "Server returned HTTP {code}.", + "btn.new_folder": "New Folder", + "ctx.new_folder": "New Folder", + "prompt.new_folder": "Folder name:", + "prompt.new_folder_default": "New Folder" } diff --git a/src/ui/webroot/lang/es.json b/src/ui/webroot/lang/es.json index 3b513af..3ad6bf7 100644 --- a/src/ui/webroot/lang/es.json +++ b/src/ui/webroot/lang/es.json @@ -58,5 +58,9 @@ "update.up_to_date_body": "OmniMTP {version} es la última versión.", "update.check_failed_title": "Error al buscar actualizaciones", "update.check_failed_body": "No se pudo contactar con el servidor de actualizaciones.", - "update.http_error": "El servidor respondió HTTP {code}." + "update.http_error": "El servidor respondió HTTP {code}.", + "btn.new_folder": "Nueva carpeta", + "ctx.new_folder": "Nueva carpeta", + "prompt.new_folder": "Nombre de la carpeta:", + "prompt.new_folder_default": "Nueva carpeta" } diff --git a/src/ui/webroot/lang/fr.json b/src/ui/webroot/lang/fr.json index 4180ba6..e6ebe18 100644 --- a/src/ui/webroot/lang/fr.json +++ b/src/ui/webroot/lang/fr.json @@ -58,5 +58,9 @@ "update.up_to_date_body": "OmniMTP {version} est la dernière version.", "update.check_failed_title": "Échec de la vérification", "update.check_failed_body": "Impossible de joindre le serveur de mises à jour.", - "update.http_error": "Le serveur a renvoyé HTTP {code}." + "update.http_error": "Le serveur a renvoyé HTTP {code}.", + "btn.new_folder": "Nouveau dossier", + "ctx.new_folder": "Nouveau dossier", + "prompt.new_folder": "Nom du dossier :", + "prompt.new_folder_default": "Nouveau dossier" } diff --git a/src/ui/webroot/lang/it.json b/src/ui/webroot/lang/it.json index b3012b6..d700857 100644 --- a/src/ui/webroot/lang/it.json +++ b/src/ui/webroot/lang/it.json @@ -58,5 +58,9 @@ "update.up_to_date_body": "OmniMTP {version} è l’ultima versione.", "update.check_failed_title": "Verifica aggiornamenti non riuscita", "update.check_failed_body": "Impossibile raggiungere il server degli aggiornamenti.", - "update.http_error": "Il server ha risposto HTTP {code}." + "update.http_error": "Il server ha risposto HTTP {code}.", + "btn.new_folder": "Nuova cartella", + "ctx.new_folder": "Nuova cartella", + "prompt.new_folder": "Nome cartella:", + "prompt.new_folder_default": "Nuova cartella" } diff --git a/src/ui/webroot/lang/ja.json b/src/ui/webroot/lang/ja.json index f0ecee0..ae0bb8b 100644 --- a/src/ui/webroot/lang/ja.json +++ b/src/ui/webroot/lang/ja.json @@ -58,5 +58,9 @@ "update.up_to_date_body": "OmniMTP {version} が最新リリースです。", "update.check_failed_title": "アップデート確認に失敗", "update.check_failed_body": "アップデートサーバーに接続できませんでした。", - "update.http_error": "サーバーが HTTP {code} を返しました。" + "update.http_error": "サーバーが HTTP {code} を返しました。", + "btn.new_folder": "新しいフォルダ", + "ctx.new_folder": "新しいフォルダ", + "prompt.new_folder": "フォルダ名:", + "prompt.new_folder_default": "新しいフォルダ" } diff --git a/src/ui/webroot/lang/nl.json b/src/ui/webroot/lang/nl.json index 6d5f58e..220d56d 100644 --- a/src/ui/webroot/lang/nl.json +++ b/src/ui/webroot/lang/nl.json @@ -58,5 +58,9 @@ "update.up_to_date_body": "OmniMTP {version} is de nieuwste release.", "update.check_failed_title": "Updatecontrole mislukt", "update.check_failed_body": "Kon de updateserver niet bereiken.", - "update.http_error": "Server gaf HTTP {code} terug." + "update.http_error": "Server gaf HTTP {code} terug.", + "btn.new_folder": "Nieuwe map", + "ctx.new_folder": "Nieuwe map", + "prompt.new_folder": "Mapnaam:", + "prompt.new_folder_default": "Nieuwe map" } diff --git a/src/ui/webroot/lang/pt.json b/src/ui/webroot/lang/pt.json index 1902483..d4a7514 100644 --- a/src/ui/webroot/lang/pt.json +++ b/src/ui/webroot/lang/pt.json @@ -58,5 +58,9 @@ "update.up_to_date_body": "OmniMTP {version} é a versão mais recente.", "update.check_failed_title": "Falha ao verificar atualizações", "update.check_failed_body": "Não foi possível contactar o servidor de atualizações.", - "update.http_error": "O servidor retornou HTTP {code}." + "update.http_error": "O servidor retornou HTTP {code}.", + "btn.new_folder": "Nova pasta", + "ctx.new_folder": "Nova pasta", + "prompt.new_folder": "Nome da pasta:", + "prompt.new_folder_default": "Nova pasta" } diff --git a/src/ui/webroot/lang/ru.json b/src/ui/webroot/lang/ru.json index 025af9a..bf10ae9 100644 --- a/src/ui/webroot/lang/ru.json +++ b/src/ui/webroot/lang/ru.json @@ -58,5 +58,9 @@ "update.up_to_date_body": "OmniMTP {version} — последний выпуск.", "update.check_failed_title": "Не удалось проверить обновления", "update.check_failed_body": "Не удалось связаться с сервером обновлений.", - "update.http_error": "Сервер вернул HTTP {code}." + "update.http_error": "Сервер вернул HTTP {code}.", + "btn.new_folder": "Новая папка", + "ctx.new_folder": "Новая папка", + "prompt.new_folder": "Имя папки:", + "prompt.new_folder_default": "Новая папка" } diff --git a/src/ui/webroot/lang/zh-Hans.json b/src/ui/webroot/lang/zh-Hans.json index c9fde95..5abe286 100644 --- a/src/ui/webroot/lang/zh-Hans.json +++ b/src/ui/webroot/lang/zh-Hans.json @@ -58,5 +58,9 @@ "update.up_to_date_body": "OmniMTP {version} 已是最新版本。", "update.check_failed_title": "检查更新失败", "update.check_failed_body": "无法连接更新服务器。", - "update.http_error": "服务器返回 HTTP {code}。" + "update.http_error": "服务器返回 HTTP {code}。", + "btn.new_folder": "新建文件夹", + "ctx.new_folder": "新建文件夹", + "prompt.new_folder": "文件夹名称:", + "prompt.new_folder_default": "新建文件夹" }