5 Commits

Author SHA1 Message Date
474f4f7714 Version bump to 1.0.2 2026-08-07 00:14:53 +02:00
846f743a5b Support recursive folder uploads via drag-and-drop and the file picker.
Create matching remote directories and enqueue contained files instead of ignoring directories.
2026-08-07 00:14:53 +02:00
bf31dbc031 Version bump to 1.0.1 2026-07-30 16:14:13 +02:00
68353cbc56 Add startup update check against Gitea releases.
Show a native alert when a newer release is available, with menu access to check manually.
2026-07-30 16:13:02 +02:00
86fa763eda Revert "Add Sparkle appcast for in-app update checks."
This reverts commit 3d66bfa666.
2026-07-30 16:13:02 +02:00
15 changed files with 421 additions and 44 deletions

View File

@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.25)
project(OmniMTP VERSION 1.0.0
project(OmniMTP VERSION 1.0.2
DESCRIPTION "Nintendo Switch MTP Client for macOS"
LANGUAGES CXX OBJCXX OBJC C
)

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>OmniMTP</title>
<link>https://git.niklascfw.de/OmniNX/OmniMTP/releases</link>
<description>OmniMTP release feed</description>
<language>en</language>
<item>
<title>OmniMTP 1.0.1</title>
<description><![CDATA[OmniMTP 1.0.1]]></description>
<pubDate>Thu, 30 Jul 2026 14:05:38 +0000</pubDate>
<sparkle:version>1.0.1</sparkle:version>
<sparkle:shortVersionString>1.0.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
<enclosure
url="https://git.niklascfw.de/OmniNX/OmniMTP/releases/download/v1.0.1/OmniMTP.zip"
length="2361597"
type="application/octet-stream"
sparkle:edSignature="dxfnBKivIh02oRdqOXsoHHPn3Ha9aw4XeJu8YG34QKMuUHPcjBnLvPAfffUEggjczYZc1L295rP03/VfSDmSCA==" />
</item>
</channel>
</rss>

View File

@@ -83,6 +83,8 @@ private:
void start_download(uint32_t handle, uint32_t storage_id,
const std::string& filename, uint64_t size);
void start_upload(const fs::path& src_path, uint64_t file_size);
// Recursively create a remote folder and enqueue files under parent_handle.
void upload_local_tree(const fs::path& dir, uint32_t parent_handle, uint32_t storage_id);
// Recursively download an MTP association (folder) into dest (must hold device_mtx_).
void download_remote_tree(uint32_t handle, uint32_t storage_id, const fs::path& dest);

View File

@@ -275,6 +275,49 @@ void App::start_upload(const fs::path& src, uint64_t sz) {
src.filename().string(), sz);
set_status("status.uploading", {src.filename().string()});
}
void App::upload_local_tree(const fs::path& dir, uint32_t parent_handle, uint32_t storage_id) {
if (!engine_ || !ops_) return;
const std::string name = dir.filename().string();
if (name.empty() || name == "." || name == "..") return;
uint32_t dir_handle = 0;
try {
std::lock_guard lock(device_mtx_);
if (!ops_) return;
dir_handle = ops_->create_directory(storage_id, parent_handle, name);
} catch (const std::exception& e) {
set_status("status.listing_error", {e.what()});
return;
} catch (...) {
set_status("status.listing_error", {"Failed to create folder: " + name});
return;
}
if (dir_handle == 0) {
set_status("status.listing_error", {"Failed to create folder: " + name});
return;
}
std::error_code ec;
for (fs::directory_iterator it(dir, ec), end; !ec && it != end; it.increment(ec)) {
const auto& entry = *it;
const std::string child_name = entry.path().filename().string();
if (child_name.empty() || child_name == "." || child_name == "..") continue;
// Skip macOS clutter that Switch MTP installers don't need.
if (child_name == ".DS_Store" || child_name.rfind("._", 0) == 0) continue;
std::error_code st_ec;
if (entry.is_directory(st_ec)) {
upload_local_tree(entry.path(), dir_handle, storage_id);
} else if (entry.is_regular_file(st_ec)) {
auto sz = entry.file_size(st_ec);
if (st_ec) continue;
engine_->enqueue_upload(entry.path().string(), dir_handle, storage_id,
child_name, sz);
}
}
}
void App::set_status(const std::string& key, std::vector<std::string> args, float dur) {
status_key_ = key;
status_args_ = std::move(args);
@@ -381,13 +424,8 @@ void App::process_finder_drops() {
drops.swap(finder_drops_);
}
for (auto& dropped : drops) {
fs::path p(dropped);
std::error_code ec;
if (fs::is_directory(p, ec)) continue;
if (!ec && connected_.load()) {
auto sz = fs::file_size(p, ec);
if (!ec) start_upload(p, sz);
}
if (connected_.load())
do_upload(dropped);
}
}
@@ -419,8 +457,17 @@ void App::set_active_storage(uint32_t id) {
}
void App::request_refresh_remote() { refresh_remote(); }
void App::do_upload(const std::string& src) {
if (!connected_.load() || !engine_ || remote_nav_stack_.empty()) return;
fs::path p(src);
std::error_code ec;
if (fs::is_directory(p, ec)) {
const auto& top = remote_nav_stack_.back();
set_status("status.uploading", {p.filename().string()});
upload_local_tree(p, top.handle, top.storage_id);
remote_needs_refresh_ = true;
return;
}
if (ec) return;
auto sz = fs::file_size(p, ec);
if (!ec) start_upload(p, sz);
}

