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.
This commit is contained in:
2026-08-07 00:24:47 +02:00
parent 474f4f7714
commit 413cc8fbde
18 changed files with 371 additions and 37 deletions

View File

@@ -22,17 +22,46 @@ void MTPOperations::put_le64(std::vector<uint8_t>& b, uint64_t v) {
// UTF-8 → MTP UTF-16LE string: [1-byte len][len × 2-byte UTF-16LE chars] // UTF-8 → MTP UTF-16LE string: [1-byte len][len × 2-byte UTF-16LE chars]
void MTPOperations::encode_mtp_string(std::vector<uint8_t>& buf, const std::string& utf8) { void MTPOperations::encode_mtp_string(std::vector<uint8_t>& buf, const std::string& utf8) {
if (utf8.empty()) { buf.push_back(0); return; } 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 std::vector<uint16_t> units;
buf.push_back(static_cast<uint8_t>(len)); units.reserve(utf8.size() + 1);
for (size_t i = 0; i < len - 1; ++i) { size_t i = 0;
uint8_t c = static_cast<uint8_t>(utf8[i]); while (i < utf8.size() && units.size() < 254) {
buf.push_back(c); const unsigned char c = static_cast<unsigned char>(utf8[i]);
buf.push_back(0); 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<uint16_t>(0xD800 + (cp >> 10)));
if (units.size() < 254)
units.push_back(static_cast<uint16_t>(0xDC00 + (cp & 0x3FF)));
} else {
units.push_back(static_cast<uint16_t>(cp));
}
} }
// Null terminator
buf.push_back(0); const size_t len = units.size() + 1; // includes null terminator
buf.push_back(0); buf.push_back(static_cast<uint8_t>(len));
for (uint16_t u : units) put_le16(buf, u);
put_le16(buf, 0);
} }
// ─── Parsing helpers ────────────────────────────────────────────────────────── // ─── Parsing helpers ──────────────────────────────────────────────────────────
@@ -203,7 +232,8 @@ std::vector<uint8_t> MTPOperations::build_object_info(uint32_t storage_id,
put_le32(b, 0); // image_pix_h put_le32(b, 0); // image_pix_h
put_le32(b, 0); // image_bit_depth put_le32(b, 0); // image_bit_depth
put_le32(b, parent_handle); 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<uint16_t>(ObjectFormat::Association) ? 0x0001 : 0);
put_le32(b, 0); // association_desc put_le32(b, 0); // association_desc
put_le32(b, 0); // sequence_number put_le32(b, 0); // sequence_number
encode_mtp_string(b, filename); 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; 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<uint32_t,2> params{ handle, PROP_OBJECT_FILENAME };
session_.send_command(OpCode::SetObjectPropValue, params);
std::vector<uint8_t> 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 } // namespace mtp

View File

@@ -48,6 +48,9 @@ public:
uint32_t parent_handle, uint32_t parent_handle,
const std::string& name); const std::string& name);
// Rename an existing object via SetObjectPropValue(ObjectFileName).
void rename_object(uint32_t handle, const std::string& new_name);
private: private:
MTPSession& session_; MTPSession& session_;

View File

@@ -58,7 +58,12 @@ public:
void do_cancel(const std::string& id); void do_cancel(const std::string& id);
void do_cancel_all(); void do_cancel_all();
void do_delete(uint32_t handle); 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(); void reconnect();
// macOS sleep/wake — tear down stale libusb state and reconnect. // macOS sleep/wake — tear down stale libusb state and reconnect.

View File

