mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-08 14:33:23 +00:00
refactor: QR device transfer flow and UI tweaks
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <QNetworkProxyQuery>
|
||||
#include <QUrl>
|
||||
#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> &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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 : ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<QtObject> 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() {
|
||||
|
||||
@@ -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 = ""
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user