View File

@@ -211,6 +211,73 @@ static NSString* OmniMenuFormat(NSString* key, NSDictionary<NSString*, NSString*
return s;
}
static NSString* const kOmniReleasesLatestURL =
@"https://git.niklascfw.de/api/v1/repos/OmniNX/OmniMTP/releases/latest";
static NSString* const kOmniReleasesPageURL =
@"https://git.niklascfw.de/OmniNX/OmniMTP/releases";
static NSString* const kOmniSkippedUpdateKey = @"omni.skippedUpdateTag";
static NSArray<NSNumber*>* OmniParseVersionParts(NSString* version) {
if (![version isKindOfClass:[NSString class]] || version.length == 0)
return @[];
NSString* v = [version stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
if ([v.lowercaseString hasPrefix:@"v"])
v = [v substringFromIndex:1];
NSString* core = [[v componentsSeparatedByString:@"-"] firstObject] ?: v;
NSMutableArray<NSNumber*>* parts = [NSMutableArray array];
for (NSString* p in [core componentsSeparatedByString:@"."]) {
NSInteger n = 0;
NSScanner* scanner = [NSScanner scannerWithString:p];
[scanner scanInteger:&n];
[parts addObject:@(n)];
}
return parts;
}
static NSComparisonResult OmniCompareVersions(NSString* a, NSString* b) {
NSArray<NSNumber*>* pa = OmniParseVersionParts(a);
NSArray<NSNumber*>* pb = OmniParseVersionParts(b);
NSUInteger n = MAX(pa.count, pb.count);
for (NSUInteger i = 0; i < n; ++i) {
NSInteger va = i < pa.count ? pa[i].integerValue : 0;
NSInteger vb = i < pb.count ? pb[i].integerValue : 0;
if (va < vb) return NSOrderedAscending;
if (va > vb) return NSOrderedDescending;
}
return NSOrderedSame;
}
static NSString* OmniCurrentAppVersion(void) {
NSString* v = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
return (v.length > 0) ? v : @"0.0.0";
}
static NSString* OmniPreferredDownloadURL(NSDictionary* release) {
id assets = release[@"assets"];
if ([assets isKindOfClass:[NSArray class]]) {
for (id asset in (NSArray*)assets) {
if (![asset isKindOfClass:[NSDictionary class]]) continue;
NSString* name = asset[@"name"];
NSString* url = asset[@"browser_download_url"];
if (![name isKindOfClass:[NSString class]] || ![url isKindOfClass:[NSString class]])
continue;
NSString* lower = name.lowercaseString;
if ([lower hasSuffix:@".zip"] || [lower hasSuffix:@".dmg"])
return url;
}
for (id asset in (NSArray*)assets) {
if (![asset isKindOfClass:[NSDictionary class]]) continue;
NSString* url = asset[@"browser_download_url"];
if ([url isKindOfClass:[NSString class]] && url.length)
return url;
}
}
NSString* html = release[@"html_url"];
if ([html isKindOfClass:[NSString class]] && html.length)
return html;
return kOmniReleasesPageURL;
}
// ─── Forward declarations ─────────────────────────────────────────────────────
@class OmniBridge;
@class OmniAppDelegate;
@@ -343,7 +410,7 @@ static NSString* OmniMenuFormat(NSString* key, NSDictionary<NSString*, NSString*
dispatch_async(dispatch_get_main_queue(), ^{
NSOpenPanel* panel = [NSOpenPanel openPanel];
panel.canChooseFiles = YES;
panel.canChooseDirectories = NO;
panel.canChooseDirectories = YES;
panel.allowsMultipleSelection = YES;
[panel beginWithCompletionHandler:^(NSModalResponse r) {
if (r == NSModalResponseOK) {
@@ -805,9 +872,10 @@ static const CGFloat kTrafficLightInset = 72.0;
NSString* path = url.path;
if (!path) continue;
BOOL isDir = NO;
if ([fm fileExistsAtPath:path isDirectory:&isDir] && !isDir)
_app->do_upload(path.UTF8String);
handled = YES;
if ([fm fileExistsAtPath:path isDirectory:&isDir]) {
self->_app->do_upload(path.UTF8String);
handled = YES;
}
}
return handled;
}
@@ -821,6 +889,7 @@ static const CGFloat kTrafficLightInset = 72.0;
- (void)selectLanguage:(id)sender;
- (void)showAbout:(id)sender;
- (void)closeAboutPanel:(id)sender;
- (void)checkForUpdates:(id)sender;
- (void)applyLanguageToWebView;
@end
@@ -831,9 +900,11 @@ static const CGFloat kTrafficLightInset = 72.0;
OmniBridge* _bridge;
NSPanel* _aboutPanel;
NSMenuItem* _aboutItem;
NSMenuItem* _checkUpdatesItem;
NSMenuItem* _optionsItem;
NSMenuItem* _languageItem;
NSMenuItem* _quitItem;
BOOL _updateCheckInFlight;
}
- (instancetype)initWithApp:(omniMTP::App*)app {
@@ -855,6 +926,12 @@ static const CGFloat kTrafficLightInset = 72.0;
keyEquivalent:@""];
_aboutItem.target = self;
[appMenu addItem:_aboutItem];
_checkUpdatesItem = [[NSMenuItem alloc] initWithTitle:OmniMenuString(@"menu.check_updates")
action:@selector(checkForUpdates:)
keyEquivalent:@""];
_checkUpdatesItem.target = self;
[appMenu addItem:_checkUpdatesItem];
[appMenu addItem:[NSMenuItem separatorItem]];
// Options → Language
@@ -1020,6 +1097,163 @@ static const CGFloat kTrafficLightInset = 72.0;
_aboutPanel = nil;
}
- (void)checkForUpdates:(id)sender {
[self startUpdateCheckManual:(sender != nil)];
}
- (void)startUpdateCheckManual:(BOOL)manual {
if (_updateCheckInFlight) return;
_updateCheckInFlight = YES;
if (_checkUpdatesItem)
_checkUpdatesItem.enabled = NO;
NSURL* url = [NSURL URLWithString:kOmniReleasesLatestURL];
NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:12.0];
[req setValue:@"OmniMTP" forHTTPHeaderField:@"User-Agent"];
[req setValue:@"application/json" forHTTPHeaderField:@"Accept"];
__weak OmniAppDelegate* weakSelf = self;
NSURLSessionDataTask* task =
[[NSURLSession sharedSession] dataTaskWithRequest:req
completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
dispatch_async(dispatch_get_main_queue(), ^{
OmniAppDelegate* selfStrong = weakSelf;
if (!selfStrong) return;
selfStrong->_updateCheckInFlight = NO;
if (selfStrong->_checkUpdatesItem)
selfStrong->_checkUpdatesItem.enabled = YES;
[selfStrong handleUpdateResponse:data
response:response
error:error
manual:manual];
});
}];
[task resume];
}
- (void)handleUpdateResponse:(NSData*)data
response:(NSURLResponse*)response
error:(NSError*)error
manual:(BOOL)manual {
if (error || !data) {
if (manual) {
NSAlert* alert = [[NSAlert alloc] init];
alert.messageText = OmniMenuString(@"update.check_failed_title");
alert.informativeText = error.localizedDescription.length
? error.localizedDescription
: OmniMenuString(@"update.check_failed_body");
[alert addButtonWithTitle:OmniMenuString(@"confirm.ok_btn")];
[alert beginSheetModalForWindow:_window completionHandler:nil];
}
return;
}
NSInteger status = 0;
if ([response isKindOfClass:[NSHTTPURLResponse class]])
status = ((NSHTTPURLResponse*)response).statusCode;
if (status == 404) {
// No releases published yet.
if (manual) [self showUpToDateAlert];
return;
}
if (status < 200 || status >= 300) {
if (manual) {
NSAlert* alert = [[NSAlert alloc] init];
alert.messageText = OmniMenuString(@"update.check_failed_title");
alert.informativeText = OmniMenuFormat(@"update.http_error", @{
@"code": [NSString stringWithFormat:@"%ld", (long)status]
});
[alert addButtonWithTitle:OmniMenuString(@"confirm.ok_btn")];
[alert beginSheetModalForWindow:_window completionHandler:nil];
}
return;
}
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (![json isKindOfClass:[NSDictionary class]]) {
if (manual) {
NSAlert* alert = [[NSAlert alloc] init];
alert.messageText = OmniMenuString(@"update.check_failed_title");
alert.informativeText = OmniMenuString(@"update.check_failed_body");
[alert addButtonWithTitle:OmniMenuString(@"confirm.ok_btn")];
[alert beginSheetModalForWindow:_window completionHandler:nil];
}
return;
}
NSDictionary* release = (NSDictionary*)json;
if ([release[@"draft"] boolValue] || [release[@"prerelease"] boolValue]) {
if (manual) [self showUpToDateAlert];
return;
}
NSString* tag = release[@"tag_name"];
if (![tag isKindOfClass:[NSString class]] || tag.length == 0)
tag = release[@"name"];
if (![tag isKindOfClass:[NSString class]] || tag.length == 0) {
if (manual) [self showUpToDateAlert];
return;
}
NSString* current = OmniCurrentAppVersion();
if (OmniCompareVersions(current, tag) != NSOrderedAscending) {
if (manual) [self showUpToDateAlert];
return;
}
if (!manual) {
NSString* skipped = [NSUserDefaults.standardUserDefaults stringForKey:kOmniSkippedUpdateKey];
if (skipped.length && OmniCompareVersions(skipped, tag) != NSOrderedAscending)
return;
}
NSString* downloadURL = OmniPreferredDownloadURL(release);
[self showUpdateAvailableForTag:tag current:current downloadURL:downloadURL];
}
- (void)showUpToDateAlert {
NSAlert* alert = [[NSAlert alloc] init];
alert.messageText = OmniMenuString(@"update.up_to_date_title");
alert.informativeText = OmniMenuFormat(@"update.up_to_date_body", @{
@"version": OmniCurrentAppVersion()
});
[alert addButtonWithTitle:OmniMenuString(@"confirm.ok_btn")];
if (_window)
[alert beginSheetModalForWindow:_window completionHandler:nil];
else
[alert runModal];
}
- (void)showUpdateAvailableForTag:(NSString*)tag
current:(NSString*)current
downloadURL:(NSString*)downloadURL {
NSAlert* alert = [[NSAlert alloc] init];
alert.messageText = OmniMenuString(@"update.available_title");
alert.informativeText = OmniMenuFormat(@"update.available_body", @{
@"latest": tag,
@"current": current
});
[alert addButtonWithTitle:OmniMenuString(@"update.download")];
[alert addButtonWithTitle:OmniMenuString(@"update.later")];
[alert addButtonWithTitle:OmniMenuString(@"update.skip")];
void (^handle)(NSModalResponse) = ^(NSModalResponse response) {
if (response == NSAlertFirstButtonReturn) {
NSURL* url = [NSURL URLWithString:downloadURL];
if (url) [NSWorkspace.sharedWorkspace openURL:url];
} else if (response == NSAlertThirdButtonReturn) {
[NSUserDefaults.standardUserDefaults setObject:tag forKey:kOmniSkippedUpdateKey];
}
};
if (_window)
[alert beginSheetModalForWindow:_window completionHandler:handle];
else
handle([alert runModal]);
}
- (BOOL)windowShouldClose:(NSWindow*)sender {
if (sender == _aboutPanel) {
[NSApp stopModal];
@@ -1194,6 +1428,12 @@ static const CGFloat kTrafficLightInset = 72.0;
[_webView loadHTMLString:fallback baseURL:nil];
NSLog(@"[OmniMTP] ERROR: webroot/index.html not found in bundle or source tree");
}
// Defer so the main window is visible before any update sheet appears.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[self startUpdateCheckManual:NO];
});
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)app {

View File

@@ -47,5 +47,16 @@
"about.tagline": "Nintendo Switch MTP-Client für macOS",
"about.version": "Version {version}",
"about.copyright": "Copyright © 2026 NiklasCFW",
"status.usb_recovering": "USB wird nach dem Ruhezustand neu verbunden…"
"status.usb_recovering": "USB wird nach dem Ruhezustand neu verbunden…",
"menu.check_updates": "Nach Updates suchen…",
"update.available_title": "Update verfügbar",
"update.available_body": "OmniMTP {latest} ist verfügbar.\nDu hast {current}.",
"update.download": "Herunterladen",
"update.later": "Später",
"update.skip": "Diese Version überspringen",
"update.up_to_date_title": "Du bist auf dem neuesten Stand",
"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}."
}

