Compare commits

...

13 Commits

Author SHA1 Message Date
Mitternacht822 6d798344d2 cleanup commented lines 2026-01-30 17:18:38 +04:00
Mitternacht822 dec1f51fd6 refactor: QR device transfer flow and UI tweaks 2026-01-29 18:51:42 +04:00
Mitternacht822 832fea3d50 refactor: transferController 2026-01-16 02:31:21 +04:00
Mitternacht822 43b51db132 transferController refactor 2026-01-14 09:21:36 +04:00
Mitternacht822 d391abb0cc temp point 2026-01-14 09:12:29 +04:00
Mitternacht822 153e33d5df refactor: chenged POST/GET methods to simple blocking request 2025-12-08 23:34:08 +04:00
Mitternacht822 1cb171698a fix: removed query from request body 2025-12-08 16:36:59 +04:00
Mitternacht822 f02128796a cleared redundant lines 2025-12-01 14:37:20 +04:00
Mitternacht822 71705f435b draft: updated version for debug 2025-11-28 12:45:50 +04:00
Mitternacht822 f821bcb01f temporary save 2025-11-17 19:20:27 +04:00
Mitternacht822 9a5e437f7b saving intermediate version 2025-10-30 15:38:30 +04:00
Mitternacht822 3d2105a439 feat(gateway): QR-based config transfer between devices 2025-09-03 20:11:22 +04:00
Nethius 16d92ddb7c fix: UI fixes after merge with d20ed4a (#1779)
* fix: ui fixes after merge with d20ed4a

* update OpenVPN settings page

* chore: page settings dns margins

---------

Co-authored-by: Cyril Anisimov <CyAn84@gmail.com>
2025-08-11 13:40:28 +08:00
25 changed files with 1439 additions and 450 deletions
@@ -154,6 +154,9 @@ void CoreController::initControllers()
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this)); m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get()); m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get());
m_transferController.reset(new TransferController(m_settings, m_serversModel, m_exportController.get()));
m_engine->rootContext()->setContextProperty("TransferController", m_transferController.get());
} }
void CoreController::initAndroidController() void CoreController::initAndroidController()
+2
View File
@@ -23,6 +23,7 @@
#include "ui/controllers/settingsController.h" #include "ui/controllers/settingsController.h"
#include "ui/controllers/sitesController.h" #include "ui/controllers/sitesController.h"
#include "ui/controllers/systemController.h" #include "ui/controllers/systemController.h"
#include "ui/controllers/transferController.h"
#include "ui/models/allowed_dns_model.h" #include "ui/models/allowed_dns_model.h"
#include "ui/models/containers_model.h" #include "ui/models/containers_model.h"
@@ -145,6 +146,7 @@ private:
#endif #endif
QScopedPointer<SftpConfigModel> m_sftpConfigModel; QScopedPointer<SftpConfigModel> m_sftpConfigModel;
QScopedPointer<Socks5ProxyConfigModel> m_socks5ConfigModel; QScopedPointer<Socks5ProxyConfigModel> m_socks5ConfigModel;
QScopedPointer<TransferController> m_transferController;
}; };
#endif // CORECONTROLLER_H #endif // CORECONTROLLER_H
@@ -8,6 +8,7 @@
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkReply> #include <QNetworkReply>
#include <QUrl> #include <QUrl>
#include <QRegularExpression>
#include "QBlockCipher.h" #include "QBlockCipher.h"
#include "QRsa.h" #include "QRsa.h"
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<!-- Monitor -->
<rect x="2.5" y="3.5" width="14" height="10" rx="2" ry="2" stroke="#D7D8DB" stroke-width="1.8"/>
<line x1="9.5" y1="13.5" x2="9.5" y2="16.5" stroke="#D7D8DB" stroke-width="1.8" stroke-linecap="round"/>
<rect x="6" y="16.5" width="7" height="1.8" rx="0.9" ry="0.9" fill="#D7D8DB"/>
<!-- Phone -->
<rect x="17.5" y="6" width="4.5" height="9.5" rx="1.2" ry="1.2" stroke="#D7D8DB" stroke-width="1.8"/>
<circle cx="19.75" cy="14.2" r="0.6" fill="#D7D8DB"/>
</svg>

After

Width:  |  Height:  |  Size: 628 B

