From e2108a28ea655c045236457433ce73405aa958e0 Mon Sep 17 00:00:00 2001 From: dranik Date: Wed, 13 May 2026 17:41:26 +0300 Subject: [PATCH] Feat: Add deep link --- client/amneziaApplication.cpp | 119 ++++++++++++++++-- client/amneziaApplication.h | 10 ++ .../src/org/amnezia/vpn/AmneziaActivity.kt | 5 +- .../org/amnezia/vpn/ImportConfigActivity.kt | 1 + client/cmake/ios.cmake | 2 + client/core/controllers/coreController.cpp | 16 +++ client/core/controllers/coreController.h | 2 + .../core/controllers/coreSignalHandlers.cpp | 8 +- .../selfhosted/importController.cpp | 1 + client/ios/app/Info.plist.in | 13 ++ client/macos/app/Info.plist.in | 13 ++ client/main.cpp | 43 +++++-- client/platforms/ios/AmneziaOpenUrlImport.h | 14 +++ client/platforms/ios/AmneziaOpenUrlImport.mm | 51 ++++++++ .../ios/AmneziaSceneDelegateHooks.mm | 31 +---- client/platforms/ios/QtAppDelegate.mm | 30 ++--- client/platforms/ios/ios_controller.mm | 2 +- .../api/subscriptionUiController.cpp | 4 + .../qml/Pages2/PageSetupWizardViewConfig.qml | 4 +- 19 files changed, 292 insertions(+), 77 deletions(-) create mode 100644 client/platforms/ios/AmneziaOpenUrlImport.h create mode 100644 client/platforms/ios/AmneziaOpenUrlImport.mm diff --git a/client/amneziaApplication.cpp b/client/amneziaApplication.cpp index 008cc345d..c0f5053b5 100644 --- a/client/amneziaApplication.cpp +++ b/client/amneziaApplication.cpp @@ -15,6 +15,9 @@ #include #include #include +#include +#include +#include #include #include @@ -81,6 +84,44 @@ AmneziaApplication::~AmneziaApplication() } } +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) +namespace { +QString vpnUrlFromArguments(const QStringList &args) +{ + for (const QString &arg : args) { + const QString t = arg.trimmed(); + if (t.startsWith(QLatin1String("vpn://"), Qt::CaseInsensitive)) { + return t; + } + } + return {}; +} +} // namespace +#endif + +#if defined(Q_OS_WIN) && !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) +namespace { +void registerWindowsVpnUrlSchemeIfNeeded() +{ + QSettings flag(ORGANIZATION_NAME, APPLICATION_NAME); + if (flag.value(QStringLiteral("protocolHandler/vpnRegistered")).toBool()) { + return; + } + + const QString exe = QDir::toNativeSeparators(QCoreApplication::applicationFilePath()); + + QSettings vpnKey(QStringLiteral("HKEY_CURRENT_USER\\Software\\Classes\\vpn"), QSettings::NativeFormat); + vpnKey.setValue(QStringLiteral("."), QStringLiteral("URL:AmneziaVPN")); + vpnKey.setValue(QStringLiteral("URL Protocol"), QString()); + + QSettings cmdKey(QStringLiteral("HKEY_CURRENT_USER\\Software\\Classes\\vpn\\shell\\open\\command"), QSettings::NativeFormat); + cmdKey.setValue(QStringLiteral("."), QStringLiteral("\"%1\" \"%2\"").arg(exe, QStringLiteral("%1"))); + + flag.setValue(QStringLiteral("protocolHandler/vpnRegistered"), true); +} +} // namespace +#endif + #ifdef Q_OS_ANDROID namespace { static void clearQtCaches() @@ -190,6 +231,18 @@ void AmneziaApplication::init() }); } } + +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) +# ifdef Q_OS_WIN + registerWindowsVpnUrlSchemeIfNeeded(); +# endif + if (!m_parser.isSet(m_optImport)) { + const QString vpnArg = vpnUrlFromArguments(QCoreApplication::arguments()); + if (!vpnArg.isEmpty()) { + QTimer::singleShot(0, this, [this, vpnArg]() { deliverVpnDeepLink(vpnArg); }); + } + } +#endif } void AmneziaApplication::registerTypes() @@ -250,23 +303,73 @@ bool AmneziaApplication::parseCommands() } #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) -void AmneziaApplication::startLocalServer() { - const QString serverName("AmneziaVPNInstance"); +void AmneziaApplication::startLocalServer() +{ + const QString serverName(QStringLiteral("AmneziaVPNInstance")); QLocalServer::removeServer(serverName); QLocalServer *server = new QLocalServer(this); - server->listen(serverName); + if (!server->listen(serverName)) { + qWarning() << "QLocalServer::listen failed:" << server->errorString(); + } - QObject::connect(server, &QLocalServer::newConnection, this, [server, this]() { - if (server) { - QLocalSocket *clientConnection = server->nextPendingConnection(); - clientConnection->deleteLater(); + QObject::connect(server, &QLocalServer::newConnection, this, [this, server]() { + QLocalSocket *sock = server->nextPendingConnection(); + if (!sock) { + return; } - emit m_coreController->pageController()->raiseMainWindow(); //TODO + + QString vpnPayload; + if (sock->waitForReadyRead(3000)) { + const QByteArray buf = sock->readAll(); + static const QByteArray prefix = QByteArrayLiteral("VPN\n"); + if (buf.startsWith(prefix)) { + vpnPayload = QString::fromUtf8(buf.mid(prefix.size())).trimmed(); + } + } + sock->deleteLater(); + + if (!vpnPayload.isEmpty()) { + QTimer::singleShot(0, this, [this, vpnPayload]() { deliverVpnDeepLink(vpnPayload); }); + } + + QTimer::singleShot(0, this, [this]() { + if (m_coreController && m_coreController->pageController()) { + emit m_coreController->pageController()->raiseMainWindow(); + } + }); }); } #endif +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) +void AmneziaApplication::deliverVpnDeepLink(const QString &payload) +{ + if (!m_coreController) { + return; + } + const QString trimmed = payload.trimmed(); + if (trimmed.isEmpty()) { + return; + } + m_coreController->openVpnKeyImportPreview(trimmed); +} + +bool AmneziaApplication::event(QEvent *event) +{ + if (event->type() == QEvent::FileOpen) { + auto *foe = static_cast(event); + const QUrl url = foe->url(); + if (url.scheme().compare(QLatin1String("vpn"), Qt::CaseInsensitive) == 0) { + const QString payload = url.toString(QUrl::PrettyDecoded); + QTimer::singleShot(0, this, [this, payload]() { deliverVpnDeepLink(payload); }); + return true; + } + } + return AMNEZIA_BASE_CLASS::event(event); +} +#endif + bool AmneziaApplication::eventFilter(QObject *watched, QEvent *event) { if (event->type() == QEvent::Close) { diff --git a/client/amneziaApplication.h b/client/amneziaApplication.h index 33b262c7f..9220b665f 100644 --- a/client/amneziaApplication.h +++ b/client/amneziaApplication.h @@ -12,6 +12,9 @@ #include #endif #include +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) +#include +#endif #include "core/controllers/coreController.h" #include "secureQSettings.h" @@ -67,12 +70,19 @@ private: QCommandLineOption m_optConnect; QCommandLineOption m_optImport; +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + void deliverVpnDeepLink(const QString &payload); +#endif + QSharedPointer m_vpnConnection; QThread m_vpnConnectionThread; QNetworkAccessManager *m_nam; protected: bool eventFilter(QObject *watched, QEvent *event) override; +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + bool event(QEvent *event) override; +#endif }; #endif // AMNEZIA_APPLICATION_H diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index dca70ee5c..b993344f9 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -243,7 +243,10 @@ class AmneziaActivity : QtActivity() { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) Log.v(TAG, "onNewIntent: $intent") - intent?.let(::processIntent) + intent?.let { + setIntent(it) + processIntent(it) + } } private fun processIntent(intent: Intent) { diff --git a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt index 49823a364..75f8f23c8 100644 --- a/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt +++ b/client/android/src/org/amnezia/vpn/ImportConfigActivity.kt @@ -36,6 +36,7 @@ class ImportConfigActivity : ComponentActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) Log.v(TAG, "onNewIntent: $intent") + setIntent(intent) intent.let(::readConfig) } diff --git a/client/cmake/ios.cmake b/client/cmake/ios.cmake index 86df23d25..11bb32d40 100644 --- a/client/cmake/ios.cmake +++ b/client/cmake/ios.cmake @@ -34,6 +34,7 @@ set(HEADERS ${HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h + ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaOpenUrlImport.h ) set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h PROPERTIES OBJECTIVE_CPP_HEADER TRUE) @@ -47,6 +48,7 @@ set(SOURCES ${SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm + ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaOpenUrlImport.mm ) diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 227850b6d..cb250df18 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -340,3 +340,19 @@ void CoreController::importConfigFromData(const QString &data) m_importController->importConfig(); } } + +void CoreController::openVpnKeyImportPreview(const QString &data) +{ + if (!m_importController || data.isEmpty()) { + return; + } + + emit m_pageController->goToPageHome(); + if (!m_importController->extractConfigFromData(data)) { + return; + } + emit m_pageController->goToPageViewConfig(); +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + emit m_pageController->raiseMainWindow(); +#endif +} diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h index 0d77c5167..c49f088fc 100644 --- a/client/core/controllers/coreController.h +++ b/client/core/controllers/coreController.h @@ -116,6 +116,8 @@ public: void openConnectionByIndex(int serverIndex); void importConfigFromData(const QString &data); + /** Navigate home, parse key, open preview (same path as mobile deep link / share). */ + void openVpnKeyImportPreview(const QString &data); void updateTranslator(const QLocale &locale); signals: diff --git a/client/core/controllers/coreSignalHandlers.cpp b/client/core/controllers/coreSignalHandlers.cpp index 449a8af1d..2296a74e1 100644 --- a/client/core/controllers/coreSignalHandlers.cpp +++ b/client/core/controllers/coreSignalHandlers.cpp @@ -361,10 +361,8 @@ void CoreSignalHandlers::initAndroidConnectionHandler() m_coreController->m_connectionController->restoreConnection(); }); connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, this, [this](QString data) { - emit m_coreController->m_pageController->goToPageHome(); - m_coreController->m_importController->extractConfigFromData(data); + m_coreController->openVpnKeyImportPreview(data); data.clear(); - emit m_coreController->m_pageController->goToPageViewConfig(); }); #endif } @@ -373,9 +371,7 @@ void CoreSignalHandlers::initIosImportHandler() { #ifdef Q_OS_IOS connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) { - emit m_coreController->m_pageController->goToPageHome(); - m_coreController->m_importController->extractConfigFromData(data); - emit m_coreController->m_pageController->goToPageViewConfig(); + m_coreController->openVpnKeyImportPreview(data); }); connect(IosController::Instance(), &IosController::importBackupFromOutside, this, [this](QString filePath) { emit m_coreController->m_pageController->goToPageHome(); diff --git a/client/core/controllers/selfhosted/importController.cpp b/client/core/controllers/selfhosted/importController.cpp index c1c7503eb..3297a86ec 100644 --- a/client/core/controllers/selfhosted/importController.cpp +++ b/client/core/controllers/selfhosted/importController.cpp @@ -387,6 +387,7 @@ void ImportController::importConfig(const QJsonObject &config) } else if (config.contains(configKey::configVersion)) { quint16 crc = qChecksum(QJsonDocument(config).toJson()); if (m_serversRepository->hasServerWithCrc(crc)) { + // Same API key / subscription blob already present (incl. deep link re-import). emit importErrorOccurred(ErrorCode::ApiConfigAlreadyAdded, true); } else { QJsonObject configWithCrc = config; diff --git a/client/ios/app/Info.plist.in b/client/ios/app/Info.plist.in index 6165daf32..decca2ca5 100644 --- a/client/ios/app/Info.plist.in +++ b/client/ios/app/Info.plist.in @@ -86,6 +86,19 @@ CFBundleIcons~ipad + CFBundleURLTypes + + + CFBundleURLName + org.amnezia.AmneziaVPN.vpn-deeplink + CFBundleURLSchemes + + vpn + + CFBundleTypeRole + Editor + + UTImportedTypeDeclarations diff --git a/client/macos/app/Info.plist.in b/client/macos/app/Info.plist.in index 1c9ad48eb..dccbafa60 100644 --- a/client/macos/app/Info.plist.in +++ b/client/macos/app/Info.plist.in @@ -46,6 +46,19 @@ CFBundleIcons + CFBundleURLTypes + + + CFBundleURLName + org.amnezia.AmneziaVPN.vpn-deeplink + CFBundleURLSchemes + + vpn + + CFBundleTypeRole + Editor + + UTImportedTypeDeclarations diff --git a/client/main.cpp b/client/main.cpp index 621692bd7..3239b01b9 100644 --- a/client/main.cpp +++ b/client/main.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include @@ -6,7 +8,7 @@ #include "core/utils/migrations.h" #include "version.h" -#include +#include #ifdef Q_OS_WIN #include "Windows.h" @@ -17,18 +19,41 @@ #endif #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) -bool isAnotherInstanceRunning() +namespace { +QString findVpnDeepLinkInArguments(const QStringList &args) +{ + for (const QString &arg : args) { + const QString t = arg.trimmed(); + if (t.startsWith(QLatin1String("vpn://"), Qt::CaseInsensitive)) { + return t; + } + } + return {}; +} + +bool notifyRunningInstanceOrExit(AmneziaApplication &app, const QString &vpnPayload) { QLocalSocket socket; - socket.connectToServer("AmneziaVPNInstance"); - if (socket.waitForConnected(500)) { - qWarning() << "AmneziaVPN is already running"; - return true; + socket.connectToServer(QStringLiteral("AmneziaVPNInstance")); + if (!socket.waitForConnected(500)) { + return false; } - return false; + qWarning() << "AmneziaVPN is already running"; + if (!vpnPayload.isEmpty()) { + const QByteArray msg = QByteArrayLiteral("VPN\n") + vpnPayload.toUtf8() + '\n'; + socket.write(msg); + socket.waitForBytesWritten(3000); + } + socket.flush(); + QTimer::singleShot(1000, &app, [&app]() { app.quit(); }); + return true; } +} // namespace #endif +// Desktop (non-NE): single-instance IPC forwards vpn:// to the running process. MACOS_NE has no IPC here; +// deep links use argv / QFileOpenEvent after registration in the app bundle Info.plist. + int main(int argc, char *argv[]) { Migrations migrationsManager; @@ -48,8 +73,8 @@ int main(int argc, char *argv[]) OsSignalHandler::setup(); #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) - if (isAnotherInstanceRunning()) { - QTimer::singleShot(1000, &app, [&]() { app.quit(); }); + const QString vpnFromArgv = findVpnDeepLinkInArguments(QCoreApplication::arguments()); + if (notifyRunningInstanceOrExit(app, vpnFromArgv)) { return app.exec(); } app.startLocalServer(); diff --git a/client/platforms/ios/AmneziaOpenUrlImport.h b/client/platforms/ios/AmneziaOpenUrlImport.h new file mode 100644 index 000000000..dcf6aeb83 --- /dev/null +++ b/client/platforms/ios/AmneziaOpenUrlImport.h @@ -0,0 +1,14 @@ +#pragma once + +#import + +#ifdef __cplusplus +extern "C" { +#endif + +/** Handles custom scheme vpn:// (full absoluteString) and file URLs for config / backup import. */ +void AmneziaHandleOpenUrl(NSURL *url); + +#ifdef __cplusplus +} +#endif diff --git a/client/platforms/ios/AmneziaOpenUrlImport.mm b/client/platforms/ios/AmneziaOpenUrlImport.mm new file mode 100644 index 000000000..3f661ca9b --- /dev/null +++ b/client/platforms/ios/AmneziaOpenUrlImport.mm @@ -0,0 +1,51 @@ +#import "AmneziaOpenUrlImport.h" + +#include "ios_controller.h" + +#include +#include + +#include + +void AmneziaHandleOpenUrl(NSURL *url) +{ + if (!url) { + return; + } + + NSString *scheme = url.scheme ? [url.scheme lowercaseString] : @""; + if ([scheme isEqualToString:@"vpn"]) { + NSString *absolute = url.absoluteString; + if (absolute.length == 0) { + return; + } + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + IosController::Instance()->importConfigFromOutside(QString::fromUtf8([absolute UTF8String])); + }); + return; + } + + if (!url.isFileURL) { + return; + } + + QString filePath = QString::fromUtf8([url.path UTF8String]); + if (filePath.isEmpty()) { + return; + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if (filePath.contains(QLatin1String("backup"))) { + IosController::Instance()->importBackupFromOutside(filePath); + return; + } + + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + return; + } + + const QByteArray data = file.readAll(); + IosController::Instance()->importConfigFromOutside(QString::fromUtf8(data)); + }); +} diff --git a/client/platforms/ios/AmneziaSceneDelegateHooks.mm b/client/platforms/ios/AmneziaSceneDelegateHooks.mm index 60cbbe0fa..c1d7111d3 100644 --- a/client/platforms/ios/AmneziaSceneDelegateHooks.mm +++ b/client/platforms/ios/AmneziaSceneDelegateHooks.mm @@ -1,12 +1,7 @@ #import #import -#include -#include -#include -#include - -#include "ios_controller.h" +#import "AmneziaOpenUrlImport.h" using SceneOpenURLContexts = void (*)(id, SEL, UIScene *, NSSet *); @@ -14,29 +9,7 @@ static SceneOpenURLContexts g_originalSceneOpenURLContexts = nullptr; static void amnezia_handleURL(NSURL *url) { - if (!url || !url.isFileURL) { - return; - } - - QString filePath(url.path.UTF8String); - if (filePath.isEmpty()) { - return; - } - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - if (filePath.contains("backup")) { - IosController::Instance()->importBackupFromOutside(filePath); - return; - } - - QFile file(filePath); - if (!file.open(QIODevice::ReadOnly)) { - return; - } - - const QByteArray data = file.readAll(); - IosController::Instance()->importConfigFromOutside(QString::fromUtf8(data)); - }); + AmneziaHandleOpenUrl(url); } static void amnezia_scene_openURLContexts(id self, SEL _cmd, UIScene *scene, NSSet *contexts) diff --git a/client/platforms/ios/QtAppDelegate.mm b/client/platforms/ios/QtAppDelegate.mm index 64ee9425f..301736f1e 100644 --- a/client/platforms/ios/QtAppDelegate.mm +++ b/client/platforms/ios/QtAppDelegate.mm @@ -1,8 +1,5 @@ #import "QtAppDelegate.h" -#import "ios_controller.h" - -#include - +#import "AmneziaOpenUrlImport.h" @implementation QIOSApplicationDelegate (AmneziaVPNDelegate) #if !MACOS_NE @@ -11,6 +8,10 @@ [application setMinimumBackgroundFetchInterval: UIApplicationBackgroundFetchIntervalMinimum]; // Override point for customization after application launch. NSLog(@"Application didFinishLaunchingWithOptions"); + NSURL *launchUrl = launchOptions[UIApplicationLaunchOptionsURLKey]; + if (launchUrl) { + AmneziaHandleOpenUrl(launchUrl); + } return YES; } @@ -35,24 +36,11 @@ - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - if (url.fileURL) { - QString filePath(url.path.UTF8String); - if (filePath.isEmpty()) return NO; - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - NSLog(@"Application openURL: %@", url); - - if (filePath.contains("backup")) { - IosController::Instance()->importBackupFromOutside(filePath); - } else { - QFile file(filePath); - bool isOpenFile = file.open(QIODevice::ReadOnly); - QByteArray data = file.readAll(); - - IosController::Instance()->importConfigFromOutside(QString(data)); - } - }); + NSLog(@"Application openURL: %@", url); + AmneziaHandleOpenUrl(url); + NSString *scheme = url.scheme ? [url.scheme lowercaseString] : @""; + if ([scheme isEqualToString:@"vpn"] || url.fileURL) { return YES; } return NO; diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm index 73aa02484..150ae7261 100644 --- a/client/platforms/ios/ios_controller.mm +++ b/client/platforms/ios/ios_controller.mm @@ -220,7 +220,7 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur m_rawConfig = configuration; m_serverAddress = configuration.value(configKey::hostName).toString().toNSString(); - const QString serverDescription = configuration.value(config_key::description).toString().trimmed(); + const QString serverDescription = configuration.value(configKey::description).toString().trimmed(); QString tunnelName; if (serverDescription.isEmpty()) { tunnelName = ProtocolUtils::protoToString(proto); diff --git a/client/ui/controllers/api/subscriptionUiController.cpp b/client/ui/controllers/api/subscriptionUiController.cpp index 75d96553e..c1276f432 100644 --- a/client/ui/controllers/api/subscriptionUiController.cpp +++ b/client/ui/controllers/api/subscriptionUiController.cpp @@ -8,6 +8,10 @@ #include "core/utils/api/apiUtils.h" #include "core/utils/qrCodeUtils.h" #include "ui/controllers/systemController.h" +#ifdef Q_OS_IOS +#include "platforms/ios/ios_controller.h" +#include +#endif #include "version.h" #include "core/models/serverConfig.h" #include diff --git a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml index 778d5bfa0..4711d47d4 100644 --- a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml +++ b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml @@ -72,7 +72,7 @@ PageType { Layout.leftMargin: 16 Layout.rightMargin: 16 - headerText: qsTr("New connection") + headerText: qsTr("Add this connection?") } RowLayout { @@ -204,7 +204,7 @@ PageType { Layout.rightMargin: 16 Layout.leftMargin: 16 - text: qsTr("Connect") + text: qsTr("Add") clickedFunc: function() { const headerItem = listView.headerItem; if (!headerItem) {