diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index dbeaac38..598645d8 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -46,7 +46,6 @@ #endif m_settings = std::shared_ptr(new Settings); - m_configurator = std::shared_ptr(new VpnConfigurator(m_settings, this)); } AmneziaApplication::~AmneziaApplication() @@ -80,10 +79,10 @@ void AmneziaApplication::init() m_serversModel.reset(new ServersModel(m_settings, this)); m_engine->rootContext()->setContextProperty("ServersModel", m_serversModel.get()); + m_configurator = std::shared_ptr(new VpnConfigurator(m_settings, this)); m_vpnConnection.reset(new VpnConnection(m_settings, m_configurator)); m_connectionController.reset(new ConnectionController(m_serversModel, m_containersModel, m_vpnConnection)); - m_engine->rootContext()->setContextProperty("ConnectionController", m_connectionController.get()); m_pageController.reset(new PageController(m_serversModel)); @@ -95,6 +94,10 @@ void AmneziaApplication::init() m_importController.reset(new ImportController(m_serversModel, m_containersModel, m_settings)); m_engine->rootContext()->setContextProperty("ImportController", m_importController.get()); + m_exportController.reset( + new ExportController(m_serversModel, m_containersModel, m_settings, m_configurator)); + m_engine->rootContext()->setContextProperty("ExportController", m_exportController.get()); + // m_engine->load(url); diff --git a/client/amnezia_application.h b/client/amnezia_application.h index 0fd6842c..510f580f 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -13,12 +13,13 @@ #include "configurators/vpn_configurator.h" -#include "ui/models/servers_model.h" -#include "ui/models/containers_model.h" #include "ui/controllers/connectionController.h" -#include "ui/controllers/pageController.h" -#include "ui/controllers/installController.h" +#include "ui/controllers/exportController.h" #include "ui/controllers/importController.h" +#include "ui/controllers/installController.h" +#include "ui/controllers/pageController.h" +#include "ui/models/containers_model.h" +#include "ui/models/servers_model.h" #define amnApp (static_cast(QCoreApplication::instance())) @@ -71,7 +72,7 @@ private: QScopedPointer m_pageController; QScopedPointer m_installController; QScopedPointer m_importController; - + QScopedPointer m_exportController; }; #endif // AMNEZIA_APPLICATION_H diff --git a/client/images/controls/check_off.png b/client/images/controls/check_off.png deleted file mode 100644 index 0a7fbf70..00000000 Binary files a/client/images/controls/check_off.png and /dev/null differ diff --git a/client/images/controls/check_on.png b/client/images/controls/check_on.png deleted file mode 100644 index 8b3b683b..00000000 Binary files a/client/images/controls/check_on.png and /dev/null differ diff --git a/client/images/controls/radio-button-inner-circle-pressed.png b/client/images/controls/radio-button-inner-circle-pressed.png new file mode 100644 index 00000000..efcd6f1f Binary files /dev/null and b/client/images/controls/radio-button-inner-circle-pressed.png differ diff --git a/client/images/controls/radio-button-inner-circle.png b/client/images/controls/radio-button-inner-circle.png new file mode 100644 index 00000000..da29d54a Binary files /dev/null and b/client/images/controls/radio-button-inner-circle.png differ diff --git a/client/images/controls/radio-button-pressed.svg b/client/images/controls/radio-button-pressed.svg new file mode 100644 index 00000000..cf302db0 --- /dev/null +++ b/client/images/controls/radio-button-pressed.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/images/controls/radio-button.svg b/client/images/controls/radio-button.svg new file mode 100644 index 00000000..75b1b5b4 --- /dev/null +++ b/client/images/controls/radio-button.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/images/controls/radio_off.png b/client/images/controls/radio_off.png deleted file mode 100644 index 685980bd..00000000 Binary files a/client/images/controls/radio_off.png and /dev/null differ diff --git a/client/images/controls/radio_on.png b/client/images/controls/radio_on.png deleted file mode 100644 index 48560e53..00000000 Binary files a/client/images/controls/radio_on.png and /dev/null differ diff --git a/client/resources.qrc b/client/resources.qrc index e2f16853..d60acfdd 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -6,10 +6,6 @@ images/favorites_disabled.png images/favorites_enabled.png images/favorites_hover.png - images/controls/check_off.png - images/controls/check_on.png - images/controls/radio_off.png - images/controls/radio_on.png images/download.png images/upload.png images/tray/active.png @@ -257,5 +253,11 @@ ui/qml/Pages2/PageSettingsServerProtocol.qml ui/qml/Components/Protocols/OpenVpnSettings.qml ui/qml/Components/TransportProtoSelector.qml + ui/qml/Controls2/ListViewType.qml + images/controls/radio-button.svg + images/controls/radio-button-inner-circle.png + images/controls/radio-button-pressed.svg + images/controls/radio-button-inner-circle-pressed.png + ui/qml/Components/ShareConnectionDrawer.qml diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp new file mode 100644 index 00000000..9ff73f33 --- /dev/null +++ b/client/ui/controllers/exportController.cpp @@ -0,0 +1,156 @@ +#include "exportController.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "qrcodegen.hpp" + +ExportController::ExportController(const QSharedPointer &serversModel, + const QSharedPointer &containersModel, + const std::shared_ptr &settings, + const std::shared_ptr &configurator, + QObject *parent) + : QObject(parent) + , m_serversModel(serversModel) + , m_containersModel(containersModel) + , m_settings(settings) + , m_configurator(configurator) +{} + +void ExportController::generateFullAccessConfig() +{ + int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + QJsonObject config = m_settings->server(serverIndex); + + QByteArray compressedConfig = QJsonDocument(config).toJson(); + compressedConfig = qCompress(compressedConfig, 8); + m_amneziaCode = QString("vpn://%1") + .arg(QString(compressedConfig.toBase64(QByteArray::Base64UrlEncoding + | QByteArray::OmitTrailingEquals))); + + m_qrCodes = generateQrCodeImageSeries(compressedConfig); +} + +void ExportController::generateConnectionConfig() +{ + int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + ServerCredentials credentials = m_serversModel->getCurrentlyProcessedServerCredentials(); + + DockerContainer container = static_cast( + m_containersModel->getCurrentlyProcessedContainerIndex()); + QModelIndex containerModelIndex = m_containersModel->index(container); + QJsonObject containerConfig = qvariant_cast( + 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); + + QString cfg = m_configurator->genVpnProtocolConfig(credentials, + container, + containerConfig, + p, + &e); + if (e) { + cfg = "Error generating config"; + break; + } + protoConfig.insert(config_key::last_config, cfg); + containerConfig.insert(ProtocolProps::protoToString(p), protoConfig); + } + + QJsonObject config = m_settings->server(serverIndex); + if (!e) { + config.remove(config_key::userName); + config.remove(config_key::password); + config.remove(config_key::port); + config.insert(config_key::containers, QJsonArray{containerConfig}); + config.insert(config_key::defaultContainer, ContainerProps::containerToString(container)); + + 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") + .arg(QString(compressedConfig.toBase64(QByteArray::Base64UrlEncoding + | QByteArray::OmitTrailingEquals))); + + m_qrCodes = generateQrCodeImageSeries(compressedConfig); +} + +QString ExportController::getAmneziaCode() +{ + return m_amneziaCode; +} + +QList ExportController::getQrCodes() +{ + return m_qrCodes; +} + +void ExportController::saveFile() +{ + QString fileExtension = ".vpn"; + QString docDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + QUrl fileName; + fileName = QFileDialog::getSaveFileUrl(nullptr, + tr("Save AmneziaVPN config"), + QUrl::fromLocalFile(docDir + "/" + "amnezia_config"), + "*" + fileExtension); + if (fileName.isEmpty()) + return; + if (!fileName.toString().endsWith(fileExtension)) { + fileName = QUrl(fileName.toString() + fileExtension); + } + if (fileName.isEmpty()) + return; + + QFile save(fileName.toLocalFile()); + + save.open(QIODevice::WriteOnly); + save.write(m_amneziaCode.toUtf8()); + save.close(); + + QFileInfo fi(fileName.toLocalFile()); + QDesktopServices::openUrl(fi.absoluteDir().absolutePath()); +} + +QList ExportController::generateQrCodeImageSeries(const QByteArray &data) +{ + double k = 850; + + quint8 chunksCount = std::ceil(data.size() / k); + QList chunks; + for (int i = 0; i < data.size(); i = i + k) { + QByteArray chunk; + QDataStream s(&chunk, QIODevice::WriteOnly); + s << amnezia::qrMagicCode << chunksCount << (quint8) std::round(i / k) << data.mid(i, k); + + QByteArray ba = chunk.toBase64(QByteArray::Base64UrlEncoding + | QByteArray::OmitTrailingEquals); + + qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(ba, qrcodegen::QrCode::Ecc::LOW); + QString svg = QString::fromStdString(toSvgString(qr, 0)); + chunks.append(svgToBase64(svg)); + } + + return chunks; +} + +QString ExportController::svgToBase64(const QString &image) +{ + return "data:image/svg;base64," + QString::fromLatin1(image.toUtf8().toBase64().data()); +} diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h new file mode 100644 index 00000000..c5054143 --- /dev/null +++ b/client/ui/controllers/exportController.h @@ -0,0 +1,44 @@ +#ifndef EXPORTCONTROLLER_H +#define EXPORTCONTROLLER_H + +#include + +#include "configurators/vpn_configurator.h" +#include "ui/models/containers_model.h" +#include "ui/models/servers_model.h" + +class ExportController : public QObject +{ + Q_OBJECT +public: + explicit ExportController(const QSharedPointer &serversModel, + const QSharedPointer &containersModel, + const std::shared_ptr &settings, + const std::shared_ptr &configurator, + QObject *parent = nullptr); + +public slots: + void generateFullAccessConfig(); + void generateConnectionConfig(); + QString getAmneziaCode(); + QList getQrCodes(); + + void saveFile(); + +signals: + void generateConfig(bool isFullAccess); + +private: + QList generateQrCodeImageSeries(const QByteArray &data); + QString svgToBase64(const QString &image); + + QSharedPointer m_serversModel; + QSharedPointer m_containersModel; + std::shared_ptr m_settings; + std::shared_ptr m_configurator; + + QString m_amneziaCode; + QList m_qrCodes; +}; + +#endif // EXPORTCONTROLLER_H diff --git a/client/ui/controllers/importController.h b/client/ui/controllers/importController.h index 114bb531..561ea19c 100644 --- a/client/ui/controllers/importController.h +++ b/client/ui/controllers/importController.h @@ -27,6 +27,7 @@ public slots: signals: void importFinished(); void importErrorOccurred(QString errorMessage); + private: QJsonObject extractAmneziaConfig(QString &data); QJsonObject extractOpenVpnConfig(const QString &data); diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index 2fe96962..8f9e1f88 100644 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -9,8 +9,7 @@ InstallController::InstallController(const QSharedPointer &servers const QSharedPointer &containersModel, const std::shared_ptr &settings, QObject *parent) : QObject(parent), m_serversModel(serversModel), m_containersModel(containersModel), m_settings(settings) -{ -} +{} void InstallController::install(DockerContainer container, int port, TransportProto transportProto) { diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index 946c1ce5..dcf2fc66 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -24,7 +24,6 @@ namespace PageLoader PageSettingsServerProtocol, PageSetupWizardStart, - PageTest, PageSetupWizardCredentials, PageSetupWizardProtocols, PageSetupWizardEasy, diff --git a/client/ui/controllers/protocolSettingsController.cpp b/client/ui/controllers/protocolSettingsController.cpp new file mode 100644 index 00000000..48414021 --- /dev/null +++ b/client/ui/controllers/protocolSettingsController.cpp @@ -0,0 +1,19 @@ +#include "protocolSettingsController.h" + +ProtocolSettingsController::ProtocolSettingsController( + const QSharedPointer &serversModel, + const QSharedPointer &containersModel, + const std::shared_ptr &settings, + QObject *parent) + : QObject(parent) + , m_serversModel(serversModel) + , m_containersModel(containersModel) + , m_settings(settings) +{} + +QByteArray ProtocolSettingsController::getOpenVpnConfig() +{ + auto containerIndex = m_containersModel->index( + m_containersModel->getCurrentlyProcessedContainerIndex()); + auto config = m_containersModel->data(containerIndex, ContainersModel::Roles::ConfigRole); +} diff --git a/client/ui/controllers/protocolSettingsController.h b/client/ui/controllers/protocolSettingsController.h new file mode 100644 index 00000000..730cbda7 --- /dev/null +++ b/client/ui/controllers/protocolSettingsController.h @@ -0,0 +1,31 @@ +#ifndef PROTOCOLSETTINGSCONTROLLER_H +#define PROTOCOLSETTINGSCONTROLLER_H + +#include + +#include "containers/containers_defs.h" +#include "core/defs.h" +#include "ui/models/containers_model.h" +#include "ui/models/servers_model.h" + +class ProtocolSettingsController : public QObject +{ + Q_OBJECT +public: + explicit ProtocolSettingsController(const QSharedPointer &serversModel, + const QSharedPointer &containersModel, + const std::shared_ptr &settings, + QObject *parent = nullptr); + +public slots: + QByteArray getOpenVpnConfig(); + +signals: + +private: + QSharedPointer m_serversModel; + QSharedPointer m_containersModel; + std::shared_ptr m_settings; +}; + +#endif // PROTOCOLSETTINGSCONTROLLER_H diff --git a/client/ui/models/containers_model.cpp b/client/ui/models/containers_model.cpp index b6574439..1caf6944 100644 --- a/client/ui/models/containers_model.cpp +++ b/client/ui/models/containers_model.cpp @@ -25,10 +25,10 @@ bool ContainersModel::setData(const QModelIndex &index, const QVariant &value, i // return ContainerProps::containerHumanNames().value(container); case DescRole: // return ContainerProps::containerDescriptions().value(container); - case ConfigRole: - m_settings->setContainerConfig(m_currentlyProcessedServerIndex, - container, - value.toJsonObject()); + case ConfigRole: //todo save to model also + m_settings->setContainerConfig(m_currentlyProcessedServerIndex, + container, + value.toJsonObject()); case ServiceTypeRole: // return ContainerProps::containerService(container); case DockerContainerRole: @@ -76,8 +76,8 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const return ContainerProps::easySetupDescription(container); case IsInstalledRole: return m_containers.contains(container); - case IsCurrentlyInstalledRole: - return container == static_cast(m_currentlyInstalledContainerIndex); + case IsCurrentlyProcessedRole: + return container == static_cast(m_currentlyProcessedContainerIndex); case IsDefaultRole: return container == m_defaultContainerIndex; case IsSupportedRole: @@ -97,9 +97,9 @@ void ContainersModel::setCurrentlyProcessedServerIndex(int index) emit defaultContainerChanged(); } -void ContainersModel::setCurrentlyInstalledContainerIndex(int index) +void ContainersModel::setCurrentlyProcessedContainerIndex(int index) { - m_currentlyInstalledContainerIndex = index; + m_currentlyProcessedContainerIndex = index; } DockerContainer ContainersModel::getDefaultContainer() @@ -112,9 +112,9 @@ QString ContainersModel::getDefaultContainerName() return ContainerProps::containerHumanNames().value(m_defaultContainerIndex); } -int ContainersModel::getCurrentlyInstalledContainerIndex() +int ContainersModel::getCurrentlyProcessedContainerIndex() { - return m_currentlyInstalledContainerIndex; + return m_currentlyProcessedContainerIndex; } void ContainersModel::removeAllContainers() @@ -153,7 +153,7 @@ QHash ContainersModel::roleNames() const { roles[EasySetupDescriptionRole] = "easySetupDescription"; roles[IsInstalledRole] = "isInstalled"; - roles[IsCurrentlyInstalledRole] = "isCurrentlyInstalled"; + roles[IsCurrentlyProcessedRole] = "isCurrentlyProcessed"; roles[IsDefaultRole] = "isDefault"; roles[IsSupportedRole] = "isSupported"; return roles; diff --git a/client/ui/models/containers_model.h b/client/ui/models/containers_model.h index 5753a1dd..7bb58755 100644 --- a/client/ui/models/containers_model.h +++ b/client/ui/models/containers_model.h @@ -27,7 +27,7 @@ public: EasySetupDescriptionRole, IsInstalledRole, - IsCurrentlyInstalledRole, + IsCurrentlyProcessedRole, IsDefaultRole, IsSupportedRole }; @@ -45,8 +45,8 @@ public slots: QString getDefaultContainerName(); void setCurrentlyProcessedServerIndex(int index); - void setCurrentlyInstalledContainerIndex(int index); - int getCurrentlyInstalledContainerIndex(); + void setCurrentlyProcessedContainerIndex(int index); + int getCurrentlyProcessedContainerIndex(); void removeAllContainers(); void clearCachedProfiles(); @@ -59,7 +59,7 @@ private: int m_currentlyProcessedServerIndex; - int m_currentlyInstalledContainerIndex; + int m_currentlyProcessedContainerIndex; DockerContainer m_defaultContainerIndex; std::shared_ptr m_settings; diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index bb08e88c..9ef87d37 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -75,6 +75,11 @@ void ServersModel::setCurrentlyProcessedServerIndex(int index) m_currenlyProcessedServerIndex = index; } +int ServersModel::getCurrentlyProcessedServerIndex() +{ + return m_currenlyProcessedServerIndex; +} + bool ServersModel::isDefaultServerCurrentlyProcessed() { return m_defaultServerIndex == m_currenlyProcessedServerIndex; diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h index 593babc3..6f55176e 100644 --- a/client/ui/models/servers_model.h +++ b/client/ui/models/servers_model.h @@ -37,6 +37,7 @@ public slots: const int getServersCount(); void setCurrentlyProcessedServerIndex(int index); + int getCurrentlyProcessedServerIndex(); ServerCredentials getCurrentlyProcessedServerCredentials(); void addServer(const QJsonObject &server); diff --git a/client/ui/qml/Components/HomeContainersListView.qml b/client/ui/qml/Components/HomeContainersListView.qml index 926302c4..28f81b2e 100644 --- a/client/ui/qml/Components/HomeContainersListView.qml +++ b/client/ui/qml/Components/HomeContainersListView.qml @@ -59,7 +59,7 @@ ListView { menuContent.currentIndex = index containersDropDown.menuVisible = false } else { - ContainersModel.setCurrentlyInstalledContainerIndex(proxyContainersModel.mapToSource(index)) + ContainersModel.setCurrentlyProcessedContainerIndex(proxyContainersModel.mapToSource(index)) InstallController.setShouldCreateServer(false) goToPage(PageEnum.PageSetupWizardProtocolSettings) containersDropDown.menuVisible = false diff --git a/client/ui/qml/Components/Protocols/OpenVpnSettings.qml b/client/ui/qml/Components/Protocols/OpenVpnSettings.qml index 8c036fc0..5c574832 100644 --- a/client/ui/qml/Components/Protocols/OpenVpnSettings.qml +++ b/client/ui/qml/Components/Protocols/OpenVpnSettings.qml @@ -8,8 +8,12 @@ import "../../Components" Item { id: root + implicitHeight: col.implicitHeight + implicitWidth: col.implicitWidth ColumnLayout { + id: col + anchors.fill: parent anchors.leftMargin: 16 @@ -51,13 +55,79 @@ Item { } DropDownType { + id: hash Layout.fillWidth: true + implicitHeight: 74 + rootButtonBorderWidth: 0 + + descriptionText: qsTr("Hash") + headerText: qsTr("Hash") + + listView: ListViewType { + 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") } + } + currentIndex: 0 + + clickedFunction: { + hash.text = selectedText + hash.menuVisible = false + } + + Component.onCompleted: { + hash.text = selectedText + } + } } DropDownType { + id: cipher Layout.fillWidth: true + implicitHeight: 74 + rootButtonBorderWidth: 0 + + descriptionText: qsTr("Cipher") + headerText: qsTr("Cipher") + + listView: ListViewType { + 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") } + } + currentIndex: 0 + + clickedFunction: { + cipher.text = selectedText + cipher.menuVisible = false + } + + Component.onCompleted: { + cipher.text = selectedText + } + } } CheckBoxType { diff --git a/client/ui/qml/Components/SettingsContainersListView.qml b/client/ui/qml/Components/SettingsContainersListView.qml index d5664541..1b8ea40c 100644 --- a/client/ui/qml/Components/SettingsContainersListView.qml +++ b/client/ui/qml/Components/SettingsContainersListView.qml @@ -88,9 +88,10 @@ ListView { onClicked: { if (isInstalled) { + ContainersModel.setCurrentlyProcessedContainerIndex(root.model.mapToSource(index)) goToPage(PageEnum.PageSettingsServerProtocol) } else { - ContainersModel.setCurrentlyInstalledContainerIndex(root.model.mapToSource(index)) + ContainersModel.setCurrentlyProcessedContainerIndex(root.model.mapToSource(index)) InstallController.setShouldCreateServer(false) goToPage(PageEnum.PageSetupWizardProtocolSettings) } diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml new file mode 100644 index 00000000..5c9ceee8 --- /dev/null +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -0,0 +1,177 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs + +import SortFilterProxyModel 0.2 + +import PageEnum 1.0 +import ContainerProps 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" + +DrawerType { + id: root + + property var qrCodes: [] + property alias configText: configContent.text + property alias headerText: header.headerText + + width: parent.width + height: parent.height * 0.9 + + Item{ + anchors.fill: parent + + FlickableType { + anchors.top: parent.top + anchors.bottom: parent.bottom + contentHeight: content.height + 32 + + ColumnLayout { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + anchors.topMargin: 20 + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + Header2Type { + id: header + Layout.fillWidth: true + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 16 + + text: qsTr("Save connection code") + + onClicked: { + ExportController.saveFile() + } + } + + 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("Copy") + + onClicked: { + configContent.selectAll() + configContent.copy() + configContent.select(0, 0) + } + } + + + 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" + + text: showContent ? qsTr("Collapse content") : qsTr("Show content") + + onClicked: { + showContent = !showContent + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: configContent.implicitHeight + configContent.anchors.topMargin + configContent.anchors.bottomMargin + + radius: 10 + color: "#2C2D30" + + visible: showContent + + height: 24 + + TextField { + id: configContent + + anchors.fill: parent + anchors.margins: 16 + + height: 24 + + color: "#D7D8DB" + + font.pixelSize: 16 + font.weight: Font.Medium + font.family: "PT Root UI VF" + + wrapMode: Text.Wrap + + enabled: false + background: Rectangle { + anchors.fill: parent + color: "transparent" + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: width + Layout.topMargin: 20 + + color: "white" + + Image { + anchors.fill: parent + smooth: false + + Timer { + property int idx: 0 + interval: 1000 + running: qrCodes.length > 0 + repeat: true + onTriggered: { + idx++ + if (idx >= qrCodes.length) { + idx = 0 + } + parent.source = qrCodes[idx] + } + } + + Behavior on source { + PropertyAnimation { duration: 200 } + } + + visible: qrCodes.length > 0 + } + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.bottomMargin: 32 + + horizontalAlignment: Text.AlignHCenter + text: qsTr("To read the QR code in the Amnezia app, select \"Add Server\" → \"I have connection details\"") + } + } + } + } +} diff --git a/client/ui/qml/Controls2/BackButtonType.qml b/client/ui/qml/Controls2/BackButtonType.qml index 389191e8..0ccb7345 100644 --- a/client/ui/qml/Controls2/BackButtonType.qml +++ b/client/ui/qml/Controls2/BackButtonType.qml @@ -11,6 +11,8 @@ Item { implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight + visible: backButtonImage !== "" + RowLayout { id: content diff --git a/client/ui/qml/Controls2/DropDownType.qml b/client/ui/qml/Controls2/DropDownType.qml index 700d9981..6456bbad 100644 --- a/client/ui/qml/Controls2/DropDownType.qml +++ b/client/ui/qml/Controls2/DropDownType.qml @@ -19,11 +19,12 @@ Item { property string rootButtonImage: "qrc:/images/controls/chevron-down.svg" property string rootButtonImageColor: "#494B50" property string rootButtonDefaultColor: "#1C1D21" - property int rootButtonMaximumWidth + property int rootButtonMaximumWidth: 0 property string rootButtonBorderColor: "#494B50" property int rootButtonBorderWidth: 1 + property real drawerHeight: 0.9 property Component listView property alias menuVisible: menu.visible @@ -55,7 +56,9 @@ Item { Layout.leftMargin: 16 LabelTextType { - horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + + horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignVCenter visible: root.descriptionText !== "" @@ -65,7 +68,9 @@ Item { } ButtonTextType { - horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + + horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignVCenter Layout.maximumWidth: rootButtonMaximumWidth ? rootButtonMaximumWidth : implicitWidth @@ -115,7 +120,7 @@ Item { id: menu width: parent.width - height: parent.height * 0.9 + height: parent.height * drawerHeight ColumnLayout { id: header diff --git a/client/ui/qml/Controls2/ListViewType.qml b/client/ui/qml/Controls2/ListViewType.qml new file mode 100644 index 00000000..421b82a3 --- /dev/null +++ b/client/ui/qml/Controls2/ListViewType.qml @@ -0,0 +1,107 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import "TextTypes" + +ListView { + id: menuContent + + property var rootWidth + property var selectedText + property string imageSource + property var clickedFunction + property bool dividerVisible: false + + width: rootWidth + height: menuContent.contentItem.height + + clip: true + interactive: false + + ButtonGroup { + id: buttonGroup + } + + delegate: Item { + implicitWidth: rootWidth + implicitHeight: content.implicitHeight + + ColumnLayout { + id: content + + anchors.fill: parent + spacing: 16 + + RadioButton { + id: radioButton + + implicitWidth: parent.width + implicitHeight: radioButtonContent.implicitHeight + + hoverEnabled: true + + indicator: Rectangle { + anchors.fill: parent + color: radioButton.hovered ? "#2C2D30" : "#1C1D21" + + Behavior on color { + PropertyAnimation { duration: 200 } + } + } + + RowLayout { + id: radioButtonContent + anchors.fill: parent + + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + z: 1 + + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 20 + Layout.bottomMargin: 20 + + text: name + } + + Image { + source: imageSource ? imageSource : "qrc:/images/controls/check.svg" + visible: imageSource ? true : radioButton.checked + + width: 24 + height: 24 + + Layout.rightMargin: 8 + } + } + + ButtonGroup.group: buttonGroup + checked: menuContent.currentIndex === index + + onClicked: { + menuContent.currentIndex = index + menuContent.selectedText = name + if (clickedFunction && typeof clickedFunction === "function") { + clickedFunction() + } + } + } + + DividerType { + Layout.fillWidth: true + Layout.bottomMargin: 16 + + visible: dividerVisible + } + } + + Component.onCompleted: { + if (menuContent.currentIndex === index) { + menuContent.selectedText = name + } + } + } +} diff --git a/client/ui/qml/Controls2/VerticalRadioButton.qml b/client/ui/qml/Controls2/VerticalRadioButton.qml index 50af3c79..7f2411fb 100644 --- a/client/ui/qml/Controls2/VerticalRadioButton.qml +++ b/client/ui/qml/Controls2/VerticalRadioButton.qml @@ -23,12 +23,6 @@ RadioButton { property string defaultBodredColor: "transparent" property int borderWidth: 0 - property string defaultCircleBorderColor: "#878B91" - property string selectedCircleBorderColor: "#A85809" - property string pressedCircleBorderColor: Qt.rgba(251/255, 178/255, 106/255, 0.3) - - property string defaultInnerCircleColor: "#FBB26A" - property string imageSource property bool showImage @@ -61,80 +55,38 @@ RadioButton { } Image { - source: imageSource - visible: showImage + source: { + if (showImage) { + return imageSource + } else if (root.pressed) { + return "qrc:/images/controls/radio-button-inner-circle-pressed.png" + } else if (root.checked) { + return "qrc:/images/controls/radio-button-inner-circle.png" + } + + return "" + } anchors.centerIn: parent width: 24 height: 24 } - - Rectangle { - id: outerCircle - - width: 24 - height: 24 - radius: 16 - - visible: !showImage + Image { + source: { + if (showImage) { + return "" + } else if (root.pressed || root.checked) { + return "qrc:/images/controls/radio-button-pressed.svg" + } else { + return "qrc:/images/controls/radio-button.svg" + } + } anchors.centerIn: parent - color: "transparent" - border.color: { - if (root.enabled) { - if (root.pressed) { - return pressedCircleBorderColor - } else if (root.checked) { - return selectedCircleBorderColor - } - } - return defaultCircleBorderColor - } - - border.width: 1 - - Behavior on border.color { - PropertyAnimation { duration: 200 } - } - - Rectangle { - id: innerCircle - - width: 12 - height: 12 - radius: 16 - - anchors.centerIn: parent - - color: "transparent" - border.color: defaultInnerCircleColor - border.width: { - if (root.enabled) { - if(root.checked) { - return 6 - } - return root.pressed ? 6 : 0 - } else { - return 0 - } - } - - Behavior on border.width { - PropertyAnimation { duration: 200 } - } - } - - DropShadow { - anchors.fill: innerCircle - horizontalOffset: 0 - verticalOffset: 0 - radius: 12 - samples: 13 - color: "#FBB26A" - source: innerCircle - } + width: 24 + height: 24 } } diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 23ca9e01..780f3aef 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -111,7 +111,7 @@ PageType { id: menu width: parent.width - height: parent.height * 0.90 + height: parent.height * 0.9 ColumnLayout { id: serversMenuHeader @@ -247,8 +247,8 @@ PageType { ColumnLayout { id: serverRadioButtonContent - anchors.fill: parent + anchors.fill: parent anchors.rightMargin: 16 anchors.leftMargin: 16 diff --git a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml index 800041f0..498006df 100644 --- a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml +++ b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml @@ -18,7 +18,64 @@ import "../Components/Protocols" PageType { id: root - OpenVpnSettings { + FlickableType { + id: fl anchors.fill: parent + contentHeight: content.height + openVpnSettings.implicitHeight + + Column { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + spacing: 16 + + ListView { + // todo change id naming + id: container + width: parent.width + height: container.contentItem.height + clip: true + interactive: false + model: SortFilterProxyModel { + id: proxyContainersModel + sourceModel: ContainersModel + filters: [ + ValueFilter { + roleName: "isCurrentlyProcessed" + value: true + } + ] + } + + delegate: Item { + implicitWidth: container.width + implicitHeight: delegateContent.implicitHeight + + ColumnLayout { + id: delegateContent + + anchors.fill: parent + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + HeaderType { + Layout.fillWidth: true + Layout.topMargin: 20 + + headerText: name + } + } + } + } + + OpenVpnSettings { + id: openVpnSettings + + width: parent.width + } + } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardEasy.qml b/client/ui/qml/Pages2/PageSetupWizardEasy.qml index 99f6ccfb..1836f892 100644 --- a/client/ui/qml/Pages2/PageSetupWizardEasy.qml +++ b/client/ui/qml/Pages2/PageSetupWizardEasy.qml @@ -134,7 +134,7 @@ PageType { text: qsTr("Continue") onClicked: function() { - ContainersModel.setCurrentlyInstalledContainerIndex(containers.dockerContainer) + ContainersModel.setCurrentlyProcessedContainerIndex(containers.dockerContainer) goToPage(PageEnum.PageSetupWizardInstalling); InstallController.install(containers.dockerContainer, containers.containerDefaultPort, diff --git a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml index c18c4d27..9631e258 100644 --- a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml +++ b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml @@ -54,7 +54,7 @@ PageType { sourceModel: ContainersModel filters: [ ValueFilter { - roleName: "isCurrentlyInstalled" + roleName: "isCurrentlyProcessed" value: true } ] diff --git a/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml b/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml index 57fa497d..c0483df4 100644 --- a/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml +++ b/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml @@ -22,7 +22,7 @@ PageType { sourceModel: ContainersModel filters: [ ValueFilter { - roleName: "isCurrentlyInstalled" + roleName: "isCurrentlyProcessed" value: true } ] diff --git a/client/ui/qml/Pages2/PageSetupWizardProtocols.qml b/client/ui/qml/Pages2/PageSetupWizardProtocols.qml index 75b16afd..e2ae4a16 100644 --- a/client/ui/qml/Pages2/PageSetupWizardProtocols.qml +++ b/client/ui/qml/Pages2/PageSetupWizardProtocols.qml @@ -90,7 +90,7 @@ PageType { buttonImage: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { - ContainersModel.setCurrentlyInstalledContainerIndex(proxyContainersModel.mapToSource(index)) + ContainersModel.setCurrentlyProcessedContainerIndex(proxyContainersModel.mapToSource(index)) goToPage(PageEnum.PageSetupWizardProtocolSettings) } } diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 5560aee7..a731e425 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -1,5 +1,330 @@ import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs -Item { +import SortFilterProxyModel 0.2 +import PageEnum 1.0 +import ContainerProps 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Components" + +PageType { + id: root + + Connections { + target: ExportController + + function onGenerateConfig(isFullAccess) { + if (isFullAccess) { + ExportController.generateFullAccessConfig() + } else { + ExportController.generateConnectionConfig() + } + + shareConnectionDrawer.configText = ExportController.getAmneziaCode() + shareConnectionDrawer.qrCodes = ExportController.getQrCodes() + } + } + + property bool showContent: false + property list connectionTypesModel: [ + amneziaConnectionFormat + ] + + QtObject { + id: amneziaConnectionFormat + property string name: qsTr("For the AmnesiaVPN app") + property var func: function() { + ExportController.generateConfig(false) + } + } + QtObject { + id: openVpnConnectionFormat + property string name: qsTr("OpenVpn native format") + property var func: function() { + console.log("Item 3 clicked") + } + } + QtObject { + id: wireGuardConnectionFormat + property string name: qsTr("WireGuard native format") + property var func: function() { + console.log("Item 3 clicked") + } + } + + FlickableType { + anchors.top: root.top + anchors.bottom: root.bottom + contentHeight: content.height + + ColumnLayout { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + HeaderType { + Layout.fillWidth: true + Layout.topMargin: 20 + + headerText: qsTr("VPN Access") + } + + Rectangle { + id: accessTypeSelector + + property int currentIndex + + Layout.topMargin: 32 + + implicitWidth: accessTypeSelectorContent.implicitWidth + implicitHeight: accessTypeSelectorContent.implicitHeight + + color: "#1C1D21" + radius: 16 + + RowLayout { + id: accessTypeSelectorContent + + spacing: 0 + + HorizontalRadioButton { + checked: accessTypeSelector.currentIndex === 0 + + implicitWidth: (root.width - 32) / 2 + text: qsTr("Connection") + + onClicked: { + accessTypeSelector.currentIndex = 0 + } + } + + HorizontalRadioButton { + checked: root.currentIndex === 1 + + implicitWidth: (root.width - 32) / 2 + text: qsTr("Full") + + onClicked: { + accessTypeSelector.currentIndex = 1 + } + } + } + } + + ParagraphTextType { + Layout.fillWidth: true + + text: qsTr("VPN access without the ability to manage the server") + color: "#878B91" + } + + DropDownType { + id: serverSelector + + Layout.fillWidth: true + Layout.topMargin: 24 + + implicitHeight: 74 + + rootButtonBorderWidth: 0 + drawerHeight: 0.4375 + + descriptionText: qsTr("Server and service") + headerText: qsTr("Server") + + listView: ListViewType { + rootWidth: root.width + dividerVisible: true + + imageSource: "qrc:/images/controls/chevron-right.svg" + + model: ServersModel + currentIndex: ServersModel.getDefaultServerIndex() + + clickedFunction: function() { + serverSelector.text = selectedText + ContainersModel.setCurrentlyProcessedServerIndex(currentIndex) + protocolSelector.visible = true + } + + Component.onCompleted: { + serverSelector.text = selectedText + ContainersModel.setCurrentlyProcessedServerIndex(currentIndex) + } + } + + DrawerType { + id: protocolSelector + + width: parent.width + height: parent.height * 0.5 + + ColumnLayout { + id: header + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + BackButtonType { + backButtonImage: "qrc:/images/controls/arrow-left.svg" + backButtonFunction: function() { + protocolSelector.visible = false + } + } + } + + FlickableType { + anchors.top: header.bottom + anchors.topMargin: 16 + contentHeight: col.implicitHeight + + Column { + id: col + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + spacing: 16 + + Header2TextType { + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + text: qsTr("Protocols and services") + wrapMode: Text.WordWrap + } + + ListViewType { + rootWidth: root.width + dividerVisible: true + + imageSource: "qrc:/images/controls/chevron-right.svg" + + model: SortFilterProxyModel { + id: proxyContainersModel + sourceModel: ContainersModel + filters: [ + ValueFilter { + roleName: "isInstalled" + value: true + } + + ] + } + + currentIndex: 0 + + clickedFunction: function () { + serverSelector.text += ", " + selectedText + shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text + + protocolSelector.visible = false + serverSelector.menuVisible = false + + fillConnectionTypeModel() + } + + Component.onCompleted: { + serverSelector.text += ", " + selectedText + shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text + + fillConnectionTypeModel() + } + + function fillConnectionTypeModel() { + connectionTypesModel = [amneziaConnectionFormat] + + if (currentIndex === ContainerProps.containerFromString("OpenVpn")) { + connectionTypesModel.push(openVpnConnectionFormat) + } else if (currentIndex === ContainerProps.containerFromString("wireGuardConnectionType")) { + connectionTypesModel.push(amneziaConnectionFormat) + } + } + } + } + } + } + } + + DropDownType { + id: connectionTypeSelector + + property int currentIndex + + Layout.fillWidth: true + Layout.topMargin: 16 + + implicitHeight: 74 + + rootButtonBorderWidth: 0 + drawerHeight: 0.4375 + + visible: accessTypeSelector.currentIndex === 0 + enabled: connectionTypesModel.length > 1 + + descriptionText: qsTr("Connection format") + headerText: qsTr("Connection format") + + listView: ListViewType { + id: connectionTypeSelectorListView + + rootWidth: root.width + dividerVisible: true + + imageSource: "qrc:/images/controls/chevron-right.svg" + + model: connectionTypesModel + currentIndex: 0 + + clickedFunction: function() { + connectionTypeSelector.text = selectedText + connectionTypeSelector.currentIndex = currentIndex + connectionTypeSelector.menuVisible = false + } + + Component.onCompleted: { + connectionTypeSelector.text = selectedText + connectionTypeSelector.currentIndex = currentIndex + } + } + } + + ShareConnectionDrawer { + id: shareConnectionDrawer + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 32 + + text: qsTr("Share") + + onClicked: { + if (accessTypeSelector.currentIndex === 0) { + connectionTypesModel[connectionTypeSelector.currentIndex].func() + } else { + ExportController.generateConfig(true) + } + shareConnectionDrawer.visible = true + } + } + } + } } diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index 16dd00d3..8b9cefa4 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -78,7 +78,9 @@ PageType { TabImageButtonType { isSelected: tabBar.currentIndex === 1 image: "qrc:/images/controls/share-2.svg" - onClicked: {} + onClicked: { + tabBarStackView.goToTabBarPage(PageEnum.PageShare) + } } TabImageButtonType { isSelected: tabBar.currentIndex === 2