From 7b14ad9616f4437b4569c10659c07d634c500300 Mon Sep 17 00:00:00 2001 From: "vladimir.kuznetsov" Date: Fri, 16 Jun 2023 13:43:55 +0900 Subject: [PATCH] added PageSettingsAbout, PageSettingsApplication, PageSettingsBackup, PageSettingsConnection, PageSettingsDns - added SettingsController --- client/amnezia_application.cpp | 4 + client/amnezia_application.h | 2 + client/images/controls/delete.svg | 5 + client/images/controls/github.svg | 4 + client/images/controls/mail.svg | 4 + client/images/controls/telegram.svg | 3 + client/resources.qrc | 9 + client/ui/controllers/exportController.cpp | 42 ++-- client/ui/controllers/exportController.h | 10 + client/ui/controllers/pageController.h | 5 + client/ui/controllers/settingsController.cpp | 113 ++++++++++ client/ui/controllers/settingsController.h | 56 +++++ .../Components/Protocols/OpenVpnSettings.qml | 4 - .../qml/Components/ShareConnectionDrawer.qml | 14 +- client/ui/qml/Controls2/DropDownType.qml | 44 +++- client/ui/qml/Controls2/ImageButtonType.qml | 9 +- client/ui/qml/Controls2/SwitcherType.qml | 37 +++- client/ui/qml/Pages2/PageHome.qml | 4 +- client/ui/qml/Pages2/PageSettings.qml | 4 + client/ui/qml/Pages2/PageSettingsAbout.qml | 207 ++++++++++++++++++ .../ui/qml/Pages2/PageSettingsApplication.qml | 74 +++++++ client/ui/qml/Pages2/PageSettingsBackup.qml | 184 ++++++++++++++++ .../ui/qml/Pages2/PageSettingsConnection.qml | 107 +++++++++ client/ui/qml/Pages2/PageSettingsDns.qml | 87 ++++++++ client/ui/qml/Pages2/PageShare.qml | 10 +- client/utilities.cpp | 54 +++++ client/utilities.h | 15 +- service/server/CMakeLists.txt | 6 +- 28 files changed, 1054 insertions(+), 63 deletions(-) create mode 100644 client/images/controls/delete.svg create mode 100644 client/images/controls/github.svg create mode 100644 client/images/controls/mail.svg create mode 100644 client/images/controls/telegram.svg create mode 100644 client/ui/controllers/settingsController.cpp create mode 100644 client/ui/controllers/settingsController.h create mode 100644 client/ui/qml/Pages2/PageSettingsAbout.qml create mode 100644 client/ui/qml/Pages2/PageSettingsApplication.qml create mode 100644 client/ui/qml/Pages2/PageSettingsBackup.qml create mode 100644 client/ui/qml/Pages2/PageSettingsConnection.qml create mode 100644 client/ui/qml/Pages2/PageSettingsDns.qml diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 598645d8..b9aa0f74 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -98,6 +98,10 @@ void AmneziaApplication::init() new ExportController(m_serversModel, m_containersModel, m_settings, m_configurator)); m_engine->rootContext()->setContextProperty("ExportController", m_exportController.get()); + m_settingsController.reset( + new SettingsController(m_serversModel, m_containersModel, m_settings)); + m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get()); + // m_engine->load(url); diff --git a/client/amnezia_application.h b/client/amnezia_application.h index 510f580f..893511b6 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -18,6 +18,7 @@ #include "ui/controllers/importController.h" #include "ui/controllers/installController.h" #include "ui/controllers/pageController.h" +#include "ui/controllers/settingsController.h" #include "ui/models/containers_model.h" #include "ui/models/servers_model.h" @@ -73,6 +74,7 @@ private: QScopedPointer m_installController; QScopedPointer m_importController; QScopedPointer m_exportController; + QScopedPointer m_settingsController; }; #endif // AMNEZIA_APPLICATION_H diff --git a/client/images/controls/delete.svg b/client/images/controls/delete.svg new file mode 100644 index 00000000..78ef3eea --- /dev/null +++ b/client/images/controls/delete.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/images/controls/github.svg b/client/images/controls/github.svg new file mode 100644 index 00000000..7b1f250a --- /dev/null +++ b/client/images/controls/github.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/images/controls/mail.svg b/client/images/controls/mail.svg new file mode 100644 index 00000000..1debe4f1 --- /dev/null +++ b/client/images/controls/mail.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/images/controls/telegram.svg b/client/images/controls/telegram.svg new file mode 100644 index 00000000..7b76e506 --- /dev/null +++ b/client/images/controls/telegram.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/resources.qrc b/client/resources.qrc index d60acfdd..a8f8718e 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -259,5 +259,14 @@ images/controls/radio-button-pressed.svg images/controls/radio-button-inner-circle-pressed.png ui/qml/Components/ShareConnectionDrawer.qml + ui/qml/Pages2/PageSettingsConnection.qml + ui/qml/Pages2/PageSettingsDns.qml + ui/qml/Pages2/PageSettingsApplication.qml + ui/qml/Pages2/PageSettingsBackup.qml + images/controls/delete.svg + ui/qml/Pages2/PageSettingsAbout.qml + images/controls/github.svg + images/controls/mail.svg + images/controls/telegram.svg diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 9ff73f33..98cbebd1 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -11,6 +11,8 @@ #include "qrcodegen.hpp" +#include "core/errorstrings.h" + ExportController::ExportController(const QSharedPointer &serversModel, const QSharedPointer &containersModel, const std::shared_ptr &settings, @@ -35,6 +37,7 @@ void ExportController::generateFullAccessConfig() | QByteArray::OmitTrailingEquals))); m_qrCodes = generateQrCodeImageSeries(compressedConfig); + emit exportConfigChanged(); } void ExportController::generateConnectionConfig() @@ -49,25 +52,25 @@ void ExportController::generateConnectionConfig() m_containersModel->data(containerModelIndex, ContainersModel::Roles::ConfigRole)); containerConfig.insert(config_key::container, ContainerProps::containerToString(container)); - ErrorCode e = ErrorCode::NoError; - for (Proto p : ContainerProps::protocolsForContainer(container)) { - QJsonObject protoConfig = m_settings->protocolConfig(serverIndex, container, p); + ErrorCode errorCode = ErrorCode::NoError; + for (Proto protocol : ContainerProps::protocolsForContainer(container)) { + QJsonObject protocolConfig = m_settings->protocolConfig(serverIndex, container, protocol); - QString cfg = m_configurator->genVpnProtocolConfig(credentials, - container, - containerConfig, - p, - &e); - if (e) { - cfg = "Error generating config"; - break; + QString vpnConfig = m_configurator->genVpnProtocolConfig(credentials, + container, + containerConfig, + protocol, + &errorCode); + if (errorCode) { + emit exportErrorOccurred(errorString(errorCode)); + return; } - protoConfig.insert(config_key::last_config, cfg); - containerConfig.insert(ProtocolProps::protoToString(p), protoConfig); + protocolConfig.insert(config_key::last_config, vpnConfig); + containerConfig.insert(ProtocolProps::protoToString(protocol), protocolConfig); } QJsonObject config = m_settings->server(serverIndex); - if (!e) { + if (!errorCode) { config.remove(config_key::userName); config.remove(config_key::password); config.remove(config_key::port); @@ -77,11 +80,8 @@ void ExportController::generateConnectionConfig() auto dns = m_configurator->getDnsForConfig(serverIndex); config.insert(config_key::dns1, dns.first); config.insert(config_key::dns2, dns.second); + } - } /*else { - set_textEditShareAmneziaCodeText(tr("Error while generating connection profile")); - return; - }*/ QByteArray compressedConfig = QJsonDocument(config).toJson(); compressedConfig = qCompress(compressedConfig, 8); m_amneziaCode = QString("vpn://%1") @@ -89,6 +89,7 @@ void ExportController::generateConnectionConfig() | QByteArray::OmitTrailingEquals))); m_qrCodes = generateQrCodeImageSeries(compressedConfig); + emit exportConfigChanged(); } QString ExportController::getAmneziaCode() @@ -154,3 +155,8 @@ QString ExportController::svgToBase64(const QString &image) { return "data:image/svg;base64," + QString::fromLatin1(image.toUtf8().toBase64().data()); } + +int ExportController::getQrCodesCount() +{ + return m_qrCodes.size(); +} diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h index c5054143..63997efd 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -17,9 +17,14 @@ public: const std::shared_ptr &configurator, QObject *parent = nullptr); + Q_PROPERTY(QList qrCodes READ getQrCodes NOTIFY exportConfigChanged) + Q_PROPERTY(int qrCodesCount READ getQrCodesCount NOTIFY exportConfigChanged) + Q_PROPERTY(QString amneziaCode READ getAmneziaCode NOTIFY exportConfigChanged) + public slots: void generateFullAccessConfig(); void generateConnectionConfig(); + QString getAmneziaCode(); QList getQrCodes(); @@ -27,11 +32,16 @@ public slots: signals: void generateConfig(bool isFullAccess); + void exportErrorOccurred(QString errorMessage); + + void exportConfigChanged(); private: QList generateQrCodeImageSeries(const QByteArray &data); QString svgToBase64(const QString &image); + int getQrCodesCount(); + QSharedPointer m_serversModel; QSharedPointer m_containersModel; std::shared_ptr m_settings; diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index dcf2fc66..587e0e38 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -22,6 +22,11 @@ namespace PageLoader PageSettingsServerProtocols, PageSettingsServerServices, PageSettingsServerProtocol, + PageSettingsConnection, + PageSettingsDns, + PageSettingsApplication, + PageSettingsBackup, + PageSettingsAbout, PageSetupWizardStart, PageSetupWizardCredentials, diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp new file mode 100644 index 00000000..b86b1cff --- /dev/null +++ b/client/ui/controllers/settingsController.cpp @@ -0,0 +1,113 @@ +#include "settingsController.h" + +#include + +#include "defines.h" +#include "logger.h" +#include "utilities.h" + +SettingsController::SettingsController(const QSharedPointer &serversModel, + const QSharedPointer &containersModel, + const std::shared_ptr &settings, + QObject *parent) + : QObject(parent) + , m_serversModel(serversModel) + , m_containersModel(containersModel) + , m_settings(settings) +{ + m_appVersion = QString("%1: %2 (%3)") + .arg(tr("Software version"), QString(APP_MAJOR_VERSION), __DATE__); +} + +void SettingsController::setAmneziaDns(bool enable) +{ + m_settings->setUseAmneziaDns(enable); +} + +bool SettingsController::isAmneziaDnsEnabled() +{ + return m_settings->useAmneziaDns(); +} + +QString SettingsController::getPrimaryDns() +{ + return m_settings->primaryDns(); +} + +void SettingsController::setPrimaryDns(const QString &dns) +{ + m_settings->setPrimaryDns(dns); + emit primaryDnsChanged(); +} + +QString SettingsController::getSecondaryDns() +{ + return m_settings->secondaryDns(); +} + +void SettingsController::setSecondaryDns(const QString &dns) +{ + return m_settings->setSecondaryDns(dns); + emit secondaryDnsChanged(); +} + +bool SettingsController::isSaveLogsEnabled() +{ + return m_settings->isSaveLogs(); +} + +void SettingsController::setSaveLogs(bool enable) +{ + m_settings->setSaveLogs(enable); +} + +void SettingsController::openLogsFolder() +{ + Logger::openLogsFolder(); +} + +void SettingsController::exportLogsFile() +{ + Utils::saveFile(".log", tr("Save log"), "AmneziaVPN", Logger::getLogFile()); +} + +void SettingsController::clearLogs() +{ + Logger::clearLogs(); + Logger::clearServiceLogs(); +} + +void SettingsController::backupAppConfig() +{ + Utils::saveFile(".backup", + tr("Backup application config"), + "AmneziaVPN", + m_settings->backupAppConfig()); +} + +void SettingsController::restoreAppConfig() +{ + QString fileName = Utils::getFileName(Q_NULLPTR, + tr("Open backup"), + QStandardPaths::writableLocation( + QStandardPaths::DocumentsLocation), + "*.backup"); + + //todo error processing + if (fileName.isEmpty()) + return; + + QFile file(fileName); + file.open(QIODevice::ReadOnly); + QByteArray data = file.readAll(); + + bool ok = m_settings->restoreAppConfig(data); + if (ok) { + // emit uiLogic()->showWarningMessage(tr("Can't import config, file is corrupted.")); + } +} + +QString SettingsController::getAppVersion() +{ + return m_appVersion; +} diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h new file mode 100644 index 00000000..77de424c --- /dev/null +++ b/client/ui/controllers/settingsController.h @@ -0,0 +1,56 @@ +#ifndef SETTINGSCONTROLLER_H +#define SETTINGSCONTROLLER_H + +#include + +#include "ui/models/containers_model.h" +#include "ui/models/servers_model.h" + +class SettingsController : public QObject +{ + Q_OBJECT +public: + explicit SettingsController(const QSharedPointer &serversModel, + const QSharedPointer &containersModel, + const std::shared_ptr &settings, + QObject *parent = nullptr); + + Q_PROPERTY(QString primaryDns READ getPrimaryDns WRITE setPrimaryDns NOTIFY primaryDnsChanged) + Q_PROPERTY( + QString secondaryDns READ getSecondaryDns WRITE setSecondaryDns NOTIFY secondaryDnsChanged) + +public slots: + void setAmneziaDns(bool enable); + bool isAmneziaDnsEnabled(); + + QString getPrimaryDns(); + void setPrimaryDns(const QString &dns); + + QString getSecondaryDns(); + void setSecondaryDns(const QString &dns); + + bool isSaveLogsEnabled(); + void setSaveLogs(bool enable); + + void openLogsFolder(); + void exportLogsFile(); + void clearLogs(); + + void backupAppConfig(); + void restoreAppConfig(); + + QString getAppVersion(); + +signals: + void primaryDnsChanged(); + void secondaryDnsChanged(); + +private: + QSharedPointer m_serversModel; + QSharedPointer m_containersModel; + std::shared_ptr m_settings; + + QString m_appVersion; +}; + +#endif // SETTINGSCONTROLLER_H diff --git a/client/ui/qml/Components/Protocols/OpenVpnSettings.qml b/client/ui/qml/Components/Protocols/OpenVpnSettings.qml index 5c574832..f937dcfc 100644 --- a/client/ui/qml/Components/Protocols/OpenVpnSettings.qml +++ b/client/ui/qml/Components/Protocols/OpenVpnSettings.qml @@ -59,8 +59,6 @@ Item { Layout.fillWidth: true implicitHeight: 74 - rootButtonBorderWidth: 0 - descriptionText: qsTr("Hash") headerText: qsTr("Hash") @@ -97,8 +95,6 @@ Item { Layout.fillWidth: true implicitHeight: 74 - rootButtonBorderWidth: 0 - descriptionText: qsTr("Cipher") headerText: qsTr("Cipher") diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml index 5c9ceee8..1b92c752 100644 --- a/client/ui/qml/Components/ShareConnectionDrawer.qml +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -15,8 +15,6 @@ import "../Controls2/TextTypes" DrawerType { id: root - property var qrCodes: [] - property alias configText: configContent.text property alias headerText: header.headerText width: parent.width @@ -120,6 +118,8 @@ DrawerType { font.weight: Font.Medium font.family: "PT Root UI VF" + text: ExportController.amneziaCode + wrapMode: Text.Wrap enabled: false @@ -135,6 +135,8 @@ DrawerType { Layout.preferredHeight: width Layout.topMargin: 20 + visible: ExportController.qrCodesCount > 0 + color: "white" Image { @@ -144,22 +146,20 @@ DrawerType { Timer { property int idx: 0 interval: 1000 - running: qrCodes.length > 0 + running: ExportController.qrCodesCount > 0 repeat: true onTriggered: { idx++ - if (idx >= qrCodes.length) { + if (idx >= ExportController.qrCodesCount) { idx = 0 } - parent.source = qrCodes[idx] + parent.source = ExportController.qrCodes[idx] } } Behavior on source { PropertyAnimation { duration: 200 } } - - visible: qrCodes.length > 0 } } diff --git a/client/ui/qml/Controls2/DropDownType.qml b/client/ui/qml/Controls2/DropDownType.qml index 6456bbad..38cd0afa 100644 --- a/client/ui/qml/Controls2/DropDownType.qml +++ b/client/ui/qml/Controls2/DropDownType.qml @@ -15,13 +15,15 @@ Item { property string headerText property string headerBackButtonImage - property var onRootButtonClicked + property var rootButtonClickedFunction property string rootButtonImage: "qrc:/images/controls/chevron-down.svg" - property string rootButtonImageColor: "#494B50" - property string rootButtonDefaultColor: "#1C1D21" + property string rootButtonImageColor: "#D7D8DB" + property string rootButtonBackgroundColor: "#1C1D21" property int rootButtonMaximumWidth: 0 - property string rootButtonBorderColor: "#494B50" + property string rootButtonHoveredBorderColor: "#494B50" + property string rootButtonDefaultBorderColor: "transparent" + property string rootButtonPressedBorderColor: "#D7D8DB" property int rootButtonBorderWidth: 1 property real drawerHeight: 0.9 @@ -32,18 +34,31 @@ Item { implicitWidth: rootButtonContent.implicitWidth implicitHeight: rootButtonContent.implicitHeight + onMenuVisibleChanged: { + if (menuVisible) { + rootButtonBackground.border.color = rootButtonPressedBorderColor + rootButtonBackground.border.width = rootButtonBorderWidth + } else { + rootButtonBackground.border.color = rootButtonDefaultBorderColor + rootButtonBackground.border.width = 0 + } + } + Rectangle { id: rootButtonBackground anchors.fill: rootButtonContent radius: 16 - color: rootButtonDefaultColor - border.color: rootButtonBorderColor - border.width: rootButtonBorderWidth + color: rootButtonBackgroundColor + border.color: rootButtonDefaultBorderColor + border.width: 0 Behavior on border.width { PropertyAnimation { duration: 200 } } + Behavior on border.color { + PropertyAnimation { duration: 200 } + } } RowLayout { @@ -83,7 +98,6 @@ Item { } } - //todo change to image type ImageButtonType { Layout.leftMargin: 4 Layout.rightMargin: 16 @@ -100,16 +114,22 @@ Item { hoverEnabled: true onEntered: { - rootButtonBackground.border.width = rootButtonBorderWidth + if (menu.visible === false) { + rootButtonBackground.border.width = rootButtonBorderWidth + rootButtonBackground.border.color = rootButtonHoveredBorderColor + } } onExited: { - rootButtonBackground.border.width = 0 + if (menu.visible === false) { + rootButtonBackground.border.width = 0 + rootButtonBackground.border.color = rootButtonDefaultBorderColor + } } onClicked: { - if (onRootButtonClicked && typeof onRootButtonClicked === "function") { - onRootButtonClicked() + if (rootButtonClickedFunction && typeof rootButtonClickedFunction === "function") { + rootButtonClickedFunction() } else { menu.visible = true } diff --git a/client/ui/qml/Controls2/ImageButtonType.qml b/client/ui/qml/Controls2/ImageButtonType.qml index 72c78342..843599a4 100644 --- a/client/ui/qml/Controls2/ImageButtonType.qml +++ b/client/ui/qml/Controls2/ImageButtonType.qml @@ -10,8 +10,10 @@ Button { property string hoveredColor: Qt.rgba(1, 1, 1, 0.08) property string defaultColor: "transparent" property string pressedColor: Qt.rgba(1, 1, 1, 0.12) + property string disableColor: "#2C2D30" property string imageColor: "#878B91" + property string disableImageColor: "#2C2D30" implicitWidth: 40 implicitHeight: 40 @@ -19,7 +21,11 @@ Button { hoverEnabled: true icon.source: image - icon.color: imageColor + icon.color: root.enabled ? imageColor : disableImageColor + + Behavior on icon.color { + PropertyAnimation { duration: 200 } + } background: Rectangle { id: background @@ -33,6 +39,7 @@ Button { } return hovered ? hoveredColor : defaultColor } + return defaultColor } Behavior on color { PropertyAnimation { duration: 200 } diff --git a/client/ui/qml/Controls2/SwitcherType.qml b/client/ui/qml/Controls2/SwitcherType.qml index b593ece8..b63c64fb 100644 --- a/client/ui/qml/Controls2/SwitcherType.qml +++ b/client/ui/qml/Controls2/SwitcherType.qml @@ -2,9 +2,13 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import "TextTypes" + Switch { id: root + property alias descriptionText: description.text + property string checkedIndicatorColor: "#412102" property string defaultIndicatorColor: "transparent" property string checkedIndicatorBorderColor: "#412102" @@ -16,10 +20,18 @@ Switch { property string hoveredIndicatorBackgroundColor: Qt.rgba(1, 1, 1, 0.08) property string defaultIndicatorBackgroundColor: "transparent" + implicitWidth: content.implicitWidth + switcher.implicitWidth + implicitHeight: content.implicitHeight + indicator: Rectangle { + id: switcher + + anchors.left: content.right + anchors.verticalCenter: parent.verticalCenter + implicitWidth: 52 implicitHeight: 32 - x: content.width - width + radius: 16 color: root.checked ? checkedIndicatorColor : defaultIndicatorColor border.color: root.checked ? checkedIndicatorBorderColor : defaultIndicatorBorderColor @@ -62,16 +74,23 @@ Switch { contentItem: ColumnLayout { id: content - Text { - text: root.text - color: "#D7D8DB" - font.pixelSize: 18 - font.weight: 400 - font.family: "PT Root UI VF" + anchors.fill: parent + anchors.rightMargin: switcher.implicitWidth - height: 22 + ListItemTitleType { Layout.fillWidth: true - Layout.bottomMargin: 16 + + text: root.text + } + + CaptionTextType { + id: description + + Layout.fillWidth: true + + color: "#878B91" + + visible: text !== "" } } diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 780f3aef..9571b1fb 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -145,14 +145,14 @@ PageType { rootButtonBorderWidth: 0 rootButtonImageColor: "#0E0E11" rootButtonMaximumWidth: 150 //todo make it dynamic - rootButtonDefaultColor: "#D7D8DB" + rootButtonBackgroundColor: "#D7D8DB" text: root.currentContainerName textColor: "#0E0E11" headerText: qsTr("Протокол подключения") headerBackButtonImage: "qrc:/images/controls/arrow-left.svg" - onRootButtonClicked: function() { + rootButtonClickedFunction: function() { ServersModel.setCurrentlyProcessedServerIndex(serversMenuContent.currentIndex) ContainersModel.setCurrentlyProcessedServerIndex(serversMenuContent.currentIndex) containersDropDown.menuVisible = true diff --git a/client/ui/qml/Pages2/PageSettings.qml b/client/ui/qml/Pages2/PageSettings.qml index 300421b7..a7472580 100644 --- a/client/ui/qml/Pages2/PageSettings.qml +++ b/client/ui/qml/Pages2/PageSettings.qml @@ -58,6 +58,7 @@ PageType { iconImage: "qrc:/images/controls/radio.svg" clickedFunction: function() { + goToPage(PageEnum.PageSettingsConnection) } } @@ -71,6 +72,7 @@ PageType { iconImage: "qrc:/images/controls/app.svg" clickedFunction: function() { + goToPage(PageEnum.PageSettingsApplication) } } @@ -84,6 +86,7 @@ PageType { iconImage: "qrc:/images/controls/save.svg" clickedFunction: function() { + goToPage(PageEnum.PageSettingsBackup) } } @@ -97,6 +100,7 @@ PageType { iconImage: "qrc:/images/controls/amnezia.svg" clickedFunction: function() { + goToPage(PageEnum.PageSettingsAbout) } } diff --git a/client/ui/qml/Pages2/PageSettingsAbout.qml b/client/ui/qml/Pages2/PageSettingsAbout.qml new file mode 100644 index 00000000..88c927b9 --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsAbout.qml @@ -0,0 +1,207 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 + +import "./" +import "../Controls2" +import "../Config" +import "../Controls2/TextTypes" +import "../Components" + +PageType { + id: root + + BackButtonType { + id: backButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 16 + anchors.leftMargin: 16 + anchors.topMargin: 20 + } + + FlickableType { + id: fl + anchors.top: backButton.bottom + anchors.bottom: root.bottom + contentHeight: content.height + + ColumnLayout { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + Image { + id: image + source: "qrc:/images/amneziaBigLogo.png" + + Layout.alignment: Qt.AlignCenter + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.preferredWidth: 344 + Layout.preferredHeight: 279 + } + + Header2TextType { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + text: qsTr("Support the project with a donation") + horizontalAlignment: Text.AlignHCenter + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + horizontalAlignment: Text.AlignHCenter + + height: 20 + font.pixelSize: 14 + + text: qsTr("This is a free and open source application. If you like it, support the developers with a donation. +And if you don't like the app, all the more support it - the donation will be used to improve the app.") + color: "#CCCAC8" + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + text: qsTr("Card on Patreon") + + onClicked: { + } + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + defaultColor: "transparent" + hoveredColor: Qt.rgba(1, 1, 1, 0.08) + pressedColor: Qt.rgba(1, 1, 1, 0.12) + disabledColor: "#878B91" + textColor: "#D7D8DB" + borderWidth: 1 + + text: qsTr("Show other methods on Github") + + onClicked: { + } + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 32 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + text: qsTr("Contacts") + } + + LabelWithButtonType { + Layout.fillWidth: true + Layout.topMargin: 16 + + text: qsTr("Telegram group") + descriptionText: qsTr("To discuss features") + buttonImage: "qrc:/images/controls/chevron-right.svg" + iconImage: "qrc:/images/controls/telegram.svg" + + clickedFunction: function() { + goToPage(PageEnum.PageSettingsAbout) + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Mail") + descriptionText: qsTr("For reviews and bug reports") + buttonImage: "qrc:/images/controls/chevron-right.svg" + iconImage: "qrc:/images/controls/mail.svg" + + clickedFunction: function() { + goToPage(PageEnum.PageSettingsAbout) + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Github") + buttonImage: "qrc:/images/controls/chevron-right.svg" + iconImage: "qrc:/images/controls/github.svg" + + clickedFunction: function() { + goToPage(PageEnum.PageSettingsAbout) + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Website") + buttonImage: "qrc:/images/controls/chevron-right.svg" + iconImage: "qrc:/images/controls/amnezia.svg" + + clickedFunction: function() { + goToPage(PageEnum.PageSettingsAbout) + } + } + + DividerType {} + + CaptionTextType { + Layout.fillWidth: true + Layout.topMargin: 40 + + horizontalAlignment: Text.AlignHCenter + + text: SettingsController.getAppVersion() + color: "#878B91" + } + + BasicButtonType { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 8 + Layout.bottomMargin: 16 + implicitHeight: 32 + + defaultColor: "transparent" + hoveredColor: Qt.rgba(1, 1, 1, 0.08) + pressedColor: Qt.rgba(1, 1, 1, 0.12) + disabledColor: "#878B91" + textColor: "#FBB26A" + + text: qsTr("Check for updates") + + onClicked: { + Qt.openUrlExternally("https://github.com/amnezia-vpn/desktop-client/releases/latest") + } + } + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsApplication.qml b/client/ui/qml/Pages2/PageSettingsApplication.qml new file mode 100644 index 00000000..9a6eab3c --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsApplication.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 + +import "./" +import "../Controls2" +import "../Config" +import "../Controls2/TextTypes" + +PageType { + id: root + + BackButtonType { + id: backButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 16 + anchors.leftMargin: 16 + anchors.topMargin: 20 + } + + FlickableType { + id: fl + anchors.top: backButton.bottom + anchors.bottom: root.bottom + contentHeight: content.height + + ColumnLayout { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + spacing: 16 + + HeaderType { + Layout.fillWidth: true + + headerText: qsTr("Application") + } + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Language") + buttonImage: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Reset settings and remove all data from the application") + buttonImage: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + } + } + + DividerType {} + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsBackup.qml b/client/ui/qml/Pages2/PageSettingsBackup.qml new file mode 100644 index 00000000..0cc62979 --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsBackup.qml @@ -0,0 +1,184 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 + +import "./" +import "../Controls2" +import "../Config" +import "../Controls2/TextTypes" + +PageType { + id: root + + BackButtonType { + id: backButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 16 + anchors.leftMargin: 16 + anchors.topMargin: 20 + } + + FlickableType { + id: fl + anchors.top: backButton.bottom + anchors.bottom: root.bottom + contentHeight: content.height + + ColumnLayout { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + spacing: 16 + + HeaderType { + Layout.fillWidth: true + + headerText: qsTr("Backup") + } + + SwitcherType { + Layout.fillWidth: true + Layout.topMargin: 16 + + text: qsTr("Save logs") + + checked: SettingsController.isSaveLogsEnabled() + onCheckedChanged: { + if (checked !== SettingsController.isSaveLogsEnabled()) { + SettingsController.setSaveLogs(checked) + } + } + } + + RowLayout { + Layout.fillWidth: true + + ColumnLayout { + Layout.alignment: Qt.AlignBaseline + Layout.preferredWidth: root.width / 3 + + ImageButtonType { + Layout.alignment: Qt.AlignHCenter + + implicitWidth: 56 + implicitHeight: 56 + + image: "qrc:/images/controls/folder-open.svg" + + onClicked: SettingsController.openLogsFolder() + } + + CaptionTextType { + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + + text: qsTr("Open folder with logs") + color: "#D7D8DB" + } + } + + ColumnLayout { + Layout.alignment: Qt.AlignBaseline + Layout.preferredWidth: root.width / 3 + + ImageButtonType { + Layout.alignment: Qt.AlignHCenter + + implicitWidth: 56 + implicitHeight: 56 + + image: "qrc:/images/controls/save.svg" + + onClicked: SettingsController.exportLogsFile() + } + + CaptionTextType { + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + + text: qsTr("Save logs to file") + color: "#D7D8DB" + } + } + + ColumnLayout { + Layout.alignment: Qt.AlignBaseline + Layout.preferredWidth: root.width / 3 + + ImageButtonType { + Layout.alignment: Qt.AlignHCenter + + implicitWidth: 56 + implicitHeight: 56 + + image: "qrc:/images/controls/delete.svg" + + onClicked: SettingsController.clearLogs() + } + + CaptionTextType { + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + + text: qsTr("Clear logs") + color: "#D7D8DB" + } + } + } + + ListItemTitleType { + Layout.fillWidth: true + Layout.topMargin: 10 + + text: qsTr("Configuration backup") + } + + CaptionTextType { + Layout.fillWidth: true + Layout.topMargin: -12 + + text: qsTr("It will help you instantly restore connection settings at the next installation") + color: "#878B91" + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 14 + + text: qsTr("Make a backup") + + onClicked: { + SettingsController.backupAppConfig() + } + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: -8 + + defaultColor: "transparent" + hoveredColor: Qt.rgba(1, 1, 1, 0.08) + pressedColor: Qt.rgba(1, 1, 1, 0.12) + disabledColor: "#878B91" + textColor: "#D7D8DB" + borderWidth: 1 + + text: qsTr("Restore from backup") + + onClicked: { + SettingsController.restoreAppConfig() + } + } + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsConnection.qml b/client/ui/qml/Pages2/PageSettingsConnection.qml new file mode 100644 index 00000000..fb0bb000 --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsConnection.qml @@ -0,0 +1,107 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 + +import "./" +import "../Controls2" +import "../Config" + +PageType { + id: root + + BackButtonType { + id: backButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 16 + anchors.leftMargin: 16 + anchors.topMargin: 20 + } + + FlickableType { + id: fl + anchors.top: backButton.bottom + anchors.bottom: root.bottom + contentHeight: content.height + + ColumnLayout { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + spacing: 16 + + HeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + headerText: qsTr("Connection") + } + + SwitcherType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + text: qsTr("Use AmnesiaDNS if installed on the server") + descriptionText: qsTr("Internal IP address 172.29.172.254") + + checked: SettingsController.isAmneziaDnsEnabled() + onCheckedChanged: { + if (checked !== SettingsController.isAmneziaDnsEnabled()) { + SettingsController.setAmneziaDns(checked) + } + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("DNS servers") + descriptionText: qsTr("If AmneziaDNS is not used or installed") + buttonImage: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + goToPage(PageEnum.PageSettingsDns) + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Split site tunneling") + descriptionText: qsTr("Allows you to connect to some sites through a secure connection, and to others bypassing it") + buttonImage: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Separate application tunneling") + descriptionText: qsTr("Allows you to use the VPN only for certain applications") + buttonImage: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + } + } + + DividerType {} + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsDns.qml b/client/ui/qml/Pages2/PageSettingsDns.qml new file mode 100644 index 00000000..2a06438b --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsDns.qml @@ -0,0 +1,87 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 + +import "./" +import "../Controls2" +import "../Config" +import "../Controls2/TextTypes" + +PageType { + id: root + + BackButtonType { + id: backButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 16 + anchors.leftMargin: 16 + anchors.topMargin: 20 + } + + FlickableType { + id: fl + anchors.top: backButton.bottom + anchors.bottom: root.bottom + contentHeight: content.height + + ColumnLayout { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + spacing: 16 + + HeaderType { + Layout.fillWidth: true + + headerText: qsTr("DNS servers") + } + + ParagraphTextType { + text: qsTr("If AmneziaDNS is not used or installed") + } + + TextFieldWithHeaderType { + id: primaryDns + + Layout.fillWidth: true + headerText: "Primary DNS" + + textFieldText: SettingsController.primaryDns + } + + TextFieldWithHeaderType { + id: secondaryDns + + Layout.fillWidth: true + headerText: "Secondary DNS" + + textFieldText: SettingsController.secondaryDns + } + + BasicButtonType { + Layout.fillWidth: true + + text: qsTr("Save") + + onClicked: function() { + if (primaryDns.textFieldText !== SettingsController.primaryDns) { + SettingsController.primaryDns = primaryDns.textFieldText + } + if (secondaryDns.textFieldText !== SettingsController.secondaryDns) { + SettingsController.secondaryDns = secondaryDns.textFieldText + } + } + } + } + } +} diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index a731e425..64641094 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -25,9 +25,6 @@ PageType { } else { ExportController.generateConnectionConfig() } - - shareConnectionDrawer.configText = ExportController.getAmneziaCode() - shareConnectionDrawer.qrCodes = ExportController.getQrCodes() } } @@ -125,7 +122,8 @@ PageType { ParagraphTextType { Layout.fillWidth: true - text: qsTr("VPN access without the ability to manage the server") + text: accessTypeSelector.currentIndex === 0 ? qsTr("VPN access without the ability to manage the server") : + qsTr("Full access to server") color: "#878B91" } @@ -137,7 +135,6 @@ PageType { implicitHeight: 74 - rootButtonBorderWidth: 0 drawerHeight: 0.4375 descriptionText: qsTr("Server and service") @@ -234,6 +231,7 @@ PageType { clickedFunction: function () { serverSelector.text += ", " + selectedText shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text + ContainersModel.setCurrentlyProcessedContainerIndex(proxyContainersModel.mapToSource(currentIndex)) protocolSelector.visible = false serverSelector.menuVisible = false @@ -244,6 +242,7 @@ PageType { Component.onCompleted: { serverSelector.text += ", " + selectedText shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text + ContainersModel.setCurrentlyProcessedContainerIndex(proxyContainersModel.mapToSource(currentIndex)) fillConnectionTypeModel() } @@ -273,7 +272,6 @@ PageType { implicitHeight: 74 - rootButtonBorderWidth: 0 drawerHeight: 0.4375 visible: accessTypeSelector.currentIndex === 0 diff --git a/client/utilities.cpp b/client/utilities.cpp index 13e36c2f..bf06ddac 100644 --- a/client/utilities.cpp +++ b/client/utilities.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -7,6 +8,7 @@ #include #include #include +#include #include "defines.h" #include "utilities.h" @@ -247,6 +249,58 @@ QString Utils::certUtilPath() #endif } +void Utils::saveFile(const QString &fileExtension, + const QString &caption, + const QString &fileName, + const QString &data) +{ + QString docDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + QUrl fileUrl = QFileDialog::getSaveFileUrl(nullptr, + caption, + QUrl::fromLocalFile(docDir + "/" + fileName), + "*" + fileExtension); + if (fileUrl.isEmpty()) + return; + if (!fileUrl.toString().endsWith(fileExtension)) { + fileUrl = QUrl(fileUrl.toString() + fileExtension); + } + if (fileUrl.isEmpty()) + return; + + QFile save(fileUrl.toLocalFile()); + + //todo check if save successful + save.open(QIODevice::WriteOnly); + save.write(data.toUtf8()); + save.close(); + + QFileInfo fi(fileUrl.toLocalFile()); + QDesktopServices::openUrl(fi.absoluteDir().absolutePath()); +} + +QString Utils::getFileName(QWidget *parent, + const QString &caption, + const QString &dir, + const QString &filter, + QString *selectedFilter, + QFileDialog::Options options) +{ + QString fileName + = QFileDialog::getOpenFileName(parent, caption, dir, filter, selectedFilter, options); + +#ifdef Q_OS_ANDROID + // patch for files containing spaces etc + const QString sep{"raw%3A%2F"}; + if (fileName.startsWith("content://") && fileName.contains(sep)) { + QString contentUrl = fileName.split(sep).at(0); + QString rawUrl = fileName.split(sep).at(1); + rawUrl.replace(" ", "%20"); + fileName = contentUrl + sep + rawUrl; + } +#endif + return fileName; +} + #ifdef Q_OS_WIN // Inspired from http://stackoverflow.com/a/15281070/1529139 // and http://stackoverflow.com/q/40059902/1529139 diff --git a/client/utilities.h b/client/utilities.h index aeb06865..191fa5c1 100644 --- a/client/utilities.h +++ b/client/utilities.h @@ -1,9 +1,10 @@ #ifndef UTILITIES_H #define UTILITIES_H +#include #include -#include #include +#include #ifdef Q_OS_WIN #include "Windows.h" @@ -50,6 +51,18 @@ public: static QString wireguardExecPath(); static QString certUtilPath(); + static void saveFile(const QString &fileExtension, + const QString &caption, + const QString &fileName, + const QString &data); + + static QString getFileName(QWidget *parent = nullptr, + const QString &caption = QString(), + const QString &dir = QString(), + const QString &filter = QString(), + QString *selectedFilter = nullptr, + QFileDialog::Options options = QFileDialog::Options()); + #ifdef Q_OS_WIN static bool signalCtrl(DWORD dwProcessId, DWORD dwCtrlEvent); #endif diff --git a/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index 687b382a..2b3ff800 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -6,7 +6,7 @@ project(${PROJECT}) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 REQUIRED COMPONENTS Core Network RemoteObjects Core5Compat) +find_package(Qt6 REQUIRED COMPONENTS Core Network RemoteObjects Core5Compat Widgets) qt_standard_project_setup() set(HEADERS @@ -89,14 +89,14 @@ include_directories( ) add_executable(${PROJECT} ${SOURCES} ${HEADERS}) -target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat ${LIBS}) +target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat Qt6::Widgets ${LIBS}) qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_interface.rep) if(NOT IOS) qt_add_repc_sources(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc_process_interface.rep) endif() -# deploy artifacts required to run the application to the debug build folder +# copy deploy artifacts required to run the application to the debug build folder if(WIN32) if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8") set(DEPLOY_ARTIFACT_PATH "windows/x64")