View File

@@ -47,5 +47,16 @@
"about.tagline": "Nintendo Switch MTP Client for macOS",
"about.version": "Version {version}",
"about.copyright": "Copyright © 2026 NiklasCFW",
"status.usb_recovering": "Reconnecting USB after sleep…"
"status.usb_recovering": "Reconnecting USB after sleep…",
"menu.check_updates": "Check for Updates…",
"update.available_title": "Update Available",
"update.available_body": "OmniMTP {latest} is available.\nYou have {current}.",
"update.download": "Download",
"update.later": "Later",
"update.skip": "Skip This Version",
"update.up_to_date_title": "Youre Up to Date",
"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}."
}

View File

@@ -47,5 +47,16 @@
"about.tagline": "Cliente MTP de Nintendo Switch para macOS",
"about.version": "Versión {version}",
"about.copyright": "Copyright © 2026 NiklasCFW",
"status.usb_recovering": "Reconectando USB tras la suspensión…"
"status.usb_recovering": "Reconectando USB tras la suspensión…",
"menu.check_updates": "Buscar actualizaciones…",
"update.available_title": "Actualización disponible",
"update.available_body": "OmniMTP {latest} está disponible.\nTienes {current}.",
"update.download": "Descargar",
"update.later": "Más tarde",
"update.skip": "Omitir esta versión",
"update.up_to_date_title": "Estás al día",
"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}."
}

