diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 29918329c..9eaade732 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -587,7 +587,6 @@ bool ImportController::decodeQrCode(const QString &code) } return mInstance->parseQrCodeChunk(code);*/ - //new version for transferController Qr QMutexLocker lock(&qrDecodeMutex); if (!mInstance->m_isQrCodeProcessed) { @@ -597,17 +596,13 @@ bool ImportController::decodeQrCode(const QString &code) mInstance->m_receivedQrCodeChunksCount = 0; } - // trying as old QR with config chunks - if (mInstance->parseQrCodeChunk(code)) { - return true; - } - - //if not chinked-QR, trying transferController QR + // First, try transferController QR (plain JSON payload). QJsonParseError err; const QJsonDocument doc = QJsonDocument::fromJson(code.toUtf8(), &err); if (err.error == QJsonParseError::NoError && doc.isObject()) { const QJsonObject obj = doc.object(); - if (obj.contains(QStringLiteral("gw")) && obj.contains(QStringLiteral("u"))) { + if (obj.contains(QStringLiteral("gw")) + && (obj.contains(QStringLiteral("uuid")) || obj.contains(QStringLiteral("u")))) { // это наш QR для передачи устройства mInstance->m_isQrCodeProcessed = false; // reset state emit mInstance->transferQrDecoded(code); // send string to QML @@ -615,6 +610,10 @@ bool ImportController::decodeQrCode(const QString &code) } } + if (mInstance->parseQrCodeChunk(code)) { + return true; + } + return false; } #endif diff --git a/client/ui/controllers/transferController.cpp b/client/ui/controllers/transferController.cpp index 91b8dc48c..89fe5647e 100644 --- a/client/ui/controllers/transferController.cpp +++ b/client/ui/controllers/transferController.cpp @@ -8,6 +8,7 @@ #include #include #include "core/api/apiUtils.h" +#include "core/qrCodeUtils.h" #include "amnezia_application.h" #include "settings.h" @@ -34,7 +35,6 @@ namespace { .arg(p.hostName()) .arg(p.port()); } - qDebug() << "TransferController: system proxies for" << urlStr << ":" << proxyDesc; } } @@ -44,7 +44,6 @@ TransferController::TransferController(const std::shared_ptr &settings QObject *parent) : QObject(parent), m_settings(settings), m_serversModel(serversModel), m_exportController(exportController) { - qDebug() << "TransferController created"; } void TransferController::handleImportControllerDestroyed() @@ -61,35 +60,36 @@ QString TransferController::buildQrPayloadJson(const QString &gatewayUrl, const QJsonObject obj; obj["gw"] = gatewayUrl; obj["uuid"] = uuid; - qDebug() << "built QrPayload with GW = " << gatewayUrl - << " uuid = " << uuid; + // Used on the sender side for human-friendly notifications (same style as "Active Devices" list). +#if defined(Q_OS_ANDROID) + obj["name"] = QStringLiteral("Android"); +#elif defined(Q_OS_IOS) + obj["name"] = QStringLiteral("iOS"); +#elif defined(Q_OS_WIN) + obj["name"] = QStringLiteral("Windows"); +#elif defined(Q_OS_MACOS) + obj["name"] = QStringLiteral("macOS"); +#elif defined(Q_OS_LINUX) + obj["name"] = QStringLiteral("Linux"); +#else + obj["name"] = QStringLiteral("Device"); +#endif return QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact)); } void TransferController::generateNewQrCode() { - // Debug mode: keep UUID/payload generation, but disable actual QR rendering (temporary). - qDebug() << "TransferController::generateNewQrCode: generating transfer payload (QR rendering disabled)"; - QString gw = m_settings->getGatewayEndpoint(); if (!gw.endsWith('/')) { gw.append('/'); } - qDebug() << "gateway:" << gw; m_currentUuid = QUuid::createUuid().toString(QUuid::WithoutBraces); - qDebug() << "uuid:" << m_currentUuid; m_currentPayload = buildQrPayloadJson(gw, m_currentUuid); - // QR generation disabled for debugging/CLI-style copy-paste flow. - // If/when QR is re-enabled, restore this block: - // - // auto qr = qrCodeUtils::generateQrCode(m_currentPayload.toUtf8()); - // const QString svg = QString::fromStdString(toSvgString(qr, 1)); - // m_qrCodeUrl = qrCodeUtils::svgToBase64(svg); - // emit qrCodeUpdated(); - - m_qrCodeUrl.clear(); + auto qr = qrCodeUtils::generateQrCode(m_currentPayload.toUtf8()); + const QString svg = QString::fromStdString(toSvgString(qr, 1)); + m_qrCodeUrl = qrCodeUtils::svgToBase64(svg); emit qrCodeUpdated(); emit currentUuidChanged(); emit currentPayloadChanged(); @@ -97,7 +97,6 @@ void TransferController::generateNewQrCode() void TransferController::stopScanner() { - qDebug() << "TransferController::stopScanner: emitting scannerShouldStop"; emit scannerShouldStop(); } @@ -110,8 +109,6 @@ QString TransferController::getCurrentApiKey(QString *vpnKeyOut) const const QJsonObject server = m_serversModel->getServerConfig(idx); - qDebug() << "server:" << server; - const QJsonObject apiConfig = server.value(apiDefs::key::apiConfig).toObject(); const QJsonObject authData = server.value(QStringLiteral("auth_data")).toObject(); @@ -120,7 +117,6 @@ QString TransferController::getCurrentApiKey(QString *vpnKeyOut) const if (vpnKeyOut) { QString vpnKey = apiConfig.value(apiDefs::key::vpnKey).toString(); if (vpnKey.isEmpty()) { - // Fallback for older Premium V1 configs where vpn_key may be derived. vpnKey = apiUtils::getPremiumV1VpnKey(server); } *vpnKeyOut = vpnKey; @@ -131,8 +127,6 @@ QString TransferController::getCurrentApiKey(QString *vpnKeyOut) const void TransferController::onTransferQrScanned(const QString &code) { - qDebug() << "TransferController has scanned the Qr"; - QJsonParseError err; const QJsonDocument doc = QJsonDocument::fromJson(code.toUtf8(), &err); if (err.error != QJsonParseError::NoError || !doc.isObject()) { @@ -154,35 +148,66 @@ void TransferController::onTransferQrScanned(const QString &code) gw.append('/'); } + int chosenServerIdx = -1; + QString apiKey; QString vpnKey; - const QString apiKey = getCurrentApiKey(&vpnKey); - qDebug() << "scanned apiKey:" << apiKey; - if (apiKey.isEmpty()) { - qWarning() << "TransferController::onTransferQrScanned: no subscription key or config to send"; - emit postFailed(QStringLiteral("No subscription key or config to send")); - return; + + auto tryServerIndex = [&](int idx) -> bool { + if (!m_serversModel || idx < 0 || idx >= m_serversModel->getServersCount()) { + return false; + } + + const QJsonObject server = m_serversModel->getServerConfig(idx); + const QJsonObject apiConfig = server.value(apiDefs::key::apiConfig).toObject(); + const QJsonObject authData = server.value(QStringLiteral("auth_data")).toObject(); + + const QString candidateApiKey = authData.value(QStringLiteral("api_key")).toString(); + QString candidateVpnKey = apiConfig.value(apiDefs::key::vpnKey).toString(); + if (candidateVpnKey.isEmpty()) { + // Fallback for older Premium V1 configs where vpn_key may be derived. + candidateVpnKey = apiUtils::getPremiumV1VpnKey(server); + } + + const bool candidateIsPremium = apiUtils::isPremiumServer(server); + const bool candidateIsFromGatewayApi = m_serversModel->data(idx, ServersModel::IsServerFromGatewayApiRole).toBool(); + + if (candidateApiKey.isEmpty() || candidateVpnKey.isEmpty()) { + return false; + } + if (!candidateIsPremium && !candidateIsFromGatewayApi) { + return false; + } + + chosenServerIdx = idx; + apiKey = candidateApiKey; + vpnKey = candidateVpnKey; + return true; + }; + + if (m_serversModel) { + tryServerIndex(m_serversModel->getProcessedServerIndex()); + if (chosenServerIdx < 0) { + tryServerIndex(m_serversModel->getDefaultServerIndex()); + } + if (chosenServerIdx < 0) { + const int n = m_serversModel->getServersCount(); + for (int i = 0; i < n; i++) { + if (tryServerIndex(i)) { + break; + } + } + } } - bool isPremium = m_serversModel && m_serversModel->processedServerIsPremium(); - qDebug() << "isPremium: " << isPremium; - bool isFromGatewayApi = m_serversModel && m_serversModel->getProcessedServerData("isServerFromGatewayApi").toBool(); - qDebug() << "is from gatewayApi: " << isFromGatewayApi; - if (!isPremium && !isFromGatewayApi) { - qWarning() << "TransferController::onTransferQrScanned: premium subscription required"; - emit postFailed(QStringLiteral("Premium subscription required")); + if (chosenServerIdx < 0) { + qWarning() << "TransferController::onTransferQrScanned: no suitable subscription key/config found to send"; + emit postFailed(QStringLiteral("No subscription key or config to send")); return; } emit postStarted(); - if (vpnKey.isEmpty()) { - qWarning() << "TransferController::onTransferQrScanned: missing vpn_key"; - emit postFailed(QStringLiteral("Missing vpn_key to send")); - return; - } - - // sendConfig can take longer on slower networks / debug setups, so use a bigger timeout than generic API calls. - const int sendTimeoutMs = 120000; + const int sendTimeoutMs = 60000; GatewayController gatewayController(gw, m_settings->isDevGatewayEnv(), sendTimeoutMs, @@ -236,7 +261,6 @@ void TransferController::startWaitForConfig(ImportController *importController) } const QString uuid = m_currentUuid; - qDebug() << "TransferController::startWaitForConfig: starting blocking wait with uuid: " << uuid; if (uuid.isEmpty()) { qWarning() << "TransferController::startWaitForConfig: no uuid"; @@ -252,7 +276,7 @@ void TransferController::startWaitForConfig(ImportController *importController) Qt::UniqueConnection); } - const int waitTimeoutMs = 300000; + const int waitTimeoutMs = 60000; QJsonObject payload; payload.insert(QStringLiteral("uuid"), uuid); @@ -265,8 +289,6 @@ void TransferController::startWaitForConfig(ImportController *importController) const QString endpoint = QStringLiteral("%1v1/waitConfig"); QByteArray responseBody; const QString fullUrl = endpoint.arg(gw); - qDebug() << "TransferController::startWaitForConfig: POST" << fullUrl - << "uuid:" << uuid; logSystemProxiesForUrl(fullUrl); const auto errorCode = gatewayController.post(endpoint, payload, responseBody); diff --git a/client/ui/qml/Pages2/PageSettingsApiAddDeviceConfirm.qml b/client/ui/qml/Pages2/PageSettingsApiAddDeviceConfirm.qml index aa74def00..271c772a0 100644 --- a/client/ui/qml/Pages2/PageSettingsApiAddDeviceConfirm.qml +++ b/client/ui/qml/Pages2/PageSettingsApiAddDeviceConfirm.qml @@ -14,9 +14,18 @@ import "../Config" PageType { id: root - // Debug-friendly: don't auto-close on send; keep the page responsive and show status. property bool isSending: false + function getAddedDeviceName() { + try { + var obj = JSON.parse(TransferController.pendingQrCode) + if (obj && obj.name && obj.name.length > 0) { + return obj.name + } + } catch (e) {} + return qsTr("Device") + } + function getAvailableCount() { var max = ApiAccountInfoModel.data("maxDeviceCount") var active = ApiAccountInfoModel.data("activeDeviceCount") @@ -38,8 +47,6 @@ PageType { BackButtonType { backButtonFunction: function() { if (root.isSending) { - // We cannot truly abort GatewayController::post() without changing it, - // but we can at least stop waiting on the other side and let the user navigate. TransferController.stopWaitForConfig() } PageController.closePage() @@ -51,8 +58,8 @@ PageType { Layout.rightMargin: 16 Layout.leftMargin: 16 - headerText: qsTr("Share VPN with a new device?") - descriptionText: qsTr("Your Amnezia Premium subscription can connect %1 more devices").arg(getAvailableCount()) + headerText: qsTr("Add a new device to the subscription?") + descriptionText: qsTr("Devices available with Amnezia Premium: (%1)").arg(getAvailableCount()) } BasicButtonType { @@ -61,7 +68,7 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 16 - text: qsTr("Yes, share") + text: qsTr("Add Device") enabled: !root.isSending && root.getAvailableCount() > 0 && TransferController.pendingQrCode !== "" clickedFunc: function() { @@ -104,7 +111,8 @@ PageType { function onPostSucceeded() { root.isSending = false - PageController.showNotificationMessage(qsTr("Configuration sent successfully")) + PageController.showNotificationMessage(qsTr("%1 has been added to your subscription").arg(root.getAddedDeviceName())) + PageController.closePage() PageController.closePage() } diff --git a/client/ui/qml/Pages2/PageSettingsApiAddDeviceScan.qml b/client/ui/qml/Pages2/PageSettingsApiAddDeviceScan.qml index 5df420465..02ca2dc16 100644 --- a/client/ui/qml/Pages2/PageSettingsApiAddDeviceScan.qml +++ b/client/ui/qml/Pages2/PageSettingsApiAddDeviceScan.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import QtQuick.Dialogs import PageEnum 1.0 +import QRCodeReader 1.0 import Style 1.0 import "./" @@ -14,159 +15,80 @@ import "../Config" PageType { id: root - BackButtonType { - id: backButton - anchors.left: parent.left - anchors.top: parent.top + property bool scanCompleted: false - anchors.topMargin: 20 + Item { + id: cameraArea + anchors.fill: parent } - ParagraphTextType { - id: header + Loader { + id: iosQrLoader + anchors.fill: cameraArea + active: Qt.platform.os === "ios" - property string progressString + sourceComponent: Component { + QRCodeReader { + id: qrCodeReader - anchors.left: parent.left - anchors.top: backButton.bottom - anchors.right: parent.right + function updateCameraSize() { + qrCodeReader.setCameraSize(Qt.rect(cameraArea.x, + cameraArea.y, + cameraArea.width, + cameraArea.height)) + } - anchors.leftMargin: 16 - anchors.rightMargin: 16 + onCodeReaded: function(code) { + if (!code || code.length === 0) { + return + } - text: qsTr("Debug mode: paste payload JSON from the other device (or enter UUID). ") + progressString - } + var obj = null + try { + obj = JSON.parse(code) + } catch (e) { + obj = null + } - ProgressBarType { - id: progressBar + if (!obj || !obj.gw || !(obj.uuid || obj.u)) { + return + } - anchors.left: parent.left - anchors.top: header.bottom - anchors.right: parent.right - - anchors.leftMargin: 16 - anchors.rightMargin: 16 - } - - // Debug: manual payload/UUID input (temporary instead of camera scanning) - ColumnLayout { - id: manualInput - anchors.left: parent.left - anchors.right: parent.right - anchors.top: progressBar.bottom - anchors.bottom: parent.bottom - - anchors.leftMargin: 16 - anchors.rightMargin: 16 - anchors.topMargin: 24 - anchors.bottomMargin: 24 - spacing: 12 - - TextFieldWithHeaderType { - id: payloadField - Layout.fillWidth: true - - headerText: qsTr("Payload JSON") - textField.placeholderText: qsTr("Paste JSON like {\"gw\":\"...\",\"uuid\":\"...\"}") - } - - TextFieldWithHeaderType { - id: uuidField - Layout.fillWidth: true - - headerText: qsTr("UUID") - textField.placeholderText: qsTr("Enter UUID") - } - - BasicButtonType { - id: usePayloadButton - Layout.fillWidth: true - text: qsTr("Next (use payload)") - enabled: payloadField.textField.text.length > 0 - - clickedFunc: function() { - TransferController.setPendingQrCode(payloadField.textField.text) + var normalizedObj = { gw: obj.gw, uuid: (obj.uuid ? obj.uuid : obj.u) } + if (obj.name && obj.name.length > 0) { + normalizedObj.name = obj.name + } + var normalized = JSON.stringify(normalizedObj) + TransferController.setPendingQrCode(normalized) + qrCodeReader.stopReading() PageController.goToPage(PageEnum.PageSettingsApiAddDeviceConfirm) } - } - BasicButtonType { - id: buildFromUuidButton - Layout.fillWidth: true - defaultColor: AmneziaStyle.color.transparent - hoveredColor: AmneziaStyle.color.translucentWhite - pressedColor: AmneziaStyle.color.sheerWhite - textColor: AmneziaStyle.color.paleGray - borderColor: AmneziaStyle.color.paleGray - borderWidth: 1 - - text: qsTr("Next (build from UUID)") - enabled: uuidField.textField.text.length > 0 - - clickedFunc: function() { - var gw = "" - if (typeof SettingsController !== "undefined") { - if (SettingsController.gatewayEndpoint !== undefined) { - gw = SettingsController.gatewayEndpoint - } else if (SettingsController.getGatewayEndpoint) { - gw = SettingsController.getGatewayEndpoint() + Component.onCompleted: { + updateCameraSize() + qrCodeReader.startReading() + } + Component.onDestruction: qrCodeReader.stopReading() } } - if (!gw || gw.length === 0) { - PageController.showErrorMessage(qsTr("Gateway endpoint is empty")) - return - } - if (!gw.endsWith("/")) { - gw = gw + "/" - } + } - var payload = JSON.stringify({ gw: gw, uuid: uuidField.textField.text }) - TransferController.setPendingQrCode(payload) - PageController.goToPage(PageEnum.PageSettingsApiAddDeviceConfirm) - } + onWidthChanged: { + if (iosQrLoader.item && iosQrLoader.item.updateCameraSize) { + iosQrLoader.item.updateCameraSize() + } + } + onHeightChanged: { + if (iosQrLoader.item && iosQrLoader.item.updateCameraSize) { + iosQrLoader.item.updateCameraSize() } } - /*Rectangle { - id: qrCodeRactangle - visible: false - anchors.right: parent.right - anchors.left: parent.left - anchors.bottom: parent.bottom - anchors.top: progressBar.bottom - - anchors.topMargin: 34 - anchors.leftMargin: 16 - anchors.rightMargin: 16 - anchors.bottomMargin: 34 - - color: AmneziaStyle.color.transparent - - QRCodeReader { - id: qrCodeReader - - onCodeReaded: function(code) { - // scanning disabled - return - } - - Component.onCompleted: { } - - Component.onDestruction: qrCodeReader.stopReading() - } - }*/ - - Component.onCompleted: { } - - /*Connections { + Connections { target: TransferController function onScannerShouldStop() { - if (qrCodeReader && qrCodeReader.stopReading) { - qrCodeReader.stopReading() - } - if (typeof ImportController !== "undefined" - && ImportController.stopDecodingQr) { - ImportController.stopDecodingQr() + if (iosQrLoader.item && iosQrLoader.item.stopReading) { + iosQrLoader.item.stopReading() } } } @@ -176,15 +98,43 @@ PageType { function onTransferQrDecoded(code) { if (!code || code.length === 0) { - console.log("ImportController.onTransferQrDecoded: empty QR code payload") - if (typeof PageController !== "undefined" && PageController.showErrorMessage) { - PageController.showErrorMessage(qsTr("QR code not recognized. Please try again.")); - } return } + var obj = null + try { + obj = JSON.parse(code) + } catch (e) { + obj = null + } + if (obj && obj.gw && (obj.uuid || obj.u)) { + var normalizedObj = { gw: obj.gw, uuid: (obj.uuid ? obj.uuid : obj.u) } + if (obj.name && obj.name.length > 0) { + normalizedObj.name = obj.name + } + code = JSON.stringify(normalizedObj) + } + + root.scanCompleted = true TransferController.setPendingQrCode(code) + Qt.callLater(function() { PageController.goToPage(PageEnum.PageSettingsApiAddDeviceConfirm) + }) } - }*/ + + function onQrDecodingFinished() { + if (Qt.platform.os === "android" && !root.scanCompleted) { + PageController.closePage() + } + } + } + + Component.onCompleted: { + TransferController.setPendingQrCode("") + root.scanCompleted = false + + if (Qt.platform.os === "android") { + ImportController.startDecodingQr() + } + } } diff --git a/client/ui/qml/Pages2/PageSettingsApiDevices.qml b/client/ui/qml/Pages2/PageSettingsApiDevices.qml index 012ed4503..c53ec5e2c 100644 --- a/client/ui/qml/Pages2/PageSettingsApiDevices.qml +++ b/client/ui/qml/Pages2/PageSettingsApiDevices.qml @@ -25,6 +25,24 @@ PageType { return listView.count >= maxDeviceCount } + function getConfigFilesCount() { + try { + var arr = ApiAccountInfoModel.getIssuedConfigsInfo() + if (!arr) return 0 + + var count = 0 + for (var i = 0; i < arr.length; i++) { + var item = arr[i] + if (item && item["source_type"] === "country_config") { + ++count + } + } + return count + } catch (e) { + return 0 + } + } + ListViewType { id: listView @@ -60,7 +78,7 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 16 - //visible: GC.isMobile() + visible: GC.isMobile() defaultColor: AmneziaStyle.color.transparent hoveredColor: AmneziaStyle.color.translucentWhite @@ -80,16 +98,13 @@ PageType { } } - WarningType { - Layout.topMargin: 16 + SmallTextType { + Layout.topMargin: 8 Layout.rightMargin: 16 Layout.leftMargin: 16 Layout.fillWidth: true - textString: qsTr("You can find the identifier on the Support tab or, for older versions of the app, " - + "by tapping '+' and then the three dots at the top of the page.") - - iconPath: "qrc:/images/controls/alert-circle.svg" + text: qsTr("On the other device, tap + at the bottom → Connect to Amnezia Premium") } } @@ -128,6 +143,37 @@ PageType { DividerType {} } + footer: ColumnLayout { + width: listView.width + + LabelWithButtonType { + Layout.fillWidth: true + Layout.topMargin: 6 + + text: qsTr("Configuration Files: {%1}").arg(root.getConfigFilesCount()) + descriptionText: qsTr("Generated configuration files also count towards the device limit") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + ApiSettingsController.updateApiCountryModel() + PageController.goToPage(PageEnum.PageSettingsApiNativeConfigs) + } + } + + DividerType {} + + WarningType { + Layout.topMargin: 16 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 8 + Layout.fillWidth: true + + textString: qsTr("The Support tag is available on the Support page. In older versions: tap + in the bottom bar, then More (...) in the top-right.") + iconPath: "qrc:/images/controls/alert-circle.svg" + } + } + Connections { target: TransferController diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 1e947bd24..190434256 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -104,7 +104,7 @@ PageType { actionButtonImage: "qrc:/images/controls/edit-3.svg" - headerText: root.processedServer.name + headerText: root.processedServer && root.processedServer.name ? root.processedServer.name : "" descriptionText: ApiAccountInfoModel.data("serviceDescription") actionButtonFunction: function() { @@ -214,9 +214,6 @@ PageType { ApiConfigsController.prepareVpnKeyExport() PageController.showBusyIndicator(false) - - // Navigate to PageShareConnection page - //PageController.goToPage(PageEnum.PageShareConnection) } } @@ -416,6 +413,6 @@ PageType { anchors.fill: parent expandedHeight: parent.height * 0.35 - serverNameText: root.processedServer.name + serverNameText: root.processedServer && root.processedServer.name ? root.processedServer.name : "" } } diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 2f9f0100b..1ca9b108d 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -45,7 +45,7 @@ PageType { Layout.rightMargin: 16 Layout.leftMargin: 16 - headerText: qsTr("Connection") + headerText: qsTr("New Connection") actionButtonImage: isVisible ? "qrc:/images/controls/more-vertical.svg" : "" actionButtonFunction: function() { @@ -156,7 +156,7 @@ PageType { Layout.leftMargin: 16 Layout.bottomMargin: 24 - text: qsTr("Insert the key, add a configuration file or scan the QR-code") + text: qsTr("Enter a connection key, import a configuration file, or scan a QR code") } TextFieldWithHeaderType { @@ -166,8 +166,8 @@ PageType { Layout.rightMargin: 16 Layout.leftMargin: 16 - headerText: qsTr("Insert key") - buttonText: qsTr("Insert") + headerText: qsTr("Connection key") + buttonText: qsTr("Paste") clickedFunc: function() { textField.text = "" @@ -250,7 +250,7 @@ PageType { disabledColor: AmneziaStyle.color.mutedGray textColor: AmneziaStyle.color.goldenApricot - text: qsTr("Site Amnezia") + text: qsTr("Amnezia Website") rightImageSource: "qrc:/images/controls/external-link.svg" @@ -263,19 +263,17 @@ PageType { property list variants: [ amneziaVpn, - selfHostVpn, - backupRestore, shareViaDevice, + selfHostVpn, fileOpen, - qrScan, - siteLink + qrScan ] QtObject { id: amneziaVpn - property string title: qsTr("VPN by Amnezia") - property string description: qsTr("Connect to classic paid and free VPN services from Amnezia") + property string title: qsTr("Choose a VPN from Amnezia") + property string description: qsTr("Paid and free VPN services from Amnezia") property string imageSource: "qrc:/images/controls/amnezia.svg" property bool isVisible: true property var handler: function() { @@ -288,11 +286,23 @@ PageType { } } + QtObject { + id: shareViaDevice + + property string title: qsTr("Connect to Amnezia Premium") + property string description: qsTr("Via QR code from a phone with an active subscription") + property string imageSource: "qrc:/images/controls/monitor-with-phone.svg" + property bool isVisible: true + property var handler: function() { + PageController.goToPage(PageEnum.PageTransferConfigViaQR) + } + } + QtObject { id: selfHostVpn - property string title: qsTr("Self-hosted VPN") - property string description: qsTr("Configure Amnezia VPN on your own server") + property string title: qsTr("Set up a Self-hosted VPN") + property string description: qsTr("On a private server") property string imageSource: "qrc:/images/controls/server.svg" property bool isVisible: true property var handler: function() { @@ -318,23 +328,11 @@ PageType { } } - QtObject { - id: shareViaDevice - - property string title: qsTr("Set up your Amnezia Premium VPN using your phone") - property string description: qsTr("Which already has a premium subscription") - property string imageSource: "qrc:/images/controls/monitor-with-phone.svg" - property bool isVisible: true - property var handler: function() { - PageController.goToPage(PageEnum.PageTransferConfigViaQR) - } - } - QtObject { id: fileOpen - property string title: qsTr("File with connection settings") - property string description: qsTr("") + property string title: qsTr("Use a configuration file") + property string description: qsTr("Supported formats: .conf, .vpn, .ovpn, .json") property string imageSource: "qrc:/images/controls/folder-search-2.svg" property bool isVisible: true property var handler: function() { @@ -352,8 +350,8 @@ PageType { QtObject { id: qrScan - property string title: qsTr("QR code") - property string description: qsTr("") + property string title: qsTr("Scan a QR code") + property string description: qsTr("To connect to a self-hosted server") property string imageSource: "qrc:/images/controls/scan-line.svg" property bool isVisible: SettingsController.isCameraPresent() property var handler: function() { diff --git a/client/ui/qml/Pages2/PageTransferConfigViaQR.qml b/client/ui/qml/Pages2/PageTransferConfigViaQR.qml index 1e0720cf5..4e7194f31 100644 --- a/client/ui/qml/Pages2/PageTransferConfigViaQR.qml +++ b/client/ui/qml/Pages2/PageTransferConfigViaQR.qml @@ -19,18 +19,6 @@ PageType { z: 0 } - MouseArea { - anchors.fill: parent - z: 0 - acceptedButtons: Qt.AllButtons - hoverEnabled: true - preventStealing: true - onPressed: mouse.accepted = true - onReleased: mouse.accepted = true - onClicked: mouse.accepted = true - onWheel: wheel.accepted = true - } - ColumnLayout { z: 1 anchors.fill: parent @@ -39,8 +27,8 @@ PageType { BackButtonType { Layout.topMargin: 20 - Layout.leftMargin: 0 - Layout.rightMargin: 0 + Layout.leftMargin: 16 + Layout.rightMargin: 16 Layout.alignment: Qt.AlignLeft } @@ -52,72 +40,60 @@ PageType { Layout.rightMargin: 16 Layout.bottomMargin: 16 - ParagraphTextType { - id: qrHeader - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - text: qsTr("Debug mode: copy the payload to the other device and send it there") - } - ColumnLayout { - id: debugPayload - anchors.left: parent.left - anchors.right: parent.right - anchors.top: qrHeader.bottom - anchors.topMargin: 12 - anchors.bottom: bottomHint.top - anchors.bottomMargin: 12 - spacing: 8 + id: qrContent + anchors.fill: parent + spacing: 16 - TextArea { - id: payloadArea + Item { Layout.fillHeight: true } + + SmallTextType { + id: topHint Layout.fillWidth: true - Layout.fillHeight: true - readOnly: true - wrapMode: TextArea.WrapAnywhere - selectByMouse: true - text: TransferController.currentPayload - placeholderText: qsTr("Payload will appear here after generating") + horizontalAlignment: Text.AlignHCenter + text: qsTr("Scan this QR code with a phone that has an active\nAmnezia Premium subscription") } - RowLayout { - Layout.fillWidth: true - spacing: 8 + Rectangle { + id: qrFrame + Layout.alignment: Qt.AlignHCenter + property real maxByHeight: qrContent.height + - topHint.implicitHeight + - bottomHint.implicitHeight + - (qrContent.spacing * 2) + property real qrSize: Math.max(180, Math.min(qrContent.width, Math.max(0, maxByHeight))) - BasicButtonType { - Layout.fillWidth: true - text: qsTr("Regenerate") - clickedFunc: function() { - TransferController.generateNewQrCode() - TransferController.startWaitForConfig(ImportController) - } + Layout.preferredWidth: qrSize + Layout.preferredHeight: qrSize + radius: 16 + color: "white" + + Image { + id: qrImage + anchors.fill: parent + anchors.margins: 12 + fillMode: Image.PreserveAspectFit + smooth: false + sourceSize: Qt.size(Math.round(width), Math.round(height)) + source: TransferController.qrCodeUrl + visible: TransferController.qrCodeUrl !== "" } - BasicButtonType { - Layout.fillWidth: true - defaultColor: AmneziaStyle.color.transparent - hoveredColor: AmneziaStyle.color.translucentWhite - pressedColor: AmneziaStyle.color.sheerWhite - textColor: AmneziaStyle.color.paleGray - borderColor: AmneziaStyle.color.paleGray - borderWidth: 1 - text: qsTr("Restart wait") - clickedFunc: function() { - TransferController.startWaitForConfig(ImportController) - } + BusyIndicator { + anchors.centerIn: parent + running: TransferController.qrCodeUrl === "" + visible: TransferController.qrCodeUrl === "" } } - } - SmallTextType { - id: bottomHint - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.bottomMargin: 8 - text: qsTr("Copy the JSON payload above, paste it on the other device, and confirm sending there") - horizontalAlignment: Text.AlignHCenter + SmallTextType { + id: bottomHint + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + text: qsTr("AmneziaVPN → Amnezia Premium →\nPersonal Dashboard → Active Devices →\nAdd Device via QR Code") + } + + Item { Layout.fillHeight: true } } } } @@ -125,7 +101,9 @@ PageType { Connections { target: TransferController function onConfigApplied() { - PageController.showNotificationMessage(qsTr("Configuration received and applied")) + PageController.showNotificationMessage(qsTr("Device has been added to subscription")) + PageController.closePage() + PageController.goToPageHome() } function onWaitError(message) { PageController.showErrorMessage(message) @@ -145,23 +123,4 @@ PageType { } Component.onDestruction: TransferController.stopWaitForConfig() -} - -/*CardWithIconsType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - - headerText: qsTr("Send connection via QR") - bodyText: qsTr("Scan a QR code from another device to send your config to it") - leftImageSource: "qrc:/images/controls/scan-line.svg" - rightImageSource: "qrc:/images/controls/chevron-right.svg" - - onClicked: { - showReceiveSection = false - showScanSection = true - isPosting = false - postStatusText = "" - } -}*/ +}