diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 83be4c91..952df01c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,8 +14,8 @@ jobs: runs-on: ubuntu-20.04 env: - QT_VERSION: 6.5.1 - QIF_VERSION: 4.6 + QT_VERSION: 6.6.2 + QIF_VERSION: 4.7 steps: - name: 'Install Qt' @@ -72,8 +72,8 @@ jobs: runs-on: windows-latest env: - QT_VERSION: 6.5.1 - QIF_VERSION: 4.6 + QT_VERSION: 6.6.2 + QIF_VERSION: 4.7 BUILD_ARCH: 64 steps: @@ -134,7 +134,7 @@ jobs: runs-on: macos-13 env: - QT_VERSION: 6.5.2 + QT_VERSION: 6.6.2 CC: cc CXX: c++ @@ -227,7 +227,7 @@ jobs: env: # Keep compat with MacOS 10.15 aka Catalina by Qt 6.4 QT_VERSION: 6.4.3 - QIF_VERSION: 4.6 + QIF_VERSION: 4.7 steps: - name: 'Setup xcode' @@ -286,7 +286,7 @@ jobs: env: ANDROID_BUILD_PLATFORM: android-34 - QT_VERSION: 6.6.1 + QT_VERSION: 6.6.2 QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' steps: diff --git a/.gitignore b/.gitignore index 7de64e4b..5b90fd55 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,6 @@ client/3rd/ShadowSocks/ss_ios.xcconfig # UML generated pics out/ + +# CMake files +CMakeFiles/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index aa8d800b..2c1a1934 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.3.0.0 +project(${PROJECT} VERSION 4.4.0.0 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 44) +set(APP_ANDROID_VERSION_CODE 46) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index e568e7d0..2fa21880 160000 --- a/client/3rd-prebuilt +++ b/client/3rd-prebuilt @@ -1 +1 @@ -Subproject commit e568e7d0e8defe8fe009c0127323f2c55fd9be76 +Subproject commit 2fa21880b9e5059cf9e8856c778bad97dcad3c91 diff --git a/client/3rd/OpenVPNAdapter b/client/3rd/OpenVPNAdapter index f95f0b2b..6f71d074 160000 --- a/client/3rd/OpenVPNAdapter +++ b/client/3rd/OpenVPNAdapter @@ -1 +1 @@ -Subproject commit f95f0b2b569be3954b93a6a9c649208cda40b879 +Subproject commit 6f71d0743d96b022863e2e4d6ebf7984842669ee diff --git a/client/3rd/amneziawg-apple b/client/3rd/amneziawg-apple index f23eee47..0829e99e 160000 --- a/client/3rd/amneziawg-apple +++ b/client/3rd/amneziawg-apple @@ -1 +1 @@ -Subproject commit f23eee4700ed4a2ef44a800d2c20466c9ab0222b +Subproject commit 0829e99ea9f4508fd1d4742546b62145d17587bb diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 0bc4e89c..439e10c5 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -15,6 +15,15 @@ set(PACKAGES Core5Compat Concurrent LinguistTools ) +execute_process( + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + COMMAND git rev-parse --short HEAD + OUTPUT_VARIABLE GIT_COMMIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}") + if(IOS) set(PACKAGES ${PACKAGES} Multimedia) endif() @@ -57,6 +66,7 @@ set(AMNEZIAVPN_TS_FILES ${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ru.ts ${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_zh_CN.ts ${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_fa_IR.ts + ${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ar.ts ) file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui) diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 58e6b543..0831b1a2 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -286,13 +286,16 @@ void AmneziaApplication::initModels() m_containersModel.reset(new ContainersModel(this)); m_engine->rootContext()->setContextProperty("ContainersModel", m_containersModel.get()); + m_defaultServerContainersModel.reset(new ContainersModel(this)); + m_engine->rootContext()->setContextProperty("DefaultServerContainersModel", m_defaultServerContainersModel.get()); + m_serversModel.reset(new ServersModel(m_settings, this)); m_engine->rootContext()->setContextProperty("ServersModel", m_serversModel.get()); connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel); - connect(m_serversModel.get(), &ServersModel::defaultContainerChanged, m_containersModel.get(), - &ContainersModel::setDefaultContainer); - m_containersModel->setDefaultContainer(m_serversModel->getDefaultContainer()); // make better? + connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(), + &ContainersModel::updateModel); + m_serversModel->resetModel(); m_languageModel.reset(new LanguageModel(m_settings, this)); m_engine->rootContext()->setContextProperty("LanguageModel", m_languageModel.get()); @@ -336,7 +339,7 @@ void AmneziaApplication::initModels() connect(m_configurator.get(), &VpnConfigurator::newVpnConfigCreated, this, [this](const QString &clientId, const QString &clientName, const DockerContainer container, ServerCredentials credentials) { - m_serversModel->reloadContainerConfig(); + m_serversModel->reloadDefaultServerContainerConfig(); m_clientManagementModel->appendClient(clientId, clientName, container, credentials); emit m_configurator->clientModelUpdated(); }); @@ -388,7 +391,13 @@ void AmneziaApplication::initControllers() m_engine->rootContext()->setContextProperty("ApiController", m_apiController.get()); connect(m_apiController.get(), &ApiController::updateStarted, this, [this]() { emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Connecting); }); - connect(m_apiController.get(), &ApiController::errorOccurred, this, - [this]() { emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); }); - connect(m_apiController.get(), &ApiController::updateFinished, m_connectionController.get(), &ConnectionController::toggleConnection); + connect(m_apiController.get(), &ApiController::errorOccurred, this, [this](const QString &errorMessage) { + if (m_connectionController->isConnectionInProgress()) { + emit m_pageController->showErrorMessage(errorMessage); + } + + emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); + }); + connect(m_apiController.get(), &ApiController::updateFinished, m_connectionController.get(), + &ConnectionController::toggleConnection); } diff --git a/client/amnezia_application.h b/client/amnezia_application.h index 0161cd3a..bdbc52a6 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -92,6 +92,7 @@ private: QCommandLineParser m_parser; QSharedPointer m_containersModel; + QSharedPointer m_defaultServerContainersModel; QSharedPointer m_serversModel; QSharedPointer m_languageModel; QSharedPointer m_protocolsModel; diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index fb417f05..22eed003 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -22,7 +22,7 @@ - + @@ -137,14 +137,13 @@ android:name=".AmneziaVpnService" android:process=":amneziaVpnService" android:permission="android.permission.BIND_VPN_SERVICE" - android:foregroundServiceType="specialUse" - android:exported="false"> + android:foregroundServiceType="systemExempted" + android:exported="false" + tools:ignore="ForegroundServicePermission"> - - = Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> FOREGROUND_SERVICE_TYPE_SPECIAL_USE + Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> FOREGROUND_SERVICE_TYPE_MANIFEST else -> 0 } diff --git a/client/android/wireguard/src/main/kotlin/com/wireguard/android/backend/GoBackend.kt b/client/android/wireguard/src/main/kotlin/GoBackend.kt similarity index 62% rename from client/android/wireguard/src/main/kotlin/com/wireguard/android/backend/GoBackend.kt rename to client/android/wireguard/src/main/kotlin/GoBackend.kt index 485df5a0..28e48a75 100644 --- a/client/android/wireguard/src/main/kotlin/com/wireguard/android/backend/GoBackend.kt +++ b/client/android/wireguard/src/main/kotlin/GoBackend.kt @@ -1,7 +1,5 @@ -package com.wireguard.android.backend +package org.amnezia.vpn.protocol.wireguard -// TODO: Refactor Amnezia wireguard project by changing the JNI method names -// to move this object to 'org.amnezia.vpn.protocol.wireguard.backend' package object GoBackend { external fun wgGetConfig(handle: Int): String? external fun wgGetSocketV4(handle: Int): Int diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/Wireguard.kt similarity index 99% rename from client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt rename to client/android/wireguard/src/main/kotlin/Wireguard.kt index 87d5e249..40fa4ec6 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/Wireguard.kt @@ -3,7 +3,6 @@ package org.amnezia.vpn.protocol.wireguard import android.content.Context import android.net.VpnService.Builder import java.util.TreeMap -import com.wireguard.android.backend.GoBackend import kotlinx.coroutines.flow.MutableStateFlow import org.amnezia.vpn.protocol.Protocol import org.amnezia.vpn.protocol.ProtocolState diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt b/client/android/wireguard/src/main/kotlin/WireguardConfig.kt similarity index 100% rename from client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt rename to client/android/wireguard/src/main/kotlin/WireguardConfig.kt diff --git a/client/cmake/android.cmake b/client/cmake/android.cmake index 7ffa680e..9458bd08 100644 --- a/client/cmake/android.cmake +++ b/client/cmake/android.cmake @@ -42,9 +42,9 @@ set(SOURCES ${SOURCES} foreach(abi IN ITEMS ${QT_ANDROID_ABIS}) set_property(TARGET ${PROJECT} PROPERTY QT_ANDROID_EXTRA_LIBS - ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/wireguard/android/${abi}/libwg.so - ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/wireguard/android/${abi}/libwg-go.so - ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/wireguard/android/${abi}/libwg-quick.so + ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/amneziawg/android/${abi}/libwg.so + ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/amneziawg/android/${abi}/libwg-go.so + ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/amneziawg/android/${abi}/libwg-quick.so ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/shadowsocks/android/${abi}/libredsocks.so ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/shadowsocks/android/${abi}/libsslocal.so ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/shadowsocks/android/${abi}/libtun2socks.so diff --git a/client/core/controllers/serverController.cpp b/client/core/controllers/serverController.cpp index 8b250f18..9a170a85 100644 --- a/client/core/controllers/serverController.cpp +++ b/client/core/controllers/serverController.cpp @@ -211,13 +211,7 @@ ErrorCode ServerController::uploadFileToHost(const ServerCredentials &credential localFile.write(data); localFile.close(); -#ifdef Q_OS_WINDOWS - error = m_sshClient.sftpFileCopy(overwriteMode, localFile.fileName().toLocal8Bit().toStdString(), remotePath.toStdString(), - "non_desc"); -#else - error = m_sshClient.sftpFileCopy(overwriteMode, localFile.fileName().toStdString(), remotePath.toStdString(), - "non_desc"); -#endif + error = m_sshClient.sftpFileCopy(overwriteMode, localFile.fileName(), remotePath, "non_desc"); if (error != ErrorCode::NoError) { return error; diff --git a/client/core/sshclient.cpp b/client/core/sshclient.cpp index 0ac95662..03670b30 100644 --- a/client/core/sshclient.cpp +++ b/client/core/sshclient.cpp @@ -222,7 +222,7 @@ namespace libssh { return fromLibsshErrorCode(); } - ErrorCode Client::sftpFileCopy(const SftpOverwriteMode overwriteMode, const std::string& localPath, const std::string& remotePath, const std::string& fileDesc) + ErrorCode Client::sftpFileCopy(const SftpOverwriteMode overwriteMode, const QString& localPath, const QString& remotePath, const QString &fileDesc) { m_sftpSession = sftp_new(m_session); @@ -245,40 +245,38 @@ namespace libssh { const size_t bufferSize = 16384; char buffer[bufferSize]; - file = sftp_open(m_sftpSession, remotePath.c_str(), accessType, S_IRWXU); + file = sftp_open(m_sftpSession, remotePath.toStdString().c_str(), accessType, S_IRWXU); if (file == nullptr) { return closeSftpSession(); } - int localFileSize = std::filesystem::file_size(localPath); + int localFileSize = QFileInfo(localPath).size(); int chunksCount = localFileSize / (bufferSize); - std::ifstream fin(localPath, std::ios::binary | std::ios::in); + QFile fin(localPath); - if (fin.is_open()) { + if (fin.open(QIODevice::ReadOnly)) { for (int currentChunkId = 0; currentChunkId < chunksCount; currentChunkId++) { - fin.read(buffer, bufferSize); + QByteArray chunk = fin.read(bufferSize); + if (chunk.size() != bufferSize) return ErrorCode::SshSftpEofError; - int bytesWritten = sftp_write(file, buffer, bufferSize); + int bytesWritten = sftp_write(file, chunk.data(), chunk.size()); - std::string chunk(buffer, bufferSize); - - if (bytesWritten != bufferSize) { + if (bytesWritten != chunk.size()) { fin.close(); sftp_close(file); return closeSftpSession(); } } - int lastChunkSize = localFileSize % (bufferSize); + int lastChunkSize = localFileSize % bufferSize; if (lastChunkSize != 0) { - fin.read(buffer, lastChunkSize); + QByteArray lastChunk = fin.read(lastChunkSize); + if (lastChunk.size() != lastChunkSize) return ErrorCode::SshSftpEofError; - std::string chunk(buffer, lastChunkSize); - - int bytesWritten = sftp_write(file, buffer, lastChunkSize); + int bytesWritten = sftp_write(file, lastChunk.data(), lastChunkSize); if (bytesWritten != lastChunkSize) { fin.close(); diff --git a/client/core/sshclient.h b/client/core/sshclient.h index 4e08faaa..74c3b724 100644 --- a/client/core/sshclient.h +++ b/client/core/sshclient.h @@ -33,9 +33,9 @@ namespace libssh { const std::function &cbReadStdErr); ErrorCode writeResponse(const QString &data); ErrorCode sftpFileCopy(const SftpOverwriteMode overwriteMode, - const std::string& localPath, - const std::string& remotePath, - const std::string& fileDesc); + const QString &localPath, + const QString &remotePath, + const QString& fileDesc); ErrorCode getDecryptedPrivateKey(const ServerCredentials &credentials, QString &decryptedPrivateKey, const std::function &passphraseCallback); private: ErrorCode closeChannel(); diff --git a/client/images/controls/split-tunneling.svg b/client/images/controls/split-tunneling.svg new file mode 100644 index 00000000..3062054d --- /dev/null +++ b/client/images/controls/split-tunneling.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/ios/networkextension/CMakeLists.txt b/client/ios/networkextension/CMakeLists.txt index b09753c9..e48215f6 100644 --- a/client/ios/networkextension/CMakeLists.txt +++ b/client/ios/networkextension/CMakeLists.txt @@ -85,6 +85,7 @@ target_sources(networkextension PRIVATE ${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift ${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider.swift ${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+OpenVPNAdapterDelegate.swift + ${CLIENT_ROOT_DIR}/platforms/ios/WGConfig.swift ${CLIENT_ROOT_DIR}/platforms/ios/iosglue.mm ) diff --git a/client/mozilla/localsocketcontroller.cpp b/client/mozilla/localsocketcontroller.cpp index d7500685..8583de4e 100644 --- a/client/mozilla/localsocketcontroller.cpp +++ b/client/mozilla/localsocketcontroller.cpp @@ -124,7 +124,10 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) { // json.insert("hopindex", QJsonValue((double)hop.m_hopindex)); json.insert("privateKey", wgConfig.value(amnezia::config_key::client_priv_key)); json.insert("deviceIpv4Address", wgConfig.value(amnezia::config_key::client_ip)); + // todo review wg ipv6 +#ifndef Q_OS_WINDOWS json.insert("deviceIpv6Address", "dead::1"); +#endif json.insert("serverPublicKey", wgConfig.value(amnezia::config_key::server_pub_key)); json.insert("serverPskKey", wgConfig.value(amnezia::config_key::psk_key)); json.insert("serverIpv4AddrIn", wgConfig.value(amnezia::config_key::hostName)); diff --git a/client/platforms/ios/PacketTunnelProvider.swift b/client/platforms/ios/PacketTunnelProvider.swift index 52f7084b..dc0403e3 100644 --- a/client/platforms/ios/PacketTunnelProvider.swift +++ b/client/platforms/ios/PacketTunnelProvider.swift @@ -59,10 +59,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider { var stopHandler: (() -> Void)? var protoType: TunnelProtoType = .none - override init() { - super.init() - } - override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { let tmpStr = String(data: messageData, encoding: .utf8)! wg_log(.error, message: tmpStr) @@ -71,7 +67,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { return } - guard let completionHandler = completionHandler else { + guard let completionHandler else { log(.error, message: "Missing message completion handler") return } @@ -179,14 +175,16 @@ class PacketTunnelProvider: NEPacketTunnelProvider { return } - let wgConfigStr = String(data: wgConfig, encoding: .utf8)! - - guard let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: wgConfigStr) else { + guard let wgConfigStr = try? JSONDecoder().decode(WGConfig.self, from: wgConfig).str, + let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: wgConfigStr) + else { wg_log(.error, message: "Can't parse WireGuard config") completionHandler(nil) return } + log(.info, message: "wgConfig: \(wgConfigStr.replacingOccurrences(of: "\n", with: " "))") + if tunnelConfiguration.peers.first!.allowedIPs .map({ $0.stringRepresentation }) .joined(separator: ", ") == "0.0.0.0/0, ::/0" { diff --git a/client/platforms/ios/WGConfig.swift b/client/platforms/ios/WGConfig.swift new file mode 100644 index 00000000..9593d289 --- /dev/null +++ b/client/platforms/ios/WGConfig.swift @@ -0,0 +1,135 @@ +import Foundation + +struct WGConfigData: Decodable { + let initPacketMagicHeader, responsePacketMagicHeader: String? + let underloadPacketMagicHeader, transportPacketMagicHeader: String? + let junkPacketCount, junkPacketMinSize, junkPacketMaxSize: String? + let initPacketJunkSize, responsePacketJunkSize: String? + + var settings: String { + junkPacketCount == nil ? "" : + """ + Jc = \(junkPacketCount!) + Jmin = \(junkPacketMinSize!) + Jmax = \(junkPacketMaxSize!) + S1 = \(initPacketJunkSize!) + S2 = \(responsePacketJunkSize!) + H1 = \(initPacketMagicHeader!) + H2 = \(responsePacketMagicHeader!) + H3 = \(underloadPacketMagicHeader!) + H4 = \(transportPacketMagicHeader!) + + """ + } + + let clientIP: String + let clientPrivateKey: String + let clientPublicKey: String + let serverPublicKey: String + let presharedKey: String + let hostName: String + let port: Int + + var allowedIPs: [String] + var persistentKeepAlive: String + + enum CodingKeys: String, CodingKey { + case initPacketMagicHeader = "H1", responsePacketMagicHeader = "H2" + case underloadPacketMagicHeader = "H3", transportPacketMagicHeader = "H4" + case junkPacketCount = "Jc", junkPacketMinSize = "Jmin", junkPacketMaxSize = "Jmax" + case initPacketJunkSize = "S1", responsePacketJunkSize = "S2" + + case clientIP = "client_ip" // "10.8.1.16" + case clientPrivateKey = "client_priv_key" + case clientPublicKey = "client_pub_key" + case serverPublicKey = "server_pub_key" + case presharedKey = "psk_key" + + case allowedIPs = "allowed_ips" + case persistentKeepAlive = "persistent_keep_alive" + case hostName + case port + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.initPacketMagicHeader = try container.decodeIfPresent(String.self, forKey: .initPacketMagicHeader) + self.responsePacketMagicHeader = try container.decodeIfPresent(String.self, forKey: .responsePacketMagicHeader) + self.underloadPacketMagicHeader = try container.decodeIfPresent(String.self, forKey: .underloadPacketMagicHeader) + self.transportPacketMagicHeader = try container.decodeIfPresent(String.self, forKey: .transportPacketMagicHeader) + self.junkPacketCount = try container.decodeIfPresent(String.self, forKey: .junkPacketCount) + self.junkPacketMinSize = try container.decodeIfPresent(String.self, forKey: .junkPacketMinSize) + self.junkPacketMaxSize = try container.decodeIfPresent(String.self, forKey: .junkPacketMaxSize) + self.initPacketJunkSize = try container.decodeIfPresent(String.self, forKey: .initPacketJunkSize) + self.responsePacketJunkSize = try container.decodeIfPresent(String.self, forKey: .responsePacketJunkSize) + self.clientIP = try container.decode(String.self, forKey: .clientIP) + self.clientPrivateKey = try container.decode(String.self, forKey: .clientPrivateKey) + self.clientPublicKey = try container.decode(String.self, forKey: .clientPublicKey) + self.serverPublicKey = try container.decode(String.self, forKey: .serverPublicKey) + self.presharedKey = try container.decode(String.self, forKey: .presharedKey) + self.allowedIPs = try container.decodeIfPresent([String].self, forKey: .allowedIPs) ?? ["0.0.0.0/0", "::/0"] + self.persistentKeepAlive = try container.decodeIfPresent(String.self, forKey: .persistentKeepAlive) ?? "25" + self.hostName = try container.decode(String.self, forKey: .hostName) + self.port = try container.decode(Int.self, forKey: .port) + } +} + +struct WGConfig: Decodable { + let data: WGConfigData + let configVersion: Int + let description: String + let dns1: String + let dns2: String + let hostName: String + let `protocol`: String + let splitTunnelSites: [String] + let splitTunnelType: Int + + enum CodingKeys: String, CodingKey { + case awgConfigData = "awg_config_data", wgConfigData = "wireguard_config_data" + case configData + case configVersion = "config_version" + case description + case dns1 + case dns2 + case hostName + case `protocol` + case splitTunnelSites + case splitTunnelType + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.contains(.awgConfigData) { + self.data = try container.decode(WGConfigData.self, forKey: .awgConfigData) + } else { + self.data = try container.decode(WGConfigData.self, forKey: .wgConfigData) + } + + self.configVersion = try container.decode(Int.self, forKey: .configVersion) + self.description = try container.decode(String.self, forKey: .description) + self.dns1 = try container.decode(String.self, forKey: .dns1) + self.dns2 = try container.decode(String.self, forKey: .dns2) + self.hostName = try container.decode(String.self, forKey: .hostName) + self.protocol = try container.decode(String.self, forKey: .protocol) + self.splitTunnelSites = try container.decode([String].self, forKey: .splitTunnelSites) + self.splitTunnelType = try container.decode(Int.self, forKey: .splitTunnelType) + } + + var str: String { + """ + [Interface] + Address = \(data.clientIP)/32 + DNS = \(dns1), \(dns2) + PrivateKey = \(data.clientPrivateKey) + \(data.settings) + [Peer] + PublicKey = \(data.serverPublicKey) + PresharedKey = \(data.presharedKey) + AllowedIPs = \(data.allowedIPs.joined(separator: ", ")) + Endpoint = \(data.hostName):\(data.port) + PersistentKeepalive = \(data.persistentKeepAlive) + """ + } +} diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm index 8fb8283d..0d5eadbc 100644 --- a/client/platforms/ios/ios_controller.mm +++ b/client/platforms/ios/ios_controller.mm @@ -400,9 +400,10 @@ bool IosController::setupCloak() bool IosController::setupWireGuard() { QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::WireGuard)].toObject(); - - QString wgConfig = config[config_key::config].toString(); + QJsonDocument doc(m_rawConfig); + QString wgConfig(doc.toJson(QJsonDocument::Compact)); + return startWireGuard(wgConfig); } @@ -410,8 +411,9 @@ bool IosController::setupAwg() { QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Awg)].toObject(); - QString wgConfig = config[config_key::config].toString(); - + QJsonDocument doc(m_rawConfig); + QString wgConfig(doc.toJson(QJsonDocument::Compact)); + return startWireGuard(wgConfig); } diff --git a/client/resources.qrc b/client/resources.qrc index dfadcb20..b9a69023 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -160,7 +160,6 @@ ui/qml/Components/SettingsContainersListView.qml ui/qml/Controls2/TextTypes/ListItemTitleType.qml ui/qml/Controls2/DividerType.qml - ui/qml/Controls2/DrawerType.qml ui/qml/Controls2/StackViewType.qml ui/qml/Pages2/PageSettings.qml images/controls/amnezia.svg @@ -225,5 +224,8 @@ ui/qml/Pages2/PageShareFullAccess.qml images/controls/close.svg images/controls/search.svg + ui/qml/Components/HomeSplitTunnelingDrawer.qml + images/controls/split-tunneling.svg + ui/qml/Controls2/DrawerType2.qml diff --git a/client/translations/amneziavpn_ar.ts b/client/translations/amneziavpn_ar.ts new file mode 100644 index 00000000..18af5612 --- /dev/null +++ b/client/translations/amneziavpn_ar.ts @@ -0,0 +1,3248 @@ + + + + + ConnectionController + + + + + + Connect + اتصل + + + + VPN Protocols is not installed. + Please install VPN container at first + لم يتم تثبيت بروتوكولات VPN, من فضلك قم بتنزيل حاوية VPN اولاً + + + + Connection... + اتصال... + + + + Connected + تم الاتصال + + + + Reconnection... + إعادة الاتصال... + + + + Disconnection... + إنهاء الاتصال... + + + + Settings updated successfully, Reconnnection... + تم تحديث الاعدادات بنجاح, جاري إعادة الاتصال... + + + + Settings updated successfully + تم تحديث الاعدادات بنجاح + + + + ConnectionTypeSelectionDrawer + + + Add new connection + إضافة اتصال جديد + + + + Configure your server + قم بتهيئة الخادم الخاص بك + + + + Open config file, key or QR code + افتح ملف تعريف, مفتاح تعريف او رمز QR + + + + ContextMenuType + + + C&ut + ق&طع + + + + &Copy + &استنتاخ + + + + &Paste + &لصق + + + + &SelectAll + &تحديد الكل + + + + ExportController + + + Access error! + خطأ في الوصول! + + + + HomeContainersListView + + + Unable change protocol while there is an active connection + قم بتغيير البروتوكول عند تواجد اتصال + + + + The selected protocol is not supported on the current platform + البروتوكول المحدد غير مدعوم علي المنصة الحالية + + + + ImportController + + + Scanned %1 of %2. + تم فحص%1 من %2. + + + + InstallController + + installed successfully. + تم التثبيت بنجاح + + + is already installed on the server. + بالفعل مٌثبت علي الخادم + + + + + %1 installed successfully. + %1 تم التثبيت بنجاح. + + + + + %1 is already installed on the server. + %1 بالفعل مٌثبت علي الخادم. + + + + +Added containers that were already installed on the server + +تمت إضافة الحاويات التي كانت مٌثبتة بالفعل علي الخادم + + + + +Already installed containers were found on the server. All installed containers have been added to the application + +تم العثور علي حاويات مٌثبتة بالفعل علي الخادم +تمت إضافة جميع الحاويات المٌثبتة إلي التطبيق + + + + Settings updated successfully + تم تحديث الاعدادات بنجاح + + + + Server '%1' was rebooted + تمت إعادة تشغيل الخادم%1 + + + + Server '%1' was removed + تمت إزالة الخادم '%1' + + + + All containers from server '%1' have been removed + قد تم حذفها '%1' جميع الحاويات من الخادم + + + + %1 has been removed from the server '%2' + %1 تم حدف '%2' اسم الخادم + + + 1% has been removed from the server '%2' + %1 من الخادم '%2' تم مسحة + + + Server ' + خادم + + + ' was removed + تم حذفة + + + has been removed from the server ' + قد تمت إزالتة من الخادم + + + + Please login as the user + من فضلك قم بتسجيل الدخول كمستخدم + + + + Server added successfully + تمت إضافة الخادم بنجاح + + + + KeyChainClass + + + Read key failed: %1 + فشل مفتاح القراءة: %1 + + + + Write key failed: %1 + فشل مفتاح الكتابة: %1 + + + + Delete key failed: %1 + فشل مفتاح الحذف: %1 + + + + NotificationHandler + + + + AmneziaVPN + AmneziaVPN + + + + VPN Connected + تم الاتصال + + + + VPN Disconnected + تم إنهاء الاتصال + + + + AmneziaVPN notification + إشعار من AmneziaVPN + + + + Unsecured network detected: + تم العثور علي شبكة غير مؤمنة: + + + + PageDeinstalling + + + Removing services from %1 + من %1 مسح الخدمة + + + + Usually it takes no more than 5 minutes + في العادة تستغرق اقل من 5 دقائق + + + + PageHome + + + VPN protocol + بروتوكول VPN + + + + Servers + الخوادم + + + + Unable change server while there is an active connection + لا يمكن تغير الخادم بينما هناك اتصال مفعل + + + + PageProtocolAwgSettings + + + AmneziaWG settings + اعدادات AmneziaWG + + + + Port + منفذ + + + + Junk packet count + عدد الحزم الغير مرغوب فيها + + + + Junk packet minimum size + الحد الادني لحجم الحزمة الغير مرغوب فيها + + + + Junk packet maximum size + الحجم الاقصي للحزمة الغير مرغوب فيها + + + + Init packet junk size + Init packet junk size + + + + Response packet junk size + حجم حزمة الاستجابة الغير مرغوب فيها + + + + Init packet magic header + إطلاق حزمة magic header + + + + Response packet magic header + حزمة الرد magic header + + + + Transport packet magic header + نقل حزمة magic header + + + + Underload packet magic header + تحميل حزمة magic header + + + + Remove AmneziaWG + قم بحذف AmneziaWG + + + + Remove AmneziaWG from server? + قم بحذف AmneziaWG من الخادم؟ + + + + All users with whom you shared a connection will no longer be able to connect to it. + جميع المستخدمين الذين قمت بمشاركة اتصال معهم لن يستطيعو الاتصال. + + + + Continue + واصل + + + + Cancel + إلغاء + + + + Save and Restart Amnezia + احفظ و اعِد تشغيل Amnezia + + + + PageProtocolCloakSettings + + + Cloak settings + Cloak إعدادات + + + + Disguised as traffic from + متنكراً في حركة مرور من + + + + Port + منفذ + + + + + Cipher + الشفرة + + + + Save and Restart Amnezia + احفظ و اعِد تشغيل Amnezia + + + + PageProtocolOpenVpnSettings + + + OpenVPN settings + OpenVPN اعدادات + + + + VPN address subnet + الشبكة الفرعية لعنوان VPN + + + + Network protocol + بروتوكول الشبكة + + + + Port + منفذ + + + + Auto-negotiate encryption + التفاوض التلقائي علي الشبكة + + + + + Hash + + + + + SHA512 + + + + + SHA384 + + + + + SHA256 + + + + + SHA3-512 + + + + + SHA3-384 + + + + + SHA3-256 + + + + + whirlpool + + + + + BLAKE2b512 + + + + + BLAKE2s256 + + + + + SHA1 + + + + + + Cipher + شفرة + + + + AES-256-GCM + + + + + AES-192-GCM + + + + + AES-128-GCM + + + + + AES-256-CBC + + + + + AES-192-CBC + + + + + AES-128-CBC + + + + + ChaCha20-Poly1305 + + + + + ARIA-256-CBC + + + + + CAMELLIA-256-CBC + + + + + none + لا شئ + + + + TLS auth + TLS مصادقة + + + + Block DNS requests outside of VPN + احظر طلبات DNS خارج ال VPN + + + + Additional client configuration commands + اوامر تكوين العميل الاضافية + + + + + Commands: + الاوامر: + + + + Additional server configuration commands + اوامر تكوين الخادم الاضافية + + + + Remove OpenVPN + احذف OpenVPN + + + + Remove OpenVpn from server? + احذف OpenVPN من الخادم? + + + + All users with whom you shared a connection will no longer be able to connect to it. + جميع المستخدمين الذين شاركت معهم الاتصال لن يستطيعو الاتصال مجدداً. + + + All users with whom you shared a connection will no longer be able to connect to it + جميع المستخدمين الذين شاركت اتصال معهم لن يستطيعو الاتصال بعد الان + + + + Continue + واصل + + + + Cancel + إلغاء + + + + Save and Restart Amnezia + احفظ واعِد تشغيل Amnezia + + + + PageProtocolRaw + + + settings + إعدادات + + + + Show connection options + اظهر اختيارات الاتصال + + + Connection options + اختيارات الاتصال + + + + Connection options %1 + %1 اختيارات الاتصال + + + + Remove + احذف + + + + Remove %1 from server? + احذف %1 من الخادم ? + + + + All users with whom you shared a connection will no longer be able to connect to it. + جميع المستخدمين الذين شاركت معهم اتصال لن يستطيعو الاتصال بعد الان. + + + from server? + من الخادم + + + All users with whom you shared a connection will no longer be able to connect to it + جميع المستخدمين الذين شاركت اتصال معهم لن يستطيعو الاتصال بعد الان + + + + Continue + واصل + + + + Cancel + إلغاء + + + + PageProtocolShadowSocksSettings + + + ShadowSocks settings + ShadowSocks إعدادات + + + + Port + منفذ + + + + + Cipher + تشفير + + + + Save and Restart Amnezia + احفظ واعِد تشغيل Amnezia + + + + PageServiceDnsSettings + + + A DNS service is installed on your server, and it is only accessible via VPN. + + تم تثبيت خدمة DNS علي الخادم الخاص بك, و فقط متاح من خلال VPN. + + + + + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. + عنوان ال DNS متطابق لنفس عنوان الخادم بك, يمكنك تهيئة DNS في الاعدادات, تحت علامة تبويب الاتصال. + + + + Remove + احذف + + + + Remove %1 from server? + احذف %1 ? + + + from server? + من الخادم + + + + Continue + واصل + + + + Cancel + إلغاء + + + + PageServiceSftpSettings + + + Settings updated successfully + تم تحديث الإعدادات بنجاح + + + + SFTP settings + SFTP إعدادات + + + + Host + استضافة + + + + + + + Copied + تم الاستنساخ + + + + Port + منفذ + + + + Login + تسجيل الدخول + + + + Password + كلمة المرور + + + + Mount folder on device + قم بتثبيت المجلد علي الجهاز + + + + In order to mount remote SFTP folder as local drive, perform following steps: <br> + لتثبيت مجلد SFTP كمحرك اقراص محلي, اتبع هذه الخطوات : <br> + + + + + <br>1. Install the latest version of + <br>1. تحميل اخر اصدار من + + + + + <br>2. Install the latest version of + <br>2. تحمير اخر اصدار من + + + + Detailed instructions + تعليمات مفصلة + + + + Remove SFTP and all data stored there + امسح SFTP وجميع البيانات المخزنة + + + + Remove SFTP and all data stored there? + امسح SFTP وجميع البيانات المخزنة؟ + + + + Continue + واصل + + + + Cancel + إلغاء + + + + PageServiceTorWebsiteSettings + + + Settings updated successfully + تم تحديث الإعدادات بنجاح + + + + Tor website settings + Tor إعدادات متصفح + + + + Website address + عنوان المتصفح + + + + Copied + تم الاستنساخ + + + + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. + + + + + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. + + + + + When configuring WordPress set the this onion address as domain. + عند تكوين WordPress قم بتعيين عنوان ال onion هذا ك domain. + + + + Remove website + احذف متصفح + + + + The site with all data will be removed from the tor network. + سيتم حذف الموقع وجميع البيانات من الشبكة. + + + + Continue + واصل + + + + Cancel + إلغاء + + + + PageSettings + + + Settings + إعدادات + + + + Servers + الخوادم + + + + Connection + الاتصال + + + + Application + تطبيق + + + + Backup + نسخة احتياطية + + + + About AmneziaVPN + عن AmneziaVPN + + + + Close application + إغلاق التطبيق + + + + PageSettingsAbout + + 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. + هذا تطبيق مجاني و مفتوح المصدر. إذا عجبك التطبيق, ادعم المطورين ب تبرع. + وإذا لما يعجبك, فهذا سبب اكبر لدعمة - تستخدم التبرعات في تطوير التطبيق + + + + Support Amnezia + دعم Amenzia + + + + 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 application, all the more reason to support it - the donation will be used for the improving the application. + وإذا لم يعجبك التطبيق,، فهذا سبب إضافي لدعمه - سيتم استخدام التبرع لتحسين التطبيق. + + + + Card on Patreon + البطاقة علي Patreon + + + + https://www.patreon.com/amneziavpn + + + + + Show other methods on Github + اظهر المزيد علي GitHub + + + + https://github.com/amnezia-vpn/amnezia-client#donate + + + + + Contacts + التواصل + + + + Telegram group + مجموعة ال Telegram + + + + To discuss features + لمناقشة الميزات + + + + https://t.me/amnezia_vpn_en + + + + + Mail + البريد + + + + For reviews and bug reports + لل مراجعات والابلاغات عن المشاكل + + + + Github + + + + + https://github.com/amnezia-vpn/amnezia-client + + + + + Website + موقع + + + + https://amnezia.org + + + + + Software version: %1 + %1 :إصدار البرنامج + + + + Check for updates + تحقق من وجود تحديثات + + + + PageSettingsApplication + + + Application + تطبيق + + + + Allow application screenshots + اسمح بلقطات شاشة التطبيق + + + + Auto start + تشغيل تلقائي + + + Launch the application every time + شغل البرنامج كل مرة + + + starts + يبدأ + + + + Launch the application every time the device is starts + قم بتشغيل التطبيق فكل مرة يتم فيها تشغيل الجهاز + + + + Start minimized + ابدأ ب الحجم الادني + + + + Launch application minimized + تشغيل التطبيق في الحد الادني + + + + Language + اللغة + + + + Logging + تسجيل + + + + Enabled + مٌفعل + + + + Disabled + مٌعطل + + + + Reset settings and remove all data from the application + إعادة ضبط الاعدادات ومسح جميع البيانات من التطبيق + + + + Reset settings and remove all data from the application? + إعادة ضبط الاعدادات ومسح جميع البيانات من التطبيق؟ + + + + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. + سيتم ضبط الاعدادات الافتراضية. جميع خدمات AmneziaVPN المٌثبتة ستبقي علي الخادم. + + + + Continue + واصل + + + + Cancel + إلغاء + + + + PageSettingsBackup + + + Settings restored from backup file + تم إعادة الاعدادات من ملف نسخة احتياطية + + + + Backup + نسخة احتياطية + + + + Configuration backup + نسخ احتياطي للإعدادات + + + It will help you instantly restore connection settings at the next installation + سيساعدك علي إعادة إعدادات الاتصال بسرعة عند إعادة تثبيت التطبيق + + + + You can save your settings to a backup file to restore them the next time you install the application. + يمكنك حفظ الإعدادات في ملف نسخة احتياطية لأعادتهم في المرة القادمة التي تثبت فيها التطبيق. + + + + Make a backup + إضافة نسخة احتياطية + + + + Save backup file + احفظ ملف النسخه الاحتياطيه + + + + + Backup files (*.backup) + ملفات نٌسخ احتياطية (*.backup) + + + + Backup file saved + تم حفظ ملف النسخ الاحتياطي + + + + Restore from backup + استرجاع من ملف يحتوي علي نسخة احتياطية + + + + Open backup file + افتح ملف نسخ احتياطي + + + + Import settings from a backup file? + استرد الإعدادات من ملف نسخ احتياطي؟ + + + + All current settings will be reset + ستتم إعادة ضبط جميع الإعدادات الحالية + + + + Continue + واصل + + + + Cancel + إلغاء + + + + PageSettingsConnection + + + Connection + الاتصال + + + + Auto connect + الاتصال التلقائي + + + + Connect to VPN on app start + اتصل ب ال VPN عند تشغيل التطبيق + + + + When AmneziaDNS is not used or installed + عندما يكون AmneziaDNS غير مٌثبت او غير مستخدم + + + + Allows you to use the VPN only for certain Apps + يسمح لك بأستخدام ال VPN علي تطبيقات معينة + + + Use AmneziaDNS if installed on the server + استخدم AmneziaDNS إذا كان مٌثبت علي الخادم + + + + Use AmneziaDNS + استخدم AmneziaDNS + + + + If AmneziaDNS is installed on the server + في حالة كان AmneziaDNS مٌثبت علي الخادم + + + + DNS servers + خوادم DNS + + + + Site-based split tunneling + انقسام الانفاق القائم علي الموقع + + + + Allows you to select which sites you want to access through the VPN + يسمح لك بتحديد اي موقع تريد الوصول له عن طريق ال VPN + + + + App-based split tunneling + انقسام الانفاق القائم علي التطبيق + + + Split site tunneling + قسم نفق الموقع + + + Allows you to connect to some sites through a secure connection, and to others bypassing it + يسمحلك بألاتصال ببعض المواقع بسرية, وعلي الاخرين تجاوزه + + + Separate application tunneling + فرق نفق التطبيق + + + + PageSettingsDns + + + Default server does not support custom dns + الخادم الافتراضي لا يدعم DNS مخصص + + + + DNS servers + خوادم ال DNS + + + + If AmneziaDNS is not used or installed + AmneziaVPN ليس مٌستخدم او مٌثبت + + + + Primary DNS + الرئيسي DNS + + + + Secondary DNS + الثانوي DNS + + + + Restore default + استعادة الافتراضي + + + + Restore default DNS settings? + قم بأعادة ضبط إعدادات ال DNS الافتراضية؟ + + + + Continue + واصل + + + + Cancel + إلغاء + + + + Settings have been reset + لم يتم إعادة ضبط الإعدادات + + + + Save + احفظ + + + + Settings saved + تم حفظ الإعدادات + + + + PageSettingsLogging + + + Logging + التسجيل + + + + Save logs + احفظ السجلات + + + + Open folder with logs + افتح مجلد يحتوي علي سجلات + + + + Save + احفظ + + + + Logs files (*.log) + ملفات الولوج (*.log) + + + + Logs file saved + تم حفظ ملف السجل + + + + Save logs to file + احفظ السجلات في ملف + + + + Clear logs? + مسح السجلات؟ + + + + Continue + واصل + + + + Cancel + إلغاء + + + + Logs have been cleaned up + تم مسح السجلات + + + + Clear logs + احذف السجلات + + + + PageSettingsServerData + + + All installed containers have been added to the application + تمت إضافة جميع الحاويات المٌثبتة للتطبيق + + + + No new installed containers found + لم يتم العثور علي اي حاويات جديدة مٌثبتة + + + + Clear Amnezia cache + حذف ذاكرة تخزين Amnezia المؤقتة + + + + May be needed when changing other settings + قد يكون ضروري عند تغير الإعدادات الاخري + + + + Clear cached profiles? + حذف الملفات الشخصية المخزنة مؤقتاً؟ + + + + Do you want to reboot the server? + هل تريد إعادة تشغيل الخادم؟ + + + + + Do you want to clear server from Amnezia software? + هل تريد حذف الخادم من Amnezia? + + + + + + + + + + + + + + Continue + واصل + + + + + + + + + Cancel + إلغاء + + + + Check the server for previously installed Amnezia services + افحص الخادم عن اي خدمات Amnezia مٌثبتة سابقاُ + + + + Add them to the application if they were not displayed + اضفهم إلي التطبيق إذا لم يكونو ظاهرين + + + + Reboot server + إعادة تشغيل الخادم + + + + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? + عملية إعادة التشغيل قد تستغرق 30 ثانية, هل تريد الاستكمال؟ + + + + Remove server from application + احذف خادم من التطبيق + + + + Do you want to remove the server from application? + هل تريد حذف الخادم من التطبيق؟ + + + + Reset API config + إعادة تكوين API + + + + Do you want to reset API config? + هل تريد إعادة تكوين API? + + + + All installed AmneziaVPN services will still remain on the server. + جميع خدمات AmneziaVPN المٌثبتة ستظل علي الخادم. + + + + + Clear server from Amnezia software + احذف خادم من Amnezia + + + + + All containers will be deleted on the server. This means that configuration files, keys and certificates will be deleted. + سيتم حذف جميع الحاويات, هذا يعني ان جميع ملفات التكوين, شهادات و مفاتيح التعريف سيتم حذفهم. + + + + PageSettingsServerInfo + + + Server name + اسم الخادم + + + + Save + احفظ + + + + Protocols + البروتوكولات + + + + Services + الخدمات + + + + Data + البيانات + + + + PageSettingsServerProtocol + + + settings + الإعدادات + + + + Remove + احذف + + + + All users with whom you shared a connection will no longer be able to connect to it. + جميع المستخدمين الذين شاركت معاهم اتصال لن يستطيعو الاتصال بعد الان. + + + from server? + من الخادم؟ + + + + Remove %1 from server? + احذف %1 من الخادم ? + + + All users with whom you shared a connection will no longer be able to connect to it + جميع المستخدمين الذين شاركت اتصال معهم لن يستطيعو الاتصال بعد الان + + + + Continue + واصل + + + + Cancel + إلغاء + + + + PageSettingsServersList + + + Servers + الخوادم + + + + PageSettingsSplitTunneling + + Only the addresses in the list must be opened via VPN + يجب فتح العنواين التي في القائمة عبر VPN + + + Addresses from the list should never be opened via VPN + لا يجب ابداً فتح العنواين التي في القائمة عن طريق VPN + + + Split site tunneling + قسم نفق الموقع + + + + Default server does not support split tunneling function + السرفر الافتراضي لا يدعم ميزة تقسيم الانفاق + + + + Addresses from the list should not be accessed via VPN + لا يجب الولوج للعنواين المذكورة هنا من خلال ال VPN + + + + Split tunneling + تقسيم الانفاق + + + + Mode + وضع + + + + Remove + احذف + + + + Continue + واصل + + + + Cancel + إلغاء + + + + Only the sites listed here will be accessed through the VPN + سيتم الولوج للمواقع المذكورة هنا فقط عن طريق ال VPN + + + + website or IP + موقع او IP + + + + Import / Export Sites + + + + + Import + استرد + + + + Save site list + احفظ قائمة المواقع + + + + Save sites + احفظ المواقع + + + + + + Sites files (*.json) + + + + + Import a list of sites + استرد قائمة من المواقع + + + + Replace site list + تبديل قائمة المواقع + + + + + Open sites file + افتح ملف المواقع + + + + Add imported sites to existing ones + إضافة المواقع المستردة للمواقع الموجودة + + + + PageSetupWizardConfigSource + + + Server connection + اتصال الخادم + + + + Do not use connection code from public sources. It may have been created to intercept your data. + +It's okay as long as it's from someone you trust. + لا تستخدم رمز الاتصال من المصادر العامة. ربما تم إنشاؤه لاعتراض بياناتك + +لا بأس طالما انه من شخص تثق به. + + + + What do you have? + ماذا لديك؟ + + + + File with connection settings or backup + ملف إعدادات اتصال او نسخ احتياطي + + + + File with connection settings + ملف إعدادات اتصال + + + + Open config file + افتح ملف تكوين + + + + QR-code + رمز QR + + + + Key as text + مفتاح كنص + + + + PageSetupWizardCredentials + + + Configure your server + تكوين الخادم الخاص بك + + + + Server IP address [:port] + عنوان خادم IP [:منفذ] + + + + Login to connect via SSH + قم بتسجيل الدخول للأتصال عن طريق SSH + + + + Continue + واصل + + + + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties + ستظل جميع البيانات التي تدخلها سرية للغاية ولن تتم مشاركتها أو الكشف عنها ل Amnezia أو أي طرف ثالث + + + + 255.255.255.255:22 + + + + + Password or SSH private key + كلمة مرور او مفتاح SSH خاص + + + + Ip address cannot be empty + لا يمكن لعنوان IP ان يكون فارغ + + + + Enter the address in the format 255.255.255.255:88 + ادخل العنوان في شكل 255.255.255.255:88 + + + + Login cannot be empty + تسجيل دخول لا يمكن ان يكون فارغ + + + + Password/private key cannot be empty + كلمة مرور/مفتاح خاص لأ يمكن ان يكونو فارغين + + + + PageSetupWizardEasy + + + What is the level of internet control in your region? + ما هو مستوي التحكم في الانترنت في منطقتك؟ + + + + Set up a VPN yourself + قم بإعداد VPN بنفسك + + + + I want to choose a VPN protocol + اريد اختيار بروتوكول VPN + + + + Continue + واصل + + + + Set up later + إعداد في وقت لاحق + + + + PageSetupWizardInstalling + + + + Usually it takes no more than 5 minutes + عادة لا تستغرق اكثر من 5 دقائق + + + + The server has already been added to the application + تمت إضافة الخادم بالفعل للتطبيق + + + + Amnezia has detected that your server is currently + اكتشف Amnezia الخادم الخاص بك موجود حاليًا + + + + busy installing other software. Amnezia installation + مشغول بتثبيت برامج اخري, تثبيت Amnezia + + + + Cancel installation + إلغاء التثبيت + + + + will pause until the server finishes installing other software + سيتوقف مؤقتًا حتى ينتهي الخادم من تثبيت البرامج الأخرى + + + + Installing + جاري التثبيت + + + + PageSetupWizardProtocolSettings + + + Installing %1 + جاري تثبيت %1 + + + + More detailed + اكثر تفصيلاً + + + + Close + اغلق + + + + Network protocol + بروتوكول شبكة + + + + Port + منفذ + + + + Install + تثبيت + + + + PageSetupWizardProtocols + + + VPN protocol + VPN بروتوكول + + + + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. + اختر بالنسبة للأولوية القصوى بالنسبة لك. ويمكنك لاحقًا تثبيت بروتوكولات وخدمات إضافية أخرى، مثل وكيل DNS وSFTP. + + + + PageSetupWizardQrReader + + + Point the camera at the QR code and hold for a couple of seconds. + قم بتوجيه الكاميرا نحو رمز QR و اثبت لبضع ثوان. + + + + PageSetupWizardStart + + + Settings restored from backup file + تم استرداد الإعدادات من ملف نسخة احتياطية + + + + Free service for creating a personal VPN on your server. + خدمة مجانية لأنشاء VPN شخصي علي الخادم الشخصي. + + + + Helps you access blocked content without revealing your privacy, even to VPN providers. + يساعدك في الولوج للمحتوي المحظور بدون إظهار خصوصيات, حتي لمزود ال VPN. + + + + I have the data to connect + لدي البيانات المطلوبة للأتصال + + + + I have nothing + ليس لدي اي شئ + + + + https://amnezia.org/instructions/0_starter-guide + + + + + PageSetupWizardTextKey + + + Connection key + مفتاح اتصال + + + + A line that starts with vpn://... + يجب ان تٌكتب بهذه الطريقة حتي بوجود الخطأ كي تظهر بشكل صحيح داخل التطبيق + سطر يبدأ ب ...//:vpn + + + + Key + مفتاح + + + + Insert + ادخل + + + + Continue + واصل + + + + PageSetupWizardViewConfig + + + New connection + اتصال جديد + + + + Do not use connection code from public sources. It could be created to intercept your data. + لا تستخدم رمز الاتصال من مصادر مفتوحة, قد تكون مصنوعة للتعارض مع بياناتك. + + + + Collapse content + طي المحتوي + + + + Show content + اظهر المحتوي + + + + Connect + اتصل + + + + PageShare + + + Save OpenVPN config + احفظ تكوين OpenVPN + + + + Save WireGuard config + احفظ تكوين WireGuard + + + + Save ShadowSocks config + احفظ تكوين ShadowSocks + + + + Save Cloak config + احفظ تكوين Cloak + + + + For the AmneziaVPN app + AmneziaVPN من اجل تطبيق + + + + OpenVpn native format + تنسيق OpenVpn الاصلي + + + + WireGuard native format + تنسيق WireGuard الاصلي + + + + ShadowSocks native format + تنسيق ShadowSocks الاصلي + + + + Cloak native format + تنسيق Cloak الاصلي + + + + Share VPN Access + شارك اتصال VPN + + + + Share full access to the server and VPN + شارك ولوج كامل للخادم و ال VPN + + + + Use for your own devices, or share with those you trust to manage the server. + استخدمه للأجهزة الخاصة بك، أو شاركه مع من تثق بهم لإدارة الخادم. + + + + + Users + المستخدمين + + + + Share VPN access without the ability to manage the server + شارك اتصال VPN بدون القدرة علي إدارة الخادم + + + + Search + ابحث + + + + Creation date: + تاريخ الإنشاء: + + + + Rename + إعادة التسمية + + + + Client name + اسم العميل + + + + Save + احفظ + + + + Revoke + سحب وإبطال + + + + Revoke the config for a user - %1? + سحب وإبطال للمستخدم - %1? + + + + The user will no longer be able to connect to your server. + المستخدم لن يكون قادر علي الاتصال بعد الان. + + + + Continue + واصل + + + + Cancel + إلغاء + + + + Connection + الاتصال + + + Full access to server + ولوج كامل للخادم + + + Servers + الخوادم + + + + + Server + خادم + + + + File with connection settings to + ملف بإعدادات إلي + + + Protocols + البروتوكولات + + + + + Protocol + بروتوكول + + + + Connection to + اتصال إلي + + + + Config revoked + تم سحب وإبطال التكوين + + + + User name + اسم المستخدم + + + + + Connection format + تنسيق الاتصال + + + + + Share + شارك + + + + PageShareFullAccess + + + Full access to the server and VPN + ولوج كامل للخادم و ال VPN + + + + We recommend that you use full access to the server only for your own additional devices. + + نحن ننصحك بأستخدام ولوج كامل للخادم فقط لأجهزتك الاضافية. + + + + + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. + إذا شاركت ولوج كامل مع الاشخاص, سيكونو قادرين علي حذف وإضافة بروتوكولات و خدمات إلي الخادم, والذي سيجعل VPN يعمل بشكل غير صحيح لجميع المستخدمين. + + + + + Server + خادم + + + + Accessing + التواصل + + + + File with accessing settings to + ملف مع إعدادات الوصول إلي + + + + Share + مشاركة + + + + Connection to + اتصال إلي + + + + File with connection settings to + معلف مع إعدادات الاتصال إلي + + + + PopupType + + + Close + اغلاق + + + + QKeychain::DeletePasswordJobPrivate + + + Password entry not found + لم يتم العثور علي مدخلات كلمة المرور + + + + Could not decrypt data + فشل فك تشفير البيانات + + + + + Unknown error + خطأ غير معروف + + + + Could not open wallet: %1; %2 + فشل فتح المحفظة: %1; %2 + + + + Password not found + لم يتم العثور علي كلمة المرور + + + + Could not open keystore + فشل فتح مخزن المفاتيح + + + + Could not remove private key from keystore + فشل حذف المفتاح الخاص من مخزن المفاتيح + + + + QKeychain::JobPrivate + + + Unknown error + خطأ غير معروف + + + + Access to keychain denied + الولو سلسلة المفاتيح محظور + + + + QKeychain::PlainTextStore + + + Could not store data in settings: access error + فشل تخزين البيانات في الإعدادات: خطأ ولوج + + + + Could not store data in settings: format error + فشل تخزين البيانات في الإعدادات: خطأ في التنسيق + + + + Could not delete data from settings: access error + فشل في حذف البيانات من الإعدادات: خطأ ولوج + + + + Could not delete data from settings: format error + فشل حذف البيانات من الإعدادات: خطأ في التنسيق + + + + Entry not found + لم يتم العثور علي المدخلات + + + + QKeychain::ReadPasswordJobPrivate + + + Password entry not found + لم يتم العثور علي مدخلات كلمة المرور + + + + + Could not decrypt data + فشل فك تشفير البيانات + + + + D-Bus is not running + D-Bus لا يعمل + + + + + Unknown error + خطأ غير معروف + + + + No keychain service available + + + + + Could not open wallet: %1; %2 + فشل فتح المحفظة: %1; %2 + + + + Access to keychain denied + الولوج إلي سلسة المفاتيح محظور + + + + Could not determine data type: %1; %2 + فشل تحديد نوع البيانات: %1; %2 + + + + + Entry not found + لم يتم العثور علي المدخلات + + + + Unsupported entry type 'Map' + نوع مدخلات غير مٌدعم 'Map' + + + + Unknown kwallet entry type '%1' + نوع المدخلات kwaller غير معروف '%1' + + + + Password not found + لم يتم العثور علي كلمة المرور + + + + Could not open keystore + فشل في فتح مخزن المفاتيح + + + + Could not retrieve private key from keystore + فشل استرداد المفتاح الخاص من مخزن المفاتيح + + + + Could not create decryption cipher + فشل في إنشاء شفرة فك تشفير + + + + QKeychain::WritePasswordJobPrivate + + + Credential size exceeds maximum size of %1 + حجم الاعتماد يتجاوز الحجم الاقصي ل: %1 + + + + Credential key exceeds maximum size of %1 + مفتاح الاعتماد يتجاوز الحد الاقصي ل: %1 + + + + Writing credentials failed: Win32 error code %1 + فشل في كتابة الاعتماد: Win32 خطأ: %1 + + + + Encryption failed + فشل التشفير + + + + D-Bus is not running + D-Bus لا يعمل + + + + + Unknown error + خطأ غير معروف + + + + Could not open wallet: %1; %2 + فشل في فتح المحفظة: %1; %2 + + + + Password not found + لم يتم العثور علي كلمة المرور + + + + Could not open keystore + فشل في فتح مخزن المفاتيح + + + + Could not create private key generator + فشل ف إنشاء مولد المفاتيح الخاصة + + + + Could not generate new private key + فشل في إنشاء مفتاح خاص جديد + + + + Could not retrieve private key from keystore + فشل في استرداد مفتاح خاص من مخزن المفاتيح + + + + Could not create encryption cipher + فشل في إنشاء شفرة التشفير + + + + Could not encrypt data + فشل في تشفير الداتا + + + + QObject + + + Sftp service + خدمة Sftp + + + + No error + لا يوجد خطأ + + + + Unknown Error + خطأ غير معروف + + + + Function not implemented + لم يتم تنفيذ الوظيفة + + + + Server check failed + فشل في فحص الخادم + + + + Server port already used. Check for another software + منفذ الخادم بالفعل مٌستخدم, تحقق من باقي التطبيقات + + + + Server error: Docker container missing + خطأ من الخادم: حاوية Docker مفقودة + + + + Server error: Docker failed + خطأ من الخادم: فشل Docker + + + + Installation canceled by user + تم اغلاق التثبيت بواسطة المستخدم + + + + The user does not have permission to use sudo + ليس لدي المستخدم الصلحيات لأستخدام sudo + + + + Ssh request was denied + طلب Ssh محظو + + + + Ssh request was interrupted + إنقطع طلب Ssh + + + + Ssh internal error + مشكلة داخلية Ssh + + + + Invalid private key or invalid passphrase entered + مفتا ح خاص غير صحيح او عبارة مرور غير صحيحة + + + + The selected private key format is not supported, use openssh ED25519 key types or PEM key types + التنسيق المٌحدد للمفتاح الخاص غير مدعوم, استخدم نوع مفتاح openssh ED25519 او نوع مفتاح PEM + + + + Timeout connecting to server + انتهت مدة الاتصال بالخادم + + + + Sftp error: End-of-file encountered + + + + + Sftp error: File does not exist + خطأ Sftp: الملف غير موجود + + + + Sftp error: Permission denied + خطأ Sftp: تم حظر الصلحيات + + + + Sftp error: Generic failure + خطأ Sftp: فشل عام + + + + Sftp error: Garbage received from server + خطأ Sftp: تم استلام نفايات من الخادم + + + + Sftp error: No connection has been set up + خطأ Sftp: لم يتم إعداد اتصال + + + + Sftp error: There was a connection, but we lost it + خطأ Sftp: كان هناك اتصال, ولكن خسرناه + + + + Sftp error: Operation not supported by libssh yet + خطأ Sftp: العملية ليست مدعومة من libssh بعد + + + + Sftp error: Invalid file handle + + + + + Sftp error: No such file or directory path exists + خطأ Sftp: لا يوجد مسار ملف او مجلد مثل هذا + + + + Sftp error: An attempt to create an already existing file or directory has been made + خطأ Sftp: محاولة إنشاء ملف او مجلد موجود بالفعل + + + + Sftp error: Write-protected filesystem + خطأ Sftp: نظام كتابة الملفات محمي + + + + Sftp error: No media was in remote drive + خطأ Sftp: لا يوجد وسائط في القرص البعيد + + + + VPN connection error + + + + + Error when retrieving configuration from API + خطأ عند استرداد التكوين من API + + + + This config has already been added to the application + هذا التكوين بالفعل تمت إضافتة للبرنامج + + + + ErrorCode: %1. + + + + + OpenVPN config missing + OpenVpn تكوين مفقود + + + + OpenVPN management server error + OpenVpn خطأ في إدارة الخادم + + + + OpenVPN executable missing + OpenVpn executeable مفقود + + + + ShadowSocks (ss-local) executable missing + ShadowSocks (ss-local) executable مفقود + + + + Cloak (ck-client) executable missing + Cloak (ck-client) executable مفقود + + + + Amnezia helper service error + خطأ في خدمة مٌساعد Amnezia + + + + OpenSSL failed + فشل OpenSSL + + + + Can't connect: another VPN connection is active + لا يمكن الاتصال: هناك اتصال VPN اخر بالفعل يعمل + + + + Can't setup OpenVPN TAP network adapter + لا يمك نتثبيت محول شبكة OpenVPN TAP + + + + VPN pool error: no available addresses + VPN pool error: لا يوجد عنواين مٌتاحة + + + + The config does not contain any containers and credentials for connecting to the server + التكوين لا يحتوي علي اي حاويات و اعتماد للأتصال بالخادم + + + + Internal error + خطأ داخلي + + + + IPsec + + + + + + Website in Tor network + موقع في شبكة Tor + + + + Amnezia DNS + + + + + Sftp file sharing service + ملف Sftp: خدمة المشاركة + + + + OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its own security protocol with SSL/TLS for key exchange. + بروتوكول OpenVPN احد اشهر بروتوكولات VPN, مع مرونة في إعدادات التكوين. يستخدم بروتوكول امان خاص به مع SSL/TLS لتغير المفاتيح. + + + + ShadowSocks - masks VPN traffic, making it similar to normal web traffic, but it may be recognized by analysis systems in some highly censored regions. + بروتوكول ShadowSocks- يتنكر في حركة مرور VPN, يبدو ك حركة مرور الويب العادية +ولكن قد يتم التعرف عليه من خلال أنظمة التحليل في بعض المناطق شديدة الرقابة. + + + + OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. + بروتوكول OpenVPN over Cloak هو OpenVPN مع VPN يتنكر كحركة مرور على الويب ويوفر الحماية + ضد عمليات الكشف النشط. مثالية لتجاوز الحجب في المناطق ذات أعلى مستويات الرقابة. + + + + Create a file vault on your server to securely store and transfer files. + انشأ مخزن ملفات علي الخادم الخاص بك حتي تخزن الملفات و تنقلها بسرية. + + + + This is a combination of the OpenVPN protocol and the Cloak plugin designed specifically for protecting against blocking. + +OpenVPN provides a secure VPN connection by encrypting all internet traffic between the client and the server. + +Cloak protects OpenVPN from detection and blocking. + +Cloak can modify packet metadata so that it completely masks VPN traffic as normal web traffic, and also protects the VPN from detection by Active Probing. This makes it very resistant to being detected + +Immediately after receiving the first data packet, Cloak authenticates the incoming connection. If authentication fails, the plugin masks the server as a fake website and your VPN becomes invisible to analysis systems. + +If there is a extreme level of Internet censorship in your region, we advise you to use only OpenVPN over Cloak from the first connection + +* Available in the AmneziaVPN across all platforms +* High power consumption on mobile devices +* Flexible settings +* Not recognised by DPI analysis systems +* Works over TCP network protocol, 443 port. + + هذه مجموعة من بروتوكول OpenVPN و برنامج Cloak المساعد مٌصمم خصيصاً للحماية ضد الحجب + +يوفر OpenVPN اتصال VPN امن عن طريق تشفير جميع حركات المرور بين العميل والخادم + +Cloak يحمي OpenVPN من ان يٌكتشف والحجب + +يمكن ان يعدل Cloak حزمة البيانات حتي يجعل حركة مرور VPN تبدو بالكامل كحركة ويب طبيعية, +وايضاُ يحمي ال VPN من ان يٌكتشف عن طريق انظمة الكشف الفعالة. هذا يجعلة مقاوم جداُ لأن يٌكتشف + +فوراُ بعد استلام اول حزمة بيانات, يصادق Cloak الاتصال القادم. +إذا فشل التصادق, البرنامج المساعد يجعل الخادم يبدو ك موقع مزيف ويصبح ال VPN مخفي لأنظمة التحليل. + +إذا كان هناك رقابة شديدة علي الانترنت في منطقتك, نحن ننصحك بأن تستخدم OpenVPN over Cloak من اول اتصال + +* مٌتاح في AmneziaVPN عبر جميع المنصات +* استهلاك طاقة عالية علي اجهزة المحمول +* مرونة في الإعدادات +* لا يٌكتشف بواسطة انظمة تحليل DPI +* يعمل عبر بروتوكول شبكة TCK, منفذ 443. + + + + + A relatively new popular VPN protocol with a simplified architecture. +WireGuard provides stable VPN connection and high performance on all devices. It uses hard-coded encryption settings. WireGuard compared to OpenVPN has lower latency and better data transfer throughput. +WireGuard is very susceptible to blocking due to its distinct packet signatures. Unlike some other VPN protocols that employ obfuscation techniques, the consistent signature patterns of WireGuard packets can be more easily identified and thus blocked by advanced Deep Packet Inspection (DPI) systems and other network monitoring tools. + +* Available in the AmneziaVPN across all platforms +* Low power consumption +* Minimum number of settings +* Easily recognised by DPI analysis systems, susceptible to blocking +* Works over UDP network protocol. + بروتوكول VPN جديد وشارع ذو بنية مبسطة. +يوفر WireGuard اتصال VPN مستقر و اداء عالي علي جميع الاجهزة. يستعمل إعدادات تشفير معقدة. WireGuard مٌقارنة مع OpenVPN يتمتع بزمن وصول أقل وتحسين إنتاجية نقل البيانات. +بسبب توقيعات الحزمة المميزة WireGuard عرضة جداُ للحجب. علي عكس باقي برتوكولات VPN التي تستعمل تقنيات تشويش. حزمة أنماط التوقيع المتسقة الخاصة ب WireGuard يمكن التعرف عليها بسهولة ولذلك تٌحجب بواسطة أنظمة الفحص العميق للحزم (DPI) المتقدمة وأدوات مراقبة الشبكة الأخرى. + +* مٌتاح في AmneziaVPN عبر جميع المنصات +* استهلاك قليل للطاقة +* عدد قليل من الإعدادات +سهل التعرف علية بواسطة انظمة تحليل DPI, عرضة للحجب +* يعمل عبر بروتوكول شبكة UDP. + + + + WireGuard - New popular VPN protocol with high performance, high speed and low power consumption. Recommended for regions with low levels of censorship. + بروتوكول WireGuard - بروتوكول شائع ب اداء عالي, سرعة عالية واستهلاك قليل للطاقة. ينصح للمناطق ذات مستوي منخفض من الرقابة. + + + + AmneziaWG - Special protocol from Amnezia, based on WireGuard. It's fast like WireGuard, but very resistant to blockages. Recommended for regions with high levels of censorship. + بروتوكول AmneziaWG - بروتوكول خاص من Amnezia, يعتمد علي WireGuard. سريع مثل WireGuard, لكن مقاوم جداً للحجب. ينصح للمناطق ذات مستوي عالي من الرقابة. + + + + IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS. + بروتوكول IKEv2 - بروتوكول مستقر حديث, اسرع بقليل من الباقي, يسترجع الاتصال بعد خسارة الاشارة. لدية يتمتع بدعم أصلي على أحدث إصدارات Android وiOS. + + + + Deploy a WordPress site on the Tor network in two clicks. + انشر موقع WordPress علي شبكة Tor في ضغطتين. + + + + Replace the current DNS server with your own. This will increase your privacy level. + استبدل خادم ال DNS الحالي مع الخادم الخاص بك, هذا سيزيد من خصوصيتك. + + + + OpenVPN stands as one of the most popular and time-tested VPN protocols available. +It employs its unique security protocol, leveraging the strength of SSL/TLS for encryption and key exchange. Furthermore, OpenVPN's support for a multitude of authentication methods makes it versatile and adaptable, catering to a wide range of devices and operating systems. Due to its open-source nature, OpenVPN benefits from extensive scrutiny by the global community, which continually reinforces its security. With a strong balance of performance, security, and compatibility, OpenVPN remains a top choice for privacy-conscious individuals and businesses alike. + +* Available in the AmneziaVPN across all platforms +* Normal power consumption on mobile devices +* Flexible customisation to suit user needs to work with different operating systems and devices +* Recognised by DPI analysis systems and therefore susceptible to blocking +* Can operate over both TCP and UDP network protocols. + يبقا OpenVPN كأحد اشهر بروتوكولات VPN و التي تم اختبارها عبر الزمن. +ينشأ بروتوكول امان مميز, يستفيد من SSL/TLS للتشفير و تغير المفاتيح. واكثر من ذلك, OpenVpn يدعم تعدد طرق المصادقة يجعلة متعدد الاستخدامات وقابلة للتكيف, تلبية مجموعة واسعة من الأجهزة وأنظمة التشغيل. بسبب طبيعتة مفتوحة المصدر, يستفيد OpenVPN من التدقيق الشامل من قبل المجتمع العالمي, مما يعزز أمنها باستمرار. مع توازن قوي بين الأداء والأمان والتوافق, يظل OpenVPN الخيار الأفضل للأفراد والشركات المهتمين بالخصوصية على حدٍ سواء. + +* مٌتاح في AmneziaVPN عبر جميع المنصات +* استهلاك طاقة عادي علي اجهزة المحمول +* مرونة في التخصيص كي يلائم احتياجات المستخدم حتي يعمل مع انظمة تشغيل واجهزة مختلفة +* يٌلاحظ بواسطة انظمة تحليل DPI و لذلك عرضة للحجب +* يمكن ان يعمل علي بروتوكولات شبكة TCP و UDP. + + + + Shadowsocks, inspired by the SOCKS5 protocol, safeguards the connection using the AEAD cipher. Although Shadowsocks is designed to be discreet and challenging to identify, it isn't identical to a standard HTTPS connection.However, certain traffic analysis systems might still detect a Shadowsocks connection. Due to limited support in Amnezia, it's recommended to use AmneziaWG protocol. + +* Available in the AmneziaVPN only on desktop platforms +* Normal power consumption on mobile devices + +* Configurable encryption protocol +* Detectable by some DPI systems +* Works over TCP network protocol. + Shadowsocks, مستوحي من بروتوكول SOCKS5, يحمي الاتصال بأستعمال شفرة AEAD. كذلك Shadowsocks صٌمم كي يكون متحفظاً ويصعب تحديدة, إنه ليس مطابقًا لاتصال HTTPS القياسي. عمتاُ. بعض انظمة تحليل حركات المرور قد تتعرف علي اتصال Shadowsocks. بسبب الدعم المحدود في Amnezia, يٌنصح بأستخدام بروتوكول AmneziaWG. + +* مٌتاح في AmneziaVPN عبر جميع المنصات +* استهلاك طاقة عادي علي اجهزة المحمول + +* بروتوكول تشفير قابل للتكوين +* قابل للكشف بواسطة بعض انظمة DPI +* يعمل عبر بروتوكول شبكة TCP. + + + + A modern iteration of the popular VPN protocol, AmneziaWG builds upon the foundation set by WireGuard, retaining its simplified architecture and high-performance capabilities across devices. +While WireGuard is known for its efficiency, it had issues with being easily detected due to its distinct packet signatures. AmneziaWG solves this problem by using better obfuscation methods, making its traffic blend in with regular internet traffic. +This means that AmneziaWG keeps the fast performance of the original while adding an extra layer of stealth, making it a great choice for those wanting a fast and discreet VPN connection. + +* Available in the AmneziaVPN across all platforms +* Low power consumption +* Minimum number of settings +* Not recognised by DPI analysis systems, resistant to blocking +* Works over UDP network protocol. + لفة سريعة من بروتوكولات VPN الحديثة والشائعة, يٌبني AmneziaWG علي الاساس الموضع من قبل WireGuard, مع الاحتفاظ ببنيته المبسطة وقدرات الأداء العالي عبر الاجهزة. +بينما WireGuard معروف بأدائة العالي. لدية مشاكل مع سهولة التعرف علية بسبب توقيعات الحزمة المميزة الخاصة بة. يٌصلح AmneziaWG هذه المشكلة عن طريق استخدام طرق تشويش افضل, يجعل حركة المرور تبقا مع حركة مرور انترنت عادية. +هذا يعني ان AmneziaWG يبقا الاداء العالي الاساسي بينما يضيف طبقة من العزل, هذا يجعلة اختيار ممتاز لهولاء الذين يريدون اتصال VPN سريع و متخفي. + +* مٌتاح في AmneziaVPN علي جميع المنصات +* استهلاك طاقة قليل +* اقل عدد من الإعدادات +* لا يٌكتشف من قبل انظمة تحليل DPI, مقاوم للحجب +* يعمل عبر بروتوكول شبكة UDP. + + + + IKEv2, paired with the IPSec encryption layer, stands as a modern and stable VPN protocol. +One of its distinguishing features is its ability to swiftly switch between networks and devices, making it particularly adaptive in dynamic network environments. +While it offers a blend of security, stability, and speed, it's essential to note that IKEv2 can be easily detected and is susceptible to blocking. + +* Available in the AmneziaVPN only on Windows +* Low power consumption, on mobile devices +* Minimal configuration +* Recognised by DPI analysis systems +* Works over UDP network protocol, ports 500 and 4500. + IKEv2, مقترن مع طبقة التشفير IPSec, يبقا بروتوكول VPN مستقر و حديث. +من مميزاتةقدرته على التبديل بسرعة بين الشبكات والأجهزة، مما يجعله قابلاً للتكيف بشكل خاص في بيئات الشبكات الديناميكية. + +*. مٌتاح في AmneziaVPN فقط علي منصة وندوز +* استهلاك طاقة قليل, علي اجهزة المحمول +* اقل تكوين +* يٌلاحظ بواسطة انظمة تحليل DPI +* يعمل عبر بروتوكول شبكة UDP, منفذ 500 و منفذ 4500. + + + + DNS Service + خدمة ال DNS + + + + Sftp file sharing service - is secure FTP service + خدمة نشر ملف Sftp - هي خدمة FTP امنة + + + + Entry not found + لم يتم العثور علي مدخلات + + + + Access to keychain denied + الولوج ل سلسلة المفاتيح محظور + + + + No keyring daemon + + + + + Already unlocked + بالفعل تم فتحة + + + + No such keyring + لا يوجد مثل هذه المفاتيح + + + + Bad arguments + معطيات سيئة + + + + I/O error + I/0 خطأ + + + + Cancelled + تم إغلاقة + + + + Keyring already exists + المفتاح موجود بالفعل + + + + No match + لا تطباق + + + + Unknown error + خطأ غير معروف + + + + error 0x%1: %2 + خطأ %1: %2 + + + + SelectLanguageDrawer + + + Choose language + اختر لغة + + + + Settings + + + Server #1 + خادم #1 + + + + + Server + خادم + + + + SettingsController + + + Backup file is corrupted + ملف النسخه الاحتياطيه تالف + + + + All settings have been reset to default values + تم استرجاع جميع الإعدادات للإعدادات الافتراضية + + + + Cached profiles cleared + تم حذف الملفات الشخصية المٌخزنة مؤقتاُ + + + + ShareConnectionDrawer + + + + Save AmneziaVPN config + احفظ تكوين AmneziaVPN + + + + Share + شارك + + + + Copy + انسخ + + + + + Copied + تم النسخ + + + + Copy config string + انسخ نص التكوين + + + + Show connection settings + اظهر إعدادات الاتصال + + + Show content + 展示内容 + + + + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" + حتي تقرأ رمز ال QR في تطبيق Amnezia, اختار "إضافة خادم" - "لدي بيانات الاتصال" - "رمز Qr, او مفتاح تعريف او ملف إعدادات" + + + + SitesController + + + Hostname not look like ip adress or domain name + اسم المضيف لا يشبه عنوان IP أو اسم ال domain + + + + New site added: %1 + تمت إضافة موقع جديد: %1 + + + + Site removed: %1 + تم حذف الموقع: %1 + + + + Can't open file: %1 + لا يمكن فتح ملف: %1 + + + + Failed to parse JSON data from file: %1 + فشل قراءه بيانات JSON من الملف: %1 + + + + The JSON data is not an array in file: %1 + بيانات ال JSON ليست مصفوفة في الملف: %1 + + + + Import completed + اكتمل الاستيراد + + + + Export completed + اكتمل التصدير + + + + SystemTrayNotificationHandler + + + + Show + اظهر + + + + + Connect + اتصل + + + + + Disconnect + اقطع الاتصال + + + + + Visit Website + زور الموقع + + + + + Quit + اغلاق + + + + TextFieldWithHeaderType + + + The field can't be empty + الحقل لا يمكن ان يكون فارغ + + + + VpnConnection + + + Mbps + + + + + VpnProtocol + + + Unknown + غير معرف + + + + Disconnected + انقطع الاتصال + + + + Preparing + جاري التحضير + + + + Connecting... + جاري الاتصال... + + + + Connected + تم الاتصال + + + + Disconnecting... + جاري قطع الاتصال... + + + + Reconnecting... + جاري إعادة الاتصال... + + + + Error + خطأ + + + + amnezia::ContainerProps + + + Low + منخفض + + + + Medium or High + متوسط او عالي + + + + Extreme + شديد + + + + I just want to increase the level of my privacy. + انا فقط اريد زيادة مستوي الخصوصية. + + + + I want to bypass censorship. This option recommended in most cases. + أريد تجاوز الرقابة. يوصى بهذا الخيار في معظم الحالات. + + + + Most VPN protocols are blocked. Recommended if other options are not working. + يتم حظر معظم بروتوكولات VPN. يوصى به إذا كانت الخيارات الأخرى لا تعمل. + + + + main2 + + + Private key passphrase + عبارة المرور الخاصة بالمفتاح + + + + Save + احفظ + + + diff --git a/client/translations/amneziavpn_fa_IR.ts b/client/translations/amneziavpn_fa_IR.ts index 488d6564..9ae98d19 100644 --- a/client/translations/amneziavpn_fa_IR.ts +++ b/client/translations/amneziavpn_fa_IR.ts @@ -144,7 +144,7 @@ Reconnect via VPN Procotol: - Переподключение через VPN протокол: + پروتکل VPN را متصل مجدد کنید" @@ -371,7 +371,7 @@ Already installed containers were found on the server. All installed containers All users who you shared a connection with will no longer be able to connect to it. - Все пользователи, с которыми вы поделились этим VPN-протоколом, больше не смогут к нему подключаться. + همه کاربرانی که با آن‌ها این پروتکل VPN را به اشتراک گذاشته‌اید دیگر نمی‌توانند به آن متصل شوند. @@ -604,7 +604,7 @@ Already installed containers were found on the server. All installed containers All users who you shared a connection with will no longer be able to connect to it. - Все пользователи, с которыми вы поделились этим VPN-протоколом, больше не смогут к нему подключаться. + همه کاربرانی که با آن این پروتکل VPN را به اشتراک گذاشته‌اید دیگر نمی‌توانند به آن متصل شوند. @@ -656,7 +656,7 @@ Already installed containers were found on the server. All installed containers All users who you shared a connection with will no longer be able to connect to it. - Все пользователи, с которыми вы поделились этим VPN-протоколом, больше не смогут к нему подключаться. + همه کاربرانی که با آن این پروتکل VPN را به اشتراک گذاشته‌اید دیگر نمی‌توانند به آن متصل شوند. @@ -697,7 +697,7 @@ Already installed containers were found on the server. All installed containers PageServerContainers Continue - Продолжить + ادامه دهید @@ -847,13 +847,13 @@ Already installed containers were found on the server. All installed containers - Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. + استفاده <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> برای باز کردن این نشانی. After creating your onion site, it takes a few minutes for the Tor network to make it available for use. - + پس از ایجاد سایت پیاز خود، چند دقیقه طول می‌کشد تا شبکه تور آن را برای استفاده فراهم کند. Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this url. @@ -870,7 +870,7 @@ Already installed containers were found on the server. All installed containers When configuring WordPress set the this address as domain. - При настройке WordPress укажите этот onion адрес в качестве домена. + هنگام تنظیم وردپرس، این آدرس پیاز را به عنوان دامنه مشخص کنید. @@ -940,7 +940,7 @@ Already installed containers were found on the server. All installed containers Support Amnezia - + پشتیبانی از Amnezia @@ -1022,6 +1022,11 @@ Already installed containers were found on the server. All installed containers https://amnezia.org https://amnezia.org + + + Software version: %1 + %1 :نسخه نرم‎افزار + Check for updates @@ -1215,7 +1220,7 @@ Already installed containers were found on the server. All installed containers When AmneziaDNS is not used or installed - + وقتی AmneziaDNS استفاده نشده یا نصب نشده است. @@ -1251,7 +1256,7 @@ Already installed containers were found on the server. All installed containers Default server does not support custom dns - + سرور پیش‌فرض از دی‌ان‌اس سفارشی پشتیبانی نمی‌کند. @@ -1433,27 +1438,27 @@ Already installed containers were found on the server. All installed containers Reboot server - + سرور را دوباره راه‌اندازی کنید. Do you want to reboot the server? - + آیا می‌خواهید سرور را دوباره راه‌اندازی کنید؟ The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? - + فرآیند راه‌اندازی ممکن است حدود ۳۰ ثانیه طول بکشد. آیا مطمئن هستید که می‌خواهید ادامه دهید؟ Do you want to remove the server from application? - + آیا می‌خواهید سرور را از برنامه حذف کنید؟ Do you want to clear server from Amnezia software? - + آیا می‌خواهید سرور را از نرم‌افزار Amnezia پاک کنید؟ @@ -1536,7 +1541,7 @@ Already installed containers were found on the server. All installed containers All users who you shared a connection with will no longer be able to connect to it. - Все пользователи, которым вы поделились VPN, больше не смогут к нему подключаться. + تمام کاربرانی که با آن VPN را به اشتراک گذاشته‌اید، دیگر نمی‌توانند به آن متصل شوند. @@ -1562,7 +1567,7 @@ Already installed containers were found on the server. All installed containers Default server does not support split tunneling function - + سرور پیش‌فرض از عملکرد تونل‌سازی تقسیم شده پشتیبانی نمی‌کند. Addresses from the list should be accessed via VPN @@ -1609,17 +1614,17 @@ Already installed containers were found on the server. All installed containers Only the sites listed here will be accessed through the VPN - + تنها سایت‌های موجود در اینجا از طریق VPN دسترسی داده خواهند شد. website or IP - + وب‌سایت یا آدرس IP Import / Export Sites - + وارد کردن / صادر کردن وب‌سایت‌ها @@ -1716,7 +1721,7 @@ It's okay as long as it's from someone you trust. PageSetupWizardCredentials Server connection - Подключение к серверу + اتصال به سرور @@ -1823,12 +1828,12 @@ and will not be shared or disclosed to the Amnezia or any third parties سرور در حال حاضر به نرم‎افزار اضافه شده است - Amnesia has detected that your server is currently - Amnesia обнаружила, что ваш сервер в настоящее время + Amnezia has detected that your server is currently + Amnezia has detected that your server is currently - busy installing other software. Amnesia installation - занят установкой других протоколов или сервисов. Установка Amnesia + busy installing other software. Amnezia installation + busy installing other software. Amnezia installation @@ -2019,7 +2024,7 @@ and will not be shared or disclosed to the Amnezia or any third parties VPN Access - VPN-Доступ + دسترسی VPN @@ -2028,11 +2033,11 @@ and will not be shared or disclosed to the Amnezia or any third parties VPN access without the ability to manage the server - Доступ к VPN, без возможности управления сервером + دسترسی به VPN بدون امکان مدیریت سرور Access to server management. The user with whom you share full access to the connection will be able to add and remove your protocols and services to the server, as well as change settings. - Доступ к управлению сервером. Пользователь, с которым вы делитесь полным доступом к соединению, сможет добавлять и удалять ваши протоколы и службы на сервере, а также изменять настройки. + دسترسی به مدیریت سرور. کاربری که با او دسترسی کامل به اتصال را به اشتراک می‌گذارید، می‌تواند پروتکل‌ها و سرویس‌های شما را در سرور اضافه و حذف کند، همچنین تنظیمات را تغییر دهد. @@ -2132,7 +2137,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Creation date: - + تاریخ ایجاد: @@ -2157,7 +2162,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Revoke the config for a user - %1? - + لغو پیکربندی برای یک کاربر %1؟ Revoke the config for a user - @@ -2732,17 +2737,17 @@ and will not be shared or disclosed to the Amnezia or any third parties ShadowSocks - masks VPN traffic, making it similar to normal web traffic, but it may be recognized by analysis systems in some highly censored regions. - + شدوساکس - ترافیک VPN را پنهان می کند، به طوری که مشابه ترافیک وب عادی می شود، اما ممکن است توسط سیستم های تجزیه و تحلیل در برخی از مناطق با سانسور شدید شناسایی شود. OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. - + OpenVPN روی Cloak - OpenVPN با VPN که به عنوان ترافیک وب پنهان می‌شود و مقاومت در برابر تشخیص فعال از طریق پیشرفته. ایده‌آل برای دور زدن مسدود کردن در مناطق با بالاترین سطوح سانسور Create a file vault on your server to securely store and transfer files. - + ساختن یک گنجانده فایل بر روی سرور شما برای ذخیره و انتقال ایمن فایل‌ها @@ -2764,7 +2769,23 @@ If there is a extreme level of Internet censorship in your region, we advise you * Not recognised by DPI analysis systems * Works over TCP network protocol, 443 port. - + این ترکیبی از پروتکل OpenVPN و پلاگین Cloak به طور خاص برای محافظت در برابر مسدود کردن طراحی شده است. + +OpenVPN ارتباط امن VPN را با رمزگذاری تمام ترافیک اینترنتی بین مشتری و سرور فراهم می‌کند. + +Cloak OpenVPN را از شناسایی و مسدود کردن محافظت می‌کند. + +Cloak می‌تواند اطلاعات فراداده بسته را تغییر دهد تا ترافیک VPN را به طور کامل به عنوان ترافیک وب عادی پنهان کند و همچنین VPN را از شناسایی توسط Active Probing محافظت کند. این باعث می‌شود این سیستم بسیار مقاوم در برابر شناسایی شود. + +فوراً پس از دریافت اولین بسته داده، Cloak اتصال ورودی را تأیید می‌کند. اگر تأیید اعتبار ناموفق باشد، پلاگین سرور را به عنوان یک وب‌سایت جعلی پنهان می‌کند و VPN شما برای سیستم‌های تجزیه و تحلیل غیر قابل دسترسی می‌شود. + +اگر در منطقه شما سطح بسیار بالایی از سانسور اینترنت وجود دارد، به شما توصیه می‌شود که از اولین اتصال فقط از OpenVPN over Cloak استفاده کنید. + + در دسترس در AmneziaVPN بر روی تمام پلتفرم‌ها + مصرف بالای برق در دستگاه‌های تلفن همراه + تنظیمات انعطاف پذیر + توسط سیستم‌های تجزیه و تحلیل DPI شناخته نمی‌شود + بر روی پروتکل شبکه TCP، پورت 443 کار می‌کند. @@ -3091,11 +3112,6 @@ This means that AmneziaWG keeps the fast performance of the original while addin SettingsController - - - Software version - نسخه نرم‎افزار - All settings have been reset to default values @@ -3233,7 +3249,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin The field can't be empty - Поле не может быть пустым + این فیلد نمی‌تواند خالی باشد. @@ -3321,23 +3337,23 @@ This means that AmneziaWG keeps the fast performance of the original while addin High - Высокий + بالایی Medium - Средний + متوسط Many foreign websites and VPN providers are blocked - Многие иностранные сайты и VPN-провайдеры заблокированы + بسیاری از وب‌سایت‌ها و ارائه‌دهندگان VPN خارجی مسدود شده‌اند. Some foreign sites are blocked, but VPN providers are not blocked - Некоторые иностранные сайты заблокированы, но VPN-провайдеры не блокируются + بعضی از وب‌سایت‌های خارجی مسدود شده‌اند، اما ارائه‌دهندگان VPN مسدود نمی‌شوند. I just want to increase the level of privacy - Хочу просто повысить уровень приватности + من فقط می‌خواهم سطح حریم خصوصی خود را افزایش دهم. diff --git a/client/translations/amneziavpn_ru.ts b/client/translations/amneziavpn_ru.ts index 0ccda3e8..1425197e 100644 --- a/client/translations/amneziavpn_ru.ts +++ b/client/translations/amneziavpn_ru.ts @@ -854,11 +854,11 @@ Already installed containers were found on the server. All installed containers - Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this url. + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. Используйте <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> для открытия этой ссылки. - After installation it takes several minutes while your onion site will become available in the Tor Network. + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. Через несколько минут после установки ваш Onion сайт станет доступен в сети Tor. @@ -938,7 +938,7 @@ Already installed containers were found on the server. All installed containers Support Amnezia - + Поддержите Amnezia @@ -1020,6 +1020,11 @@ Already installed containers were found on the server. All installed containers https://amnezia.org https://amnezia.org + + + Software version: %1 + Версия ПО: %1 + Check for updates @@ -1221,7 +1226,7 @@ Already installed containers were found on the server. All installed containers - If AmneziaDNS is not used or installed + When AmneziaDNS is not used or installed Эти адреса будут использоваться, если не включен AmneziaDNS @@ -1258,7 +1263,7 @@ Already installed containers were found on the server. All installed containers - If AmneziaDNS is not used or installed + When AmneziaDNS is not used or installed Эти адреса будут использоваться, если не включен или не установлен AmneziaDNS @@ -1431,27 +1436,27 @@ Already installed containers were found on the server. All installed containers Reboot server - + Перезагрузить сервер Do you want to reboot the server? - + Вы уверены, что хотите перезагрузить сервер? The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? - + Процесс перезагрузки может занять около 30 секунд. Вы уверены, что хотите продолжить? Do you want to remove the server from application? - + Вы уверена что хотите удалить сервер из приложения? Do you want to clear server from Amnezia software? - + Вы хотите очистить сервер от всех сервисов Amnezia? @@ -1563,7 +1568,7 @@ Already installed containers were found on the server. All installed containers - Addresses from the list should be accessed via VPN + Only the sites listed here will be accesed via VPN Только адреса из списка должны открываться через VPN @@ -1597,11 +1602,11 @@ Already installed containers were found on the server. All installed containers Отменить - Site or IP + Website or IP Сайт или IP - Import/Export Sites + Import / Export Sites Импорт/экспорт Сайтов @@ -2126,7 +2131,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Creation date: - + Дата создания @@ -2808,11 +2813,11 @@ While it offers a blend of security, stability, and speed, it's essential t OpenVPN - популярный VPN-протокол, с гибкой настройкой. Имеет собственный протокол безопасности с SSL/TLS для обмена ключами. - ShadowSocks - masks VPN traffic, making it similar to normal web traffic, but is recognised by analysis systems in some highly censored regions. + ShadowSocks - masks VPN traffic, making it similar to normal web traffic, but it may be recognized by analysis systems in some highly censored regions. ShadowSocks - маскирует VPN-трафик под обычный веб-трафик, но распознается системами анализа в некоторых регионах с высоким уровнем цензуры. - OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probbing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. + OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. OpenVPN over Cloak - OpenVPN с маскировкой VPN под web-трафик и защитой от обнаружения active-probbing. Подходит для регионов с самым высоким уровнем цензуры. @@ -2841,7 +2846,7 @@ While it offers a blend of security, stability, and speed, it's essential t Замените DNS-сервер на Amnezia DNS. Это повысит уровень конфиденциальности. - Creates a file vault on your server to securely store and transfer files. + Create a file vault on your server to securely store and transfer files. Создайте на сервере файловое хранилище для безопасного хранения и передачи файлов. @@ -3058,11 +3063,6 @@ This means that AmneziaWG keeps the fast performance of the original while addin SettingsController - - - Software version - Версия ПО - All settings have been reset to default values diff --git a/client/translations/amneziavpn_zh_CN.ts b/client/translations/amneziavpn_zh_CN.ts index 1f400b99..ad996ced 100644 --- a/client/translations/amneziavpn_zh_CN.ts +++ b/client/translations/amneziavpn_zh_CN.ts @@ -200,7 +200,7 @@ Already installed containers were found on the server. All installed containers Server '%1' was rebooted - + 服务器 '%1' 已重新启动 @@ -891,10 +891,10 @@ Already installed containers were found on the server. All installed containers After creating your onion site, it takes a few minutes for the Tor network to make it available for use. - + 创建您的洋葱网站后,需要几分钟时间,才能使其在Tor网络上可用 - Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this url. + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. 用 <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor 浏览器</a> 打开上面网址 @@ -984,7 +984,7 @@ And if you don't like the app, all the more support it - the donation will Support Amnezia - + 支持Amnezia @@ -1067,6 +1067,11 @@ And if you don't like the app, all the more support it - the donation will https://amnezia.org + + + Software version: %1 + 软件版本: %1 + Check for updates @@ -1261,7 +1266,7 @@ And if you don't like the app, all the more support it - the donation will When AmneziaDNS is not used or installed - + 当未使用或未安装AmneziaDNS时 @@ -1479,12 +1484,12 @@ And if you don't like the app, all the more support it - the donation will Do you want to reboot the server? - + 您想重新启动服务器吗? Do you want to clear server from Amnezia software? - + 您要清除服务器上的Amnezia软件吗? @@ -1520,12 +1525,12 @@ And if you don't like the app, all the more support it - the donation will Reboot server - + 重新启动服务器 The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? - + 重新启动过程可能需要大约30秒。您确定要继续吗? @@ -1535,7 +1540,7 @@ And if you don't like the app, all the more support it - the donation will Do you want to remove the server from application? - + 您想要从应用程序中移除服务器吗? Remove server? @@ -1706,17 +1711,17 @@ And if you don't like the app, all the more support it - the donation will Only the sites listed here will be accessed through the VPN - + 只有这里列出的网站将通过VPN访问 website or IP - + 网站或IP Import / Export Sites - + 导入/导出网站 @@ -2138,12 +2143,12 @@ and will not be shared or disclosed to the Amnezia or any third parties ShadowSocks native format - + ShadowSocks原生格式 Cloak native format - + Cloak原生格式 @@ -2153,18 +2158,18 @@ and will not be shared or disclosed to the Amnezia or any third parties Share full access to the server and VPN - + 共享服务器和VPN的完全访问权限 Use for your own devices, or share with those you trust to manage the server. - + 用于您自己的设备,或与您信任的人共享以管理服务器 Users - + 用户 @@ -2199,17 +2204,17 @@ and will not be shared or disclosed to the Amnezia or any third parties Revoke - + 撤销 Revoke the config for a user - %1? - + 撤销用户的配置- %1? The user will no longer be able to connect to your server. - + 该用户将无法再连接到您的服务器 @@ -2295,12 +2300,12 @@ and will not be shared or disclosed to the Amnezia or any third parties Config revoked - + 配置已撤销 User name - + 用户名 @@ -2320,7 +2325,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Full access to the server and VPN - + 对服务器和VPN的完全访问权限 @@ -2331,7 +2336,7 @@ and will not be shared or disclosed to the Amnezia or any third parties If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. - + 如果您与其他人共享完全访问权限,他们可以从服务器中删除和添加协议和服务,这将导致VPN对所有用户的工作出现问题。 @@ -2822,7 +2827,7 @@ and will not be shared or disclosed to the Amnezia or any third parties The config does not contain any containers and credentials for connecting to the server - + 配置不包含任何用于连接服务器的容器和凭据 The config does not contain any containers and credentiaks for connecting to the server @@ -2831,7 +2836,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Internal error - 内部错误 + @@ -2862,17 +2867,17 @@ and will not be shared or disclosed to the Amnezia or any third parties ShadowSocks - masks VPN traffic, making it similar to normal web traffic, but it may be recognized by analysis systems in some highly censored regions. - + ShadowSocks - 掩盖VPN流量,使其类似于正常的网络流量,但在一些高度审查的地区可能会被分析系统识别 - OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. - + OpenVPN over Cloak - OpenVPN with masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. + OpenVPN over Cloak - OpenVPN与VPN结合,伪装成Web流量,并保护免受主动探测的侦测。非常适合在具有最高审查水平的地区绕过封锁 Create a file vault on your server to securely store and transfer files. - + 在您的服务器上创建一个文件保险库,用于安全存储和传输文件。 @@ -2907,15 +2912,23 @@ WireGuard is very susceptible to blocking due to its distinct packet signatures. * Minimum number of settings * Easily recognised by DPI analysis systems, susceptible to blocking * Works over UDP network protocol. - + 一个相对较新的流行VPN协议,具有简化的架构。 +WireGuard提供稳定的VPN连接,并在所有设备上具有高性能。它使用硬编码的加密设置。与OpenVPN相比,WireGuard具有较低的延迟和更好的数据传输吞吐量。 +WireGuard非常容易被阻挡,因为其独特的数据包签名。与一些其他VPN协议不同,这些协议使用混淆技术,WireGuard数据包的一致签名模式更容易被高级深度数据包检测(DPI)系统和其他网络监控工具识别和阻挡。 + + 在AmneziaVPN上适用于所有平台 + 低功耗 + 最少的设置 + 易于被DPI分析系统识别,容易被阻挡 + 通过UDP网络协议运行。 ShadowSocks - masks VPN traffic, making it similar to normal web traffic, but is recognised by analysis systems in some highly censored regions. ShadowSocks - 混淆 VPN 流量,使其与正常的 Web 流量相似,但在一些审查力度高的地区可以被分析系统识别。 - OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probbing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. - OpenVPN over Cloak - OpenVPN 与 VPN 具有伪装成网络流量和防止主动探测检测的保护。非常适合绕过审查力度特别强的地区的封锁。 + OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against active-probing detection. Ideal for bypassing blocking in regions with the highest levels of censorship. + OpenVPN over Cloak - OpenVPN与VPN结合,伪装成Web流量,并保护免受主动探测的侦测。非常适合在具有最高审查水平的地区绕过封锁 @@ -3198,11 +3211,6 @@ While it offers a blend of security, stability, and speed, it's essential t SettingsController - - - Software version - 软件版本 - Backup file is corrupted @@ -3246,7 +3254,7 @@ While it offers a blend of security, stability, and speed, it's essential t Copy config string - + 复制配置字符串 diff --git a/client/ui/controllers/apiController.cpp b/client/ui/controllers/apiController.cpp index 43bea9fc..0d8c75e3 100644 --- a/client/ui/controllers/apiController.cpp +++ b/client/ui/controllers/apiController.cpp @@ -70,14 +70,17 @@ QJsonObject ApiController::fillApiPayload(const QString &protocol, const ApiCont void ApiController::updateServerConfigFromApi() { QtConcurrent::run([this]() { + if (m_isConfigUpdateStarted) { + emit updateFinished(false); + return; + } + auto serverConfig = m_serversModel->getDefaultServerConfig(); auto containerConfig = serverConfig.value(config_key::containers).toArray(); - bool isConfigUpdateStarted = false; - if (serverConfig.value(config_key::configVersion).toInt() && containerConfig.isEmpty()) { emit updateStarted(); - isConfigUpdateStarted = true; + m_isConfigUpdateStarted = true; QNetworkAccessManager manager; @@ -110,6 +113,12 @@ void ApiController::updateServerConfigFromApi() QByteArray ba = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + if (ba.isEmpty()) { + emit errorOccurred(errorString(ApiConfigDownloadError)); + m_isConfigUpdateStarted = false; + return; + } + QByteArray ba_uncompressed = qUncompress(ba); if (!ba_uncompressed.isEmpty()) { ba = ba_uncompressed; @@ -127,17 +136,18 @@ void ApiController::updateServerConfigFromApi() auto defaultContainer = apiConfig.value(config_key::defaultContainer).toString(); serverConfig.insert(config_key::defaultContainer, defaultContainer); - m_serversModel->editServer(serverConfig); - emit m_serversModel->defaultContainerChanged(ContainerProps::containerFromString(defaultContainer)); + m_serversModel->editServer(serverConfig, m_serversModel->getDefaultServerIndex()); } else { qDebug() << reply->error(); qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); emit errorOccurred(errorString(ApiConfigDownloadError)); + m_isConfigUpdateStarted = false; return; } } - emit updateFinished(isConfigUpdateStarted); + emit updateFinished(m_isConfigUpdateStarted); + m_isConfigUpdateStarted = false; return; }); } @@ -153,5 +163,5 @@ void ApiController::clearApiConfig() serverConfig.insert(config_key::defaultContainer, ContainerProps::containerToString(DockerContainer::None)); - m_serversModel->editServer(serverConfig); + m_serversModel->editServer(serverConfig, m_serversModel->getDefaultServerIndex()); } diff --git a/client/ui/controllers/apiController.h b/client/ui/controllers/apiController.h index c5d79ded..2a1393c4 100644 --- a/client/ui/controllers/apiController.h +++ b/client/ui/controllers/apiController.h @@ -39,6 +39,8 @@ private: QSharedPointer m_serversModel; QSharedPointer m_containersModel; + + bool m_isConfigUpdateStarted = false; }; #endif // APICONTROLLER_H diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index 12a16367..7b59c901 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -33,7 +33,7 @@ void ConnectionController::openConnection() int serverIndex = m_serversModel->getDefaultServerIndex(); ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); - DockerContainer container = m_containersModel->getDefaultContainer(); + DockerContainer container = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole)); const QJsonObject &containerConfig = m_containersModel->getContainerConfig(container); if (container == DockerContainer::None) { diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 2b102e13..57d864e9 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -8,6 +8,7 @@ #include #include +#include "configurators/awg_configurator.h" #include "configurators/cloak_configurator.h" #include "configurators/openvpn_configurator.h" #include "configurators/shadowsocks_configurator.h" @@ -45,7 +46,7 @@ void ExportController::generateFullAccessConfig() { clearPreviousConfig(); - int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + int serverIndex = m_serversModel->getProcessedServerIndex(); QJsonObject config = m_settings->server(serverIndex); QJsonArray containers = config.value(config_key::containers).toArray(); @@ -99,7 +100,7 @@ void ExportController::generateConnectionConfig(const QString &clientName) { clearPreviousConfig(); - int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + int serverIndex = m_serversModel->getProcessedServerIndex(); ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); DockerContainer container = static_cast(m_containersModel->getCurrentlyProcessedContainerIndex()); @@ -155,7 +156,7 @@ void ExportController::generateOpenVpnConfig(const QString &clientName) { clearPreviousConfig(); - int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + int serverIndex = m_serversModel->getProcessedServerIndex(); ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); DockerContainer container = static_cast(m_containersModel->getCurrentlyProcessedContainerIndex()); @@ -193,7 +194,7 @@ void ExportController::generateWireGuardConfig(const QString &clientName) { clearPreviousConfig(); - int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + int serverIndex = m_serversModel->getProcessedServerIndex(); ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); DockerContainer container = static_cast(m_containersModel->getCurrentlyProcessedContainerIndex()); @@ -228,11 +229,50 @@ void ExportController::generateWireGuardConfig(const QString &clientName) emit exportConfigChanged(); } +void ExportController::generateAwgConfig(const QString &clientName) +{ + clearPreviousConfig(); + + int serverIndex = m_serversModel->getProcessedServerIndex(); + ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); + + DockerContainer container = static_cast(m_containersModel->getCurrentlyProcessedContainerIndex()); + QJsonObject containerConfig = m_containersModel->getContainerConfig(container); + containerConfig.insert(config_key::container, ContainerProps::containerToString(container)); + + QString clientId; + ErrorCode errorCode = ErrorCode::NoError; + QString config = m_configurator->awgConfigurator->genAwgConfig(credentials, container, containerConfig, + clientId, &errorCode); + if (errorCode) { + emit exportErrorOccurred(errorString(errorCode)); + return; + } + config = m_configurator->processConfigWithExportSettings(serverIndex, container, Proto::Awg, config); + + auto configJson = QJsonDocument::fromJson(config.toUtf8()).object(); + QStringList lines = configJson.value(config_key::config).toString().replace("\r", "").split("\n"); + for (const QString &line : lines) { + m_config.append(line + "\n"); + } + + qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(m_config.toUtf8(), qrcodegen::QrCode::Ecc::LOW); + m_qrCodes << svgToBase64(QString::fromStdString(toSvgString(qr, 1))); + + errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials); + if (errorCode) { + emit exportErrorOccurred(errorString(errorCode)); + return; + } + + emit exportConfigChanged(); +} + void ExportController::generateShadowSocksConfig() { clearPreviousConfig(); - int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + int serverIndex = m_serversModel->getProcessedServerIndex(); ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); DockerContainer container = static_cast(m_containersModel->getCurrentlyProcessedContainerIndex()); @@ -268,7 +308,7 @@ void ExportController::generateCloakConfig() { clearPreviousConfig(); - int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + int serverIndex = m_serversModel->getProcessedServerIndex(); ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); DockerContainer container = static_cast(m_containersModel->getCurrentlyProcessedContainerIndex()); @@ -328,7 +368,7 @@ void ExportController::updateClientManagementModel(const DockerContainer contain void ExportController::revokeConfig(const int row, const DockerContainer container, ServerCredentials credentials) { ErrorCode errorCode = m_clientManagementModel->revokeClient(row, container, credentials, - m_serversModel->getCurrentlyProcessedServerIndex()); + m_serversModel->getProcessedServerIndex()); if (errorCode != ErrorCode::NoError) { emit exportErrorOccurred(errorString(errorCode)); } diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h index a6dc468b..e5bd1657 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -34,6 +34,7 @@ public slots: void generateConnectionConfig(const QString &clientName); void generateOpenVpnConfig(const QString &clientName); void generateWireGuardConfig(const QString &clientName); + void generateAwgConfig(const QString &clientName); void generateShadowSocksConfig(); void generateCloakConfig(); diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index 1c510782..25ce155f 100644 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -176,7 +176,7 @@ void InstallController::installServer(DockerContainer container, QJsonObject &co void InstallController::installContainer(DockerContainer container, QJsonObject &config) { - int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + int serverIndex = m_serversModel->getProcessedServerIndex(); ServerCredentials serverCredentials = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole)); @@ -238,7 +238,7 @@ bool InstallController::isServerAlreadyExists() void InstallController::scanServerForInstalledContainers() { - int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + int serverIndex = m_serversModel->getProcessedServerIndex(); ServerCredentials serverCredentials = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole)); @@ -267,7 +267,7 @@ void InstallController::scanServerForInstalledContainers() void InstallController::updateContainer(QJsonObject config) { - int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + int serverIndex = m_serversModel->getProcessedServerIndex(); ServerCredentials serverCredentials = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole)); @@ -283,8 +283,8 @@ void InstallController::updateContainer(QJsonObject config) m_serversModel->updateContainerConfig(container, config); m_protocolModel->updateModel(config); - if ((serverIndex == m_serversModel->getDefaultServerIndex()) - && (container == m_containersModel->getDefaultContainer())) { + auto defaultContainer = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole)); + if ((serverIndex == m_serversModel->getDefaultServerIndex()) && (container == defaultContainer)) { emit currentContainerUpdated(); } else { emit updateContainerFinished(tr("Settings updated successfully")); @@ -296,27 +296,27 @@ void InstallController::updateContainer(QJsonObject config) emit installationErrorOccurred(errorString(errorCode)); } -void InstallController::rebootCurrentlyProcessedServer() +void InstallController::rebootProcessedServer() { - int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + int serverIndex = m_serversModel->getProcessedServerIndex(); QString serverName = m_serversModel->data(serverIndex, ServersModel::Roles::NameRole).toString(); m_serversModel->rebootServer(); - emit rebootCurrentlyProcessedServerFinished(tr("Server '%1' was rebooted").arg(serverName)); + emit rebootProcessedServerFinished(tr("Server '%1' was rebooted").arg(serverName)); } -void InstallController::removeCurrentlyProcessedServer() +void InstallController::removeProcessedServer() { - int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + int serverIndex = m_serversModel->getProcessedServerIndex(); QString serverName = m_serversModel->data(serverIndex, ServersModel::Roles::NameRole).toString(); m_serversModel->removeServer(); - emit removeCurrentlyProcessedServerFinished(tr("Server '%1' was removed").arg(serverName)); + emit removeProcessedServerFinished(tr("Server '%1' was removed").arg(serverName)); } void InstallController::removeAllContainers() { - int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + int serverIndex = m_serversModel->getProcessedServerIndex(); QString serverName = m_serversModel->data(serverIndex, ServersModel::Roles::NameRole).toString(); ErrorCode errorCode = m_serversModel->removeAllContainers(); @@ -329,7 +329,7 @@ void InstallController::removeAllContainers() void InstallController::removeCurrentlyProcessedContainer() { - int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + int serverIndex = m_serversModel->getProcessedServerIndex(); QString serverName = m_serversModel->data(serverIndex, ServersModel::Roles::NameRole).toString(); int container = m_containersModel->getCurrentlyProcessedContainerIndex(); @@ -377,7 +377,7 @@ void InstallController::mountSftpDrive(const QString &port, const QString &passw QString mountPath; QString cmd; - int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex(); + int serverIndex = m_serversModel->getProcessedServerIndex(); ServerCredentials serverCredentials = qvariant_cast(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole)); QString hostname = serverCredentials.hostName; diff --git a/client/ui/controllers/installController.h b/client/ui/controllers/installController.h index a67912cf..6b5295dc 100644 --- a/client/ui/controllers/installController.h +++ b/client/ui/controllers/installController.h @@ -30,8 +30,8 @@ public slots: void updateContainer(QJsonObject config); - void removeCurrentlyProcessedServer(); - void rebootCurrentlyProcessedServer(); + void removeProcessedServer(); + void rebootProcessedServer(); void removeAllContainers(); void removeCurrentlyProcessedContainer(); @@ -54,8 +54,8 @@ signals: void scanServerFinished(bool isInstalledContainerFound); - void rebootCurrentlyProcessedServerFinished(const QString &finishedMessage); - void removeCurrentlyProcessedServerFinished(const QString &finishedMessage); + void rebootProcessedServerFinished(const QString &finishedMessage); + void removeProcessedServerFinished(const QString &finishedMessage); void removeAllContainersFinished(const QString &finishedMessage); void removeCurrentlyProcessedContainerFinished(const QString &finishedMessage); diff --git a/client/ui/controllers/pageController.cpp b/client/ui/controllers/pageController.cpp index 105f2115..467da3ac 100644 --- a/client/ui/controllers/pageController.cpp +++ b/client/ui/controllers/pageController.cpp @@ -118,36 +118,6 @@ void PageController::showOnStartup() } } -void PageController::updateDrawerRootPage(PageLoader::PageEnum page) -{ - m_drawerLayer = 0; - m_currentRootPage = page; -} - -void PageController::goToDrawerRootPage() -{ - - m_drawerLayer = 0; - - emit showTopCloseButton(false); - emit forceCloseDrawer(); -} - -void PageController::drawerOpen() -{ - m_drawerLayer = m_drawerLayer + 1; - emit showTopCloseButton(true); -} - -void PageController::drawerClose() -{ - m_drawerLayer = m_drawerLayer -1; - if (m_drawerLayer <= 0) { - emit showTopCloseButton(false); - m_drawerLayer = 0; - } -} - bool PageController::isTriggeredByConnectButton() { return m_isTriggeredByConnectButton; diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index f7e697fb..5c0909fb 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -82,11 +82,6 @@ public slots: void showOnStartup(); - void updateDrawerRootPage(PageLoader::PageEnum page); - void goToDrawerRootPage(); - void drawerOpen(); - void drawerClose(); - bool isTriggeredByConnectButton(); void setTriggeredBtConnectButton(bool trigger); @@ -118,17 +113,11 @@ signals: void showPassphraseRequestDrawer(); void passphraseRequestDrawerClosed(QString passphrase); - void showTopCloseButton(bool visible); - void forceCloseDrawer(); - private: QSharedPointer m_serversModel; std::shared_ptr m_settings; - PageLoader::PageEnum m_currentRootPage; - int m_drawerLayer; - bool m_isTriggeredByConnectButton; }; diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 99645cde..6ec55321 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -28,7 +28,7 @@ SettingsController::SettingsController(const QSharedPointer &serve m_sitesModel(sitesModel), m_settings(settings) { - m_appVersion = QString("%1: %2 (%3)").arg(tr("Software version"), QString(APP_VERSION), __DATE__); + m_appVersion = QString("%1 (%2, %3)").arg(QString(APP_VERSION), __DATE__, GIT_COMMIT_HASH); #ifdef Q_OS_ANDROID if (!m_settings->isScreenshotsEnabled()) { diff --git a/client/ui/models/containers_model.cpp b/client/ui/models/containers_model.cpp index 55ee5b28..c060343f 100644 --- a/client/ui/models/containers_model.cpp +++ b/client/ui/models/containers_model.cpp @@ -39,7 +39,6 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const case EasySetupOrderRole: return ContainerProps::easySetupOrder(container); case IsInstalledRole: return m_containers.contains(container); case IsCurrentlyProcessedRole: return container == static_cast(m_currentlyProcessedContainerIndex); - case IsDefaultRole: return container == m_defaultContainerIndex; case IsSupportedRole: return ContainerProps::isSupportedByCurrentPlatform(container); case IsShareableRole: return ContainerProps::isShareable(container); } @@ -64,18 +63,6 @@ void ContainersModel::updateModel(const QJsonArray &containers) endResetModel(); } -void ContainersModel::setDefaultContainer(const int containerIndex) -{ - m_defaultContainerIndex = static_cast(containerIndex); - emit dataChanged(index(containerIndex, 0), index(containerIndex, 0)); -} - - -DockerContainer ContainersModel::getDefaultContainer() -{ - return m_defaultContainerIndex; -} - void ContainersModel::setCurrentlyProcessedContainerIndex(int index) { m_currentlyProcessedContainerIndex = index; @@ -127,7 +114,6 @@ QHash ContainersModel::roleNames() const roles[IsInstalledRole] = "isInstalled"; roles[IsCurrentlyProcessedRole] = "isCurrentlyProcessed"; - roles[IsDefaultRole] = "isDefault"; roles[IsSupportedRole] = "isSupported"; roles[IsShareableRole] = "isShareable"; return roles; diff --git a/client/ui/models/containers_model.h b/client/ui/models/containers_model.h index 0deb0f4e..320a762a 100644 --- a/client/ui/models/containers_model.h +++ b/client/ui/models/containers_model.h @@ -42,9 +42,6 @@ public: public slots: void updateModel(const QJsonArray &containers); - DockerContainer getDefaultContainer(); - void setDefaultContainer(const int containerIndex); - void setCurrentlyProcessedContainerIndex(int containerIndex); int getCurrentlyProcessedContainerIndex(); @@ -58,14 +55,12 @@ protected: QHash roleNames() const override; signals: - void defaultContainerChanged(); void containersModelUpdated(); private: QMap m_containers; int m_currentlyProcessedContainerIndex; - DockerContainer m_defaultContainerIndex; }; #endif // CONTAINERS_MODEL_H diff --git a/client/ui/models/languageModel.cpp b/client/ui/models/languageModel.cpp index c3552ea7..47e41708 100644 --- a/client/ui/models/languageModel.cpp +++ b/client/ui/models/languageModel.cpp @@ -45,6 +45,7 @@ QString LanguageModel::getLocalLanguageName(const LanguageSettings::AvailableLan case LanguageSettings::AvailableLanguageEnum::Russian: strLanguage = "Русский"; break; case LanguageSettings::AvailableLanguageEnum::China_cn: strLanguage = "\347\256\200\344\275\223\344\270\255\346\226\207"; break; case LanguageSettings::AvailableLanguageEnum::Persian: strLanguage = "فارسی"; break; + case LanguageSettings::AvailableLanguageEnum::Arabic: strLanguage = "العربية"; break; default: break; } @@ -59,6 +60,7 @@ void LanguageModel::changeLanguage(const LanguageSettings::AvailableLanguageEnum case LanguageSettings::AvailableLanguageEnum::Russian: emit updateTranslations(QLocale::Russian); break; case LanguageSettings::AvailableLanguageEnum::China_cn: emit updateTranslations(QLocale::Chinese); break; case LanguageSettings::AvailableLanguageEnum::Persian: emit updateTranslations(QLocale::Persian); break; + case LanguageSettings::AvailableLanguageEnum::Arabic: emit updateTranslations(QLocale::Arabic); break; default: emit updateTranslations(QLocale::English); break; } } @@ -71,6 +73,7 @@ int LanguageModel::getCurrentLanguageIndex() case QLocale::Russian: return static_cast(LanguageSettings::AvailableLanguageEnum::Russian); break; case QLocale::Chinese: return static_cast(LanguageSettings::AvailableLanguageEnum::China_cn); break; case QLocale::Persian: return static_cast(LanguageSettings::AvailableLanguageEnum::Persian); break; + case QLocale::Arabic: return static_cast(LanguageSettings::AvailableLanguageEnum::Arabic); break; default: return static_cast(LanguageSettings::AvailableLanguageEnum::English); break; } } diff --git a/client/ui/models/languageModel.h b/client/ui/models/languageModel.h index 8a55263c..b07a5c78 100644 --- a/client/ui/models/languageModel.h +++ b/client/ui/models/languageModel.h @@ -13,7 +13,8 @@ namespace LanguageSettings English, Russian, China_cn, - Persian + Persian, + Arabic }; Q_ENUM_NS(AvailableLanguageEnum) diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index 36b3d574..3c72ee49 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -5,19 +5,13 @@ ServersModel::ServersModel(std::shared_ptr settings, QObject *parent) : m_settings(settings), QAbstractListModel(parent) { - m_servers = m_settings->serversArray(); - m_defaultServerIndex = m_settings->defaultServerIndex(); - m_currentlyProcessedServerIndex = m_defaultServerIndex; - connect(this, &ServersModel::defaultServerIndexChanged, this, &ServersModel::defaultServerNameChanged); - connect(this, &ServersModel::defaultContainerChanged, this, &ServersModel::defaultServerDescriptionChanged); + connect(this, &ServersModel::defaultServerIndexChanged, this, [this](const int serverIndex) { auto defaultContainer = ContainerProps::containerFromString(m_servers.at(serverIndex).toObject().value(config_key::defaultContainer).toString()); - emit ServersModel::defaultContainerChanged(defaultContainer); - }); - connect(this, &ServersModel::currentlyProcessedServerIndexChanged, this, [this](const int serverIndex) { - auto defaultContainer = ContainerProps::containerFromString(m_servers.at(serverIndex).toObject().value(config_key::defaultContainer).toString()); - emit ServersModel::defaultContainerChanged(defaultContainer); + emit ServersModel::defaultServerDefaultContainerChanged(defaultContainer); + emit ServersModel::defaultServerNameChanged(); + updateDefaultServerContainersModel(); }); } @@ -74,16 +68,14 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const return name; } case ServerDescriptionRole: { - if (configVersion) { - return server.value(config_key::description).toString(); - } - return server.value(config_key::hostName).toString(); + auto description = getServerDescription(server, index.row()); + return configVersion ? description : description + server.value(config_key::hostName).toString(); } case HostNameRole: return server.value(config_key::hostName).toString(); case CredentialsRole: return QVariant::fromValue(serverCredentials(index.row())); case CredentialsLoginRole: return serverCredentials(index.row()).userName; case IsDefaultRole: return index.row() == m_defaultServerIndex; - case IsCurrentlyProcessedRole: return index.row() == m_currentlyProcessedServerIndex; + case IsCurrentlyProcessedRole: return index.row() == m_processedServerIndex; case HasWriteAccessRole: { auto credentials = serverCredentials(index.row()); return (!credentials.userName.isEmpty() && !credentials.secretData.isEmpty()); @@ -95,6 +87,13 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const case DefaultContainerRole: { return ContainerProps::containerFromString(server.value(config_key::defaultContainer).toString()); } + case IsServerFromApiRole: { + return server.value(config_key::configVersion).toInt(); + } + case HasAmneziaDns: { + QString primaryDns = server.value(config_key::dns1).toString(); + return primaryDns == protocols::dns::amneziaDnsIp; + } } return QVariant(); @@ -111,8 +110,9 @@ void ServersModel::resetModel() beginResetModel(); m_servers = m_settings->serversArray(); m_defaultServerIndex = m_settings->defaultServerIndex(); - m_currentlyProcessedServerIndex = m_defaultServerIndex; + m_processedServerIndex = m_defaultServerIndex; endResetModel(); + emit defaultServerIndexChanged(m_defaultServerIndex); } void ServersModel::setDefaultServerIndex(const int index) @@ -132,12 +132,7 @@ const QString ServersModel::getDefaultServerName() return qvariant_cast(data(m_defaultServerIndex, NameRole)); } -const QString ServersModel::getDefaultServerHostName() -{ - return qvariant_cast(data(m_defaultServerIndex, HostNameRole)); -} - -QString ServersModel::getDefaultServerDescription(const QJsonObject &server) +QString ServersModel::getServerDescription(const QJsonObject &server, const int index) const { const auto configVersion = server.value(config_key::configVersion).toInt(); @@ -145,13 +140,12 @@ QString ServersModel::getDefaultServerDescription(const QJsonObject &server) if (configVersion) { return server.value(config_key::description).toString(); - } else if (isDefaultServerHasWriteAccess()) { - if (m_isAmneziaDnsEnabled - && isAmneziaDnsContainerInstalled(m_defaultServerIndex)) { + } else if (data(index, HasWriteAccessRole).toBool()) { + if (m_isAmneziaDnsEnabled && isAmneziaDnsContainerInstalled(index)) { description += "Amnezia DNS | "; } } else { - if (isDefaultServerConfigContainsAmneziaDns()) { + if (data(index, HasAmneziaDns).toBool()) { description += "Amnezia DNS | "; } } @@ -162,7 +156,7 @@ const QString ServersModel::getDefaultServerDescriptionCollapsed() { const QJsonObject server = m_servers.at(m_defaultServerIndex).toObject(); const auto configVersion = server.value(config_key::configVersion).toInt(); - auto description = getDefaultServerDescription(server); + auto description = getServerDescription(server, m_defaultServerIndex); if (configVersion) { return description; } @@ -176,7 +170,7 @@ const QString ServersModel::getDefaultServerDescriptionExpanded() { const QJsonObject server = m_servers.at(m_defaultServerIndex).toObject(); const auto configVersion = server.value(config_key::configVersion).toInt(); - auto description = getDefaultServerDescription(server); + auto description = getServerDescription(server, m_defaultServerIndex); if (configVersion) { return description; } @@ -199,26 +193,21 @@ bool ServersModel::hasServerWithWriteAccess() return false; } -void ServersModel::setCurrentlyProcessedServerIndex(const int index) +void ServersModel::setProcessedServerIndex(const int index) { - m_currentlyProcessedServerIndex = index; + m_processedServerIndex = index; updateContainersModel(); - emit currentlyProcessedServerIndexChanged(m_currentlyProcessedServerIndex); + emit processedServerIndexChanged(m_processedServerIndex); } -int ServersModel::getCurrentlyProcessedServerIndex() +int ServersModel::getProcessedServerIndex() { - return m_currentlyProcessedServerIndex; + return m_processedServerIndex; } -QString ServersModel::getCurrentlyProcessedServerHostName() +const ServerCredentials ServersModel::getProcessedServerCredentials() { - return qvariant_cast(data(m_currentlyProcessedServerIndex, HostNameRole)); -} - -const ServerCredentials ServersModel::getCurrentlyProcessedServerCredentials() -{ - return serverCredentials(m_currentlyProcessedServerIndex); + return serverCredentials(m_processedServerIndex); } const ServerCredentials ServersModel::getServerCredentials(const int index) @@ -228,12 +217,17 @@ const ServerCredentials ServersModel::getServerCredentials(const int index) bool ServersModel::isDefaultServerCurrentlyProcessed() { - return m_defaultServerIndex == m_currentlyProcessedServerIndex; + return m_defaultServerIndex == m_processedServerIndex; } -bool ServersModel::isCurrentlyProcessedServerHasWriteAccess() +bool ServersModel::isDefaultServerFromApi() { - return qvariant_cast(data(m_currentlyProcessedServerIndex, HasWriteAccessRole)); + return qvariant_cast(data(m_defaultServerIndex, IsServerFromApiRole)); +} + +bool ServersModel::isProcessedServerHasWriteAccess() +{ + return qvariant_cast(data(m_processedServerIndex, HasWriteAccessRole)); } bool ServersModel::isDefaultServerHasWriteAccess() @@ -249,40 +243,42 @@ void ServersModel::addServer(const QJsonObject &server) endResetModel(); } -void ServersModel::editServer(const QJsonObject &server) +void ServersModel::editServer(const QJsonObject &server, const int serverIndex) { - m_settings->editServer(m_currentlyProcessedServerIndex, server); - m_servers.replace(m_currentlyProcessedServerIndex, m_settings->serversArray().at(m_currentlyProcessedServerIndex)); - emit dataChanged(index(m_currentlyProcessedServerIndex, 0), index(m_currentlyProcessedServerIndex, 0)); + m_settings->editServer(serverIndex, server); + m_servers.replace(serverIndex, m_settings->serversArray().at(serverIndex)); + emit dataChanged(index(serverIndex, 0), index(serverIndex, 0)); + + if (serverIndex == m_defaultServerIndex) { + updateDefaultServerContainersModel(); + } updateContainersModel(); + + if (serverIndex == m_defaultServerIndex) { + auto defaultContainer = qvariant_cast(getDefaultServerData("defaultContainer")); + emit defaultServerDefaultContainerChanged(defaultContainer); + } } void ServersModel::removeServer() { beginResetModel(); - m_settings->removeServer(m_currentlyProcessedServerIndex); + m_settings->removeServer(m_processedServerIndex); m_servers = m_settings->serversArray(); - if (m_settings->defaultServerIndex() == m_currentlyProcessedServerIndex) { + if (m_settings->defaultServerIndex() == m_processedServerIndex) { setDefaultServerIndex(0); - } else if (m_settings->defaultServerIndex() > m_currentlyProcessedServerIndex) { + } else if (m_settings->defaultServerIndex() > m_processedServerIndex) { setDefaultServerIndex(m_settings->defaultServerIndex() - 1); } if (m_settings->serversCount() == 0) { setDefaultServerIndex(-1); } - setCurrentlyProcessedServerIndex(m_defaultServerIndex); + setProcessedServerIndex(m_defaultServerIndex); endResetModel(); } -bool ServersModel::isDefaultServerConfigContainsAmneziaDns() -{ - const QJsonObject server = m_servers.at(m_defaultServerIndex).toObject(); - QString primaryDns = server.value(config_key::dns1).toString(); - return primaryDns == protocols::dns::amneziaDnsIp; -} - QHash ServersModel::roleNames() const { QHash roles; @@ -290,6 +286,8 @@ QHash ServersModel::roleNames() const roles[NameRole] = "serverName"; roles[NameRole] = "name"; roles[ServerDescriptionRole] = "serverDescription"; + roles[CollapsedServerDescriptionRole] = "collapsedServerDescription"; + roles[ExpandedServerDescriptionRole] = "expandedServerDescription"; roles[HostNameRole] = "hostName"; @@ -304,6 +302,8 @@ QHash ServersModel::roleNames() const roles[ContainsAmneziaDnsRole] = "containsAmneziaDns"; roles[DefaultContainerRole] = "defaultContainer"; + + roles[IsServerFromApiRole] = "isServerFromApi"; return roles; } @@ -322,28 +322,29 @@ ServerCredentials ServersModel::serverCredentials(int index) const void ServersModel::updateContainersModel() { - auto containers = m_servers.at(m_currentlyProcessedServerIndex).toObject().value(config_key::containers).toArray(); + auto containers = m_servers.at(m_processedServerIndex).toObject().value(config_key::containers).toArray(); emit containersUpdated(containers); } +void ServersModel::updateDefaultServerContainersModel() +{ + auto containers = m_servers.at(m_defaultServerIndex).toObject().value(config_key::containers).toArray(); + emit defaultServerContainersUpdated(containers); +} + QJsonObject ServersModel::getDefaultServerConfig() { return m_servers.at(m_defaultServerIndex).toObject(); } -QJsonObject ServersModel::getCurrentlyProcessedServerConfig() +void ServersModel::reloadDefaultServerContainerConfig() { - return m_servers.at(m_currentlyProcessedServerIndex).toObject(); -} - -void ServersModel::reloadContainerConfig() -{ - QJsonObject server = m_servers.at(m_currentlyProcessedServerIndex).toObject(); + QJsonObject server = m_servers.at(m_defaultServerIndex).toObject(); auto container = ContainerProps::containerFromString(server.value(config_key::defaultContainer).toString()); auto containers = server.value(config_key::containers).toArray(); - auto config = m_settings->containerConfig(m_currentlyProcessedServerIndex, container); + auto config = m_settings->containerConfig(m_defaultServerIndex, container); for (auto i = 0; i < containers.size(); i++) { auto c = ContainerProps::containerFromString(containers.at(i).toObject().value(config_key::container).toString()); if (c == container) { @@ -353,13 +354,13 @@ void ServersModel::reloadContainerConfig() } server.insert(config_key::containers, containers); - editServer(server); + editServer(server, m_defaultServerIndex); } void ServersModel::updateContainerConfig(const int containerIndex, const QJsonObject config) { auto container = static_cast(containerIndex); - QJsonObject server = m_servers.at(m_currentlyProcessedServerIndex).toObject(); + QJsonObject server = m_servers.at(m_processedServerIndex).toObject(); auto containers = server.value(config_key::containers).toArray(); for (auto i = 0; i < containers.size(); i++) { @@ -377,49 +378,38 @@ void ServersModel::updateContainerConfig(const int containerIndex, const QJsonOb server.insert(config_key::defaultContainer, ContainerProps::containerToString(container)); } - editServer(server); + editServer(server, m_processedServerIndex); } void ServersModel::addContainerConfig(const int containerIndex, const QJsonObject config) { auto container = static_cast(containerIndex); - QJsonObject server = m_servers.at(m_currentlyProcessedServerIndex).toObject(); + QJsonObject server = m_servers.at(m_processedServerIndex).toObject(); auto containers = server.value(config_key::containers).toArray(); containers.push_back(config); server.insert(config_key::containers, containers); - bool isDefaultContainerChanged = false; auto defaultContainer = server.value(config_key::defaultContainer).toString(); if ((ContainerProps::containerFromString(defaultContainer) == DockerContainer::None || ContainerProps::containerService(container) != ServiceType::Other)) { server.insert(config_key::defaultContainer, ContainerProps::containerToString(container)); - isDefaultContainerChanged = true; } - editServer(server); - if (isDefaultContainerChanged) { - emit defaultContainerChanged(container); - } + editServer(server, m_processedServerIndex); } -void ServersModel::setDefaultContainer(const int containerIndex) +void ServersModel::setDefaultContainer(const int serverIndex, const int containerIndex) { auto container = static_cast(containerIndex); - QJsonObject s = m_servers.at(m_currentlyProcessedServerIndex).toObject(); + QJsonObject s = m_servers.at(serverIndex).toObject(); s.insert(config_key::defaultContainer, ContainerProps::containerToString(container)); - editServer(s); //check - emit defaultContainerChanged(container); + editServer(s, serverIndex); //check } -DockerContainer ServersModel::getDefaultContainer() +const QString ServersModel::getDefaultServerDefaultContainerName() { - return qvariant_cast(data(m_currentlyProcessedServerIndex, DefaultContainerRole)); -} - -const QString ServersModel::getDefaultContainerName() -{ - auto defaultContainer = getDefaultContainer(); + auto defaultContainer = qvariant_cast(getDefaultServerData("defaultContainer")); return ContainerProps::containerHumanNames().value(defaultContainer); } @@ -427,15 +417,14 @@ ErrorCode ServersModel::removeAllContainers() { ServerController serverController(m_settings); ErrorCode errorCode = - serverController.removeAllContainers(m_settings->serverCredentials(m_currentlyProcessedServerIndex)); + serverController.removeAllContainers(m_settings->serverCredentials(m_processedServerIndex)); if (errorCode == ErrorCode::NoError) { - QJsonObject s = m_servers.at(m_currentlyProcessedServerIndex).toObject(); + QJsonObject s = m_servers.at(m_processedServerIndex).toObject(); s.insert(config_key::containers, {}); s.insert(config_key::defaultContainer, ContainerProps::containerToString(DockerContainer::None)); - editServer(s); - emit defaultContainerChanged(DockerContainer::None); + editServer(s, m_processedServerIndex); } return errorCode; } @@ -443,7 +432,7 @@ ErrorCode ServersModel::removeAllContainers() ErrorCode ServersModel::rebootServer() { ServerController serverController(m_settings); - auto credentials = m_settings->serverCredentials(m_currentlyProcessedServerIndex); + auto credentials = m_settings->serverCredentials(m_processedServerIndex); ErrorCode errorCode = serverController.rebootServer(credentials); return errorCode; @@ -452,13 +441,13 @@ ErrorCode ServersModel::rebootServer() ErrorCode ServersModel::removeContainer(const int containerIndex) { ServerController serverController(m_settings); - auto credentials = m_settings->serverCredentials(m_currentlyProcessedServerIndex); + auto credentials = m_settings->serverCredentials(m_processedServerIndex); auto dockerContainer = static_cast(containerIndex); ErrorCode errorCode = serverController.removeContainer(credentials, dockerContainer); if (errorCode == ErrorCode::NoError) { - QJsonObject server = m_servers.at(m_currentlyProcessedServerIndex).toObject(); + QJsonObject server = m_servers.at(m_processedServerIndex).toObject(); auto containers = server.value(config_key::containers).toArray(); for (auto it = containers.begin(); it != containers.end(); it++) { @@ -480,32 +469,37 @@ ErrorCode ServersModel::removeContainer(const int containerIndex) server.insert(config_key::defaultContainer, ContainerProps::containerToString(defaultContainer)); } - editServer(server); - emit defaultContainerChanged(defaultContainer); + editServer(server, m_processedServerIndex); } return errorCode; } void ServersModel::clearCachedProfiles() { - const auto &containers = m_settings->containers(m_currentlyProcessedServerIndex); + const auto &containers = m_settings->containers(m_processedServerIndex); for (DockerContainer container : containers.keys()) { - m_settings->clearLastConnectionConfig(m_currentlyProcessedServerIndex, container); + m_settings->clearLastConnectionConfig(m_processedServerIndex, container); } - m_servers.replace(m_currentlyProcessedServerIndex, m_settings->server(m_currentlyProcessedServerIndex)); + m_servers.replace(m_processedServerIndex, m_settings->server(m_processedServerIndex)); + if (m_processedServerIndex == m_defaultServerIndex) { + updateDefaultServerContainersModel(); + } updateContainersModel(); } void ServersModel::clearCachedProfile(const DockerContainer container) { - m_settings->clearLastConnectionConfig(m_currentlyProcessedServerIndex, container); + m_settings->clearLastConnectionConfig(m_processedServerIndex, container); - m_servers.replace(m_currentlyProcessedServerIndex, m_settings->server(m_currentlyProcessedServerIndex)); + m_servers.replace(m_processedServerIndex, m_settings->server(m_processedServerIndex)); + if (m_processedServerIndex == m_defaultServerIndex) { + updateDefaultServerContainersModel(); + } updateContainersModel(); } -bool ServersModel::isAmneziaDnsContainerInstalled(const int serverIndex) +bool ServersModel::isAmneziaDnsContainerInstalled(const int serverIndex) const { QJsonObject server = m_servers.at(serverIndex).toObject(); auto containers = server.value(config_key::containers).toArray(); @@ -544,16 +538,6 @@ void ServersModel::toggleAmneziaDns(bool enabled) emit defaultServerDescriptionChanged(); } -bool ServersModel::isDefaultServerFromApi() -{ - return m_settings->server(m_defaultServerIndex).value(config_key::configVersion).toInt(); -} - -bool ServersModel::isCurrentlyProcessedServerFromApi() -{ - return m_settings->server(m_currentlyProcessedServerIndex).value(config_key::configVersion).toInt(); -} - bool ServersModel::isServerFromApiAlreadyExists(const quint16 crc) { for (const auto &server : qAsConst(m_servers)) { @@ -564,3 +548,52 @@ bool ServersModel::isServerFromApiAlreadyExists(const quint16 crc) return false; } +QVariant ServersModel::getDefaultServerData(const QString roleString) +{ + auto roles = roleNames(); + for (auto it = roles.begin(); it != roles.end(); it++) { + if (QString(it.value()) == roleString) { + return data(m_defaultServerIndex, it.key()); + } + } + + return {}; +} + +void ServersModel::setDefaultServerData(const QString roleString, const QVariant &value) +{ + +} + +QVariant ServersModel::getProcessedServerData(const QString roleString) +{ + auto roles = roleNames(); + for (auto it = roles.begin(); it != roles.end(); it++) { + if (QString(it.value()) == roleString) { + return data(m_processedServerIndex, it.key()); + } + } + + return {}; +} + +void ServersModel::setProcessedServerData(const QString roleString, const QVariant &value) +{ + +} + +bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling() +{ + auto server = m_servers.at(m_defaultServerIndex).toObject(); + auto defaultContainer = ContainerProps::containerFromString(server.value(config_key::defaultContainer).toString()); + auto containerConfig = server.value(config_key::containers).toArray().at(defaultContainer).toObject(); + auto protocolConfig = containerConfig.value(ContainerProps::containerTypeToString(defaultContainer)).toObject(); + + if (defaultContainer == DockerContainer::Awg || defaultContainer == DockerContainer::WireGuard) { + return !(protocolConfig.value(config_key::last_config).toString().contains("AllowedIPs = 0.0.0.0/0, ::/0")); + } else if (defaultContainer == DockerContainer::Cloak || defaultContainer == DockerContainer::OpenVpn || defaultContainer == DockerContainer::ShadowSocks) { + return !(protocolConfig.value(config_key::last_config).toString().contains("redirect-gateway")); + } + + return false; +} diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h index ce2ca7de..3e24e46c 100644 --- a/client/ui/models/servers_model.h +++ b/client/ui/models/servers_model.h @@ -12,7 +12,8 @@ public: enum Roles { NameRole = Qt::UserRole + 1, ServerDescriptionRole, - + CollapsedServerDescriptionRole, + ExpandedServerDescriptionRole, HostNameRole, CredentialsRole, @@ -25,7 +26,11 @@ public: ContainsAmneziaDnsRole, - DefaultContainerRole + DefaultContainerRole, + + IsServerFromApiRole, + + HasAmneziaDns }; ServersModel(std::shared_ptr settings, QObject *parent = nullptr); @@ -40,47 +45,43 @@ public: Q_PROPERTY(int defaultIndex READ getDefaultServerIndex WRITE setDefaultServerIndex NOTIFY defaultServerIndexChanged) Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerNameChanged) - Q_PROPERTY(QString defaultServerHostName READ getDefaultServerHostName NOTIFY defaultServerIndexChanged) - Q_PROPERTY(QString defaultContainerName READ getDefaultContainerName NOTIFY defaultContainerChanged) - Q_PROPERTY(QString defaultServerDescriptionCollapsed READ getDefaultServerDescriptionCollapsed NOTIFY defaultServerDescriptionChanged) - Q_PROPERTY(QString defaultServerDescriptionExpanded READ getDefaultServerDescriptionExpanded NOTIFY defaultServerDescriptionChanged) + Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerDefaultContainerChanged) + Q_PROPERTY(QString defaultServerDescriptionCollapsed READ getDefaultServerDescriptionCollapsed NOTIFY defaultServerDefaultContainerChanged) + Q_PROPERTY(QString defaultServerDescriptionExpanded READ getDefaultServerDescriptionExpanded NOTIFY defaultServerDefaultContainerChanged) + Q_PROPERTY(bool isDefaultServerDefaultContainerHasSplitTunneling READ isDefaultServerDefaultContainerHasSplitTunneling NOTIFY defaultServerDefaultContainerChanged) + Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIndexChanged) - Q_PROPERTY(int currentlyProcessedIndex READ getCurrentlyProcessedServerIndex WRITE setCurrentlyProcessedServerIndex - NOTIFY currentlyProcessedServerIndexChanged) + Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged) public slots: void setDefaultServerIndex(const int index); const int getDefaultServerIndex(); const QString getDefaultServerName(); - const QString getDefaultServerHostName(); const QString getDefaultServerDescriptionCollapsed(); const QString getDefaultServerDescriptionExpanded(); + const QString getDefaultServerDefaultContainerName(); bool isDefaultServerCurrentlyProcessed(); + bool isDefaultServerFromApi(); - bool isCurrentlyProcessedServerHasWriteAccess(); + bool isProcessedServerHasWriteAccess(); bool isDefaultServerHasWriteAccess(); bool hasServerWithWriteAccess(); const int getServersCount(); - void setCurrentlyProcessedServerIndex(const int index); - int getCurrentlyProcessedServerIndex(); + void setProcessedServerIndex(const int index); + int getProcessedServerIndex(); - QString getCurrentlyProcessedServerHostName(); - const ServerCredentials getCurrentlyProcessedServerCredentials(); + const ServerCredentials getProcessedServerCredentials(); const ServerCredentials getServerCredentials(const int index); void addServer(const QJsonObject &server); - void editServer(const QJsonObject &server); + void editServer(const QJsonObject &server, const int serverIndex); void removeServer(); - bool isDefaultServerConfigContainsAmneziaDns(); - bool isAmneziaDnsContainerInstalled(const int serverIndex); - QJsonObject getDefaultServerConfig(); - QJsonObject getCurrentlyProcessedServerConfig(); - void reloadContainerConfig(); + void reloadDefaultServerContainerConfig(); void updateContainerConfig(const int containerIndex, const QJsonObject config); void addContainerConfig(const int containerIndex, const QJsonObject config); @@ -91,43 +92,50 @@ public slots: ErrorCode removeAllContainers(); ErrorCode rebootServer(); - void setDefaultContainer(const int containerIndex); - DockerContainer getDefaultContainer(); - const QString getDefaultContainerName(); + void setDefaultContainer(const int serverIndex, const int containerIndex); QStringList getAllInstalledServicesName(const int serverIndex); void toggleAmneziaDns(bool enabled); - bool isDefaultServerFromApi(); - bool isCurrentlyProcessedServerFromApi(); - bool isServerFromApiAlreadyExists(const quint16 crc); + QVariant getDefaultServerData(const QString roleString); + void setDefaultServerData(const QString roleString, const QVariant &value); + + QVariant getProcessedServerData(const QString roleString); + void setProcessedServerData(const QString roleString, const QVariant &value); + + bool isDefaultServerDefaultContainerHasSplitTunneling(); + protected: QHash roleNames() const override; signals: - void currentlyProcessedServerIndexChanged(const int index); + void processedServerIndexChanged(const int index); void defaultServerIndexChanged(const int index); void defaultServerNameChanged(); void defaultServerDescriptionChanged(); void containersUpdated(const QJsonArray &containers); - void defaultContainerChanged(const int containerIndex); + void defaultServerContainersUpdated(const QJsonArray &containers); + void defaultServerDefaultContainerChanged(const int containerIndex); private: ServerCredentials serverCredentials(int index) const; void updateContainersModel(); + void updateDefaultServerContainersModel(); - QString getDefaultServerDescription(const QJsonObject &server); + QString getServerDescription(const QJsonObject &server, const int index) const; + + bool isAmneziaDnsContainerInstalled(const int serverIndex) const; QJsonArray m_servers; std::shared_ptr m_settings; int m_defaultServerIndex; - int m_currentlyProcessedServerIndex; + int m_processedServerIndex; bool m_isAmneziaDnsEnabled = m_settings->useAmneziaDns(); }; diff --git a/client/ui/models/sites_model.cpp b/client/ui/models/sites_model.cpp index f6cb9b13..96b6ca60 100644 --- a/client/ui/models/sites_model.cpp +++ b/client/ui/models/sites_model.cpp @@ -113,6 +113,7 @@ void SitesModel::toggleSplitTunneling(bool enabled) m_settings->setRouteMode(Settings::RouteMode::VpnAllSites); } m_isSplitTunnelingEnabled = enabled; + emit splitTunnelingToggled(); } QVector > SitesModel::getCurrentSites() diff --git a/client/ui/models/sites_model.h b/client/ui/models/sites_model.h index ad16b7a3..803b7fd1 100644 --- a/client/ui/models/sites_model.h +++ b/client/ui/models/sites_model.h @@ -22,6 +22,7 @@ public: QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; Q_PROPERTY(int routeMode READ getRouteMode WRITE setRouteMode NOTIFY routeModeChanged) + Q_PROPERTY(bool isTunnelingEnabled READ isSplitTunnelingEnabled NOTIFY splitTunnelingToggled) public slots: bool addSite(const QString &hostname, const QString &ip); @@ -38,6 +39,7 @@ public slots: signals: void routeModeChanged(); + void splitTunnelingToggled(); protected: QHash roleNames() const override; diff --git a/client/ui/qml/Components/ConnectButton.qml b/client/ui/qml/Components/ConnectButton.qml index f628b6a5..a915eb21 100644 --- a/client/ui/qml/Components/ConnectButton.qml +++ b/client/ui/qml/Components/ConnectButton.qml @@ -138,8 +138,7 @@ Button { } onClicked: { - if (!ConnectionController.isConnectionInProgress) { - ApiController.updateServerConfigFromApi() - } + ServersModel.setProcessedServerIndex(ServersModel.defaultIndex) + ApiController.updateServerConfigFromApi() } } diff --git a/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml b/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml index 1f7b2f29..d9dc21f4 100644 --- a/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml +++ b/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml @@ -8,18 +8,24 @@ import "../Controls2" import "../Controls2/TextTypes" import "../Config" -DrawerType { +DrawerType2 { id: root width: parent.width - height: parent.height * 0.4375 + height: parent.height + + expandedContent: ColumnLayout { + id: content - ColumnLayout { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right spacing: 0 + Component.onCompleted: { + root.expandedHeight = content.implicitHeight + 32 + } + Header2Type { Layout.fillWidth: true Layout.topMargin: 24 @@ -40,7 +46,7 @@ DrawerType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSetupWizardCredentials) - root.visible = false + root.close() } } @@ -54,7 +60,7 @@ DrawerType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSetupWizardConfigSource) - root.visible = false + root.close() } } diff --git a/client/ui/qml/Components/HomeContainersListView.qml b/client/ui/qml/Components/HomeContainersListView.qml index 3043e97f..c785af8b 100644 --- a/client/ui/qml/Components/HomeContainersListView.qml +++ b/client/ui/qml/Components/HomeContainersListView.qml @@ -15,6 +15,7 @@ ListView { id: menuContent property var rootWidth + property var selectedText width: rootWidth height: menuContent.contentItem.height @@ -26,24 +27,6 @@ ListView { id: containersRadioButtonGroup } - Connections { - target: ServersModel - - function onCurrentlyProcessedServerIndexChanged() { - if (ContainersModel.getDefaultContainer()) { - menuContent.checkCurrentItem() - } - } - } - - function checkCurrentItem() { - var item = menuContent.itemAtIndex(currentIndex) - if (item !== null) { - var radioButton = item.children[0].children[0] - radioButton.checked = true - } - } - delegate: Item { implicitWidth: rootWidth implicitHeight: content.implicitHeight @@ -69,7 +52,7 @@ ListView { showImage: !isInstalled checkable: isInstalled && !ConnectionController.isConnected && isSupported - checked: isDefault + checked: proxyDefaultServerContainersModel.mapToSource(index) === ServersModel.getDefaultServerData("defaultContainer") onClicked: { if (ConnectionController.isConnected && isInstalled) { @@ -78,18 +61,18 @@ ListView { } if (checked) { - containersDropDown.menuVisible = false - ServersModel.setDefaultContainer(proxyContainersModel.mapToSource(index)) + containersDropDown.close() + ServersModel.setDefaultContainer(ServersModel.defaultIndex, proxyDefaultServerContainersModel.mapToSource(index)) } else { if (!isSupported && isInstalled) { PageController.showErrorMessage(qsTr("The selected protocol is not supported on the current platform")) return } - ContainersModel.setCurrentlyProcessedContainerIndex(proxyContainersModel.mapToSource(index)) + ContainersModel.setCurrentlyProcessedContainerIndex(proxyDefaultServerContainersModel.mapToSource(index)) InstallController.setShouldCreateServer(false) PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings) - containersDropDown.menuVisible = false + containersDropDown.close() } } diff --git a/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml b/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml new file mode 100644 index 00000000..bc1f1008 --- /dev/null +++ b/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml @@ -0,0 +1,92 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 + +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" + +DrawerType2 { + id: root + + anchors.fill: parent + expandedHeight: parent.height * 0.7 + + expandedContent: ColumnLayout { + id: content + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + spacing: 0 + + Header2Type { + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 16 + + headerText: qsTr("Split tunneling") + descriptionText: qsTr("Allows you to connect to some sites or applications through a VPN connection and bypass others") + } + + LabelWithButtonType { + Layout.fillWidth: true + Layout.topMargin: 16 + + visible: ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && ServersModel.getDefaultServerData("isServerFromApi") + + text: qsTr("Split tunneling on the server") + descriptionText: qsTr("Enabled \nCan't be disabled for current server") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { +// PageController.goToPage(PageEnum.PageSettingsSplitTunneling) +// root.close() + } + } + + DividerType { + visible: ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && ServersModel.getDefaultServerData("isServerFromApi") + } + + LabelWithButtonType { + Layout.fillWidth: true + Layout.topMargin: 16 + + enabled: ! ServersModel.isDefaultServerDefaultContainerHasSplitTunneling || !ServersModel.getDefaultServerData("isServerFromApi") + + text: qsTr("Site-based split tunneling") + descriptionText: enabled && SitesModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + PageController.goToPage(PageEnum.PageSettingsSplitTunneling) + root.close() + } + } + + DividerType { + } + + LabelWithButtonType { + Layout.fillWidth: true + visible: false + + text: qsTr("App-based split tunneling") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { +// PageController.goToPage(PageEnum.PageSetupWizardConfigSource) + root.close() + } + } + + DividerType { + visible: false + } + } +} diff --git a/client/ui/qml/Components/QuestionDrawer.qml b/client/ui/qml/Components/QuestionDrawer.qml index 16cdcb39..c63c07b4 100644 --- a/client/ui/qml/Components/QuestionDrawer.qml +++ b/client/ui/qml/Components/QuestionDrawer.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import "../Controls2" import "../Controls2/TextTypes" -DrawerType { +DrawerType2 { id: root property string headerText @@ -16,23 +16,24 @@ DrawerType { property var yesButtonFunction property var noButtonFunction - width: parent.width - height: content.implicitHeight + 32 - - ColumnLayout { + expandedContent: ColumnLayout { id: content anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: 16 - anchors.rightMargin: 16 - anchors.leftMargin: 16 spacing: 8 + onImplicitHeightChanged: { + root.expandedHeight = content.implicitHeight + 32 + } + Header2TextType { Layout.fillWidth: true + Layout.topMargin: 16 + Layout.rightMargin: 16 + Layout.leftMargin: 16 text: headerText } @@ -40,6 +41,8 @@ DrawerType { ParagraphTextType { Layout.fillWidth: true Layout.topMargin: 8 + Layout.rightMargin: 16 + Layout.leftMargin: 16 text: descriptionText } @@ -47,10 +50,12 @@ DrawerType { BasicButtonType { Layout.fillWidth: true Layout.topMargin: 16 + Layout.rightMargin: 16 + Layout.leftMargin: 16 text: yesButtonText - onClicked: { + clickedFunc: function() { if (yesButtonFunction && typeof yesButtonFunction === "function") { yesButtonFunction() } @@ -59,6 +64,8 @@ DrawerType { BasicButtonType { Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 defaultColor: "transparent" hoveredColor: Qt.rgba(1, 1, 1, 0.08) @@ -69,7 +76,7 @@ DrawerType { text: noButtonText - onClicked: { + clickedFunc: function() { if (noButtonFunction && typeof noButtonFunction === "function") { noButtonFunction() } diff --git a/client/ui/qml/Components/SelectLanguageDrawer.qml b/client/ui/qml/Components/SelectLanguageDrawer.qml index d318aab8..260a3a98 100644 --- a/client/ui/qml/Components/SelectLanguageDrawer.qml +++ b/client/ui/qml/Components/SelectLanguageDrawer.qml @@ -5,129 +5,136 @@ import QtQuick.Layouts import "../Controls2" import "../Controls2/TextTypes" -DrawerType { +DrawerType2 { id: root - width: parent.width - height: parent.height * 0.9 + expandedContent: Item { + id: container - ColumnLayout { - id: backButton + implicitHeight: root.height * 0.9 - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 16 - - BackButtonType { - backButtonImage: "qrc:/images/controls/arrow-left.svg" - backButtonFunction: function() { - root.close() - } + Component.onCompleted: { + root.expandedHeight = container.implicitHeight } - } - - FlickableType { - anchors.top: backButton.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - contentHeight: content.implicitHeight ColumnLayout { - id: content + id: backButton - anchors.fill: parent + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 - Header2Type { - id: header - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.rightMargin: 16 - Layout.leftMargin: 16 - - headerText: qsTr("Choose language") + BackButtonType { + backButtonImage: "qrc:/images/controls/arrow-left.svg" + backButtonFunction: function() { + root.close() + } } + } - ListView { - id: listView + FlickableType { + anchors.top: backButton.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + contentHeight: content.implicitHeight - Layout.fillWidth: true - height: listView.contentItem.height + ColumnLayout { + id: content - clip: true - interactive: false + anchors.fill: parent - model: LanguageModel - currentIndex: LanguageModel.currentLanguageIndex + Header2Type { + id: header + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.rightMargin: 16 + Layout.leftMargin: 16 - ButtonGroup { - id: buttonGroup + headerText: qsTr("Choose language") } - delegate: Item { - implicitWidth: root.width - implicitHeight: delegateContent.implicitHeight + ListView { + id: listView - ColumnLayout { - id: delegateContent + Layout.fillWidth: true + height: listView.contentItem.height - anchors.fill: parent + clip: true + interactive: false - RadioButton { - id: radioButton + model: LanguageModel + currentIndex: LanguageModel.currentLanguageIndex - implicitWidth: parent.width - implicitHeight: radioButtonContent.implicitHeight + ButtonGroup { + id: buttonGroup + } - hoverEnabled: true + delegate: Item { + implicitWidth: root.width + implicitHeight: delegateContent.implicitHeight - indicator: Rectangle { - anchors.fill: parent - color: radioButton.hovered ? "#2C2D30" : "#1C1D21" + ColumnLayout { + id: delegateContent - Behavior on color { - PropertyAnimation { duration: 200 } - } - } + anchors.fill: parent - RowLayout { - id: radioButtonContent - anchors.fill: parent + RadioButton { + id: radioButton - anchors.rightMargin: 16 - anchors.leftMargin: 16 + implicitWidth: parent.width + implicitHeight: radioButtonContent.implicitHeight - spacing: 0 + hoverEnabled: true - z: 1 + indicator: Rectangle { + anchors.fill: parent + color: radioButton.hovered ? "#2C2D30" : "#1C1D21" - ParagraphTextType { - Layout.fillWidth: true - Layout.topMargin: 20 - Layout.bottomMargin: 20 - - text: languageName + Behavior on color { + PropertyAnimation { duration: 200 } + } } - Image { - source: "qrc:/images/controls/check.svg" - visible: radioButton.checked + RowLayout { + id: radioButtonContent + anchors.fill: parent - width: 24 - height: 24 + anchors.rightMargin: 16 + anchors.leftMargin: 16 - Layout.rightMargin: 8 + spacing: 0 + + z: 1 + + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 20 + Layout.bottomMargin: 20 + + text: languageName + } + + Image { + source: "qrc:/images/controls/check.svg" + visible: radioButton.checked + + width: 24 + height: 24 + + Layout.rightMargin: 8 + } } - } - ButtonGroup.group: buttonGroup - checked: listView.currentIndex === index + ButtonGroup.group: buttonGroup + checked: listView.currentIndex === index - onClicked: { - listView.currentIndex = index - LanguageModel.changeLanguage(languageIndex) - root.close() + onClicked: { + listView.currentIndex = index + LanguageModel.changeLanguage(languageIndex) + root.close() + } } } } diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml index 45ef84a6..d816cd41 100644 --- a/client/ui/qml/Components/ShareConnectionDrawer.qml +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -16,19 +16,18 @@ import "../Controls2/TextTypes" import "../Config" import "../Components" -DrawerType { +DrawerType2 { id: root - property alias headerText: header.headerText - property alias configContentHeaderText: configContentHeader.headerText - property alias contentVisible: content.visible + property string headerText + property string configContentHeaderText + property string contentVisible property string configExtension: ".vpn" property string configCaption: qsTr("Save AmneziaVPN config") property string configFileName: "amnezia_config" - width: parent.width - height: parent.height * 0.9 + expandedHeight: parent.height * 0.9 onClosed: { configExtension = ".vpn" @@ -36,8 +35,8 @@ DrawerType { configFileName = "amnezia_config" } - Item { - anchors.fill: parent + expandedContent: Item { + implicitHeight: root.expandedHeight Header2Type { id: header @@ -47,6 +46,8 @@ DrawerType { anchors.topMargin: 20 anchors.leftMargin: 16 anchors.rightMargin: 16 + + headerText: root.headerText } FlickableType { @@ -64,6 +65,8 @@ DrawerType { anchors.leftMargin: 16 anchors.rightMargin: 16 + visible: root.contentVisible + BasicButtonType { Layout.fillWidth: true Layout.topMargin: 16 @@ -71,7 +74,7 @@ DrawerType { text: qsTr("Share") imageSource: "qrc:/images/controls/share-2.svg" - onClicked: { + clickedFunc: function() { var fileName = "" if (GC.isMobile()) { fileName = configFileName + configExtension @@ -91,6 +94,7 @@ DrawerType { } BasicButtonType { + id: copyConfigTextButton Layout.fillWidth: true Layout.topMargin: 8 @@ -103,20 +107,14 @@ DrawerType { text: qsTr("Copy") imageSource: "qrc:/images/controls/copy.svg" - - onClicked: { - configText.selectAll() - configText.copy() - configText.select(0, 0) - PageController.showNotificationMessage(qsTr("Copied")) - } } BasicButtonType { + id: copyNativeConfigStringButton Layout.fillWidth: true Layout.topMargin: 8 - visible: nativeConfigString.text !== "" + visible: false defaultColor: "transparent" hoveredColor: Qt.rgba(1, 1, 1, 0.08) @@ -127,13 +125,6 @@ DrawerType { text: qsTr("Copy config string") imageSource: "qrc:/images/controls/copy.svg" - - onClicked: { - nativeConfigString.selectAll() - nativeConfigString.copy() - nativeConfigString.select(0, 0) - PageController.showNotificationMessage(qsTr("Copied")) - } } BasicButtonType { @@ -149,83 +140,117 @@ DrawerType { text: qsTr("Show connection settings") - onClicked: { - configContentDrawer.visible = true + clickedFunc: function() { + configContentDrawer.open() } } - DrawerType { + DrawerType2 { id: configContentDrawer - width: parent.width - height: parent.height * 0.9 + parent: root.parent - BackButtonType { - id: backButton + anchors.fill: parent + expandedHeight: parent.height * 0.9 - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 16 + expandedContent: Item { + id: configContentContainer - backButtonFunction: function() { - configContentDrawer.visible = false + implicitHeight: configContentDrawer.expandedHeight + + Connections { + target: copyNativeConfigStringButton + function onClicked() { + nativeConfigString.selectAll() + nativeConfigString.copy() + nativeConfigString.select(0, 0) + PageController.showNotificationMessage(qsTr("Copied")) + } } - } - FlickableType { - anchors.top: backButton.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - contentHeight: configContent.implicitHeight + configContent.anchors.topMargin + configContent.anchors.bottomMargin - - ColumnLayout { - id: configContent - - anchors.fill: parent - anchors.rightMargin: 16 - anchors.leftMargin: 16 - - Header2Type { - id: configContentHeader - Layout.fillWidth: true - Layout.topMargin: 16 + Connections { + target: copyConfigTextButton + function onClicked() { + configText.selectAll() + configText.copy() + configText.select(0, 0) + PageController.showNotificationMessage(qsTr("Copied")) } + } - TextField { - id: nativeConfigString - visible: false - text: ExportController.nativeConfigString + BackButtonType { + id: backButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 + + backButtonFunction: function() { + configContentDrawer.close() } + } - TextArea { - id: configText + FlickableType { + anchors.top: backButton.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + contentHeight: configContent.implicitHeight + configContent.anchors.topMargin + configContent.anchors.bottomMargin - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.bottomMargin: 16 + ColumnLayout { + id: configContent - padding: 0 - leftPadding: 0 - height: 24 + anchors.fill: parent + anchors.rightMargin: 16 + anchors.leftMargin: 16 - readOnly: true + Header2Type { + id: configContentHeader + Layout.fillWidth: true + Layout.topMargin: 16 - color: "#D7D8DB" - selectionColor: "#633303" - selectedTextColor: "#D7D8DB" + headerText: root.configContentHeaderText + } - font.pixelSize: 16 - font.weight: Font.Medium - font.family: "PT Root UI VF" + TextField { + id: nativeConfigString + visible: false + text: ExportController.nativeConfigString - text: ExportController.config + onTextChanged: { + copyNativeConfigStringButton.visible = nativeConfigString.text !== "" + } + } - wrapMode: Text.Wrap + TextArea { + id: configText - background: Rectangle { - color: "transparent" + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.bottomMargin: 16 + + padding: 0 + leftPadding: 0 + height: 24 + + readOnly: true + + color: "#D7D8DB" + selectionColor: "#633303" + selectedTextColor: "#D7D8DB" + + font.pixelSize: 16 + font.weight: Font.Medium + font.family: "PT Root UI VF" + + text: ExportController.config + + wrapMode: Text.Wrap + + background: Rectangle { + color: "transparent" + } } } } diff --git a/client/ui/qml/Controls2/BasicButtonType.qml b/client/ui/qml/Controls2/BasicButtonType.qml index a5cde951..257486d6 100644 --- a/client/ui/qml/Controls2/BasicButtonType.qml +++ b/client/ui/qml/Controls2/BasicButtonType.qml @@ -16,47 +16,39 @@ Button { property string textColor: "#0E0E11" property string borderColor: "#D7D8DB" + property string borderFocusedColor: "#D7D8DB" property int borderWidth: 0 + property int borderFocusedWidth: 1 property string imageSource + property string rightImageSource + property string leftImageColor: textColor property bool squareLeftSide: false + property var clickedFunc + implicitHeight: 56 hoverEnabled: true background: Rectangle { - id: background + id: background_border + + color: "transparent" + border.color: root.activeFocus ? root.borderFocusedColor : "transparent" + border.width: root.activeFocus ? root.borderFocusedWidth : "transparent" + anchors.fill: parent radius: 16 - color: { - if (root.enabled) { - if (root.pressed) { - return pressedColor - } - return root.hovered ? hoveredColor : defaultColor - } else { - return disabledColor - } - } - border.color: borderColor - border.width: borderWidth - - Behavior on color { - PropertyAnimation { duration: 200 } - } Rectangle { - visible: root.squareLeftSide + id: background - z: 1 + anchors.fill: background_border + anchors.margins: root.activeFocus ? 2: 0 - width: parent.radius - height: parent.radius - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left + radius: 16 color: { if (root.enabled) { if (root.pressed) { @@ -67,24 +59,53 @@ Button { return disabledColor } } + border.color: root.activeFocus ? "transparent" : borderColor + border.width: root.activeFocus ? 0 : borderWidth Behavior on color { PropertyAnimation { duration: 200 } } + + Rectangle { + visible: root.squareLeftSide + + z: 1 + + width: parent.radius + height: parent.radius + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + color: { + if (root.enabled) { + if (root.pressed) { + return pressedColor + } + return root.hovered ? hoveredColor : defaultColor + } else { + return disabledColor + } + } + + Behavior on color { + PropertyAnimation { duration: 200 } + } + } } } MouseArea { - anchors.fill: background + anchors.fill: background_border enabled: false cursorShape: Qt.PointingHandCursor } contentItem: Item { - anchors.fill: background + anchors.fill: background_border implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight + RowLayout { id: content anchors.centerIn: parent @@ -99,7 +120,7 @@ Button { layer { enabled: true effect: ColorOverlay { - color: textColor + color: leftImageColor } } } @@ -112,6 +133,39 @@ Button { horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignVCenter } + + Image { + Layout.preferredHeight: 20 + Layout.preferredWidth: 20 + + source: root.rightImageSource + visible: root.rightImageSource === "" ? false : true + + layer { + enabled: true + effect: ColorOverlay { + color: textColor + } + } + } + } + } + + Keys.onEnterPressed: { + if (root.clickedFunc && typeof root.clickedFunc === "function") { + root.clickedFunc() + } + } + + Keys.onReturnPressed: { + if (root.clickedFunc && typeof root.clickedFunc === "function") { + root.clickedFunc() + } + } + + onClicked: { + if (root.clickedFunc && typeof root.clickedFunc === "function") { + root.clickedFunc() } } } diff --git a/client/ui/qml/Controls2/DrawerType.qml b/client/ui/qml/Controls2/DrawerType.qml deleted file mode 100644 index 830f59f9..00000000 --- a/client/ui/qml/Controls2/DrawerType.qml +++ /dev/null @@ -1,84 +0,0 @@ -import QtQuick -import QtQuick.Controls - -import "../Config" - -Drawer { - id: drawer - property bool needCloseButton: true - - Connections { - target: PageController - - function onForceCloseDrawer() { - visible = false - } - } - - edge: Qt.BottomEdge - - clip: true - modal: true - dragMargin: -10 - - enter: Transition { - SmoothedAnimation { - velocity: 4 - } - } - - exit: Transition { - SmoothedAnimation { - velocity: 4 - } - } - - background: Rectangle { - anchors.fill: parent - anchors.bottomMargin: -radius - radius: 16 - color: "#1C1D21" - - border.color: "#2C2D30" - border.width: 1 - - Rectangle { - visible: GC.isMobile() - - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: 10 - - width: 20 - height: 2 - color: "#2C2D30" - } - } - - Overlay.modal: Rectangle { - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) - } - - onAboutToShow: { - if (PageController.getInitialPageNavigationBarColor() !== 0xFF1C1D21) { - PageController.updateNavigationBarColor(0xFF1C1D21) - } - } - - onOpened: { - if (needCloseButton) { - PageController.drawerOpen() - } - } - - onClosed: { - if (needCloseButton) { - PageController.drawerClose() - } - - var initialPageNavigationBarColor = PageController.getInitialPageNavigationBarColor() - if (initialPageNavigationBarColor !== 0xFF1C1D21) { - PageController.updateNavigationBarColor(initialPageNavigationBarColor) - } - } -} diff --git a/client/ui/qml/Controls2/DrawerType2.qml b/client/ui/qml/Controls2/DrawerType2.qml new file mode 100644 index 00000000..5e6f785c --- /dev/null +++ b/client/ui/qml/Controls2/DrawerType2.qml @@ -0,0 +1,242 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import "TextTypes" + +Item { + id: root + + readonly property string drawerExpanded: "expanded" + readonly property string drawerCollapsed: "collapsed" + + readonly property bool isOpened: drawerContent.state === root.drawerExpanded || (drawerContent.state === root.drawerCollapsed && dragArea.drag.active === true) + readonly property bool isClosed: drawerContent.state === root.drawerCollapsed && dragArea.drag.active === false + + readonly property bool isExpanded: drawerContent.state === root.drawerExpanded + readonly property bool isCollapsed: drawerContent.state === root.drawerCollapsed + + property Component collapsedContent + property Component expandedContent + + property string defaultColor: "#1C1D21" + property string borderColor: "#2C2D30" + + property real expandedHeight + property real collapsedHeight: 0 + + signal entered + signal exited + signal pressed(bool pressed, bool entered) + + signal aboutToHide + signal aboutToShow + signal close + signal open + signal closed + signal opened + + Connections { + target: root + + function onClose() { + if (isCollapsed) { + return + } + + aboutToHide() + + drawerContent.state = root.drawerCollapsed + closed() + } + + function onOpen() { + if (isExpanded) { + return + } + + aboutToShow() + + drawerContent.state = root.drawerExpanded + opened() + } + } + + /** Set once based on first implicit height change once all children are layed out */ + Component.onCompleted: { + if (root.isCollapsed && root.collapsedHeight == 0) { + root.collapsedHeight = drawerContent.implicitHeight + } + } + + Rectangle { + id: background + + anchors.fill: parent + color: root.isCollapsed ? "transparent" : Qt.rgba(14/255, 14/255, 17/255, 0.8) + + Behavior on color { + PropertyAnimation { duration: 200 } + } + } + + MouseArea { + id: emptyArea + anchors.fill: parent + enabled: root.isExpanded + visible: enabled + onClicked: { + root.close() + } + } + + MouseArea { + id: dragArea + + anchors.fill: drawerContentBackground + cursorShape: root.isCollapsed ? Qt.PointingHandCursor : Qt.ArrowCursor + hoverEnabled: true + + enabled: drawerContent.implicitHeight > 0 + + drag.target: drawerContent + drag.axis: Drag.YAxis + drag.maximumY: root.height - root.collapsedHeight + drag.minimumY: root.height - root.expandedHeight + + /** If drag area is released at any point other than min or max y, transition to the other state */ + onReleased: { + if (root.isCollapsed && drawerContent.y < dragArea.drag.maximumY) { + root.open() + return + } + if (root.isExpanded && drawerContent.y > dragArea.drag.minimumY) { + root.close() + return + } + } + + onEntered: { + root.entered() + } + onExited: { + root.exited() + } + onPressedChanged: { + root.pressed(pressed, entered) + } + + onClicked: { + if (root.isCollapsed) { + root.open() + } + } + } + + Rectangle { + id: drawerContentBackground + + anchors { left: drawerContent.left; right: drawerContent.right; top: drawerContent.top } + height: root.height + radius: 16 + color: root.defaultColor + border.color: root.borderColor + border.width: 1 + + Rectangle { + width: parent.radius + height: parent.radius + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.left: parent.left + color: parent.color + } + } + + Item { + id: drawerContent + + Drag.active: dragArea.drag.active + anchors.right: root.right + anchors.left: root.left + y: root.height - drawerContent.height + state: root.drawerCollapsed + + implicitHeight: root.isCollapsed ? collapsedLoader.implicitHeight : expandedLoader.implicitHeight + + onStateChanged: { + if (root.isCollapsed) { + var initialPageNavigationBarColor = PageController.getInitialPageNavigationBarColor() + if (initialPageNavigationBarColor !== 0xFF1C1D21) { + PageController.updateNavigationBarColor(initialPageNavigationBarColor) + } + return + } + if (root.isExpanded) { + if (PageController.getInitialPageNavigationBarColor() !== 0xFF1C1D21) { + PageController.updateNavigationBarColor(0xFF1C1D21) + } + return + } + } + + states: [ + State { + name: root.drawerCollapsed + PropertyChanges { + target: drawerContent + y: root.height - root.collapsedHeight + } + }, + State { + name: root.drawerExpanded + PropertyChanges { + target: drawerContent + y: dragArea.drag.minimumY + + } + } + ] + + transitions: [ + Transition { + from: root.drawerCollapsed + to: root.drawerExpanded + PropertyAnimation { + target: drawerContent + properties: "y" + duration: 200 + } + }, + Transition { + from: root.drawerExpanded + to: root.drawerCollapsed + PropertyAnimation { + target: drawerContent + properties: "y" + duration: 200 + } + } + ] + + Loader { + id: collapsedLoader + + visible: root.isCollapsed + sourceComponent: root.collapsedContent + + anchors.right: parent.right + anchors.left: parent.left + } + + Loader { + id: expandedLoader + + visible: root.isExpanded + sourceComponent: root.expandedContent + + anchors.right: parent.right + anchors.left: parent.left + } + } +} diff --git a/client/ui/qml/Controls2/DropDownType.qml b/client/ui/qml/Controls2/DropDownType.qml index b91f0b7a..5b876d79 100644 --- a/client/ui/qml/Controls2/DropDownType.qml +++ b/client/ui/qml/Controls2/DropDownType.qml @@ -36,19 +36,23 @@ Item { property int rootButtonTextBottomMargin: 16 property real drawerHeight: 0.9 + property Item drawerParent property Component listView - property alias menuVisible: menu.visible + signal open + signal close implicitWidth: rootButtonContent.implicitWidth implicitHeight: rootButtonContent.implicitHeight - onMenuVisibleChanged: { - if (menuVisible) { - rootButtonBackground.border.color = rootButtonPressedBorderColor - } else { - rootButtonBackground.border.color = rootButtonDefaultBorderColor - } + onOpen: { + menu.open() + rootButtonBackground.border.color = rootButtonPressedBorderColor + } + + onClose: { + menu.close() + rootButtonBackground.border.color = rootButtonDefaultBorderColor } onEnabledChanged: { @@ -133,21 +137,21 @@ Item { hoverEnabled: root.enabled ? true : false onEntered: { - if (menu.visible === false) { + if (menu.isClosed) { rootButtonBackground.border.color = rootButtonHoveredBorderColor rootButtonBackground.color = rootButtonBackgroundHoveredColor } } onExited: { - if (menu.visible === false) { + if (menu.isClosed) { rootButtonBackground.border.color = rootButtonDefaultBorderColor rootButtonBackground.color = rootButtonBackgroundColor } } onPressed: { - if (menu.visible === false) { + if (menu.isClosed) { rootButtonBackground.color = pressed ? rootButtonBackgroundPressedColor : entered ? rootButtonHoveredBorderColor : rootButtonDefaultBorderColor } } @@ -156,60 +160,68 @@ Item { if (rootButtonClickedFunction && typeof rootButtonClickedFunction === "function") { rootButtonClickedFunction() } else { - menu.visible = true + menu.open() } } } - DrawerType { + DrawerType2 { id: menu - width: parent.width - height: parent.height * drawerHeight + parent: drawerParent - ColumnLayout { - id: header + anchors.fill: parent + expandedHeight: drawerParent.height * drawerHeight - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 16 + expandedContent: Item { + id: container - BackButtonType { - backButtonImage: root.headerBackButtonImage - backButtonFunction: function() { - root.menuVisible = false - } - } - } + implicitHeight: menu.expandedHeight - FlickableType { - anchors.top: header.bottom - anchors.topMargin: 16 - contentHeight: col.implicitHeight + ColumnLayout { + id: header - Column { - id: col anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right + anchors.topMargin: 16 - spacing: 16 + BackButtonType { + backButtonImage: root.headerBackButtonImage + backButtonFunction: function() { + menu.close() + } + } + } - Header2Type { + 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 - anchors.leftMargin: 16 - anchors.rightMargin: 16 - headerText: root.headerText + spacing: 16 - width: parent.width - } + Header2Type { + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 16 + anchors.rightMargin: 16 - Loader { - id: listViewLoader - sourceComponent: root.listView + headerText: root.headerText + + width: parent.width + } + + Loader { + id: listViewLoader + sourceComponent: root.listView + } } } } diff --git a/client/ui/qml/Controls2/PageType.qml b/client/ui/qml/Controls2/PageType.qml index 2c176b40..706a32e1 100644 --- a/client/ui/qml/Controls2/PageType.qml +++ b/client/ui/qml/Controls2/PageType.qml @@ -7,6 +7,8 @@ Item { property StackView stackView: StackView.view + property var defaultActiveFocusItem: null + // MouseArea { // id: globalMouseArea // z: 99 @@ -19,4 +21,17 @@ Item { // mouse.accepted = false // } // } + + // Set a timer to set focus after a short delay + Timer { + id: timer + interval: 100 // Milliseconds + onTriggered: { + if (defaultActiveFocusItem) { + defaultActiveFocusItem.forceActiveFocus() + } + } + repeat: false // Stop the timer after one trigger + running: true // Start the timer + } } diff --git a/client/ui/qml/Controls2/PopupType.qml b/client/ui/qml/Controls2/PopupType.qml index e4d2a449..c85997dc 100644 --- a/client/ui/qml/Controls2/PopupType.qml +++ b/client/ui/qml/Controls2/PopupType.qml @@ -66,7 +66,7 @@ Popup { borderWidth: 0 text: qsTr("Close") - onClicked: { + clickedFunc: function() { root.close() } } diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index ac0473cf..606aa83e 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -69,10 +69,13 @@ Item { TextField { id: textField + activeFocusOnTab: false enabled: root.textFieldEditable color: root.enabled ? root.textFieldTextColor : root.textFieldTextDisabledColor + inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhSensitiveData | Qt.ImhNoPredictiveText + placeholderText: root.textFieldPlaceholderText placeholderTextColor: "#494B50" @@ -142,7 +145,7 @@ Item { Layout.preferredWidth: content.implicitHeight squareLeftSide: true - onClicked: { + clickedFunc: function() { if (root.clickedFunc && typeof root.clickedFunc === "function") { root.clickedFunc() } @@ -186,4 +189,12 @@ Item { function getBackgroundBorderColor(noneFocusedColor) { return textField.focus ? root.borderFocusedColor : noneFocusedColor } + + Keys.onEnterPressed: { + KeyNavigation.tab.forceActiveFocus(); + } + + Keys.onReturnPressed: { + KeyNavigation.tab.forceActiveFocus(); + } } diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index db6ec11c..536fc951 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -18,484 +18,383 @@ import "../Components" PageType { id: root - property string defaultColor: "#1C1D21" - - property string borderColor: "#2C2D30" - Connections { target: PageController function onRestorePageHomeState(isContainerInstalled) { - buttonContent.state = "expanded" + drawer.open() if (isContainerInstalled) { containersDropDown.rootButtonClickedFunction() } } - - function onForceCloseDrawer() { - buttonContent.state = "collapsed" - } - } - - MouseArea { - anchors.fill: parent - enabled: buttonContent.state === "expanded" - onClicked: { - buttonContent.state = "collapsed" - } } Item { anchors.fill: parent - anchors.bottomMargin: buttonContent.collapsedHeight + anchors.bottomMargin: drawer.collapsedHeight ConnectButton { + id: connectButton anchors.centerIn: parent } - } - MouseArea { - id: dragArea - - anchors.fill: buttonBackground - cursorShape: buttonContent.state === "collapsed" ? Qt.PointingHandCursor : Qt.ArrowCursor - hoverEnabled: true - - drag.target: buttonContent - drag.axis: Drag.YAxis - drag.maximumY: root.height - buttonContent.collapsedHeight - drag.minimumY: root.height - root.height * 0.9 - - /** If drag area is released at any point other than min or max y, transition to the other state */ - onReleased: { - if (buttonContent.state === "collapsed" && buttonContent.y < dragArea.drag.maximumY) { - buttonContent.state = "expanded" - return - } - if (buttonContent.state === "expanded" && buttonContent.y > dragArea.drag.minimumY) { - buttonContent.state = "collapsed" - return - } - } - - onEntered: { - collapsedButtonChevron.backgroundColor = collapsedButtonChevron.hoveredColor - collapsedButtonHeader.opacity = 0.8 - } - onExited: { - collapsedButtonChevron.backgroundColor = collapsedButtonChevron.defaultColor - collapsedButtonHeader.opacity = 1 - } - onPressedChanged: { - collapsedButtonChevron.backgroundColor = pressed ? collapsedButtonChevron.pressedColor : entered ? collapsedButtonChevron.hoveredColor : collapsedButtonChevron.defaultColor - collapsedButtonHeader.opacity = 0.7 - } - - - onClicked: { - if (buttonContent.state === "collapsed") { - buttonContent.state = "expanded" - } - } - } - - Rectangle { - id: buttonBackground - - anchors { left: buttonContent.left; right: buttonContent.right; top: buttonContent.top } - height: root.height - radius: 16 - color: root.defaultColor - border.color: root.borderColor - border.width: 1 - - Rectangle { - width: parent.radius - height: parent.radius + BasicButtonType { + anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.left: parent.left - color: parent.color + anchors.bottomMargin: 34 + leftPadding: 16 + rightPadding: 16 + + implicitHeight: 36 + + defaultColor: "transparent" + hoveredColor: Qt.rgba(1, 1, 1, 0.08) + pressedColor: Qt.rgba(1, 1, 1, 0.12) + disabledColor: "#878B91" + textColor: "#878B91" + leftImageColor: "transparent" + borderWidth: 0 + + property bool isSplitTunnelingEnabled: SitesModel.isTunnelingEnabled || + (ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && ServersModel.getDefaultServerData("isServerFromApi")) + + text: isSplitTunnelingEnabled ? qsTr("Split tunneling enabled") : qsTr("Split tunneling disabled") + + imageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : "" + rightImageSource: "qrc:/images/controls/chevron-down.svg" + + onClicked: { + homeSplitTunnelingDrawer.open() + } + + HomeSplitTunnelingDrawer { + id: homeSplitTunnelingDrawer + + parent: root + } } } - ColumnLayout { - id: buttonContent - /** Initial height of button content */ - property int collapsedHeight: 0 - /** True when expanded objects should be visible */ - property bool expandedVisibility: buttonContent.state === "expanded" || (buttonContent.state === "collapsed" && dragArea.drag.active === true) - /** True when collapsed objects should be visible */ - property bool collapsedVisibility: buttonContent.state === "collapsed" && dragArea.drag.active === false + DrawerType2 { + id: drawer + anchors.fill: parent - Drag.active: dragArea.drag.active - anchors.right: root.right - anchors.left: root.left - y: root.height - buttonContent.height - - Component.onCompleted: { - buttonContent.state = "collapsed" - } - - /** Set once based on first implicit height change once all children are layed out */ - onImplicitHeightChanged: { - if (buttonContent.state === "collapsed" && collapsedHeight == 0) { - collapsedHeight = implicitHeight - } - } - - onStateChanged: { - if (buttonContent.state === "collapsed") { - var initialPageNavigationBarColor = PageController.getInitialPageNavigationBarColor() - if (initialPageNavigationBarColor !== 0xFF1C1D21) { - PageController.updateNavigationBarColor(initialPageNavigationBarColor) - } - PageController.drawerClose() - return - } - if (buttonContent.state === "expanded") { - if (PageController.getInitialPageNavigationBarColor() !== 0xFF1C1D21) { - PageController.updateNavigationBarColor(0xFF1C1D21) - } - PageController.drawerOpen() - return - } - } - - /** Two states of buttonContent, great place to add any future animations for the drawer */ - states: [ - State { - name: "collapsed" - PropertyChanges { - target: buttonContent - y: root.height - collapsedHeight - } - }, - State { - name: "expanded" - PropertyChanges { - target: buttonContent - y: dragArea.drag.minimumY - - } - } - ] - - transitions: [ - Transition { - from: "collapsed" - to: "expanded" - PropertyAnimation { - target: buttonContent - properties: "y" - duration: 200 - } - }, - Transition { - from: "expanded" - to: "collapsed" - PropertyAnimation { - target: buttonContent - properties: "y" - duration: 200 - } - } - ] - - DividerType { - Layout.topMargin: 10 - Layout.fillWidth: false - Layout.preferredWidth: 20 - Layout.preferredHeight: 2 - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - - visible: (buttonContent.collapsedVisibility || buttonContent.expandedVisibility) - } - - RowLayout { - Layout.topMargin: 14 - Layout.leftMargin: 24 - Layout.rightMargin: 24 - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - visible: buttonContent.collapsedVisibility - - spacing: 0 - - Header1TextType { - id: collapsedButtonHeader - Layout.maximumWidth: buttonContent.width - 48 - 18 - 12 // todo - - maximumLineCount: 2 - elide: Qt.ElideRight - - text: ServersModel.defaultServerName - horizontalAlignment: Qt.AlignHCenter - - Behavior on opacity { - PropertyAnimation { duration: 200 } - } - } - - ImageButtonType { - id: collapsedButtonChevron - - Layout.leftMargin: 8 - - hoverEnabled: false - image: "qrc:/images/controls/chevron-down.svg" - imageColor: "#d7d8db" - - icon.width: 18 - icon.height: 18 - backgroundRadius: 16 - horizontalPadding: 4 - topPadding: 4 - bottomPadding: 3 - - onClicked: { - if (buttonContent.state === "collapsed") { - buttonContent.state = "expanded" - } - } - } - } - - LabelTextType { - id: collapsedServerMenuDescription - Layout.bottomMargin: 44 - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - visible: buttonContent.collapsedVisibility - text: ServersModel.defaultServerDescriptionCollapsed - } - - ColumnLayout { - id: serversMenuHeader - - Layout.alignment: Qt.AlignTop | Qt.AlignHCenter - Layout.fillWidth: true - visible: buttonContent.expandedVisibility - - Header1TextType { - Layout.fillWidth: true - Layout.topMargin: 14 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - text: ServersModel.defaultServerName - horizontalAlignment: Qt.AlignHCenter - maximumLineCount: 2 - elide: Qt.ElideRight - } - - LabelTextType { - id: expandedServersMenuDescription - Layout.bottomMargin: 24 - Layout.fillWidth: true - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - text: ServersModel.defaultServerDescriptionExpanded + collapsedContent: ColumnLayout { + DividerType { + Layout.topMargin: 10 + Layout.fillWidth: false + Layout.preferredWidth: 20 + Layout.preferredHeight: 2 + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter } RowLayout { + Layout.topMargin: 14 + Layout.leftMargin: 24 + Layout.rightMargin: 24 Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - spacing: 8 - DropDownType { - id: containersDropDown + spacing: 0 - rootButtonImageColor: "#0E0E11" - rootButtonBackgroundColor: "#D7D8DB" - rootButtonBackgroundHoveredColor: Qt.rgba(215, 216, 219, 0.8) - rootButtonBackgroundPressedColor: Qt.rgba(215, 216, 219, 0.65) - rootButtonHoveredBorderColor: "transparent" - rootButtonDefaultBorderColor: "transparent" - rootButtonTextTopMargin: 8 - rootButtonTextBottomMargin: 8 - - text: ServersModel.defaultContainerName - textColor: "#0E0E11" - headerText: qsTr("VPN protocol") - headerBackButtonImage: "qrc:/images/controls/arrow-left.svg" - - rootButtonClickedFunction: function() { - ServersModel.currentlyProcessedIndex = serversMenuContent.currentIndex - containersDropDown.menuVisible = true + Connections { + target: drawer + function onEntered() { + collapsedButtonChevron.backgroundColor = collapsedButtonChevron.hoveredColor + collapsedButtonHeader.opacity = 0.8 } - listView: HomeContainersListView { - rootWidth: root.width + function onExited() { + collapsedButtonChevron.backgroundColor = collapsedButtonChevron.defaultColor + collapsedButtonHeader.opacity = 1 + } + + function onPressed(pressed, entered) { + collapsedButtonChevron.backgroundColor = pressed ? collapsedButtonChevron.pressedColor : entered ? collapsedButtonChevron.hoveredColor : collapsedButtonChevron.defaultColor + collapsedButtonHeader.opacity = 0.7 + } + } + + Header1TextType { + id: collapsedButtonHeader + Layout.maximumWidth: drawer.width - 48 - 18 - 12 // todo + + maximumLineCount: 2 + elide: Qt.ElideRight + + text: ServersModel.defaultServerName + horizontalAlignment: Qt.AlignHCenter + + Behavior on opacity { + PropertyAnimation { duration: 200 } + } + } + + ImageButtonType { + id: collapsedButtonChevron + + Layout.leftMargin: 8 + + hoverEnabled: false + image: "qrc:/images/controls/chevron-down.svg" + imageColor: "#d7d8db" + + icon.width: 18 + icon.height: 18 + backgroundRadius: 16 + horizontalPadding: 4 + topPadding: 4 + bottomPadding: 3 + + onClicked: { + if (drawer.isCollapsed) { + drawer.open() + } + } + } + } + + LabelTextType { + id: collapsedServerMenuDescription + Layout.bottomMargin: 44 + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + text: ServersModel.defaultServerDescriptionCollapsed + } + + } + expandedContent: Item { + id: serverMenuContainer + + implicitHeight: root.height * 0.9 + + Component.onCompleted: { + drawer.expandedHeight = serverMenuContainer.implicitHeight + } + + ColumnLayout { + id: serversMenuHeader + + anchors.top: parent.top + anchors.right: parent.right + anchors.left: parent.left + + + Header1TextType { + Layout.fillWidth: true + Layout.topMargin: 14 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + text: ServersModel.defaultServerName + horizontalAlignment: Qt.AlignHCenter + maximumLineCount: 2 + elide: Qt.ElideRight + } + + LabelTextType { + id: expandedServersMenuDescription + Layout.bottomMargin: ServersModel.isDefaultServerFromApi ? 69 : 24 + Layout.fillWidth: true + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + text: ServersModel.defaultServerDescriptionExpanded + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + spacing: 8 + + visible: !ServersModel.isDefaultServerFromApi + onVisibleChanged: expandedServersMenuDescription.Layout + + DropDownType { + id: containersDropDown + + rootButtonImageColor: "#0E0E11" + rootButtonBackgroundColor: "#D7D8DB" + rootButtonBackgroundHoveredColor: Qt.rgba(215, 216, 219, 0.8) + rootButtonBackgroundPressedColor: Qt.rgba(215, 216, 219, 0.65) + rootButtonHoveredBorderColor: "transparent" + rootButtonDefaultBorderColor: "transparent" + rootButtonTextTopMargin: 8 + rootButtonTextBottomMargin: 8 + + text: ServersModel.defaultServerDefaultContainerName + textColor: "#0E0E11" + headerText: qsTr("VPN protocol") + headerBackButtonImage: "qrc:/images/controls/arrow-left.svg" + + rootButtonClickedFunction: function() { + containersDropDown.open() + } + + drawerParent: root + + listView: HomeContainersListView { + rootWidth: root.width + + Connections { + target: ServersModel + + function onDefaultServerIndexChanged() { + updateContainersModelFilters() + } + } + + function updateContainersModelFilters() { + if (ServersModel.isDefaultServerHasWriteAccess()) { + proxyDefaultServerContainersModel.filters = ContainersModelFilters.getWriteAccessProtocolsListFilters() + } else { + proxyDefaultServerContainersModel.filters = ContainersModelFilters.getReadAccessProtocolsListFilters() + } + } + + model: SortFilterProxyModel { + id: proxyDefaultServerContainersModel + sourceModel: DefaultServerContainersModel + + sorters: [ + RoleSorter { roleName: "isInstalled"; sortOrder: Qt.DescendingOrder } + ] + } + + Component.onCompleted: updateContainersModelFilters() + } + } + } + + Header2Type { + Layout.fillWidth: true + Layout.topMargin: 48 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + headerText: qsTr("Servers") + } + } + + Flickable { + id: serversContainer + + anchors.top: serversMenuHeader.bottom + anchors.right: parent.right + anchors.left: parent.left + anchors.topMargin: 16 + + contentHeight: col.height + col.anchors.bottomMargin + implicitHeight: parent.height - serversMenuHeader.implicitHeight + clip: true + + ScrollBar.vertical: ScrollBar { + id: scrollBar + policy: serversContainer.height >= serversContainer.contentHeight ? ScrollBar.AlwaysOff : ScrollBar.AlwaysOn + } + + Keys.onUpPressed: scrollBar.decrease() + Keys.onDownPressed: scrollBar.increase() + + Column { + id: col + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottomMargin: 32 + + spacing: 16 + + ButtonGroup { + id: serversRadioButtonGroup + } + + ListView { + id: serversMenuContent + width: parent.width + height: serversMenuContent.contentItem.height + + model: ServersModel + currentIndex: ServersModel.defaultIndex Connections { target: ServersModel - - function onCurrentlyProcessedServerIndexChanged() { - updateContainersModelFilters() + function onDefaultServerIndexChanged(serverIndex) { + serversMenuContent.currentIndex = serverIndex } } - function updateContainersModelFilters() { - if (ServersModel.isCurrentlyProcessedServerHasWriteAccess()) { - proxyContainersModel.filters = ContainersModelFilters.getWriteAccessProtocolsListFilters() - } else { - proxyContainersModel.filters = ContainersModelFilters.getReadAccessProtocolsListFilters() - } - } + clip: true + interactive: false - model: SortFilterProxyModel { - id: proxyContainersModel - sourceModel: ContainersModel - } + delegate: Item { + id: menuContentDelegate - Component.onCompleted: updateContainersModelFilters() - } - } - } + property variant delegateData: model - Header2Type { - Layout.fillWidth: true - Layout.topMargin: 48 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - visible: buttonContent.expandedVisibility + implicitWidth: serversMenuContent.width + implicitHeight: serverRadioButtonContent.implicitHeight - headerText: qsTr("Servers") - } - } + ColumnLayout { + id: serverRadioButtonContent - Flickable { - id: serversContainer - Layout.alignment: Qt.AlignTop | Qt.AlignHCenter - Layout.fillWidth: true - Layout.topMargin: 16 - contentHeight: col.implicitHeight - implicitHeight: root.height - (root.height * 0.1) - serversMenuHeader.implicitHeight - 52 //todo 52 is tabbar height - visible: buttonContent.expandedVisibility - clip: true + anchors.fill: parent + anchors.rightMargin: 16 + anchors.leftMargin: 16 - ScrollBar.vertical: ScrollBar { - id: scrollBar - policy: serversContainer.height >= serversContainer.contentHeight ? ScrollBar.AlwaysOff : ScrollBar.AlwaysOn - } + spacing: 0 - Keys.onUpPressed: scrollBar.decrease() - Keys.onDownPressed: scrollBar.increase() + RowLayout { + VerticalRadioButton { + id: serverRadioButton - Column { - id: col - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + Layout.fillWidth: true - spacing: 16 + text: name + descriptionText: serverDescription - ButtonGroup { - id: serversRadioButtonGroup - } + checked: index === serversMenuContent.currentIndex + checkable: !ConnectionController.isConnected - ListView { - id: serversMenuContent - width: parent.width - height: serversMenuContent.contentItem.height + ButtonGroup.group: serversRadioButtonGroup - model: ServersModel - currentIndex: ServersModel.defaultIndex + onClicked: { + if (ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("Unable change server while there is an active connection")) + return + } - Connections { - target: ServersModel - function onDefaultServerIndexChanged(serverIndex) { - serversMenuContent.currentIndex = serverIndex - } - } + serversMenuContent.currentIndex = index - clip: true - interactive: false + ServersModel.defaultIndex = index + } - delegate: Item { - id: menuContentDelegate + MouseArea { + anchors.fill: serverRadioButton + cursorShape: Qt.PointingHandCursor + enabled: false + } + } - property variant delegateData: model + ImageButtonType { + image: "qrc:/images/controls/settings.svg" + imageColor: "#D7D8DB" - implicitWidth: serversMenuContent.width - implicitHeight: serverRadioButtonContent.implicitHeight + implicitWidth: 56 + implicitHeight: 56 - ColumnLayout { - id: serverRadioButtonContent + z: 1 - anchors.fill: parent - anchors.rightMargin: 16 - anchors.leftMargin: 16 - - spacing: 0 - - RowLayout { - VerticalRadioButton { - id: serverRadioButton + onClicked: function() { + ServersModel.processedIndex = index + PageController.goToPage(PageEnum.PageSettingsServerInfo) + drawer.close() + } + } + } + DividerType { Layout.fillWidth: true - - text: name - descriptionText: { - var fullDescription = "" - if (hasWriteAccess) { - if (SettingsController.isAmneziaDnsEnabled() - && ServersModel.isAmneziaDnsContainerInstalled(index)) { - fullDescription += "Amnezia DNS | " - } - } else { - if (containsAmneziaDns) { - fullDescription += "Amnezia DNS | " - } - } - - return fullDescription += serverDescription - } - - checked: index === serversMenuContent.currentIndex - checkable: !ConnectionController.isConnected - - ButtonGroup.group: serversRadioButtonGroup - - onClicked: { - if (ConnectionController.isConnected) { - PageController.showNotificationMessage(qsTr("Unable change server while there is an active connection")) - return - } - - serversMenuContent.currentIndex = index - - ServersModel.currentlyProcessedIndex = index - ServersModel.defaultIndex = index - } - - MouseArea { - anchors.fill: serverRadioButton - cursorShape: Qt.PointingHandCursor - enabled: false - } + Layout.leftMargin: 0 + Layout.rightMargin: 0 } - - ImageButtonType { - image: "qrc:/images/controls/settings.svg" - imageColor: "#D7D8DB" - - implicitWidth: 56 - implicitHeight: 56 - - z: 1 - - onClicked: function() { - ServersModel.currentlyProcessedIndex = index - PageController.goToPage(PageEnum.PageSettingsServerInfo) - buttonContent.state = "collapsed" - } - } - } - - DividerType { - Layout.fillWidth: true - Layout.leftMargin: 0 - Layout.rightMargin: 0 } } } diff --git a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml index a4f5abe3..df9f0b9f 100644 --- a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml @@ -12,9 +12,12 @@ import "../Controls2/TextTypes" import "../Config" import "../Components" + PageType { id: root + defaultActiveFocusItem: listview.currentItem.portTextField.textField + ColumnLayout { id: backButton @@ -41,9 +44,11 @@ PageType { anchors.left: parent.left anchors.right: parent.right - enabled: ServersModel.isCurrentlyProcessedServerHasWriteAccess() + enabled: ServersModel.isProcessedServerHasWriteAccess() ListView { + + id: listview width: parent.width @@ -55,9 +60,13 @@ PageType { model: AwgConfigModel delegate: Item { + id: _delegate + implicitWidth: listview.width implicitHeight: col.implicitHeight + property alias portTextField:portTextField + ColumnLayout { id: col @@ -93,6 +102,8 @@ PageType { } checkEmptyText: true + + KeyNavigation.tab: junkPacketCountTextField.textField } TextFieldWithHeaderType { @@ -116,6 +127,8 @@ PageType { } checkEmptyText: true + + KeyNavigation.tab: junkPacketMinSizeTextField.textField } TextFieldWithHeaderType { @@ -134,6 +147,8 @@ PageType { } checkEmptyText: true + + KeyNavigation.tab: junkPacketMaxSizeTextField.textField } TextFieldWithHeaderType { @@ -152,6 +167,8 @@ PageType { } checkEmptyText: true + + KeyNavigation.tab: initPacketJunkSizeTextField.textField } TextFieldWithHeaderType { @@ -170,6 +187,8 @@ PageType { } checkEmptyText: true + + KeyNavigation.tab: responsePacketJunkSizeTextField.textField } TextFieldWithHeaderType { @@ -188,6 +207,8 @@ PageType { } checkEmptyText: true + + KeyNavigation.tab: initPacketMagicHeaderTextField.textField } TextFieldWithHeaderType { @@ -206,6 +227,8 @@ PageType { } checkEmptyText: true + + KeyNavigation.tab: responsePacketMagicHeaderTextField.textField } TextFieldWithHeaderType { @@ -224,6 +247,8 @@ PageType { } checkEmptyText: true + + KeyNavigation.tab: transportPacketMagicHeaderTextField.textField } TextFieldWithHeaderType { @@ -242,6 +267,8 @@ PageType { } checkEmptyText: true + + KeyNavigation.tab: underloadPacketMagicHeaderTextField.textField } TextFieldWithHeaderType { @@ -260,6 +287,8 @@ PageType { } checkEmptyText: true + + KeyNavigation.tab: saveRestartButton } BasicButtonType { @@ -275,24 +304,24 @@ PageType { text: qsTr("Remove AmneziaWG") onClicked: { - questionDrawer.headerText = qsTr("Remove AmneziaWG from server?") - questionDrawer.descriptionText = qsTr("All users with whom you shared a connection will no longer be able to connect to it.") - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + var headerText = qsTr("Remove AmneziaWG from server?") + var descriptionText = qsTr("All users with whom you shared a connection will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { PageController.goToPage(PageEnum.PageDeinstalling) InstallController.removeCurrentlyProcessedContainer() } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } BasicButtonType { + id: saveRestartButton + Layout.fillWidth: true Layout.topMargin: 24 Layout.bottomMargin: 24 @@ -310,7 +339,7 @@ PageType { text: qsTr("Save and Restart Amnezia") - onClicked: { + clickedFunc: function() { forceActiveFocus() PageController.goToPage(PageEnum.PageSetupWizardInstalling); InstallController.updateContainer(AwgConfigModel.getConfig()) @@ -318,11 +347,8 @@ PageType { } } } - } - } - QuestionDrawer { - id: questionDrawer + } } } } diff --git a/client/ui/qml/Pages2/PageProtocolCloakSettings.qml b/client/ui/qml/Pages2/PageProtocolCloakSettings.qml index 98e9c28f..b86397ba 100644 --- a/client/ui/qml/Pages2/PageProtocolCloakSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolCloakSettings.qml @@ -15,6 +15,8 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: listview.currentItem.trafficFromField.textField + ColumnLayout { id: backButton @@ -41,7 +43,7 @@ PageType { anchors.left: parent.left anchors.right: parent.right - enabled: ServersModel.isCurrentlyProcessedServerHasWriteAccess() + enabled: ServersModel.isProcessedServerHasWriteAccess() ListView { id: listview @@ -58,6 +60,8 @@ PageType { implicitWidth: listview.width implicitHeight: col.implicitHeight + property alias trafficFromField: trafficFromField + ColumnLayout { id: col @@ -77,6 +81,8 @@ PageType { } TextFieldWithHeaderType { + id: trafficFromField + Layout.fillWidth: true Layout.topMargin: 32 @@ -96,9 +102,13 @@ PageType { } } } + + KeyNavigation.tab: portTextField.textField } TextFieldWithHeaderType { + id: portTextField + Layout.fillWidth: true Layout.topMargin: 16 @@ -112,6 +122,8 @@ PageType { port = textFieldText } } + + KeyNavigation.tab: saveRestartButton } DropDownType { @@ -122,6 +134,8 @@ PageType { descriptionText: qsTr("Cipher") headerText: qsTr("Cipher") + drawerParent: root + listView: ListViewWithRadioButtonType { id: cipherListView @@ -138,7 +152,7 @@ PageType { clickedFunction: function() { cipherDropDown.text = selectedText cipher = cipherDropDown.text - cipherDropDown.menuVisible = false + cipherDropDown.close() } Component.onCompleted: { @@ -154,13 +168,15 @@ PageType { } BasicButtonType { + id: saveRestartButton + Layout.fillWidth: true Layout.topMargin: 24 Layout.bottomMargin: 24 text: qsTr("Save and Restart Amnezia") - onClicked: { + clickedFunc: function() { forceActiveFocus() PageController.goToPage(PageEnum.PageSetupWizardInstalling); InstallController.updateContainer(CloakConfigModel.getConfig()) diff --git a/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml b/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml index 571b8eaa..7fb4634c 100644 --- a/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml @@ -16,6 +16,8 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: listview.currentItem.vpnAddressSubnetTextField.textField + ColumnLayout { id: backButton @@ -42,7 +44,7 @@ PageType { anchors.left: parent.left anchors.right: parent.right - enabled: ServersModel.isCurrentlyProcessedServerHasWriteAccess() + enabled: ServersModel.isProcessedServerHasWriteAccess() ListView { id: listview @@ -53,12 +55,14 @@ PageType { clip: true interactive: false - model: OpenVpnConfigModel + model: OpenVpnConfigModel delegate: Item { implicitWidth: listview.width implicitHeight: col.implicitHeight + property alias vpnAddressSubnetTextField: vpnAddressSubnetTextField + ColumnLayout { id: col @@ -78,6 +82,8 @@ PageType { } TextFieldWithHeaderType { + id: vpnAddressSubnetTextField + Layout.fillWidth: true Layout.topMargin: 32 @@ -89,6 +95,8 @@ PageType { subnetAddress = textFieldText } } + + KeyNavigation.tab: portTextField.enabled ? portTextField.textField : saveRestartButton } ParagraphTextType { @@ -119,6 +127,9 @@ PageType { } TextFieldWithHeaderType { + id: portTextField + + Layout.fillWidth: true Layout.topMargin: 40 @@ -134,6 +145,8 @@ PageType { port = textFieldText } } + + KeyNavigation.tab: saveRestartButton } SwitcherType { @@ -162,6 +175,8 @@ PageType { descriptionText: qsTr("Hash") headerText: qsTr("Hash") + drawerParent: root + listView: ListViewWithRadioButtonType { id: hashListView @@ -183,7 +198,7 @@ PageType { clickedFunction: function() { hashDropDown.text = selectedText hash = hashDropDown.text - hashDropDown.menuVisible = false + hashDropDown.close() } Component.onCompleted: { @@ -208,6 +223,8 @@ PageType { descriptionText: qsTr("Cipher") headerText: qsTr("Cipher") + drawerParent: root + listView: ListViewWithRadioButtonType { id: cipherListView @@ -229,7 +246,7 @@ PageType { clickedFunction: function() { cipherDropDown.text = selectedText cipher = cipherDropDown.text - cipherDropDown.menuVisible = false + cipherDropDown.close() } Component.onCompleted: { @@ -363,32 +380,33 @@ PageType { text: qsTr("Remove OpenVPN") - onClicked: { - questionDrawer.headerText = qsTr("Remove OpenVpn from server?") - questionDrawer.descriptionText = qsTr("All users with whom you shared a connection will no longer be able to connect to it.") - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + clickedFunc: function() { + var headerText = qsTr("Remove OpenVpn from server?") + var descriptionText = qsTr("All users with whom you shared a connection will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { PageController.goToPage(PageEnum.PageDeinstalling) InstallController.removeCurrentlyProcessedContainer() } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } BasicButtonType { + id: saveRestartButton + Layout.fillWidth: true Layout.topMargin: 24 Layout.bottomMargin: 24 text: qsTr("Save and Restart Amnezia") - onClicked: { + clickedFunc: function() { forceActiveFocus() PageController.goToPage(PageEnum.PageSetupWizardInstalling); InstallController.updateContainer(OpenVpnConfigModel.getConfig()) @@ -398,9 +416,5 @@ PageType { } } } - - QuestionDrawer { - id: questionDrawer - } } } diff --git a/client/ui/qml/Pages2/PageProtocolRaw.qml b/client/ui/qml/Pages2/PageProtocolRaw.qml index 967b605b..12d1722b 100644 --- a/client/ui/qml/Pages2/PageProtocolRaw.qml +++ b/client/ui/qml/Pages2/PageProtocolRaw.qml @@ -90,71 +90,77 @@ PageType { DividerType {} - DrawerType { + DrawerType2 { id: configContentDrawer - width: parent.width - height: parent.height * 0.9 + expandedHeight: root.height * 0.9 - BackButtonType { - id: backButton + parent: root + anchors.fill: parent - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 16 + expandedContent: Item { + implicitHeight: configContentDrawer.expandedHeight - backButtonFunction: function() { - configContentDrawer.visible = false - } - } + BackButtonType { + id: backButton - FlickableType { - anchors.top: backButton.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - contentHeight: configContent.implicitHeight + configContent.anchors.topMargin + configContent.anchors.bottomMargin + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 - ColumnLayout { - id: configContent - - anchors.fill: parent - anchors.rightMargin: 16 - anchors.leftMargin: 16 - - Header2Type { - Layout.fillWidth: true - Layout.topMargin: 16 - - headerText: qsTr("Connection options %1").arg(protocolName) + backButtonFunction: function() { + configContentDrawer.close() } + } - TextArea { - id: configText + FlickableType { + anchors.top: backButton.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + contentHeight: configContent.implicitHeight + configContent.anchors.topMargin + configContent.anchors.bottomMargin - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.bottomMargin: 16 + ColumnLayout { + id: configContent - padding: 0 - leftPadding: 0 - height: 24 + anchors.fill: parent + anchors.rightMargin: 16 + anchors.leftMargin: 16 - color: "#D7D8DB" - selectionColor: "#633303" - selectedTextColor: "#D7D8DB" + Header2Type { + Layout.fillWidth: true + Layout.topMargin: 16 - font.pixelSize: 16 - font.weight: Font.Medium - font.family: "PT Root UI VF" + headerText: qsTr("Connection options %1").arg(protocolName) + } - text: rawConfig + TextArea { + id: configText - wrapMode: Text.Wrap + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.bottomMargin: 16 - background: Rectangle { - color: "transparent" + padding: 0 + leftPadding: 0 + height: 24 + + color: "#D7D8DB" + selectionColor: "#633303" + selectedTextColor: "#D7D8DB" + + font.pixelSize: 16 + font.weight: Font.Medium + font.family: "PT Root UI VF" + + text: rawConfig + + wrapMode: Text.Wrap + + background: Rectangle { + color: "transparent" + } } } } @@ -169,26 +175,25 @@ PageType { width: parent.width - visible: ServersModel.isCurrentlyProcessedServerHasWriteAccess() + visible: ServersModel.isProcessedServerHasWriteAccess() text: qsTr("Remove ") + ContainersModel.getCurrentlyProcessedContainerName() textColor: "#EB5757" clickedFunction: function() { - questionDrawer.headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getCurrentlyProcessedContainerName()) - questionDrawer.descriptionText = qsTr("All users with whom you shared a connection will no longer be able to connect to it.") - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getCurrentlyProcessedContainerName()) + var descriptionText = qsTr("All users with whom you shared a connection will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { PageController.goToPage(PageEnum.PageDeinstalling) InstallController.removeCurrentlyProcessedContainer() } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } MouseArea { @@ -200,9 +205,5 @@ PageType { DividerType {} } - - QuestionDrawer { - id: questionDrawer - } } } diff --git a/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml b/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml index 573aca06..5284f807 100644 --- a/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml @@ -15,6 +15,8 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: listview.currentItem.portTextField.textField + ColumnLayout { id: backButton @@ -41,7 +43,7 @@ PageType { anchors.left: parent.left anchors.right: parent.right - enabled: ServersModel.isCurrentlyProcessedServerHasWriteAccess() + enabled: ServersModel.isProcessedServerHasWriteAccess() ListView { id: listview @@ -58,6 +60,8 @@ PageType { implicitWidth: listview.width implicitHeight: col.implicitHeight + property alias portTextField: portTextField + ColumnLayout { id: col @@ -77,6 +81,8 @@ PageType { } TextFieldWithHeaderType { + id: portTextField + Layout.fillWidth: true Layout.topMargin: 40 @@ -90,6 +96,8 @@ PageType { port = textFieldText } } + + KeyNavigation.tab: saveRestartButton } DropDownType { @@ -100,6 +108,8 @@ PageType { descriptionText: qsTr("Cipher") headerText: qsTr("Cipher") + drawerParent: root + listView: ListViewWithRadioButtonType { id: cipherListView @@ -116,7 +126,7 @@ PageType { clickedFunction: function() { cipherDropDown.text = selectedText cipher = cipherDropDown.text - cipherDropDown.menuVisible = false + cipherDropDown.close() } Component.onCompleted: { @@ -132,13 +142,15 @@ PageType { } BasicButtonType { + id: saveRestartButton + Layout.fillWidth: true Layout.topMargin: 24 Layout.bottomMargin: 24 text: qsTr("Save and Restart Amnezia") - onClicked: { + clickedFunc: function() { forceActiveFocus() PageController.goToPage(PageEnum.PageSetupWizardInstalling); InstallController.updateContainer(ShadowSocksConfigModel.getConfig()) diff --git a/client/ui/qml/Pages2/PageServiceDnsSettings.qml b/client/ui/qml/Pages2/PageServiceDnsSettings.qml index 10fe6f56..c24bdd42 100644 --- a/client/ui/qml/Pages2/PageServiceDnsSettings.qml +++ b/client/ui/qml/Pages2/PageServiceDnsSettings.qml @@ -63,19 +63,18 @@ PageType { textColor: "#EB5757" clickedFunction: function() { - questionDrawer.headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getCurrentlyProcessedContainerName()) - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getCurrentlyProcessedContainerName()) + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { PageController.goToPage(PageEnum.PageDeinstalling) InstallController.removeCurrentlyProcessedContainer() } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + + showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } MouseArea { @@ -86,10 +85,6 @@ PageType { } DividerType {} - - QuestionDrawer { - id: questionDrawer - } } } } diff --git a/client/ui/qml/Pages2/PageServiceSftpSettings.qml b/client/ui/qml/Pages2/PageServiceSftpSettings.qml index b12302dd..9a0e5ff1 100644 --- a/client/ui/qml/Pages2/PageServiceSftpSettings.qml +++ b/client/ui/qml/Pages2/PageServiceSftpSettings.qml @@ -49,7 +49,7 @@ PageType { anchors.left: parent.left anchors.right: parent.right - enabled: ServersModel.isCurrentlyProcessedServerHasWriteAccess() + enabled: ServersModel.isProcessedServerHasWriteAccess() ListView { id: listview @@ -88,7 +88,7 @@ PageType { Layout.topMargin: 32 text: qsTr("Host") - descriptionText: ServersModel.getCurrentlyProcessedServerHostName() + descriptionText: ServersModel.getProcessedServerData("HostName") descriptionOnTop: true @@ -170,7 +170,7 @@ PageType { text: qsTr("Mount folder on device") - onClicked: { + clickedFunc: function() { PageController.showBusyIndicator(true) InstallController.mountSftpDrive(port, password, username) PageController.showBusyIndicator(false) @@ -229,7 +229,7 @@ PageType { text: qsTr("Detailed instructions") - onClicked: { + clickedFunc: function() { // Qt.openUrlExternally("https://github.com/amnezia-vpn/desktop-client/releases/latest") } } @@ -247,29 +247,24 @@ PageType { text: qsTr("Remove SFTP and all data stored there") - onClicked: { - questionDrawer.headerText = qsTr("Remove SFTP and all data stored there?") - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + clickedFunc: function() { + var headerText = qsTr("Remove SFTP and all data stored there?") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { PageController.goToPage(PageEnum.PageDeinstalling) InstallController.removeCurrentlyProcessedContainer() } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + + showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } } } } } - - QuestionDrawer { - id: questionDrawer - } } } diff --git a/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml b/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml index eef174c3..002e0310 100644 --- a/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml +++ b/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml @@ -125,26 +125,21 @@ PageType { text: qsTr("Remove website") - onClicked: { - questionDrawer.headerText = qsTr("The site with all data will be removed from the tor network.") - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + clickedFunc: function() { + var headerText = qsTr("The site with all data will be removed from the tor network.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { PageController.goToPage(PageEnum.PageDeinstalling) InstallController.removeCurrentlyProcessedContainer() } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + + showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } } - - QuestionDrawer { - id: questionDrawer - } } } diff --git a/client/ui/qml/Pages2/PageSettingsAbout.qml b/client/ui/qml/Pages2/PageSettingsAbout.qml index f8128507..b912e2cd 100644 --- a/client/ui/qml/Pages2/PageSettingsAbout.qml +++ b/client/ui/qml/Pages2/PageSettingsAbout.qml @@ -81,7 +81,7 @@ PageType { text: qsTr("Card on Patreon") - onClicked: function() { + clickedFunc: function() { Qt.openUrlExternally(qsTr("https://www.patreon.com/amneziavpn")) } } @@ -101,7 +101,9 @@ PageType { text: qsTr("Show other methods on Github") - onClicked: Qt.openUrlExternally(qsTr("https://github.com/amnezia-vpn/amnezia-client#donate")) + clickedFunc: function() { + Qt.openUrlExternally(qsTr("https://github.com/amnezia-vpn/amnezia-client#donate")) + } } ParagraphTextType { @@ -173,7 +175,7 @@ PageType { horizontalAlignment: Text.AlignHCenter - text: SettingsController.getAppVersion() + text: qsTr("Software version: %1").arg(SettingsController.getAppVersion()) color: "#878B91" } @@ -191,7 +193,7 @@ PageType { text: qsTr("Check for updates") - onClicked: { + clickedFunc: function() { 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 index 05e468f0..af029b7b 100644 --- a/client/ui/qml/Pages2/PageSettingsApplication.qml +++ b/client/ui/qml/Pages2/PageSettingsApplication.qml @@ -84,6 +84,27 @@ PageType { visible: !GC.isMobile() } + SwitcherType { + visible: !GC.isMobile() + + Layout.fillWidth: true + Layout.margins: 16 + + text: qsTr("Auto connect") + descriptionText: qsTr("Connect to VPN on app start") + + checked: SettingsController.isAutoConnectEnabled() + onCheckedChanged: { + if (checked !== SettingsController.isAutoConnectEnabled()) { + SettingsController.toggleAutoConnect(checked) + } + } + } + + DividerType { + visible: !GC.isMobile() + } + SwitcherType { visible: !GC.isMobile() @@ -117,10 +138,6 @@ PageType { } } - SelectLanguageDrawer { - id: selectLanguageDrawer - } - DividerType {} @@ -143,30 +160,33 @@ PageType { text: qsTr("Reset settings and remove all data from the application") rightImageSource: "qrc:/images/controls/chevron-right.svg" + textColor: "#EB5757" clickedFunction: function() { - questionDrawer.headerText = qsTr("Reset settings and remove all data from the application?") - questionDrawer.descriptionText = qsTr("All settings will be reset to default. All installed AmneziaVPN services will still remain on the server.") - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + var headerText = qsTr("Reset settings and remove all data from the application?") + var descriptionText = qsTr("All settings will be reset to default. All installed AmneziaVPN services will still remain on the server.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { SettingsController.clearSettings() PageController.replaceStartPage() } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } DividerType {} - - QuestionDrawer { - id: questionDrawer - } } } + + SelectLanguageDrawer { + id: selectLanguageDrawer + + width: root.width + height: root.height + } } diff --git a/client/ui/qml/Pages2/PageSettingsBackup.qml b/client/ui/qml/Pages2/PageSettingsBackup.qml index 81be0465..6b3b3fb5 100644 --- a/client/ui/qml/Pages2/PageSettingsBackup.qml +++ b/client/ui/qml/Pages2/PageSettingsBackup.qml @@ -88,7 +88,7 @@ PageType { text: qsTr("Make a backup") - onClicked: { + clickedFunc: function() { var fileName = "" if (GC.isMobile()) { fileName = "AmneziaVPN.backup" @@ -121,7 +121,7 @@ PageType { text: qsTr("Restore from backup") - onClicked: { + clickedFunc: function() { var filePath = SystemController.getFileName(qsTr("Open backup file"), qsTr("Backup files (*.backup)")) if (filePath !== "") { @@ -133,24 +133,19 @@ PageType { } function restoreBackup(filePath) { - questionDrawer.headerText = qsTr("Import settings from a backup file?") - questionDrawer.descriptionText = qsTr("All current settings will be reset"); - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + var headerText = qsTr("Import settings from a backup file?") + var descriptionText = qsTr("All current settings will be reset"); + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { PageController.showBusyIndicator(true) SettingsController.restoreAppConfig(filePath) PageController.showBusyIndicator(false) } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true - } - QuestionDrawer { - id: questionDrawer + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } diff --git a/client/ui/qml/Pages2/PageSettingsConnection.qml b/client/ui/qml/Pages2/PageSettingsConnection.qml index 3a0c5c3c..4d88b397 100644 --- a/client/ui/qml/Pages2/PageSettingsConnection.qml +++ b/client/ui/qml/Pages2/PageSettingsConnection.qml @@ -41,27 +41,6 @@ PageType { headerText: qsTr("Connection") } - SwitcherType { - visible: !GC.isMobile() - - Layout.fillWidth: true - Layout.margins: 16 - - text: qsTr("Auto connect") - descriptionText: qsTr("Connect to VPN on app start") - - checked: SettingsController.isAutoConnectEnabled() - onCheckedChanged: { - if (checked !== SettingsController.isAutoConnectEnabled()) { - SettingsController.toggleAutoConnect(checked) - } - } - } - - DividerType { - visible: !GC.isMobile() - } - SwitcherType { Layout.fillWidth: true Layout.margins: 16 diff --git a/client/ui/qml/Pages2/PageSettingsDns.qml b/client/ui/qml/Pages2/PageSettingsDns.qml index 1970da52..be2bd290 100644 --- a/client/ui/qml/Pages2/PageSettingsDns.qml +++ b/client/ui/qml/Pages2/PageSettingsDns.qml @@ -13,6 +13,8 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: primaryDns.textField + BackButtonType { id: backButton @@ -28,10 +30,12 @@ PageType { anchors.bottom: parent.bottom contentHeight: content.height - enabled: !ServersModel.isDefaultServerFromApi() + property var isServerFromApi: ServersModel.getDefaultServerData("isServerFromApi") + + enabled: !isServerFromApi Component.onCompleted: { - if (ServersModel.isDefaultServerFromApi()) { + if (isServerFromApi) { PageController.showNotificationMessage(qsTr("Default server does not support custom dns")) } } @@ -68,6 +72,8 @@ PageType { textField.validator: RegularExpressionValidator { regularExpression: InstallController.ipAddressRegExp() } + + KeyNavigation.tab: secondaryDns.textField } TextFieldWithHeaderType { @@ -80,6 +86,8 @@ PageType { textField.validator: RegularExpressionValidator { regularExpression: InstallController.ipAddressRegExp() } + + KeyNavigation.tab: saveButton } BasicButtonType { @@ -94,32 +102,33 @@ PageType { text: qsTr("Restore default") - onClicked: function() { - questionDrawer.headerText = qsTr("Restore default DNS settings?") - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + clickedFunc: function() { + var headerText = qsTr("Restore default DNS settings?") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { SettingsController.primaryDns = "1.1.1.1" primaryDns.textFieldText = SettingsController.primaryDns SettingsController.secondaryDns = "1.0.0.1" secondaryDns.textFieldText = SettingsController.secondaryDns PageController.showNotificationMessage(qsTr("Settings have been reset")) } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + + showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } BasicButtonType { + id: saveButton + Layout.fillWidth: true text: qsTr("Save") - onClicked: function() { + clickedFunc: function() { if (primaryDns.textFieldText !== SettingsController.primaryDns) { SettingsController.primaryDns = primaryDns.textFieldText } @@ -130,8 +139,6 @@ PageType { } } } - QuestionDrawer { - id: questionDrawer - } } + } diff --git a/client/ui/qml/Pages2/PageSettingsLogging.qml b/client/ui/qml/Pages2/PageSettingsLogging.qml index b302bffc..b05ffd2b 100644 --- a/client/ui/qml/Pages2/PageSettingsLogging.qml +++ b/client/ui/qml/Pages2/PageSettingsLogging.qml @@ -143,21 +143,20 @@ PageType { image: "qrc:/images/controls/delete.svg" onClicked: function() { - questionDrawer.headerText = qsTr("Clear logs?") - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + var headerText = qsTr("Clear logs?") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { PageController.showBusyIndicator(true) SettingsController.clearLogs() PageController.showBusyIndicator(false) PageController.showNotificationMessage(qsTr("Logs have been cleaned up")) } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + + showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } @@ -170,10 +169,6 @@ PageType { } } } - - QuestionDrawer { - id: questionDrawer - } } } } diff --git a/client/ui/qml/Pages2/PageSettingsServerData.qml b/client/ui/qml/Pages2/PageSettingsServerData.qml index a9365af4..96945213 100644 --- a/client/ui/qml/Pages2/PageSettingsServerData.qml +++ b/client/ui/qml/Pages2/PageSettingsServerData.qml @@ -28,7 +28,7 @@ PageType { PageController.showErrorMessage(message) } - function onRemoveCurrentlyProcessedServerFinished(finishedMessage) { + function onRemoveProcessedServerFinished(finishedMessage) { if (!ServersModel.getServersCount()) { PageController.replaceStartPage() } else { @@ -38,7 +38,7 @@ PageType { PageController.showNotificationMessage(finishedMessage) } - function onRebootCurrentlyProcessedServerFinished(finishedMessage) { + function onRebootProcessedServerFinished(finishedMessage) { PageController.showNotificationMessage(finishedMessage) } @@ -64,8 +64,8 @@ PageType { Connections { target: ServersModel - function onCurrentlyProcessedServerIndexChanged() { - content.isServerWithWriteAccess = ServersModel.isCurrentlyProcessedServerHasWriteAccess() + function onProcessedServerIndexChanged() { + content.isServerWithWriteAccess = ServersModel.isProcessedServerHasWriteAccess() } } @@ -82,7 +82,7 @@ PageType { anchors.left: parent.left anchors.right: parent.right - property bool isServerWithWriteAccess: ServersModel.isCurrentlyProcessedServerHasWriteAccess() + property bool isServerWithWriteAccess: ServersModel.isProcessedServerHasWriteAccess() LabelWithButtonType { visible: content.isServerWithWriteAccess @@ -92,21 +92,20 @@ PageType { descriptionText: qsTr("May be needed when changing other settings") clickedFunction: function() { - questionDrawer.headerText = qsTr("Clear cached profiles?") - questionDrawer.descriptionText = qsTr("") - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + var headerText = qsTr("Clear cached profiles?") + var descriptionText = qsTr("") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { PageController.showBusyIndicator(true) SettingsController.clearCachedProfiles() PageController.showBusyIndicator(false) } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } @@ -140,24 +139,23 @@ PageType { textColor: "#EB5757" clickedFunction: function() { - questionDrawer.headerText = qsTr("Do you want to reboot the server?") - questionDrawer.descriptionText = qsTr("The reboot process may take approximately 30 seconds. Are you sure you wish to proceed?") - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + var headerText = qsTr("Do you want to reboot the server?") + var descriptionText = qsTr("The reboot process may take approximately 30 seconds. Are you sure you wish to proceed?") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { PageController.showBusyIndicator(true) if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) { ConnectionController.closeConnection() } - InstallController.rebootCurrentlyProcessedServer() + InstallController.rebootProcessedServer() PageController.showBusyIndicator(false) } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } @@ -172,24 +170,23 @@ PageType { textColor: "#EB5757" clickedFunction: function() { - questionDrawer.headerText = qsTr("Do you want to remove the server from application?") - questionDrawer.descriptionText = qsTr("All installed AmneziaVPN services will still remain on the server.") - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + var headerText = qsTr("Do you want to remove the server from application?") + var descriptionText = qsTr("All installed AmneziaVPN services will still remain on the server.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { PageController.showBusyIndicator(true) if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) { ConnectionController.closeConnection() } - InstallController.removeCurrentlyProcessedServer() + InstallController.removeProcessedServer() PageController.showBusyIndicator(false) } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } @@ -203,23 +200,22 @@ PageType { textColor: "#EB5757" clickedFunction: function() { - questionDrawer.headerText = qsTr("Do you want to clear server from Amnezia software?") - questionDrawer.descriptionText = qsTr("All containers will be deleted on the server. This means that configuration files, keys and certificates will be deleted.") - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + var headerText = qsTr("Do you want to clear server from Amnezia software?") + var descriptionText = qsTr("All containers will be deleted on the server. This means that configuration files, keys and certificates will be deleted.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { PageController.goToPage(PageEnum.PageDeinstalling) if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) { ConnectionController.closeConnection() } InstallController.removeAllContainers() } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } @@ -228,69 +224,32 @@ PageType { } LabelWithButtonType { - visible: content.isServerWithWriteAccess - Layout.fillWidth: true - - text: qsTr("Clear server from Amnezia software") - textColor: "#EB5757" - - clickedFunction: function() { - questionDrawer.headerText = qsTr("Do you want to clear server from Amnezia software?") - questionDrawer.descriptionText = qsTr("All containers will be deleted on the server. This means that configuration files, keys and certificates will be deleted.") - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") - - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false - PageController.goToPage(PageEnum.PageDeinstalling) - if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) { - ConnectionController.closeConnection() - } - InstallController.removeAllContainers() - } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false - } - questionDrawer.visible = true - } - } - - DividerType { - visible: content.isServerWithWriteAccess - } - - LabelWithButtonType { - visible: ServersModel.isCurrentlyProcessedServerFromApi() + visible: ServersModel.getProcessedServerData("isServerFromApi") Layout.fillWidth: true text: qsTr("Reset API config") textColor: "#EB5757" clickedFunction: function() { - questionDrawer.headerText = qsTr("Do you want to reset API config?") - questionDrawer.descriptionText = "" - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + var headerText = qsTr("Do you want to reset API config?") + var descriptionText = "" + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { PageController.showBusyIndicator(true) ApiController.clearApiConfig() PageController.showBusyIndicator(false) } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } DividerType { - visible: ServersModel.isCurrentlyProcessedServerFromApi() - } - - QuestionDrawer { - id: questionDrawer + visible: ServersModel.getProcessedServerData("isServerFromApi") } } } diff --git a/client/ui/qml/Pages2/PageSettingsServerInfo.qml b/client/ui/qml/Pages2/PageSettingsServerInfo.qml index df740ff9..01db5695 100644 --- a/client/ui/qml/Pages2/PageSettingsServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsServerInfo.qml @@ -63,7 +63,7 @@ PageType { headerText: name descriptionText: { - if (ServersModel.isCurrentlyProcessedServerHasWriteAccess()) { + if (ServersModel.isProcessedServerHasWriteAccess()) { return credentialsLogin + " · " + hostName } else { return hostName @@ -71,30 +71,33 @@ PageType { } actionButtonFunction: function() { - serverNameEditDrawer.visible = true + serverNameEditDrawer.open() } } - DrawerType { + DrawerType2 { id: serverNameEditDrawer - width: root.width - height: root.height * 0.35 + parent: root - onVisibleChanged: { - if (serverNameEditDrawer.visible) { - serverName.textField.forceActiveFocus() - } - } + anchors.fill: parent + expandedHeight: root.height * 0.35 - ColumnLayout { + expandedContent: ColumnLayout { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: 16 + anchors.topMargin: 32 anchors.leftMargin: 16 anchors.rightMargin: 16 + Connections { + target: serverNameEditDrawer + function onOpened() { + serverName.textField.forceActiveFocus() + } + } + TextFieldWithHeaderType { id: serverName @@ -103,14 +106,18 @@ PageType { textFieldText: name textField.maximumLength: 30 checkEmptyText: true + + KeyNavigation.tab: saveButton } BasicButtonType { + id: saveButton + Layout.fillWidth: true text: qsTr("Save") - onClicked: { + clickedFunc: function() { if (serverName.textFieldText === "") { return } @@ -118,7 +125,13 @@ PageType { if (serverName.textFieldText !== name) { name = serverName.textFieldText } - serverNameEditDrawer.visible = false + serverNameEditDrawer.close() + } + } + + Component.onCompleted: { + if (header.itemAt(0)) { + defaultActiveFocusItem = serverName.textField } } } diff --git a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml index a961cf56..b75e7342 100644 --- a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml +++ b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml @@ -107,26 +107,25 @@ PageType { width: parent.width - visible: ServersModel.isCurrentlyProcessedServerHasWriteAccess() + visible: ServersModel.isProcessedServerHasWriteAccess() text: qsTr("Remove ") + ContainersModel.getCurrentlyProcessedContainerName() textColor: "#EB5757" clickedFunction: function() { - questionDrawer.headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getCurrentlyProcessedContainerName()) - questionDrawer.descriptionText = qsTr("All users with whom you shared a connection will no longer be able to connect to it.") - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getCurrentlyProcessedContainerName()) + var descriptionText = qsTr("All users with whom you shared a connection will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { PageController.goToPage(PageEnum.PageDeinstalling) InstallController.removeCurrentlyProcessedContainer() } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } MouseArea { @@ -138,9 +137,9 @@ PageType { DividerType {} } + } - QuestionDrawer { - id: questionDrawer - } + QuestionDrawer { + id: questionDrawer } } diff --git a/client/ui/qml/Pages2/PageSettingsServerProtocols.qml b/client/ui/qml/Pages2/PageSettingsServerProtocols.qml index 9fc2abde..1dfd2886 100644 --- a/client/ui/qml/Pages2/PageSettingsServerProtocols.qml +++ b/client/ui/qml/Pages2/PageSettingsServerProtocols.qml @@ -38,13 +38,13 @@ PageType { Connections { target: ServersModel - function onCurrentlyProcessedServerIndexChanged() { + function onProcessedServerIndexChanged() { settingsContainersListView.updateContainersModelFilters() } } function updateContainersModelFilters() { - if (ServersModel.isCurrentlyProcessedServerHasWriteAccess()) { + if (ServersModel.isProcessedServerHasWriteAccess()) { proxyContainersModel.filters = ContainersModelFilters.getWriteAccessProtocolsListFilters() } else { proxyContainersModel.filters = ContainersModelFilters.getReadAccessProtocolsListFilters() diff --git a/client/ui/qml/Pages2/PageSettingsServerServices.qml b/client/ui/qml/Pages2/PageSettingsServerServices.qml index 4f832651..8795bd23 100644 --- a/client/ui/qml/Pages2/PageSettingsServerServices.qml +++ b/client/ui/qml/Pages2/PageSettingsServerServices.qml @@ -38,13 +38,13 @@ PageType { Connections { target: ServersModel - function onCurrentlyProcessedServerIndexChanged() { + function onProcessedServerIndexChanged() { settingsContainersListView.updateContainersModelFilters() } } function updateContainersModelFilters() { - if (ServersModel.isCurrentlyProcessedServerHasWriteAccess()) { + if (ServersModel.isProcessedServerHasWriteAccess()) { proxyContainersModel.filters = ContainersModelFilters.getWriteAccessServicesListFilters() } else { proxyContainersModel.filters = ContainersModelFilters.getReadAccessServicesListFilters() @@ -55,6 +55,9 @@ PageType { model: SortFilterProxyModel { id: proxyContainersModel sourceModel: ContainersModel + sorters: [ + RoleSorter { roleName: "isInstalled"; sortOrder: Qt.DescendingOrder } + ] } Component.onCompleted: updateContainersModelFilters() diff --git a/client/ui/qml/Pages2/PageSettingsServersList.qml b/client/ui/qml/Pages2/PageSettingsServersList.qml index dca904ae..eb07eaf4 100644 --- a/client/ui/qml/Pages2/PageSettingsServersList.qml +++ b/client/ui/qml/Pages2/PageSettingsServersList.qml @@ -87,7 +87,7 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { - ServersModel.currentlyProcessedIndex = index + ServersModel.processedIndex = index PageController.goToPage(PageEnum.PageSettingsServerInfo) } } diff --git a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml index 3aa5742a..1ce3cd64 100644 --- a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml @@ -20,13 +20,23 @@ import "../Components" PageType { id: root + property var isServerFromApi: ServersModel.getDefaultServerData("isServerFromApi") + + defaultActiveFocusItem: website_ip_field.textField + property bool pageEnabled: { - return !ConnectionController.isConnected && !ServersModel.isDefaultServerFromApi() + return !ConnectionController.isConnected && !isServerFromApi } Component.onCompleted: { - if (ServersModel.isDefaultServerFromApi()) { + if (ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("Cannot change split tunneling settings during active connection")) + root.pageEnabled = false + } else if (ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && isServerFromApi) { PageController.showNotificationMessage(qsTr("Default server does not support split tunneling function")) + root.pageEnabled = false + } else { + root.pageEnabled = true } } @@ -104,7 +114,7 @@ PageType { Layout.fillWidth: true Layout.rightMargin: 16 - checked: SitesModel.isSplitTunnelingEnabled() + checked: SitesModel.isTunnelingEnabled onToggled: { SitesModel.toggleSplitTunneling(checked) selector.text = root.routeModesModel[getRouteModesModelIndex()].name @@ -121,6 +131,7 @@ PageType { Layout.rightMargin: 16 drawerHeight: 0.4375 + drawerParent: root enabled: root.pageEnabled @@ -135,7 +146,7 @@ PageType { clickedFunction: function() { selector.text = selectedText - selector.menuVisible = false + selector.close() if (SitesModel.routeMode !== root.routeModesModel[currentIndex].type) { SitesModel.routeMode = root.routeModesModel[currentIndex].type } @@ -202,26 +213,21 @@ PageType { rightImageColor: "#D7D8DB" clickedFunction: function() { - questionDrawer.headerText = qsTr("Remove ") + url + "?" - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + var headerText = qsTr("Remove ") + url + "?" + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.visible = false + var yesButtonFunction = function() { SitesController.removeSite(index) } - questionDrawer.noButtonFunction = function() { - questionDrawer.visible = false + var noButtonFunction = function() { } - questionDrawer.visible = true + + showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } DividerType {} - - QuestionDrawer { - id: questionDrawer - } } } } @@ -249,6 +255,8 @@ PageType { anchors.bottomMargin: 24 TextFieldWithHeaderType { + id: website_ip_field + Layout.fillWidth: true textFieldPlaceholderText: qsTr("website or IP") @@ -275,151 +283,155 @@ PageType { } } - DrawerType { + DrawerType2 { id: moreActionsDrawer - width: parent.width - height: parent.height * 0.4375 + anchors.fill: parent + expandedHeight: parent.height * 0.4375 - FlickableType { - anchors.fill: parent - contentHeight: moreActionsDrawerContent.height - ColumnLayout { - id: moreActionsDrawerContent - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - - Header2Type { - Layout.fillWidth: true - Layout.margins: 16 - - headerText: qsTr("Import / Export Sites") - } - - LabelWithButtonType { - Layout.fillWidth: true - - text: qsTr("Import") - rightImageSource: "qrc:/images/controls/chevron-right.svg" - - clickedFunction: function() { - importSitesDrawer.open() - } - } - - DividerType {} - - LabelWithButtonType { - Layout.fillWidth: true - text: qsTr("Save site list") - - clickedFunction: function() { - var fileName = "" - if (GC.isMobile()) { - fileName = "amnezia_sites.json" - } else { - fileName = SystemController.getFileName(qsTr("Save sites"), - qsTr("Sites files (*.json)"), - StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/amnezia_sites", - true, - ".json") - } - if (fileName !== "") { - PageController.showBusyIndicator(true) - SitesController.exportSites(fileName) - moreActionsDrawer.close() - PageController.showBusyIndicator(false) - } - } - } - - DividerType {} - } - } - } - - DrawerType { - id: importSitesDrawer - - width: parent.width - height: parent.height * 0.4375 - - BackButtonType { - id: importSitesDrawerBackButton + expandedContent: ColumnLayout { + id: moreActionsDrawerContent anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: 16 - backButtonFunction: function() { - importSitesDrawer.close() + Header2Type { + Layout.fillWidth: true + Layout.margins: 16 + + headerText: qsTr("Import / Export Sites") } + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Import") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + importSitesDrawer.open() + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Save site list") + + clickedFunction: function() { + var fileName = "" + if (GC.isMobile()) { + fileName = "amnezia_sites.json" + } else { + fileName = SystemController.getFileName(qsTr("Save sites"), + qsTr("Sites files (*.json)"), + StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/amnezia_sites", + true, + ".json") + } + if (fileName !== "") { + PageController.showBusyIndicator(true) + SitesController.exportSites(fileName) + moreActionsDrawer.close() + PageController.showBusyIndicator(false) + } + } + } + + DividerType {} } + } - FlickableType { - anchors.top: importSitesDrawerBackButton.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom + DrawerType2 { + id: importSitesDrawer - contentHeight: importSitesDrawerContent.height + anchors.fill: parent + expandedHeight: parent.height * 0.4375 - ColumnLayout { - id: importSitesDrawerContent + expandedContent: Item { + implicitHeight: importSitesDrawer.expandedHeight + + BackButtonType { + id: importSitesDrawerBackButton anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right + anchors.topMargin: 16 - Header2Type { - Layout.fillWidth: true - Layout.margins: 16 - - headerText: qsTr("Import a list of sites") - } - - LabelWithButtonType { - Layout.fillWidth: true - - text: qsTr("Replace site list") - - clickedFunction: function() { - var fileName = SystemController.getFileName(qsTr("Open sites file"), - qsTr("Sites files (*.json)")) - if (fileName !== "") { - importSitesDrawerContent.importSites(fileName, true) - } - } - } - - DividerType {} - - LabelWithButtonType { - Layout.fillWidth: true - text: qsTr("Add imported sites to existing ones") - - clickedFunction: function() { - var fileName = SystemController.getFileName(qsTr("Open sites file"), - qsTr("Sites files (*.json)")) - if (fileName !== "") { - importSitesDrawerContent.importSites(fileName, false) - } - } - } - - function importSites(fileName, replaceExistingSites) { - PageController.showBusyIndicator(true) - SitesController.importSites(fileName, replaceExistingSites) - PageController.showBusyIndicator(false) + backButtonFunction: function() { importSitesDrawer.close() - moreActionsDrawer.close() } + } - DividerType {} + FlickableType { + anchors.top: importSitesDrawerBackButton.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + contentHeight: importSitesDrawerContent.height + + ColumnLayout { + id: importSitesDrawerContent + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + Header2Type { + Layout.fillWidth: true + Layout.margins: 16 + + headerText: qsTr("Import a list of sites") + } + + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Replace site list") + + clickedFunction: function() { + var fileName = SystemController.getFileName(qsTr("Open sites file"), + qsTr("Sites files (*.json)")) + if (fileName !== "") { + importSitesDrawerContent.importSites(fileName, true) + } + } + } + + DividerType {} + + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Add imported sites to existing ones") + + clickedFunction: function() { + var fileName = SystemController.getFileName(qsTr("Open sites file"), + qsTr("Sites files (*.json)")) + if (fileName !== "") { + importSitesDrawerContent.importSites(fileName, false) + } + } + } + + function importSites(fileName, replaceExistingSites) { + PageController.showBusyIndicator(true) + SitesController.importSites(fileName, replaceExistingSites) + PageController.showBusyIndicator(false) + importSitesDrawer.close() + moreActionsDrawer.close() + } + + DividerType {} + } } } } + + QuestionDrawer { + id: questionDrawer + } } diff --git a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml index 60ff9baa..c256f3a4 100644 --- a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml +++ b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml @@ -12,6 +12,8 @@ import "../Controls2/TextTypes" PageType { id: root + defaultActiveFocusItem: hostname.textField + BackButtonType { id: backButton @@ -57,6 +59,8 @@ PageType { onFocusChanged: { textField.text = textField.text.replace(/^\s+|\s+$/g, ''); } + + KeyNavigation.tab: username.textField } TextFieldWithHeaderType { @@ -65,6 +69,8 @@ PageType { Layout.fillWidth: true headerText: qsTr("Login to connect via SSH") textFieldPlaceholderText: "root" + + KeyNavigation.tab: secretData.textField } TextFieldWithHeaderType { @@ -85,15 +91,19 @@ PageType { onFocusChanged: { textField.text = textField.text.replace(/^\s+|\s+$/g, ''); } + + KeyNavigation.tab: continueButton } BasicButtonType { + id: continueButton + Layout.fillWidth: true Layout.topMargin: 24 text: qsTr("Continue") - onClicked: function() { + clickedFunc: function() { forceActiveFocus() if (!isCredentialsFilled()) { return diff --git a/client/ui/qml/Pages2/PageSetupWizardEasy.qml b/client/ui/qml/Pages2/PageSetupWizardEasy.qml index 95951507..69109a85 100644 --- a/client/ui/qml/Pages2/PageSetupWizardEasy.qml +++ b/client/ui/qml/Pages2/PageSetupWizardEasy.qml @@ -158,7 +158,7 @@ PageType { text: qsTr("Continue") - onClicked: function() { + clickedFunc: function() { if (root.isEasySetup) { ContainersModel.setCurrentlyProcessedContainerIndex(containers.dockerContainer) PageController.goToPage(PageEnum.PageSetupWizardInstalling) @@ -192,13 +192,12 @@ PageType { return ContainersModel.isAnyContainerInstalled() } - return true } text: qsTr("Set up later") - onClicked: function() { + clickedFunc: function() { PageController.goToPage(PageEnum.PageSetupWizardInstalling) InstallController.addEmptyServer() } diff --git a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml index 1a3e7c07..2a5cafd8 100644 --- a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml +++ b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml @@ -26,7 +26,7 @@ PageType { function onInstallContainerFinished(finishedMessage, isServiceInstall) { if (!ConnectionController.isConnected && !isServiceInstall) { - ServersModel.setDefaultContainer(ContainersModel.getCurrentlyProcessedContainerIndex()) + ServersModel.setDefaultContainer(ServersModel.processedIndex, ContainersModel.getCurrentlyProcessedContainerIndex()) } PageController.closePage() // close installing page @@ -42,7 +42,7 @@ PageType { function onInstallServerFinished(finishedMessage) { if (!ConnectionController.isConnected) { ServersModel.setDefaultServerIndex(ServersModel.getServersCount() - 1); - ServersModel.currentlyProcessedIndex = ServersModel.defaultIndex + ServersModel.processedIndex = ServersModel.defaultIndex } PageController.goToStartPage() @@ -55,7 +55,7 @@ PageType { function onServerAlreadyExists(serverIndex) { PageController.goToStartPage() - ServersModel.currentlyProcessedIndex = serverIndex + ServersModel.processedIndex = serverIndex PageController.goToPage(PageEnum.PageSettingsServerInfo, false) PageController.showErrorMessage(qsTr("The server has already been added to the application")) @@ -165,7 +165,7 @@ PageType { text: qsTr("Cancel installation") - onClicked: { + clickedFunc: function() { InstallController.cancelInstallation() PageController.showBusyIndicator(true) } diff --git a/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml b/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml index 7698c755..02e4ee6c 100644 --- a/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml +++ b/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml @@ -52,6 +52,8 @@ PageType { implicitWidth: processedContainerListView.width implicitHeight: (delegateContent.implicitHeight > root.height) ? delegateContent.implicitHeight : root.height + property alias port:port + ColumnLayout { id: delegateContent @@ -92,81 +94,85 @@ PageType { text: qsTr("More detailed") - onClicked: { + clickedFunc: function() { showDetailsDrawer.open() } } - DrawerType { + DrawerType2 { id: showDetailsDrawer + parent: root - width: parent.width - height: parent.height * 0.9 + anchors.fill: parent + expandedHeight: parent.height * 0.9 + expandedContent: Item { + implicitHeight: showDetailsDrawer.expandedHeight - BackButtonType { - id: showDetailsBackButton - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 16 - - backButtonFunction: function() { - showDetailsDrawer.close() - } - } - - FlickableType { - anchors.top: showDetailsBackButton.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - contentHeight: { - var emptySpaceHeight = parent.height - showDetailsBackButton.implicitHeight - showDetailsBackButton.anchors.topMargin - return (showDetailsDrawerContent.height > emptySpaceHeight) ? - showDetailsDrawerContent.height : emptySpaceHeight - } - - ColumnLayout { - id: showDetailsDrawerContent + BackButtonType { + id: showDetailsBackButton anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.rightMargin: 16 - anchors.leftMargin: 16 + anchors.topMargin: 16 - Header2Type { - id: showDetailsDrawerHeader - Layout.fillWidth: true - Layout.topMargin: 16 + backButtonFunction: function() { + showDetailsDrawer.close() + } + } - headerText: name + FlickableType { + anchors.top: showDetailsBackButton.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + contentHeight: { + var emptySpaceHeight = parent.height - showDetailsBackButton.implicitHeight - showDetailsBackButton.anchors.topMargin + return (showDetailsDrawerContent.height > emptySpaceHeight) ? + showDetailsDrawerContent.height : emptySpaceHeight } - ParagraphTextType { - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.bottomMargin: 16 + ColumnLayout { + id: showDetailsDrawerContent - text: detailedDescription - textFormat: Text.MarkdownText - } + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + Header2Type { + id: showDetailsDrawerHeader + Layout.fillWidth: true + Layout.topMargin: 16 + + headerText: name + } + + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.bottomMargin: 16 + + text: detailedDescription + textFormat: Text.MarkdownText + } - Rectangle { - Layout.fillHeight: true - color: "transparent" - } + Rectangle { + Layout.fillHeight: true + color: "transparent" + } - BasicButtonType { - Layout.fillWidth: true - Layout.bottomMargin: 32 + BasicButtonType { + Layout.fillWidth: true + Layout.bottomMargin: 32 - text: qsTr("Close") + text: qsTr("Close") - onClicked: function() { - showDetailsDrawer.close() + clickedFunc: function() { + showDetailsDrawer.close() + } } } } @@ -197,6 +203,8 @@ PageType { headerText: qsTr("Port") textField.maximumLength: 5 textField.validator: IntValidator { bottom: 1; top: 65535 } + + KeyNavigation.tab: installButton } Rectangle { @@ -212,7 +220,7 @@ PageType { text: qsTr("Install") - onClicked: function() { + clickedFunc: function() { PageController.goToPage(PageEnum.PageSetupWizardInstalling); InstallController.install(dockerContainer, port.textFieldText, transportProtoSelector.currentIndex) } @@ -232,6 +240,8 @@ PageType { var protocolSelectorVisible = ProtocolProps.defaultTransportProtoChangeable(defaultContainerProto) transportProtoSelector.visible = protocolSelectorVisible transportProtoHeader.visible = protocolSelectorVisible + + defaultActiveFocusItem = port.textField } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardStart.qml b/client/ui/qml/Pages2/PageSetupWizardStart.qml index a820ac71..161b85da 100644 --- a/client/ui/qml/Pages2/PageSetupWizardStart.qml +++ b/client/ui/qml/Pages2/PageSetupWizardStart.qml @@ -115,8 +115,8 @@ PageType { text: qsTr("I have the data to connect") - onClicked: { - connectionTypeSelection.visible = true + clickedFunc: function() { + connectionTypeSelection.open() } } @@ -135,13 +135,15 @@ PageType { text: qsTr("I have nothing") - onClicked: Qt.openUrlExternally(qsTr("https://amnezia.org/instructions/0_starter-guide")) + clickedFunc: function() { + Qt.openUrlExternally(qsTr("https://amnezia.org/instructions/0_starter-guide")) + } } } + } - ConnectionTypeSelectionDrawer { - id: connectionTypeSelection - } + ConnectionTypeSelectionDrawer { + id: connectionTypeSelection } BusyIndicatorType { diff --git a/client/ui/qml/Pages2/PageSetupWizardTextKey.qml b/client/ui/qml/Pages2/PageSetupWizardTextKey.qml index 4cdfc444..4a322042 100644 --- a/client/ui/qml/Pages2/PageSetupWizardTextKey.qml +++ b/client/ui/qml/Pages2/PageSetupWizardTextKey.qml @@ -12,6 +12,8 @@ import "../Config" PageType { id: root + defaultActiveFocusItem: textKey.textField + FlickableType { id: fl anchors.top: parent.top @@ -56,11 +58,15 @@ PageType { textField.text = "" textField.paste() } + + KeyNavigation.tab: continueButton } } } BasicButtonType { + id: continueButton + anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right @@ -70,7 +76,7 @@ PageType { text: qsTr("Continue") - onClicked: function() { + clickedFunc: function() { ImportController.extractConfigFromCode(textKey.textFieldText) PageController.goToPage(PageEnum.PageSetupWizardViewConfig) } diff --git a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml index 79b51bdb..c755d88d 100644 --- a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml +++ b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml @@ -30,7 +30,7 @@ PageType { function onImportFinished() { if (!ConnectionController.isConnected) { ServersModel.setDefaultServerIndex(ServersModel.getServersCount() - 1); - ServersModel.currentlyProcessedIndex = ServersModel.defaultIndex + ServersModel.processedIndex = ServersModel.defaultIndex } PageController.goToStartPage() @@ -109,7 +109,7 @@ PageType { text: showContent ? qsTr("Collapse content") : qsTr("Show content") - onClicked: { + clickedFunc: function() { showContent = !showContent } } @@ -151,7 +151,7 @@ PageType { Layout.bottomMargin: 32 text: qsTr("Connect") - onClicked: { + clickedFunc: function() { ImportController.importConfig() } } diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 3a769ebe..cd7e7958 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -16,10 +16,13 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: clientNameTextField.textField + enum ConfigType { AmneziaConnection, OpenVpn, WireGuard, + Awg, ShadowSocks, Cloak } @@ -29,7 +32,7 @@ PageType { PageController.showBusyIndicator(true) ExportController.revokeConfig(index, ContainersModel.getCurrentlyProcessedContainerIndex(), - ServersModel.getCurrentlyProcessedServerCredentials()) + ServersModel.getProcessedServerCredentials()) PageController.showBusyIndicator(false) PageController.showNotificationMessage(qsTr("Config revoked")) } @@ -41,14 +44,15 @@ PageType { shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text shareConnectionDrawer.configContentHeaderText = qsTr("File with connection settings to ") + serverSelector.text - shareConnectionDrawer.needCloseButton = false - shareConnectionDrawer.open() shareConnectionDrawer.contentVisible = false PageController.showBusyIndicator(true) switch (type) { - case PageShare.ConfigType.AmneziaConnection: ExportController.generateConnectionConfig(clientNameTextField.textFieldText); break; + case PageShare.ConfigType.AmneziaConnection: { + ExportController.generateConnectionConfig(clientNameTextField.textFieldText); + break; + } case PageShare.ConfigType.OpenVpn: { ExportController.generateOpenVpnConfig(clientNameTextField.textFieldText) shareConnectionDrawer.configCaption = qsTr("Save OpenVPN config") @@ -63,6 +67,13 @@ PageType { shareConnectionDrawer.configFileName = "amnezia_for_wireguard" break } + case PageShare.ConfigType.Awg: { + ExportController.generateAwgConfig(clientNameTextField.textFieldText) + shareConnectionDrawer.configCaption = qsTr("Save AmneziaWG config") + shareConnectionDrawer.configExtension = ".conf" + shareConnectionDrawer.configFileName = "amnezia_for_awg" + break + } case PageShare.ConfigType.ShadowSocks: { ExportController.generateShadowSocksConfig() shareConnectionDrawer.configCaption = qsTr("Save ShadowSocks config") @@ -80,11 +91,6 @@ PageType { } PageController.showBusyIndicator(false) - - shareConnectionDrawer.needCloseButton = true - PageController.showTopCloseButton(true) - - shareConnectionDrawer.contentVisible = true } function onExportErrorOccurred(errorMessage) { @@ -115,6 +121,11 @@ PageType { property string name: qsTr("WireGuard native format") property var type: PageShare.ConfigType.WireGuard } + QtObject { + id: awgConnectionFormat + property string name: qsTr("AmneziaWG native format") + property var type: PageShare.ConfigType.Awg + } QtObject { id: shadowSocksConnectionFormat property string name: qsTr("ShadowSocks native format") @@ -129,7 +140,7 @@ PageType { FlickableType { anchors.top: parent.top anchors.bottom: parent.bottom - contentHeight: content.height + contentHeight: content.height + 10 ColumnLayout { id: content @@ -154,14 +165,15 @@ PageType { shareFullAccessDrawer.open() } - DrawerType { + DrawerType2 { id: shareFullAccessDrawer - width: root.width - height: root.height * 0.45 + parent: root + anchors.fill: parent + expandedHeight: root.height * 0.45 - ColumnLayout { + expandedContent: ColumnLayout { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -234,7 +246,7 @@ PageType { accessTypeSelector.currentIndex = 1 PageController.showBusyIndicator(true) ExportController.updateClientManagementModel(ContainersModel.getCurrentlyProcessedContainerIndex(), - ServersModel.getCurrentlyProcessedServerCredentials()) + ServersModel.getProcessedServerCredentials()) PageController.showBusyIndicator(false) } } @@ -264,6 +276,8 @@ PageType { textField.maximumLength: 20 checkEmptyText: true + + KeyNavigation.tab: shareButton } DropDownType { @@ -276,6 +290,7 @@ PageType { Layout.topMargin: 16 drawerHeight: 0.4375 + drawerParent: root descriptionText: qsTr("Server") headerText: qsTr("Server") @@ -305,7 +320,7 @@ PageType { serverSelector.severSelectorIndexChanged() } - serverSelector.menuVisible = false + serverSelector.close() } Component.onCompleted: { @@ -316,7 +331,7 @@ PageType { function handler() { serverSelector.text = selectedText - ServersModel.currentlyProcessedIndex = proxyServersModel.mapToSource(currentIndex) + ServersModel.processedIndex = proxyServersModel.mapToSource(currentIndex) } } } @@ -328,6 +343,7 @@ PageType { Layout.topMargin: 16 drawerHeight: 0.5 + drawerParent: root descriptionText: qsTr("Protocol") headerText: qsTr("Protocol") @@ -358,14 +374,15 @@ PageType { clickedFunction: function() { handler() - protocolSelector.menuVisible = false + protocolSelector.close() } Connections { target: serverSelector function onSeverSelectorIndexChanged() { - protocolSelectorListView.currentIndex = proxyContainersModel.mapFromSource(ServersModel.getDefaultContainer()) + var defaultContainer = proxyContainersModel.mapFromSource(ServersModel.getProcessedServerData("defaultContainer")) + protocolSelectorListView.currentIndex = defaultContainer protocolSelectorListView.triggerCurrentItem() } } @@ -387,7 +404,7 @@ PageType { if (accessTypeSelector.currentIndex === 1) { PageController.showBusyIndicator(true) ExportController.updateClientManagementModel(ContainersModel.getCurrentlyProcessedContainerIndex(), - ServersModel.getCurrentlyProcessedServerCredentials()) + ServersModel.getProcessedServerCredentials()) PageController.showBusyIndicator(false) } } @@ -401,6 +418,8 @@ PageType { root.connectionTypesModel.push(openVpnConnectionFormat) } else if (index === ContainerProps.containerFromString("amnezia-wireguard")) { root.connectionTypesModel.push(wireGuardConnectionFormat) + } else if (index === ContainerProps.containerFromString("amnezia-awg")) { + root.connectionTypesModel.push(awgConnectionFormat) } else if (index === ContainerProps.containerFromString("amnezia-shadowsocks")) { root.connectionTypesModel.push(openVpnConnectionFormat) root.connectionTypesModel.push(shadowSocksConnectionFormat) @@ -422,6 +441,7 @@ PageType { Layout.topMargin: 16 drawerHeight: 0.4375 + drawerParent: root visible: accessTypeSelector.currentIndex === 0 enabled: root.connectionTypesModel.length > 1 @@ -445,7 +465,7 @@ PageType { clickedFunction: function() { exportTypeSelector.text = selectedText exportTypeSelector.currentIndex = currentIndex - exportTypeSelector.menuVisible = false + exportTypeSelector.close() } Component.onCompleted: { @@ -455,11 +475,9 @@ PageType { } } - ShareConnectionDrawer { - id: shareConnectionDrawer - } - BasicButtonType { + id: shareButton + Layout.fillWidth: true Layout.topMargin: 40 @@ -469,7 +487,7 @@ PageType { text: qsTr("Share") imageSource: "qrc:/images/controls/share-2.svg" - onClicked: { + clickedFunc: function(){ if (clientNameTextField.textFieldText !== "") { ExportController.generateConfig(root.connectionTypesModel[exportTypeSelector.currentIndex].type) } @@ -560,13 +578,15 @@ PageType { DividerType {} - DrawerType { + DrawerType2 { id: clientInfoDrawer - width: root.width - height: root.height * 0.5 + parent: root - ColumnLayout { + anchors.fill: parent + expandedHeight: root.height * 0.5 + + expandedContent: ColumnLayout { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -597,30 +617,33 @@ PageType { text: qsTr("Rename") - onClicked: function() { + clickedFunc: function() { clientNameEditDrawer.open() } - DrawerType { + DrawerType2 { id: clientNameEditDrawer - width: root.width - height: root.height * 0.35 + parent: root - onVisibleChanged: { - if (clientNameEditDrawer.visible) { - clientNameEditor.textField.forceActiveFocus() - } - } + anchors.fill: parent + expandedHeight: root.height * 0.35 - ColumnLayout { + expandedContent: ColumnLayout { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: 16 + anchors.topMargin: 32 anchors.leftMargin: 16 anchors.rightMargin: 16 + Connections { + target: clientNameEditDrawer + function onOpened() { + clientNameEditor.textField.forceActiveFocus() + } + } + TextFieldWithHeaderType { id: clientNameEditor Layout.fillWidth: true @@ -628,14 +651,18 @@ PageType { textFieldText: clientName textField.maximumLength: 20 checkEmptyText: true + + KeyNavigation.tab: saveButton } BasicButtonType { + id: saveButton + Layout.fillWidth: true text: qsTr("Save") - onClicked: { + clickedFunc: function() { if (clientNameEditor.textFieldText === "") { return } @@ -645,7 +672,7 @@ PageType { ExportController.renameClient(index, clientNameEditor.textFieldText, ContainersModel.getCurrentlyProcessedContainerIndex(), - ServersModel.getCurrentlyProcessedServerCredentials()) + ServersModel.getProcessedServerCredentials()) PageController.showBusyIndicator(false) clientNameEditDrawer.close() } @@ -667,21 +694,20 @@ PageType { text: qsTr("Revoke") - onClicked: function() { - questionDrawer.headerText = qsTr("Revoke the config for a user - %1?").arg(clientName) - questionDrawer.descriptionText = qsTr("The user will no longer be able to connect to your server.") - questionDrawer.yesButtonText = qsTr("Continue") - questionDrawer.noButtonText = qsTr("Cancel") + clickedFunc: function() { + var headerText = qsTr("Revoke the config for a user - %1?").arg(clientName) + var descriptionText = qsTr("The user will no longer be able to connect to your server.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") - questionDrawer.yesButtonFunction = function() { - questionDrawer.close() + var yesButtonFunction = function() { clientInfoDrawer.close() root.revokeConfig(index) } - questionDrawer.noButtonFunction = function() { - questionDrawer.close() + var noButtonFunction = function() { } - questionDrawer.open() + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } } @@ -689,12 +715,15 @@ PageType { } } } - - QuestionDrawer { - id: questionDrawer - } } } + + ShareConnectionDrawer { + id: shareConnectionDrawer + + anchors.fill: parent + } + MouseArea { anchors.fill: parent onPressed: function(mouse) { diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml index df0e2a00..8acb42c2 100644 --- a/client/ui/qml/Pages2/PageShareFullAccess.qml +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -69,6 +69,7 @@ PageType { Layout.topMargin: 16 drawerHeight: 0.4375 + drawerParent: root descriptionText: qsTr("Server") headerText: qsTr("Server") @@ -99,7 +100,7 @@ PageType { shareConnectionDrawer.headerText = qsTr("Accessing ") + serverSelector.text shareConnectionDrawer.configContentHeaderText = qsTr("File with accessing settings to ") + serverSelector.text - serverSelector.menuVisible = false + serverSelector.close() } Component.onCompleted: { @@ -110,7 +111,7 @@ PageType { function handler() { serverSelector.text = selectedText - ServersModel.currentlyProcessedIndex = proxyServersModel.mapToSource(currentIndex) + ServersModel.processedIndex = proxyServersModel.mapToSource(currentIndex) } } } @@ -122,12 +123,10 @@ PageType { text: qsTr("Share") imageSource: "qrc:/images/controls/share-2.svg" - onClicked: function() { + clickedFunc: function() { shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text shareConnectionDrawer.configContentHeaderText = qsTr("File with connection settings to ") + serverSelector.text - shareConnectionDrawer.needCloseButton = false - shareConnectionDrawer.open() shareConnectionDrawer.contentVisible = false PageController.showBusyIndicator(true) @@ -140,16 +139,15 @@ PageType { PageController.showBusyIndicator(false) - shareConnectionDrawer.needCloseButton = true - PageController.showTopCloseButton(true) - shareConnectionDrawer.contentVisible = true } } - - ShareConnectionDrawer { - id: shareConnectionDrawer - } } } + + ShareConnectionDrawer { + id: shareConnectionDrawer + + anchors.fill: parent + } } diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index 3ade9af8..dac9db93 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -20,22 +20,16 @@ PageType { function onGoToPageHome() { tabBar.setCurrentIndex(0) tabBarStackView.goToTabBarPage(PageEnum.PageHome) - - PageController.updateDrawerRootPage(PageEnum.PageHome) } function onGoToPageSettings() { tabBar.setCurrentIndex(2) tabBarStackView.goToTabBarPage(PageEnum.PageSettings) - - PageController.updateDrawerRootPage(PageEnum.PageSettings) } function onGoToPageViewConfig() { var pagePath = PageController.getPagePath(PageEnum.PageSetupWizardViewConfig) tabBarStackView.push(pagePath, { "objectName" : pagePath }, StackView.PushTransition) - - PageController.updateDrawerRootPage(PageEnum.PageSetupWizardViewConfig) } function onShowBusyIndicator(visible) { @@ -44,15 +38,14 @@ PageType { tabBar.enabled = !visible } -// function onShowTopCloseButton(visible) { -// topCloseButton.visible = visible -// } - function onEnableTabBar(enabled) { tabBar.enabled = enabled } function onClosePage() { + tabBar.isServerInfoShow = tabBarStackView.currentItem.objectName !== PageController.getPagePath(PageEnum.PageSettingsServerInfo) + && tabBarStackView.currentItem.objectName !== PageController.getPagePath(PageEnum.PageSettingsSplitTunneling) + if (tabBarStackView.depth <= 1) { return } @@ -61,13 +54,14 @@ PageType { function onGoToPage(page, slide) { var pagePath = PageController.getPagePath(page) + if (slide) { tabBarStackView.push(pagePath, { "objectName" : pagePath }, StackView.PushTransition) } else { tabBarStackView.push(pagePath, { "objectName" : pagePath }, StackView.Immediate) } - - PageController.updateDrawerRootPage(page) + + tabBar.isServerInfoShow = page === PageEnum.PageSettingsServerInfo || PageEnum.PageSettingsSplitTunneling || tabBar.isServerInfoShow } function onGoToStartPage() { @@ -115,20 +109,12 @@ PageType { function onNoInstalledContainers() { PageController.setTriggeredBtConnectButton(true) - ServersModel.currentlyProcessedIndex = ServersModel.getDefaultServerIndex() + ServersModel.processedIndex = ServersModel.getDefaultServerIndex() InstallController.setShouldCreateServer(false) PageController.goToPage(PageEnum.PageSetupWizardEasy) } } - Connections { - target: ApiController - - function onErrorOccurred(errorMessage) { - PageController.showErrorMessage(errorMessage) - } - } - StackViewType { id: tabBarStackView @@ -146,26 +132,21 @@ PageType { var pagePath = PageController.getPagePath(page) tabBarStackView.clear(StackView.Immediate) tabBarStackView.replace(pagePath, { "objectName" : pagePath }, StackView.Immediate) - - PageController.updateDrawerRootPage(page) + tabBar.isServerInfoShow = false } Component.onCompleted: { var pagePath = PageController.getPagePath(PageEnum.PageHome) - ServersModel.currentlyProcessedIndex = ServersModel.defaultIndex + ServersModel.processedIndex = ServersModel.defaultIndex tabBarStackView.push(pagePath, { "objectName" : pagePath }) } - -// onWidthChanged: { -// topCloseButton.x = tabBarStackView.x + tabBarStackView.width - -// topCloseButton.buttonWidth - topCloseButton.rightPadding -// } } TabBar { id: tabBar property int previousIndex: 0 + property bool isServerInfoShow: false anchors.right: parent.right anchors.left: parent.left @@ -196,11 +177,11 @@ PageType { } TabImageButtonType { - isSelected: tabBar.currentIndex === 0 + isSelected: tabBar.isServerInfoShow ? false : tabBar.currentIndex === 0 image: "qrc:/images/controls/home.svg" onClicked: { tabBarStackView.goToTabBarPage(PageEnum.PageHome) - ServersModel.currentlyProcessedIndex = ServersModel.defaultIndex + ServersModel.processedIndex = ServersModel.defaultIndex tabBar.previousIndex = 0 } } @@ -230,7 +211,7 @@ PageType { } TabImageButtonType { - isSelected: tabBar.currentIndex === 2 + isSelected: tabBar.isServerInfoShow ? true : tabBar.currentIndex === 2 image: "qrc:/images/controls/settings-2.svg" onClicked: { tabBarStackView.goToTabBarPage(PageEnum.PageSettings) @@ -253,13 +234,6 @@ PageType { z: 1 } -// TopCloseButtonType { -// id: topCloseButton - -// x: tabBarStackView.width - topCloseButton.buttonWidth - topCloseButton.rightPadding -// z: 1 -// } - ConnectionTypeSelectionDrawer { id: connectionTypeSelection diff --git a/client/ui/qml/main2.qml b/client/ui/qml/main2.qml index 073fb4ad..bfecf46b 100644 --- a/client/ui/qml/main2.qml +++ b/client/ui/qml/main2.qml @@ -8,6 +8,7 @@ import PageEnum 1.0 import "Config" import "Controls2" +import "Components" Window { id: root @@ -130,32 +131,15 @@ Window { } Item { - anchors.right: parent.right - anchors.left: parent.left - anchors.bottom: parent.bottom + anchors.fill: parent - implicitHeight: popupErrorMessage.height - - DrawerType { + DrawerType2 { id: privateKeyPassphraseDrawer - width: root.width - height: root.height * 0.35 + anchors.fill: parent + expandedHeight: root.height * 0.35 - onVisibleChanged: { - if (privateKeyPassphraseDrawer.visible) { - passphrase.textFieldText = "" - passphrase.textField.forceActiveFocus() - } - } - onAboutToHide: { - PageController.showBusyIndicator(true) - } - onAboutToShow: { - PageController.showBusyIndicator(false) - } - - ColumnLayout { + expandedContent: ColumnLayout { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -163,6 +147,24 @@ Window { anchors.leftMargin: 16 anchors.rightMargin: 16 + Connections { + target: privateKeyPassphraseDrawer + function onOpened() { + passphrase.textFieldText = "" + passphrase.textField.forceActiveFocus() + } + + function onAboutToHide() { + if (passphrase.textFieldText !== "") { + PageController.showBusyIndicator(true) + } + } + + function onAboutToShow() { + PageController.showBusyIndicator(false) + } + } + TextFieldWithHeaderType { id: passphrase @@ -176,9 +178,13 @@ Window { clickedFunc: function() { hidePassword = !hidePassword } + + KeyNavigation.tab: saveButton } BasicButtonType { + id: saveButton + Layout.fillWidth: true defaultColor: "transparent" @@ -190,7 +196,7 @@ Window { text: qsTr("Save") - onClicked: { + clickedFunc: function() { privateKeyPassphraseDrawer.close() PageController.passphraseRequestDrawerClosed(passphrase.textFieldText) } @@ -199,6 +205,37 @@ Window { } } + Item { + anchors.fill: parent + + QuestionDrawer { + id: questionDrawer + + anchors.fill: parent + } + } + + function showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) { + questionDrawer.headerText = headerText + questionDrawer.descriptionText = descriptionText + questionDrawer.yesButtonText = yesButtonText + questionDrawer.noButtonText = noButtonText + + questionDrawer.yesButtonFunction = function() { + questionDrawer.close() + if (yesButtonFunction && typeof yesButtonFunction === "function") { + yesButtonFunction() + } + } + questionDrawer.noButtonFunction = function() { + questionDrawer.close() + if (noButtonFunction && typeof noButtonFunction === "function") { + noButtonFunction() + } + } + questionDrawer.open() + } + FileDialog { id: mainFileDialog diff --git a/deploy/build_macos.sh b/deploy/build_macos.sh index 0d64a9ec..ffdbd20b 100755 --- a/deploy/build_macos.sh +++ b/deploy/build_macos.sh @@ -146,7 +146,8 @@ if [ "${MAC_CERT_PW+x}" ]; then fi echo "Building DMG installer..." -hdiutil create -size 120mb -volname AmneziaVPN -srcfolder $BUILD_DIR/installer/$APP_NAME.app -ov -format UDZO $DMG_FILENAME +# Allow Terminal to make changes in Privacy & Security > App Management +hdiutil create -size 256mb -volname AmneziaVPN -srcfolder $BUILD_DIR/installer/$APP_NAME.app -ov -format UDZO $DMG_FILENAME if [ "${MAC_CERT_PW+x}" ]; then echo "Signing DMG installer..."