Add startup update check against Gitea releases.
Show a native alert when a newer release is available, with menu access to check manually.
This commit is contained in:
239
src/ui/WebUI.mm
239
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;
|
||||
@@ -821,6 +888,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 +899,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 +925,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 +1096,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 +1427,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