Compare commits
5 Commits
3d66bfa666
...
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 474f4f7714 | |||
| 846f743a5b | |||
| bf31dbc031 | |||
| 68353cbc56 | |||
| 86fa763eda |
@@ -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
|
||||
)
|
||||
|
||||
22
appcast.xml
22
appcast.xml
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
248
src/ui/WebUI.mm
248
src/ui/WebUI.mm
@@ -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 {
|
||||
|
||||
@@ -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}."
|
||||
}
|
||||
|
||||
@@ -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": "You’re 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}."
|
||||
}
|
||||
|
||||
@@ -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}."
|
||||
}
|
||||
|
||||
@@ -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}."
|
||||
}
|
||||
|
||||
@@ -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} è 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}."
|
||||
}
|
||||
|
||||
@@ -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} を返しました。"
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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}."
|
||||
}
|
||||
|
||||
@@ -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}."
|
||||
}
|
||||
|
||||
@@ -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}。"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user