+5
View File
@@ -229,6 +229,7 @@
<file>ui/qml/Pages2/PageShare.qml</file> <file>ui/qml/Pages2/PageShare.qml</file>
<file>ui/qml/Pages2/PageShareFullAccess.qml</file> <file>ui/qml/Pages2/PageShareFullAccess.qml</file>
<file>ui/qml/Pages2/PageShareConnection.qml</file> <file>ui/qml/Pages2/PageShareConnection.qml</file>
<file>ui/qml/Pages2/PageTransferConfigViaQR.qml</file>
<file>ui/qml/Pages2/PageStart.qml</file> <file>ui/qml/Pages2/PageStart.qml</file>
<file>ui/qml/Components/RenameServerDrawer.qml</file> <file>ui/qml/Components/RenameServerDrawer.qml</file>
<file>ui/qml/Controls2/ListViewType.qml</file> <file>ui/qml/Controls2/ListViewType.qml</file>
@@ -236,7 +237,11 @@
<file>ui/qml/Pages2/PageSettingsApiInstructions.qml</file> <file>ui/qml/Pages2/PageSettingsApiInstructions.qml</file>
<file>ui/qml/Pages2/PageSettingsApiNativeConfigs.qml</file> <file>ui/qml/Pages2/PageSettingsApiNativeConfigs.qml</file>
<file>ui/qml/Pages2/PageSettingsApiDevices.qml</file> <file>ui/qml/Pages2/PageSettingsApiDevices.qml</file>
<file>ui/qml/Pages2/PageSettingsApiDevicesLimit.qml</file>
<file>ui/qml/Pages2/PageSettingsApiAddDeviceScan.qml</file>
<file>ui/qml/Pages2/PageSettingsApiAddDeviceConfirm.qml</file>
<file>images/controls/monitor.svg</file> <file>images/controls/monitor.svg</file>
<file>images/controls/monitor-with-phone.svg</file>
<file>ui/qml/Components/ApiPremV1MigrationDrawer.qml</file> <file>ui/qml/Components/ApiPremV1MigrationDrawer.qml</file>
<file>ui/qml/Components/ApiPremV1SubListDrawer.qml</file> <file>ui/qml/Components/ApiPremV1SubListDrawer.qml</file>
<file>ui/qml/Components/OtpCodeDrawer.qml</file> <file>ui/qml/Components/OtpCodeDrawer.qml</file>
+20 -1
View File
@@ -584,7 +584,26 @@ bool ImportController::decodeQrCode(const QString &code)
mInstance->m_totalQrCodeChunksCount = 0; mInstance->m_totalQrCodeChunksCount = 0;
mInstance->m_receivedQrCodeChunksCount = 0; mInstance->m_receivedQrCodeChunksCount = 0;
} }
return mInstance->parseQrCodeChunk(code);
// 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("uuid")) || obj.contains(QStringLiteral("u")))) {
// это наш QR для передачи устройства
mInstance->m_isQrCodeProcessed = false;
emit mInstance->transferQrDecoded(code);
return true;
}
}
if (mInstance->parseQrCodeChunk(code)) {
return true;
}
return false;
} }
#endif #endif
+1
View File
@@ -57,6 +57,7 @@ signals:
void importErrorOccurred(ErrorCode errorCode, bool goToPageHome); void importErrorOccurred(ErrorCode errorCode, bool goToPageHome);
void qrDecodingFinished(); void qrDecodingFinished();
void transferQrDecoded(const QString &code);
void restoreAppConfig(const QByteArray &data); void restoreAppConfig(const QByteArray &data);
+4
View File
@@ -38,6 +38,9 @@ namespace PageLoader
PageSettingsApiInstructions, PageSettingsApiInstructions,
PageSettingsApiNativeConfigs, PageSettingsApiNativeConfigs,
PageSettingsApiDevices, PageSettingsApiDevices,
PageSettingsApiDevicesLimit,
PageSettingsApiAddDeviceConfirm,
PageSettingsApiAddDeviceScan,
PageSettingsApiSubscriptionKey, PageSettingsApiSubscriptionKey,
PageSettingsKillSwitchExceptions, PageSettingsKillSwitchExceptions,
@@ -73,6 +76,7 @@ namespace PageLoader
PageShareFullAccess, PageShareFullAccess,
PageShareConnection, PageShareConnection,
PageTransferConfigViaQR,
PageDevMenu PageDevMenu
}; };
@@ -0,0 +1,344 @@
#include "transferController.h"
#include <QVariant>
#include <QJsonParseError>
#include <QDebug>
#include <qeventloop.h>
#include <QNetworkProxyFactory>
#include <QNetworkProxyQuery>
#include <QUrl>
#include "core/api/apiUtils.h"
#include "core/qrCodeUtils.h"
#include "amnezia_application.h"
#include "settings.h"
#include "ui/models/servers_model.h"
#include "ui/controllers/exportController.h"
#include "ui/controllers/importController.h"
#include "core/api/apiDefs.h"
#include "core/controllers/gatewayController.h"
#include "core/errorstrings.h"
namespace {
void logSystemProxiesForUrl(const QString &urlStr)
{
const QUrl url(urlStr);
const QList<QNetworkProxy> proxies = QNetworkProxyFactory::systemProxyForQuery(QNetworkProxyQuery(url));
QStringList proxyDesc;
proxyDesc.reserve(proxies.size());
for (const auto &p : proxies) {
proxyDesc << QStringLiteral("%1 %2:%3")
.arg(p.type() == QNetworkProxy::NoProxy ? QStringLiteral("NoProxy")
: p.type() == QNetworkProxy::HttpProxy ? QStringLiteral("HttpProxy")
: p.type() == QNetworkProxy::Socks5Proxy ? QStringLiteral("Socks5Proxy")
: QStringLiteral("Proxy"))
.arg(p.hostName())
.arg(p.port());
}
}
}
TransferController::TransferController(const std::shared_ptr<Settings> &settings,
const QSharedPointer<ServersModel> &serversModel,
ExportController *exportController,
QObject *parent)
: QObject(parent), m_settings(settings), m_serversModel(serversModel), m_exportController(exportController)
{
}
void TransferController::handleImportControllerDestroyed()
{
m_importController = nullptr;
stopWaitForConfig();
}
TransferController::~TransferController() {
}
QString TransferController::buildQrPayloadJson(const QString &gatewayUrl, const QString &uuid) const
{
QJsonObject obj;
obj["gw"] = gatewayUrl;
obj["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()
{
QString gw = m_settings->getGatewayEndpoint();
if (!gw.endsWith('/')) {
gw.append('/');
}
m_currentUuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
m_currentPayload = buildQrPayloadJson(gw, m_currentUuid);
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();
}
void TransferController::stopScanner()
{
emit scannerShouldStop();
}
QString TransferController::getCurrentApiKey(QString *vpnKeyOut) const
{
const int idx = m_serversModel ? m_serversModel->getProcessedServerIndex() : -1;
if (idx < 0 || !m_serversModel) {
return QString();
}
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 apiKey = authData.value(QStringLiteral("api_key")).toString();
if (vpnKeyOut) {
QString vpnKey = apiConfig.value(apiDefs::key::vpnKey).toString();
if (vpnKey.isEmpty()) {
vpnKey = apiUtils::getPremiumV1VpnKey(server);
}
*vpnKeyOut = vpnKey;
}
return apiKey;
}
void TransferController::onTransferQrScanned(const QString &code)
{
QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(code.toUtf8(), &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
qWarning() << "TransferController::onTransferQrScanned: invalid QR JSON " << err.errorString();
emit postFailed(QStringLiteral("Invalid QR JSON"));
return;
}
const QJsonObject obj = doc.object();
QString gw = obj.value("gw").toString();
const QString uuid = obj.value("uuid").toString();
if (gw.isEmpty() || uuid.isEmpty()) {
qWarning() << "TransferController::onTransferQrScanned: QR missing gw or uuid";
emit postFailed(QStringLiteral("QR missing gw or uuid"));
return;
}
if (!gw.endsWith('/')) {
gw.append('/');
}
int chosenServerIdx = -1;
QString apiKey;
QString vpnKey;
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;
}
}
}
}
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();
const int sendTimeoutMs = 60000;
GatewayController gatewayController(gw,
m_settings->isDevGatewayEnv(),
sendTimeoutMs,
m_settings->isStrictKillSwitchEnabled());
QJsonObject payload;
payload.insert(QStringLiteral("uuid"), uuid);
payload.insert(QStringLiteral("api_key"), apiKey);
payload.insert(QStringLiteral("config"), vpnKey);
const QString endpoint = QStringLiteral("%1v1/sendConfig");
QByteArray responseBody;
const QString fullUrl = endpoint.arg(gw);
qDebug() << "TransferController::onTransferQrScanned: POST" << fullUrl
<< "uuid:" << uuid;
logSystemProxiesForUrl(fullUrl);
const auto errorCode = gatewayController.post(endpoint, payload, responseBody);
qDebug() << "TransferController::onTransferQrScanned: sendConfig finished with code"
<< static_cast<int>(errorCode)
<< "response size:" << responseBody.size();
if (errorCode != ErrorCode::NoError) {
qWarning() << "TransferController::onTransferQrScanned: sendConfig failed with code" << static_cast<int>(errorCode);
emit postFailed(QStringLiteral("sendConfig failed: %1").arg(errorString(errorCode)));
return;
}
QJsonParseError parseErr;
const QJsonDocument respDoc = QJsonDocument::fromJson(responseBody, &parseErr);
if (parseErr.error == QJsonParseError::NoError && respDoc.isObject()
&& respDoc.object().value(QStringLiteral("status")).toString() == QStringLiteral("success")) {
emit postSucceeded();
stopScanner();
return;
}
qWarning() << "TransferController::onTransferQrScanned: unexpected gateway response:" << responseBody;
emit postFailed(QStringLiteral("Gateway response error"));
}
QString TransferController::qrCodeUrl() const
{
return m_qrCodeUrl;
}
void TransferController::startWaitForConfig(ImportController *importController)
{
QString gw = m_settings->getGatewayEndpoint();
if (!gw.endsWith(QLatin1Char('/'))) {
gw.append(QLatin1Char('/'));
}
const QString uuid = m_currentUuid;
if (uuid.isEmpty()) {
qWarning() << "TransferController::startWaitForConfig: no uuid";
emit waitError(QStringLiteral("No UUID"));
return;
}
m_importController = importController;
if (m_importController) {
connect(m_importController, &ImportController::destroyed,
this,
&TransferController::handleImportControllerDestroyed,
Qt::UniqueConnection);
}
const int waitTimeoutMs = 60000;
QJsonObject payload;
payload.insert(QStringLiteral("uuid"), uuid);
GatewayController gatewayController(gw,
m_settings->isDevGatewayEnv(),
waitTimeoutMs,
m_settings->isStrictKillSwitchEnabled());
const QString endpoint = QStringLiteral("%1v1/waitConfig");
QByteArray responseBody;
const QString fullUrl = endpoint.arg(gw);
logSystemProxiesForUrl(fullUrl);
const auto errorCode = gatewayController.post(endpoint, payload, responseBody);
if (errorCode != ErrorCode::NoError) {
qWarning() << "TransferController::startWaitForConfig: waitConfig failed with code" << static_cast<int>(errorCode);
emit waitError(QStringLiteral("waitConfig failed (%1)").arg(static_cast<int>(errorCode)));
return;
}
if (!m_importController) {
qWarning() << "TransferController::startWaitForConfig: import controller is null";
emit waitError(QStringLiteral("Import Controller destroyed"));
return;
}
QJsonParseError parseErr;
const QJsonDocument respDoc = QJsonDocument::fromJson(responseBody, &parseErr);
if (parseErr.error != QJsonParseError::NoError || !respDoc.isObject()) {
qWarning() << "TransferController::startWaitForConfig: invalid JSON response:" << responseBody;
emit waitError(QStringLiteral("Invalid gateway response"));
return;
}
const QJsonObject respObj = respDoc.object();
const QString status = respObj.value(QStringLiteral("status")).toString();
const QString configStr = respObj.value(QStringLiteral("config")).toString();
if (status != QStringLiteral("success")) {
qWarning() << "TransferController::startWaitForConfig: gateway status not success:" << status;
emit waitError(QStringLiteral("Gateway error"));
return;
}
if (configStr.isEmpty()) {
emit waitError(QStringLiteral("Empty config"));
return;
}
if (configStr == QStringLiteral("timeout")) {
emit waitError(QStringLiteral("Timeout"));
return;
}
if (!m_importController->extractConfigFromData(configStr)) {
qWarning() << "TransferController::startWaitForConfig: failed to parse config string";
emit waitError(QStringLiteral("Invalid config payload"));
return;
}
m_importController->importConfig();
emit configApplied();
}
void TransferController::stopWaitForConfig()
{
qDebug() << "TransferController::stopWaitForConfig: stop flag set";
}
@@ -0,0 +1,77 @@
#ifndef TRANSFERCONTROLLER_H
#define TRANSFERCONTROLLER_H
#include <QObject>
#include <QScopedPointer>
#include <QJsonObject>
#include <QJsonDocument>
#include <QUuid>
class Settings;
class ServersModel;
class ExportController;
class ImportController;
class TransferController : public QObject
{
Q_OBJECT
Q_PROPERTY(QString qrCodeUrl READ qrCodeUrl NOTIFY qrCodeUpdated)
Q_PROPERTY(QString pendingQrCode READ pendingQrCode WRITE setPendingQrCode NOTIFY pendingQrCodeChanged)
Q_PROPERTY(QString currentUuid READ currentUuid NOTIFY currentUuidChanged)
Q_PROPERTY(QString currentPayload READ currentPayload NOTIFY currentPayloadChanged)
public:
explicit TransferController(const std::shared_ptr<Settings> &settings,
const QSharedPointer<ServersModel> &serversModel,
ExportController *exportController,
QObject *parent = nullptr);
~TransferController() override;
Q_INVOKABLE void generateNewQrCode();
Q_INVOKABLE void stopScanner();
Q_INVOKABLE void onTransferQrScanned(const QString &code);
Q_INVOKABLE void setPendingQrCode(const QString &code) { m_pendingQrCode = code; emit pendingQrCodeChanged(); }
QString pendingQrCode() const { return m_pendingQrCode; }
Q_INVOKABLE void startWaitForConfig(ImportController *importController);
Q_INVOKABLE void stopWaitForConfig();
QString qrCodeUrl() const;
QString currentUuid() const { return m_currentUuid; }
QString currentPayload() const { return m_currentPayload; }
signals:
void qrCodeUpdated();
void scannerShouldStop();
void pendingQrCodeChanged();
void currentUuidChanged();
void currentPayloadChanged();
void waitError(const QString &message);
void configApplied();
void postStarted();
void postSucceeded();
void postFailed(const QString &message);
private slots:
void handleImportControllerDestroyed();
private:
QString buildQrPayloadJson(const QString &gatewayUrl, const QString &uuid) const;
QString m_pendingQrCode;
QString getCurrentApiKey(QString *vpnKeyOut = nullptr) const;
std::shared_ptr<Settings> m_settings;
QSharedPointer<ServersModel> m_serversModel;
ExportController *m_exportController { nullptr };
ImportController *m_importController { nullptr };
QString m_qrCodeUrl;
QString m_currentUuid;
QString m_currentPayload;
};
#endif // TRANSFERCONTROLLER_H
@@ -34,7 +34,7 @@ PageType {
ListViewType { ListViewType {
id: listView id: listView
anchors.top: backButtonLayout.bottom anchors.top: backButton.bottom
anchors.bottom: saveButton.top anchors.bottom: saveButton.top
anchors.right: parent.right anchors.right: parent.right
anchors.left: parent.left anchors.left: parent.left
@@ -37,7 +37,7 @@ PageType {
ListViewType { ListViewType {
id: listView id: listView
anchors.top: backButtonLayout.bottom anchors.top: backButton.bottom
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@@ -17,428 +17,414 @@ import "../Components"
PageType { PageType {
id: root id: root
ColumnLayout { BackButtonType {
id: backButtonLayout id: backButton
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20
BackButtonType { onActiveFocusChanged: {
id: backButton if(backButton.enabled && backButton.activeFocus) {
listView.positionViewAtBeginning()
}
} }
} }
FlickableType { ListViewType {
id: fl id: listView
anchors.top: backButtonLayout.bottom
anchors.top: backButton.bottom
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
contentHeight: content.implicitHeight anchors.right: parent.right
anchors.left: parent.left
Column { enabled: ServersModel.isProcessedServerHasWriteAccess()
id: content
anchors.top: parent.top header: ColumnLayout {
anchors.left: parent.left width: listView.width
anchors.right: parent.right
enabled: ServersModel.isProcessedServerHasWriteAccess() BaseHeaderType {
id: header
ListView { Layout.fillWidth: true
id: listview Layout.rightMargin: 16
Layout.leftMargin: 16
width: parent.width headerText: qsTr("OpenVPN Settings")
height: listview.contentItem.height }
}
clip: true model: OpenVpnConfigModel
interactive: false
model: OpenVpnConfigModel delegate: ColumnLayout {
width: listView.width
delegate: Item { spacing: 0
id: delegateItem
property alias vpnAddressSubnetTextField: vpnAddressSubnetTextField TextFieldWithHeaderType {
property bool isEnabled: ServersModel.isProcessedServerHasWriteAccess() id: vpnAddressSubnetTextField
implicitWidth: listview.width Layout.fillWidth: true
implicitHeight: col.implicitHeight Layout.topMargin: 32
Layout.leftMargin: 16
Layout.rightMargin: 16
ColumnLayout { enabled: listView.enabled
id: col
anchors.top: parent.top headerText: qsTr("VPN address subnet")
anchors.left: parent.left textField.text: subnetAddress
anchors.right: parent.right
anchors.leftMargin: 16 textField.onEditingFinished: {
anchors.rightMargin: 16 if (textField.text !== subnetAddress) {
subnetAddress = textField.text
}
}
spacing: 0 checkEmptyText: true
}
BaseHeaderType { ParagraphTextType {
Layout.fillWidth: true Layout.fillWidth: true
headerText: qsTr("OpenVPN settings") Layout.topMargin: 32
} Layout.leftMargin: 16
Layout.rightMargin: 16
TextFieldWithHeaderType { text: qsTr("Network protocol")
id: vpnAddressSubnetTextField }
Layout.fillWidth: true TransportProtoSelector {
Layout.topMargin: 32 id: transportProtoSelector
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: delegateItem.isEnabled rootWidth: root.width
headerText: qsTr("VPN address subnet") enabled: isTransportProtoEditable
textField.text: subnetAddress
parentFlickable: fl currentIndex: {
return transportProto === "tcp" ? 1 : 0
}
textField.onEditingFinished: { onCurrentIndexChanged: {
if (textField.text !== subnetAddress) { if (transportProto === "tcp" && currentIndex === 0) {
subnetAddress = textField.text transportProto = "udp"
} } else if (transportProto === "udp" && currentIndex === 1) {
transportProto = "tcp"
}
}
}
TextFieldWithHeaderType {
id: portTextField
Layout.fillWidth: true
Layout.topMargin: 40
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: listView.enabled
headerText: qsTr("Port")
textField.text: port
textField.maximumLength: 5
textField.validator: IntValidator { bottom: 1; top: 65535 }
textField.onEditingFinished: {
if (textField.text !== port) {
port = textField.text
}
}
checkEmptyText: true
}
SwitcherType {
id: autoNegotiateEncryprionSwitcher
Layout.fillWidth: true
Layout.topMargin: 24
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Auto-negotiate encryption")
checked: autoNegotiateEncryprion
onCheckedChanged: {
if (checked !== autoNegotiateEncryprion) {
autoNegotiateEncryprion = checked
}
}
}
DropDownType {
id: hashDropDown
Layout.fillWidth: true
Layout.topMargin: 20
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: !autoNegotiateEncryprionSwitcher.checked
descriptionText: qsTr("Hash")
headerText: qsTr("Hash")
drawerParent: root
listView: ListViewWithRadioButtonType {
id: hashListView
rootWidth: root.width
model: ListModel {
ListElement { name : qsTr("SHA512") }
ListElement { name : qsTr("SHA384") }
ListElement { name : qsTr("SHA256") }
ListElement { name : qsTr("SHA3-512") }
ListElement { name : qsTr("SHA3-384") }
ListElement { name : qsTr("SHA3-256") }
ListElement { name : qsTr("whirlpool") }
ListElement { name : qsTr("BLAKE2b512") }
ListElement { name : qsTr("BLAKE2s256") }
ListElement { name : qsTr("SHA1") }
}
clickedFunction: function() {
hashDropDown.text = selectedText
hash = hashDropDown.text
hashDropDown.closeTriggered()
}
Component.onCompleted: {
hashDropDown.text = hash
for (var i = 0; i < hashListView.model.count; i++) {
if (hashListView.model.get(i).name === hashDropDown.text) {
currentIndex = i
} }
checkEmptyText: true
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 32
text: qsTr("Network protocol")
}
TransportProtoSelector {
id: transportProtoSelector
Layout.fillWidth: true
Layout.topMargin: 16
rootWidth: root.width
enabled: isTransportProtoEditable
currentIndex: {
return transportProto === "tcp" ? 1 : 0
}
onCurrentIndexChanged: {
if (transportProto === "tcp" && currentIndex === 0) {
transportProto = "udp"
} else if (transportProto === "udp" && currentIndex === 1) {
transportProto = "tcp"
}
}
}
TextFieldWithHeaderType {
id: portTextField
Layout.fillWidth: true
Layout.topMargin: 40
parentFlickable: fl
enabled: delegateItem.isEnabled
headerText: qsTr("Port")
textField.text: port
textField.maximumLength: 5
textField.validator: IntValidator { bottom: 1; top: 65535 }
textField.onEditingFinished: {
if (textField.text !== port) {
port = textField.text
}
}
checkEmptyText: true
}
SwitcherType {
id: autoNegotiateEncryprionSwitcher
Layout.fillWidth: true
Layout.topMargin: 24
parentFlickable: fl
text: qsTr("Auto-negotiate encryption")
checked: autoNegotiateEncryprion
onCheckedChanged: {
if (checked !== autoNegotiateEncryprion) {
autoNegotiateEncryprion = checked
}
}
}
DropDownType {
id: hashDropDown
Layout.fillWidth: true
Layout.topMargin: 20
enabled: !autoNegotiateEncryprionSwitcher.checked
descriptionText: qsTr("Hash")
headerText: qsTr("Hash")
drawerParent: root
listView: ListViewWithRadioButtonType {
id: hashListView
rootWidth: root.width
model: ListModel {
ListElement { name : qsTr("SHA512") }
ListElement { name : qsTr("SHA384") }
ListElement { name : qsTr("SHA256") }
ListElement { name : qsTr("SHA3-512") }
ListElement { name : qsTr("SHA3-384") }
ListElement { name : qsTr("SHA3-256") }
ListElement { name : qsTr("whirlpool") }
ListElement { name : qsTr("BLAKE2b512") }
ListElement { name : qsTr("BLAKE2s256") }
ListElement { name : qsTr("SHA1") }
}
clickedFunction: function() {
hashDropDown.text = selectedText
hash = hashDropDown.text
hashDropDown.closeTriggered()
}
Component.onCompleted: {
hashDropDown.text = hash
for (var i = 0; i < hashListView.model.count; i++) {
if (hashListView.model.get(i).name === hashDropDown.text) {
currentIndex = i
}
}
}
}
}
DropDownType {
id: cipherDropDown
Layout.fillWidth: true
Layout.topMargin: 16
enabled: !autoNegotiateEncryprionSwitcher.checked
descriptionText: qsTr("Cipher")
headerText: qsTr("Cipher")
drawerParent: root
listView: ListViewWithRadioButtonType {
id: cipherListView
rootWidth: root.width
model: ListModel {
ListElement { name : qsTr("AES-256-GCM") }
ListElement { name : qsTr("AES-192-GCM") }
ListElement { name : qsTr("AES-128-GCM") }
ListElement { name : qsTr("AES-256-CBC") }
ListElement { name : qsTr("AES-192-CBC") }
ListElement { name : qsTr("AES-128-CBC") }
ListElement { name : qsTr("ChaCha20-Poly1305") }
ListElement { name : qsTr("ARIA-256-CBC") }
ListElement { name : qsTr("CAMELLIA-256-CBC") }
ListElement { name : qsTr("none") }
}
clickedFunction: function() {
cipherDropDown.text = selectedText
cipher = cipherDropDown.text
cipherDropDown.closeTriggered()
}
Component.onCompleted: {
cipherDropDown.text = cipher
for (var i = 0; i < cipherListView.model.count; i++) {
if (cipherListView.model.get(i).name === cipherDropDown.text) {
currentIndex = i
}
}
}
}
}
Rectangle {
id: contentRect
Layout.fillWidth: true
Layout.topMargin: 32
Layout.preferredHeight: checkboxLayout.implicitHeight
color: AmneziaStyle.color.onyxBlack
radius: 16
Connections {
target: tlsAuthCheckBox
enabled: !GC.isMobile()
function onFocusChanged() {
if (tlsAuthCheckBox.activeFocus) {
fl.ensureVisible(contentRect)
}
}
}
ColumnLayout {
id: checkboxLayout
anchors.fill: parent
CheckBoxType {
id: tlsAuthCheckBox
Layout.fillWidth: true
text: qsTr("TLS auth")
checked: tlsAuth
onCheckedChanged: {
if (checked !== tlsAuth) {
console.log("tlsAuth changed to: " + checked)
tlsAuth = checked
}
}
}
DividerType {}
CheckBoxType {
id: blockDnsCheckBox
Layout.fillWidth: true
text: qsTr("Block DNS requests outside of VPN")
checked: blockDns
onCheckedChanged: {
if (checked !== blockDns) {
blockDns = checked
}
}
}
}
}
SwitcherType {
id: additionalClientCommandsSwitcher
Layout.fillWidth: true
Layout.topMargin: 32
parentFlickable: fl
checked: additionalClientCommands !== ""
text: qsTr("Additional client configuration commands")
onCheckedChanged: {
if (!checked) {
additionalClientCommands = ""
}
}
}
TextAreaType {
id: additionalClientCommandsTextArea
Layout.fillWidth: true
Layout.topMargin: 16
visible: additionalClientCommandsSwitcher.checked
parentFlickable: fl
textAreaText: additionalClientCommands
placeholderText: qsTr("Commands:")
textArea.onEditingFinished: {
if (additionalClientCommands !== textAreaText) {
additionalClientCommands = textAreaText
}
}
}
SwitcherType {
id: additionalServerCommandsSwitcher
Layout.fillWidth: true
Layout.topMargin: 16
parentFlickable: fl
checked: additionalServerCommands !== ""
text: qsTr("Additional server configuration commands")
onCheckedChanged: {
if (!checked) {
additionalServerCommands = ""
}
}
}
TextAreaType {
id: additionalServerCommandsTextArea
Layout.fillWidth: true
Layout.topMargin: 16
visible: additionalServerCommandsSwitcher.checked
textAreaText: additionalServerCommands
placeholderText: qsTr("Commands:")
parentFlickable: fl
textArea.onEditingFinished: {
if (additionalServerCommands !== textAreaText) {
additionalServerCommands = textAreaText
}
}
}
BasicButtonType {
id: saveButton
Layout.fillWidth: true
Layout.topMargin: 24
Layout.bottomMargin: 24
enabled: vpnAddressSubnetTextField.errorText === "" &&
portTextField.errorText === ""
text: qsTr("Save")
parentFlickable: fl
onClicked: function() {
forceActiveFocus()
var headerText = qsTr("Save settings?")
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) {
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling);
InstallController.updateContainer(OpenVpnConfigModel.getConfig())
}
var noButtonFunction = function() {
if (!GC.isMobile()) {
saveButton.forceActiveFocus()
}
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
Keys.onEnterPressed: saveButton.clicked()
Keys.onReturnPressed: saveButton.clicked()
} }
} }
} }
} }
DropDownType {
id: cipherDropDown
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: !autoNegotiateEncryprionSwitcher.checked
descriptionText: qsTr("Cipher")
headerText: qsTr("Cipher")
drawerParent: root
listView: ListViewWithRadioButtonType {
id: cipherListView
rootWidth: root.width
model: ListModel {
ListElement { name : qsTr("AES-256-GCM") }
ListElement { name : qsTr("AES-192-GCM") }
ListElement { name : qsTr("AES-128-GCM") }
ListElement { name : qsTr("AES-256-CBC") }
ListElement { name : qsTr("AES-192-CBC") }
ListElement { name : qsTr("AES-128-CBC") }
ListElement { name : qsTr("ChaCha20-Poly1305") }
ListElement { name : qsTr("ARIA-256-CBC") }
ListElement { name : qsTr("CAMELLIA-256-CBC") }
ListElement { name : qsTr("none") }
}
clickedFunction: function() {
cipherDropDown.text = selectedText
cipher = cipherDropDown.text
cipherDropDown.closeTriggered()
}
Component.onCompleted: {
cipherDropDown.text = cipher
for (var i = 0; i < cipherListView.model.count; i++) {
if (cipherListView.model.get(i).name === cipherDropDown.text) {
currentIndex = i
}
}
}
}
}
Rectangle {
id: contentRect
Layout.fillWidth: true
Layout.topMargin: 32
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.preferredHeight: checkboxLayout.implicitHeight
color: AmneziaStyle.color.onyxBlack
radius: 16
ColumnLayout {
id: checkboxLayout
anchors.fill: parent
CheckBoxType {
id: tlsAuthCheckBox
Layout.fillWidth: true
text: qsTr("TLS auth")
checked: tlsAuth
onCheckedChanged: {
if (checked !== tlsAuth) {
console.log("tlsAuth changed to: " + checked)
tlsAuth = checked
}
}
}
DividerType {}
CheckBoxType {
id: blockDnsCheckBox
Layout.fillWidth: true
text: qsTr("Block DNS requests outside of VPN")
checked: blockDns
onCheckedChanged: {
if (checked !== blockDns) {
blockDns = checked
}
}
}
}
}
SwitcherType {
id: additionalClientCommandsSwitcher
Layout.fillWidth: true
Layout.topMargin: 32
Layout.leftMargin: 16
Layout.rightMargin: 16
checked: additionalClientCommands !== ""
text: qsTr("Additional client configuration commands")
onCheckedChanged: {
if (!checked) {
additionalClientCommands = ""
}
}
}
TextAreaType {
id: additionalClientCommandsTextArea
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: additionalClientCommandsSwitcher.checked
textAreaText: additionalClientCommands
placeholderText: qsTr("Commands:")
textArea.onEditingFinished: {
if (additionalClientCommands !== textAreaText) {
additionalClientCommands = textAreaText
}
}
}
SwitcherType {
id: additionalServerCommandsSwitcher
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
checked: additionalServerCommands !== ""
text: qsTr("Additional server configuration commands")
onCheckedChanged: {
if (!checked) {
additionalServerCommands = ""
}
}
}
TextAreaType {
id: additionalServerCommandsTextArea
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: additionalServerCommandsSwitcher.checked
textAreaText: additionalServerCommands
placeholderText: qsTr("Commands:")
textArea.onEditingFinished: {
if (additionalServerCommands !== textAreaText) {
additionalServerCommands = textAreaText
}
}
}
BasicButtonType {
id: saveButton
Layout.fillWidth: true
Layout.topMargin: 24
Layout.bottomMargin: 24
Layout.leftMargin: 16
Layout.rightMargin: 16
enabled: vpnAddressSubnetTextField.errorText === "" &&
portTextField.errorText === ""
text: qsTr("Save")
onClicked: function() {
forceActiveFocus()
var headerText = qsTr("Save settings?")
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) {
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling);
InstallController.updateContainer(OpenVpnConfigModel.getConfig())
}
var noButtonFunction = function() {
if (!GC.isMobile()) {
saveButton.forceActiveFocus()
}
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
Keys.onEnterPressed: saveButton.clicked()
Keys.onReturnPressed: saveButton.clicked()
}
} }
} }
} }
@@ -66,8 +66,6 @@ PageType {
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
enabled: delegateItem.isEnabled
headerText: qsTr("VPN address subnet") headerText: qsTr("VPN address subnet")
textField.text: subnetAddress textField.text: subnetAddress
@@ -87,8 +85,6 @@ PageType {
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
enabled: delegateItem.isEnabled
headerText: qsTr("Port") headerText: qsTr("Port")
textField.text: port textField.text: port
textField.maximumLength: 5 textField.maximumLength: 5
-4
View File
@@ -43,8 +43,6 @@ PageType {
LabelWithButtonType { LabelWithButtonType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: isVisible visible: isVisible
@@ -68,8 +66,6 @@ PageType {
visible: GC.isDesktop() visible: GC.isDesktop()
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Close application") text: qsTr("Close application")
leftImageSource: "qrc:/images/controls/x-circle.svg" leftImageSource: "qrc:/images/controls/x-circle.svg"
@@ -0,0 +1,126 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
PageType {
id: root
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")
if (!max || max <= 0) max = 7
if (!active || active < 0) active = 0
var remain = max - active
return remain > 0 ? remain : 0
}
ListViewType {
id: listView
anchors.fill: parent
anchors.topMargin: 20
header: ColumnLayout {
width: listView.width
BackButtonType {
backButtonFunction: function() {
if (root.isSending) {
TransferController.stopWaitForConfig()
}
PageController.closePage()
}
}
BaseHeaderType {
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Add a new device to the subscription?")
descriptionText: qsTr("Devices available with Amnezia Premium: (%1)").arg(getAvailableCount())
}
BasicButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
text: qsTr("Add Device")
enabled: !root.isSending && root.getAvailableCount() > 0 && TransferController.pendingQrCode !== ""
clickedFunc: function() {
if (TransferController.pendingQrCode !== "") {
root.isSending = true
TransferController.onTransferQrScanned(TransferController.pendingQrCode)
}
}
}
BasicButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.paleGray
borderColor: AmneziaStyle.color.paleGray
borderWidth: 1
text: qsTr("Cancel")
enabled: !root.isSending
clickedFunc: function() {
PageController.closePage()
}
}
}
}
Connections {
target: TransferController
function onPostStarted() {
PageController.showNotificationMessage(qsTr("Sending configuration..."))
}
function onPostSucceeded() {
root.isSending = false
PageController.showNotificationMessage(qsTr("%1 has been added to your subscription").arg(root.getAddedDeviceName()))
PageController.closePage()
PageController.closePage()
}
function onPostFailed(message) {
root.isSending = false
PageController.showErrorMessage(message)
}
}
}
@@ -0,0 +1,140 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import PageEnum 1.0
import QRCodeReader 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
PageType {
id: root
property bool scanCompleted: false
Item {
id: cameraArea
anchors.fill: parent
}
Loader {
id: iosQrLoader
anchors.fill: cameraArea
active: Qt.platform.os === "ios"
sourceComponent: Component {
QRCodeReader {
id: qrCodeReader
function updateCameraSize() {
qrCodeReader.setCameraSize(Qt.rect(cameraArea.x,
cameraArea.y,
cameraArea.width,
cameraArea.height))
}
onCodeReaded: function(code) {
if (!code || code.length === 0) {
return
}
var obj = null
try {
obj = JSON.parse(code)
} catch (e) {
obj = null
}
if (!obj || !obj.gw || !(obj.uuid || obj.u)) {
return
}
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)
}
Component.onCompleted: {
updateCameraSize()
qrCodeReader.startReading()
}
Component.onDestruction: qrCodeReader.stopReading()
}
}
}
onWidthChanged: {
if (iosQrLoader.item && iosQrLoader.item.updateCameraSize) {
iosQrLoader.item.updateCameraSize()
}
}
onHeightChanged: {
if (iosQrLoader.item && iosQrLoader.item.updateCameraSize) {
iosQrLoader.item.updateCameraSize()
}
}
Connections {
target: TransferController
function onScannerShouldStop() {
if (iosQrLoader.item && iosQrLoader.item.stopReading) {
iosQrLoader.item.stopReading()
}
}
}
Connections {
target: ImportController
function onTransferQrDecoded(code) {
if (!code || code.length === 0) {
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()
}
}
}
+106 -5
View File
@@ -4,6 +4,7 @@ import QtQuick.Layouts
import QtQuick.Dialogs import QtQuick.Dialogs
import QtCore import QtCore
import QRCodeReader 1.0
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
@@ -19,6 +20,29 @@ import "../Components"
PageType { PageType {
id: root id: root
function isAtDeviceLimit() {
var maxDeviceCount = ApiAccountInfoModel.data("maxDeviceCount")
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 { ListViewType {
id: listView id: listView
@@ -46,16 +70,41 @@ PageType {
descriptionText: qsTr("Manage currently connected devices") descriptionText: qsTr("Manage currently connected devices")
} }
WarningType { BasicButtonType {
id: addDeviceQrButton
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16 Layout.topMargin: 16
visible: GC.isMobile()
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.paleGray
borderColor: AmneziaStyle.color.paleGray
borderWidth: 1
text: qsTr("Add device by QR code")
clickedFunc: function() {
if (root.isAtDeviceLimit()) {
PageController.goToPage(PageEnum.PageSettingsApiDevicesLimit)
} else {
PageController.goToPage(PageEnum.PageSettingsApiAddDeviceScan)
}
}
}
SmallTextType {
Layout.topMargin: 8
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.fillWidth: true Layout.fillWidth: true
textString: qsTr("You can find the identifier on the Support tab or, for older versions of the app, " text: qsTr("On the other device, tap + at the bottom → Connect to Amnezia Premium")
+ "by tapping '+' and then the three dots at the top of the page.")
iconPath: "qrc:/images/controls/alert-circle.svg"
} }
} }
@@ -93,6 +142,58 @@ PageType {
DividerType {} 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
function onPostStarted() {
PageController.showBusyIndicator(true)
}
function onPostSucceeded() {
PageController.showBusyIndicator(false)
ApiSettingsController.getAccountInfo(true)
PageController.showNotificationMessage(qsTr("New device added to subscription"))
}
function onPostFailed(message) {
PageController.showBusyIndicator(false)
PageController.showErrorMessage(message)
}
function onScannerShouldStop() {}
}
} }
function deactivateExternalDevice(supportTag, countryCode) { function deactivateExternalDevice(supportTag, countryCode) {
@@ -0,0 +1,53 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
PageType {
id: root
ListViewType {
id: listView
anchors.fill: parent
anchors.topMargin: 20
header: ColumnLayout {
width: listView.width
BackButtonType {}
BaseHeaderType {
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Your Amnezia Premium subscription already has the maximum number of devices — ") + ApiAccountInfoModel.data("connectedDevices")
descriptionText: qsTr("Remove one of the previously connected devices to add a new one")
}
BasicButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
text: qsTr("Show all devices")
clickedFunc: function() {
PageController.goToPage(PageEnum.PageSettingsApiDevices)
}
}
}
}
}
@@ -104,7 +104,7 @@ PageType {
actionButtonImage: "qrc:/images/controls/edit-3.svg" actionButtonImage: "qrc:/images/controls/edit-3.svg"
headerText: root.processedServer.name headerText: root.processedServer && root.processedServer.name ? root.processedServer.name : ""
descriptionText: ApiAccountInfoModel.data("serviceDescription") descriptionText: ApiAccountInfoModel.data("serviceDescription")
actionButtonFunction: function() { actionButtonFunction: function() {
@@ -214,9 +214,6 @@ PageType {
ApiConfigsController.prepareVpnKeyExport() ApiConfigsController.prepareVpnKeyExport()
PageController.showBusyIndicator(false) PageController.showBusyIndicator(false)
// Navigate to PageShareConnection page
//PageController.goToPage(PageEnum.PageShareConnection)
} }
} }
@@ -416,6 +413,6 @@ PageType {
anchors.fill: parent anchors.fill: parent
expandedHeight: parent.height * 0.35 expandedHeight: parent.height * 0.35
serverNameText: root.processedServer.name serverNameText: root.processedServer && root.processedServer.name ? root.processedServer.name : ""
} }
} }
+7 -11
View File
@@ -66,6 +66,13 @@ PageType {
text: qsTr("If AmneziaDNS is not used or installed") text: qsTr("If AmneziaDNS is not used or installed")
} }
}
model: 1 // fake model to force the ListView to be created without a model
delegate: ColumnLayout {
width: listView.width
spacing: 16
TextFieldWithHeaderType { TextFieldWithHeaderType {
id: primaryDns id: primaryDns
@@ -96,13 +103,6 @@ PageType {
regularExpression: InstallController.ipAddressRegExp() regularExpression: InstallController.ipAddressRegExp()
} }
} }
}
model: 1 // fake model to force the ListView to be created without a model
spacing: 16
delegate: ColumnLayout {
width: listView.width
BasicButtonType { BasicButtonType {
id: restoreDefaultButton id: restoreDefaultButton
@@ -139,10 +139,6 @@ PageType {
showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
} }
} }
}
footer: ColumnLayout {
width: listView.width
BasicButtonType { BasicButtonType {
id: saveButton id: saveButton
@@ -18,6 +18,8 @@ PageType {
signal lastItemTabClickedSignal() signal lastItemTabClickedSignal()
property bool isServerWithWriteAccess: ServersModel.isProcessedServerHasWriteAccess()
Connections { Connections {
target: InstallController target: InstallController
@@ -59,15 +61,13 @@ PageType {
target: ServersModel target: ServersModel
function onProcessedServerIndexChanged() { function onProcessedServerIndexChanged() {
listView.isServerWithWriteAccess = ServersModel.isProcessedServerHasWriteAccess() root.isServerWithWriteAccess = ServersModel.isProcessedServerHasWriteAccess()
} }
} }
ListViewType { ListViewType {
id: listView id: listView
property bool isServerWithWriteAccess: ServersModel.isProcessedServerHasWriteAccess()
anchors.fill: parent anchors.fill: parent
model: serverActions model: serverActions
@@ -107,7 +107,7 @@ PageType {
QtObject { QtObject {
id: check id: check
property bool isVisible: true property bool isVisible: root.isServerWithWriteAccess
readonly property string title: qsTr("Check the server for previously installed Amnezia services") readonly property string title: qsTr("Check the server for previously installed Amnezia services")
readonly property string description: qsTr("Add them to the application if they were not displayed") readonly property string description: qsTr("Add them to the application if they were not displayed")
readonly property var tColor: AmneziaStyle.color.paleGray readonly property var tColor: AmneziaStyle.color.paleGray
@@ -121,7 +121,7 @@ PageType {
QtObject { QtObject {
id: reboot id: reboot
property bool isVisible: true property bool isVisible: root.isServerWithWriteAccess
readonly property string title: qsTr("Reboot server") readonly property string title: qsTr("Reboot server")
readonly property string description: "" readonly property string description: ""
readonly property var tColor: AmneziaStyle.color.vibrantRed readonly property var tColor: AmneziaStyle.color.vibrantRed
@@ -181,7 +181,7 @@ PageType {
QtObject { QtObject {
id: clear id: clear
property bool isVisible: true property bool isVisible: root.isServerWithWriteAccess
readonly property string title: qsTr("Clear server from Amnezia software") readonly property string title: qsTr("Clear server from Amnezia software")
readonly property string description: "" readonly property string description: ""
readonly property var tColor: AmneziaStyle.color.vibrantRed readonly property var tColor: AmneziaStyle.color.vibrantRed
@@ -240,7 +240,7 @@ PageType {
QtObject { QtObject {
id: switch_to_premium id: switch_to_premium
property bool isVisible: ServersModel.getProcessedServerData("isServerFromTelegramApi") property bool isVisible: ServersModel.getProcessedServerData("isServerFromTelegramApi") && ServersModel.processedServerIsPremium
readonly property string title: qsTr("Switch to the new Amnezia Premium subscription") readonly property string title: qsTr("Switch to the new Amnezia Premium subscription")
readonly property string description: "" readonly property string description: ""
readonly property var tColor: AmneziaStyle.color.vibrantRed readonly property var tColor: AmneziaStyle.color.vibrantRed
@@ -45,7 +45,7 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
headerText: qsTr("Connection") headerText: qsTr("New Connection")
actionButtonImage: isVisible ? "qrc:/images/controls/more-vertical.svg" : "" actionButtonImage: isVisible ? "qrc:/images/controls/more-vertical.svg" : ""
actionButtonFunction: function() { actionButtonFunction: function() {
@@ -156,7 +156,7 @@ PageType {
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.bottomMargin: 24 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 { TextFieldWithHeaderType {
@@ -166,8 +166,8 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
headerText: qsTr("Insert key") headerText: qsTr("Connection key")
buttonText: qsTr("Insert") buttonText: qsTr("Paste")
clickedFunc: function() { clickedFunc: function() {
textField.text = "" textField.text = ""
@@ -250,7 +250,7 @@ PageType {
disabledColor: AmneziaStyle.color.mutedGray disabledColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.goldenApricot textColor: AmneziaStyle.color.goldenApricot
text: qsTr("Site Amnezia") text: qsTr("Amnezia Website")
rightImageSource: "qrc:/images/controls/external-link.svg" rightImageSource: "qrc:/images/controls/external-link.svg"
@@ -263,18 +263,17 @@ PageType {
property list<QtObject> variants: [ property list<QtObject> variants: [
amneziaVpn, amneziaVpn,
shareViaDevice,
selfHostVpn, selfHostVpn,
backupRestore,
fileOpen, fileOpen,
qrScan, qrScan
siteLink
] ]
QtObject { QtObject {
id: amneziaVpn id: amneziaVpn
property string title: qsTr("VPN by Amnezia") property string title: qsTr("Choose a VPN from Amnezia")
property string description: qsTr("Connect to classic paid and free VPN services from Amnezia") property string description: qsTr("Paid and free VPN services from Amnezia")
property string imageSource: "qrc:/images/controls/amnezia.svg" property string imageSource: "qrc:/images/controls/amnezia.svg"
property bool isVisible: true property bool isVisible: true
property var handler: function() { property var handler: function() {
@@ -287,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 { QtObject {
id: selfHostVpn id: selfHostVpn
property string title: qsTr("Self-hosted VPN") property string title: qsTr("Set up a Self-hosted VPN")
property string description: qsTr("Configure Amnezia VPN on your own server") property string description: qsTr("On a private server")
property string imageSource: "qrc:/images/controls/server.svg" property string imageSource: "qrc:/images/controls/server.svg"
property bool isVisible: true property bool isVisible: true
property var handler: function() { property var handler: function() {
@@ -320,8 +331,8 @@ PageType {
QtObject { QtObject {
id: fileOpen id: fileOpen
property string title: qsTr("File with connection settings") property string title: qsTr("Use a configuration file")
property string description: qsTr("") property string description: qsTr("Supported formats: .conf, .vpn, .ovpn, .json")
property string imageSource: "qrc:/images/controls/folder-search-2.svg" property string imageSource: "qrc:/images/controls/folder-search-2.svg"
property bool isVisible: true property bool isVisible: true
property var handler: function() { property var handler: function() {
@@ -334,13 +345,13 @@ PageType {
} }
} }
} }
} }
QtObject { QtObject {
id: qrScan id: qrScan
property string title: qsTr("QR code") property string title: qsTr("Scan a QR code")
property string description: qsTr("") property string description: qsTr("To connect to a self-hosted server")
property string imageSource: "qrc:/images/controls/scan-line.svg" property string imageSource: "qrc:/images/controls/scan-line.svg"
property bool isVisible: SettingsController.isCameraPresent() property bool isVisible: SettingsController.isCameraPresent()
property var handler: function() { property var handler: function() {
@@ -349,7 +360,7 @@ PageType {
PageController.goToPage(PageEnum.PageSetupWizardQrReader) PageController.goToPage(PageEnum.PageSetupWizardQrReader)
} }
} }
} }
QtObject { QtObject {
id: siteLink id: siteLink
@@ -161,10 +161,4 @@ PageType {
} }
} }
} }
ShareConnectionDrawer {
id: shareConnectionDrawer
anchors.fill: parent
}
} }
@@ -0,0 +1,126 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import PageEnum 1.0
import Style 1.0
import "../Controls2"
import "../Components"
import "../Controls2/TextTypes"
PageType {
id: root
objectName: "PageTransferConfigViaQR"
Rectangle {
anchors.fill: parent
color: AmneziaStyle.color.midnightBlack
z: 0
}
ColumnLayout {
z: 1
anchors.fill: parent
anchors.topMargin: 24
spacing: 12
BackButtonType {
Layout.topMargin: 20
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.alignment: Qt.AlignLeft
}
Item {
id: contentArea
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
ColumnLayout {
id: qrContent
anchors.fill: parent
spacing: 16
Item { Layout.fillHeight: true }
SmallTextType {
id: topHint
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
text: qsTr("Scan this QR code with a phone that has an active\nAmnezia Premium subscription")
}
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)))
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 !== ""
}
BusyIndicator {
anchors.centerIn: parent
running: TransferController.qrCodeUrl === ""
visible: TransferController.qrCodeUrl === ""
}
}
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 }
}
}
}
Connections {
target: TransferController
function onConfigApplied() {
PageController.showNotificationMessage(qsTr("Device has been added to subscription"))
PageController.closePage()
PageController.goToPageHome()
}
function onWaitError(message) {
PageController.showErrorMessage(message)
}
}
Connections {
target: ImportController
function onImportErrorOccurred(errorCode, goToPageHome) {
PageController.showErrorMessage(errorCode)
}
}
Component.onCompleted: {
TransferController.generateNewQrCode()
TransferController.startWaitForConfig(ImportController)
}
Component.onDestruction: TransferController.stopWaitForConfig()
}