@@ -10,6 +10,7 @@
#include <thread> #include <thread>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <unordered_set>
namespace fs = std::filesystem; namespace fs = std::filesystem;
namespace omniMTP { namespace omniMTP {
@@ -515,12 +516,94 @@ void App::do_delete(uint32_t h) {
catch (...) {} catch (...) {}
refresh_remote(); refresh_remote();
} }
void App::do_create_folder(const std::string& name) { bool App::do_create_folder(const std::string& preferred_name,
if (remote_nav_stack_.empty()) return; 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<std::string> 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(); 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); } uint32_t handle = 0;
catch (...) {} 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(); 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() { void App::reconnect() {
usb_recovery_pending_.store(true); usb_recovery_pending_.store(true);

View File

@@ -399,8 +399,34 @@ static NSString* OmniPreferredDownloadURL(NSDictionary* release) {
if (handle) _app->do_delete(handle.unsignedIntValue); if (handle) _app->do_delete(handle.unsignedIntValue);
} else if ([action isEqualToString:@"create_folder"]) { } 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"]; 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"]) { } else if ([action isEqualToString:@"reconnect"]) {
_app->reconnect(); _app->reconnect();

View File

@@ -361,6 +361,28 @@ body {
align-items: center; align-items: center;
gap: 7px; gap: 7px;
overflow: hidden; 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 { .dot {
width: 7px; width: 7px;

View File

@@ -127,6 +127,70 @@ async function confirmDialog(message) {
return result === true || result === 'true'; 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 ───────────────────────────────────────────────────────────────────── // ── State ─────────────────────────────────────────────────────────────────────
const S = { const S = {
connected: false, deviceName: '', connected: false, deviceName: '',
@@ -251,7 +315,7 @@ function buildFileRows(files, selSet, idKey) {
return `<tr class="frow${sel}" data-idx="${i}" data-key="${esc(key)}" data-is-dir="${f.isDir ? '1' : '0'}"> return `<tr class="frow${sel}" data-idx="${i}" data-key="${esc(key)}" data-is-dir="${f.isDir ? '1' : '0'}">
<td class="col-name"><div class="fname-cell"> <td class="col-name"><div class="fname-cell">
<span class="dot ${cc}"></span> <span class="dot ${cc}"></span>
<span class="${cc}">${esc(f.name)}</span> <span class="fname-text ${cc}">${esc(f.name)}</span>
</div></td> </div></td>
<td class="col-size">${f.isDir ? '' : esc(f.sizeStr)}</td> <td class="col-size">${f.isDir ? '' : esc(f.sizeStr)}</td>
<td class="col-date">${esc(f.date)}</td> <td class="col-date">${esc(f.date)}</td>
@@ -327,6 +391,9 @@ function renderRemoteFiles() {
const el = document.getElementById('remote-files'); const el = document.getElementById('remote-files');
const empty = document.getElementById('remote-empty-state'); const empty = document.getElementById('remote-empty-state');
// Don't clobber an in-progress inline rename.
if (_inlineRenameActive) return;
if (!S.connected) { if (!S.connected) {
if (empty) empty.style.display = ''; if (empty) empty.style.display = '';
el.querySelectorAll('table,.loading,.empty-state:not(#remote-empty-state)') el.querySelectorAll('table,.loading,.empty-state:not(#remote-empty-state)')
@@ -360,6 +427,11 @@ function renderRemoteFiles() {
} else { } else {
applySelectionToDOM(el, S.selRemote); applySelectionToDOM(el, S.selRemote);
} }
if (_pendingRenameHandle) {
const handle = _pendingRenameHandle;
requestAnimationFrame(() => startInlineRename(handle));
}
} }
function attachRemoteEvents(el) { function attachRemoteEvents(el) {
@@ -443,6 +515,7 @@ function renderRemoteToolbar() {
crumb.querySelectorAll('.crumb-link').forEach(el => crumb.querySelectorAll('.crumb-link').forEach(el =>
el.addEventListener('click', () => call('navigate_remote_to', {index: +el.dataset.idx}))); el.addEventListener('click', () => call('navigate_remote_to', {index: +el.dataset.idx})));
document.getElementById('btn-remote-up').disabled = stack.length <= 1; document.getElementById('btn-remote-up').disabled = stack.length <= 1;
document.getElementById('btn-new-folder').disabled = !S.connected;
} }
function renderDeviceBar() { function renderDeviceBar() {
@@ -539,25 +612,53 @@ function renderStatusBar() {
} }
let _ctxFile = null; let _ctxFile = null;
let _ctxMode = 'file'; // 'file' | 'blank'
function showRemoteCtxMenu(file, e) { function positionCtxMenu(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);
}
const m = document.getElementById('ctx-menu'); const m = document.getElementById('ctx-menu');
m.style.left = e.clientX + 'px'; m.style.left = e.clientX + 'px';
m.style.top = e.clientY + 'px'; m.style.top = e.clientY + 'px';
m.classList.remove('hidden'); 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 => { document.addEventListener('click', e => {
if (e.target.closest('#ctx-menu')) return; if (e.target.closest('#ctx-menu')) return;
document.getElementById('ctx-menu').classList.add('hidden'); 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() { window.omniClearSelection = function() {
const sel = window.getSelection(); const sel = window.getSelection();
@@ -588,6 +689,7 @@ document.addEventListener('selectstart', e => {
document.addEventListener('mousedown', e => { document.addEventListener('mousedown', e => {
if (e.button !== 0) return; if (e.button !== 0) return;
if (e.target.closest('.fname-edit')) return;
const row = e.target.closest('#remote-files .frow'); const row = e.target.closest('#remote-files .frow');
if (!row) return; if (!row) return;
e.preventDefault(); 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-up') .addEventListener('click', () => call('navigate_remote_up'));
document.getElementById('btn-remote-refresh').addEventListener('click', () => call('refresh_remote')); 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-scan') .addEventListener('click', () => call('reconnect'));
document.getElementById('btn-open-picker') .addEventListener('click', () => call('open_file_picker')); document.getElementById('btn-open-picker') .addEventListener('click', () => call('open_file_picker'));
document.getElementById('btn-cancel-all') .addEventListener('click', () => call('cancel_all')); document.getElementById('btn-cancel-all') .addEventListener('click', () => call('cancel_all'));
@@ -613,12 +716,15 @@ document.addEventListener('DOMContentLoaded', async () => {
const bindCtx = (id, fn) => { const bindCtx = (id, fn) => {
document.getElementById(id).addEventListener('click', e => { document.getElementById(id).addEventListener('click', e => {
e.stopPropagation(); e.stopPropagation();
document.getElementById('ctx-menu').classList.add('hidden');
fn(); fn();
}); });
}; };
bindCtx('ctx-new-folder', () => createFolderInline());
bindCtx('ctx-delete', async () => { 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 multi = S.selRemote.size > 1 && S.selRemote.has(String(_ctxFile.handle));
const message = multi const message = multi
? t('confirm.delete_items', { n: S.selRemote.size }) ? t('confirm.delete_items', { n: S.selRemote.size })

View File

@@ -33,6 +33,9 @@
</div> </div>
<select id="storage-sel" class="storage-select"></select> <select id="storage-sel" class="storage-select"></select>
<div id="remote-breadcrumb" class="breadcrumb"></div> <div id="remote-breadcrumb" class="breadcrumb"></div>
<button class="nav-btn icon-only" id="btn-new-folder" data-i18n-title="btn.new_folder" title="New Folder" disabled>
<svg viewBox="0 0 16 16"><path d="M2 3.5A1.5 1.5 0 0 1 3.5 2H7l1.5 1.5H12.5A1.5 1.5 0 0 1 14 5v7.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 12.5v-9z" fill="none" stroke="currentColor" stroke-width="1.25"/><path d="M8 6.5v5M5.5 9h5" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/></svg>
</button>
<button class="nav-btn icon-only" id="btn-remote-refresh" data-i18n-title="btn.refresh" title="Refresh"> <button class="nav-btn icon-only" id="btn-remote-refresh" data-i18n-title="btn.refresh" title="Refresh">
<svg viewBox="0 0 16 16"><path d="M13.5 8A5.5 5.5 0 1 1 8 2.5V1l3 2.5L8 6V4.5A3.5 3.5 0 1 0 11.5 8h2z" fill="currentColor"/></svg> <svg viewBox="0 0 16 16"><path d="M13.5 8A5.5 5.5 0 1 1 8 2.5V1l3 2.5L8 6V4.5A3.5 3.5 0 1 0 11.5 8h2z" fill="currentColor"/></svg>
</button> </button>
@@ -69,6 +72,8 @@
<!-- Context menu --> <!-- Context menu -->
<div id="ctx-menu" class="ctx-menu hidden"> <div id="ctx-menu" class="ctx-menu hidden">
<div class="ctx-item" id="ctx-new-folder" data-i18n="ctx.new_folder">New Folder</div>
<div class="ctx-item ctx-sep" id="ctx-sep-delete"></div>
<div class="ctx-item danger" id="ctx-delete" data-i18n="ctx.delete">Delete…</div> <div class="ctx-item danger" id="ctx-delete" data-i18n="ctx.delete">Delete…</div>
</div> </div>

View File

@@ -58,5 +58,9 @@
"update.up_to_date_body": "OmniMTP {version} ist die aktuelle Version.", "update.up_to_date_body": "OmniMTP {version} ist die aktuelle Version.",
"update.check_failed_title": "Update-Prüfung fehlgeschlagen", "update.check_failed_title": "Update-Prüfung fehlgeschlagen",
"update.check_failed_body": "Der Update-Server konnte nicht erreicht werden.", "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"
} }

View File

@@ -58,5 +58,9 @@
"update.up_to_date_body": "OmniMTP {version} is the latest release.", "update.up_to_date_body": "OmniMTP {version} is the latest release.",
"update.check_failed_title": "Update Check Failed", "update.check_failed_title": "Update Check Failed",
"update.check_failed_body": "Could not reach the update server.", "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"
} }

View File

@@ -58,5 +58,9 @@
"update.up_to_date_body": "OmniMTP {version} es la última versión.", "update.up_to_date_body": "OmniMTP {version} es la última versión.",
"update.check_failed_title": "Error al buscar actualizaciones", "update.check_failed_title": "Error al buscar actualizaciones",
"update.check_failed_body": "No se pudo contactar con el servidor de 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"
} }

View File

@@ -58,5 +58,9 @@
"update.up_to_date_body": "OmniMTP {version} est la dernière version.", "update.up_to_date_body": "OmniMTP {version} est la dernière version.",
"update.check_failed_title": "Échec de la vérification", "update.check_failed_title": "Échec de la vérification",
"update.check_failed_body": "Impossible de joindre le serveur de mises à jour.", "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"
} }

View File

@@ -58,5 +58,9 @@
"update.up_to_date_body": "OmniMTP {version} è lultima versione.", "update.up_to_date_body": "OmniMTP {version} è lultima versione.",
"update.check_failed_title": "Verifica aggiornamenti non riuscita", "update.check_failed_title": "Verifica aggiornamenti non riuscita",
"update.check_failed_body": "Impossibile raggiungere il server degli aggiornamenti.", "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"
} }

View File

@@ -58,5 +58,9 @@
"update.up_to_date_body": "OmniMTP {version} が最新リリースです。", "update.up_to_date_body": "OmniMTP {version} が最新リリースです。",
"update.check_failed_title": "アップデート確認に失敗", "update.check_failed_title": "アップデート確認に失敗",
"update.check_failed_body": "アップデートサーバーに接続できませんでした。", "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": "新しいフォルダ"
} }

View File

@@ -58,5 +58,9 @@
"update.up_to_date_body": "OmniMTP {version} is de nieuwste release.", "update.up_to_date_body": "OmniMTP {version} is de nieuwste release.",
"update.check_failed_title": "Updatecontrole mislukt", "update.check_failed_title": "Updatecontrole mislukt",
"update.check_failed_body": "Kon de updateserver niet bereiken.", "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"
} }

View File

@@ -58,5 +58,9 @@
"update.up_to_date_body": "OmniMTP {version} é a versão mais recente.", "update.up_to_date_body": "OmniMTP {version} é a versão mais recente.",
"update.check_failed_title": "Falha ao verificar atualizações", "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.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"
} }

View File

@@ -58,5 +58,9 @@
"update.up_to_date_body": "OmniMTP {version} — последний выпуск.", "update.up_to_date_body": "OmniMTP {version} — последний выпуск.",
"update.check_failed_title": "Не удалось проверить обновления", "update.check_failed_title": "Не удалось проверить обновления",
"update.check_failed_body": "Не удалось связаться с сервером обновлений.", "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": "Новая папка"
} }

View File

@@ -58,5 +58,9 @@
"update.up_to_date_body": "OmniMTP {version} 已是最新版本。", "update.up_to_date_body": "OmniMTP {version} 已是最新版本。",
"update.check_failed_title": "检查更新失败", "update.check_failed_title": "检查更新失败",
"update.check_failed_body": "无法连接更新服务器。", "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": "新建文件夹"
} }