View File

@@ -47,5 +47,16 @@
"about.tagline": "Client MTP Nintendo Switch pour macOS",
"about.version": "Version {version}",
"about.copyright": "Copyright © 2026 NiklasCFW",
"status.usb_recovering": "Reconnexion USB après veille…"
"status.usb_recovering": "Reconnexion USB après veille…",
"menu.check_updates": "Rechercher des mises à jour…",
"update.available_title": "Mise à jour disponible",
"update.available_body": "OmniMTP {latest} est disponible.\nVous avez {current}.",
"update.download": "Télécharger",
"update.later": "Plus tard",
"update.skip": "Ignorer cette version",
"update.up_to_date_title": "À jour",
"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}."
}

View File

@@ -47,5 +47,16 @@
"about.tagline": "Client MTP Nintendo Switch per macOS",
"about.version": "Versione {version}",
"about.copyright": "Copyright © 2026 NiklasCFW",
"status.usb_recovering": "Riconnessione USB dopo lo standby…"
"status.usb_recovering": "Riconnessione USB dopo lo standby…",
"menu.check_updates": "Verifica aggiornamenti…",
"update.available_title": "Aggiornamento disponibile",
"update.available_body": "OmniMTP {latest} è disponibile.\nHai {current}.",
"update.download": "Scarica",
"update.later": "Dopo",
"update.skip": "Salta questa versione",
"update.up_to_date_title": "Sei aggiornato",
"update.up_to_date_body": "OmniMTP {version} è lultima 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}."
}

View File

@@ -47,5 +47,16 @@
"about.tagline": "macOS向け Nintendo Switch MTPクライアント",
"about.version": "バージョン {version}",
"about.copyright": "Copyright © 2026 NiklasCFW",
"status.usb_recovering": "スリープ後、USBを再接続しています…"
"status.usb_recovering": "スリープ後、USBを再接続しています…",
"menu.check_updates": "アップデートを確認…",
"update.available_title": "アップデートがあります",
"update.available_body": "OmniMTP {latest} が利用可能です。\n現在のバージョンは {current} です。",
"update.download": "ダウンロード",
"update.later": "後で",
"update.skip": "このバージョンをスキップ",
"update.up_to_date_title": "最新です",
"update.up_to_date_body": "OmniMTP {version} が最新リリースです。",
"update.check_failed_title": "アップデート確認に失敗",
"update.check_failed_body": "アップデートサーバーに接続できませんでした。",
"update.http_error": "サーバーが HTTP {code} を返しました。"
}

View File

@@ -47,5 +47,16 @@
"about.tagline": "Nintendo Switch MTP-client voor macOS",
"about.version": "Versie {version}",
"about.copyright": "Copyright © 2026 NiklasCFW",
"status.usb_recovering": "USB opnieuw verbinden na slaapstand…"
"status.usb_recovering": "USB opnieuw verbinden na slaapstand…",
"menu.check_updates": "Controleren op updates…",
"update.available_title": "Update beschikbaar",
"update.available_body": "OmniMTP {latest} is beschikbaar.\nJe hebt {current}.",
"update.download": "Downloaden",
"update.later": "Later",
"update.skip": "Deze versie overslaan",
"update.up_to_date_title": "Je bent up-to-date",
"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."
}

View File

@@ -47,5 +47,16 @@
"about.tagline": "Cliente MTP Nintendo Switch para macOS",
"about.version": "Versão {version}",
"about.copyright": "Copyright © 2026 NiklasCFW",
"status.usb_recovering": "Reconectando USB após a suspensão…"
"status.usb_recovering": "Reconectando USB após a suspensão…",
"menu.check_updates": "Procurar atualizações…",
"update.available_title": "Atualização disponível",
"update.available_body": "OmniMTP {latest} está disponível.\nVocê tem {current}.",
"update.download": "Baixar",
"update.later": "Mais tarde",
"update.skip": "Ignorar esta versão",
"update.up_to_date_title": "Você está atualizado",
"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}."
}

View File

@@ -47,5 +47,16 @@
"about.tagline": "MTP-клиент Nintendo Switch для macOS",
"about.version": "Версия {version}",
"about.copyright": "Copyright © 2026 NiklasCFW",
"status.usb_recovering": "Повторное подключение USB после сна…"
"status.usb_recovering": "Повторное подключение USB после сна…",
"menu.check_updates": "Проверить обновления…",
"update.available_title": "Доступно обновление",
"update.available_body": "Доступна OmniMTP {latest}.\nУ вас {current}.",
"update.download": "Скачать",
"update.later": "Позже",
"update.skip": "Пропустить эту версию",
"update.up_to_date_title": "У вас актуальная версия",
"update.up_to_date_body": "OmniMTP {version} — последний выпуск.",
"update.check_failed_title": "Не удалось проверить обновления",
"update.check_failed_body": "Не удалось связаться с сервером обновлений.",
"update.http_error": "Сервер вернул HTTP {code}."
}

View File

@@ -47,5 +47,16 @@
"about.tagline": "适用于 macOS 的 Nintendo Switch MTP 客户端",
"about.version": "版本 {version}",
"about.copyright": "Copyright © 2026 NiklasCFW",
"status.usb_recovering": "休眠后正在重新连接 USB…"
"status.usb_recovering": "休眠后正在重新连接 USB…",
"menu.check_updates": "检查更新…",
"update.available_title": "有可用更新",
"update.available_body": "OmniMTP {latest} 可用。\n当前版本为 {current}。",
"update.download": "下载",
"update.later": "稍后",
"update.skip": "跳过此版本",
"update.up_to_date_title": "已是最新",
"update.up_to_date_body": "OmniMTP {version} 已是最新版本。",
"update.check_failed_title": "检查更新失败",
"update.check_failed_body": "无法连接更新服务器。",
"update.http_error": "服务器返回 HTTP {code}。"
}