mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-08 14:33:23 +00:00
add new qml padding, update model
This commit is contained in:
@@ -1,23 +1,22 @@
|
||||
#include "xrayConfigurator.h"
|
||||
|
||||
#include "logger.h"
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QUuid>
|
||||
#include "logger.h"
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/utils/selfhosted/scriptsRegistry.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/utils/selfhosted/scriptsRegistry.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
|
||||
namespace {
|
||||
Logger logger("XrayConfigurator");
|
||||
@@ -35,11 +34,19 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
{
|
||||
// Generate new UUID for client
|
||||
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
|
||||
// Get flow value from settings (default xtls-rprx-vision)
|
||||
QString flowValue = "xtls-rprx-vision";
|
||||
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
if (!xrayCfg->serverConfig.flow.isEmpty()) {
|
||||
flowValue = xrayCfg->serverConfig.flow;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current server config
|
||||
QString currentConfig = m_sshSession->getTextFileFromContainer(
|
||||
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
|
||||
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to get server config file";
|
||||
return "";
|
||||
@@ -54,7 +61,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
}
|
||||
|
||||
QJsonObject serverConfig = doc.object();
|
||||
|
||||
|
||||
// Validate server config structure
|
||||
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
@@ -68,7 +75,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains(amnezia::protocols::xray::settings)) {
|
||||
logger.error() << "Inbound missing 'settings' field";
|
||||
@@ -84,26 +91,29 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
}
|
||||
|
||||
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
|
||||
|
||||
|
||||
// Create configuration for new client
|
||||
QJsonObject clientConfig {
|
||||
{amnezia::protocols::xray::id, clientId},
|
||||
{amnezia::protocols::xray::flow, "xtls-rprx-vision"}
|
||||
};
|
||||
|
||||
clientConfig[amnezia::protocols::xray::id] = clientId;
|
||||
if (!flowValue.isEmpty()) {
|
||||
clientConfig[amnezia::protocols::xray::flow] = flowValue;
|
||||
}
|
||||
|
||||
clients.append(clientConfig);
|
||||
|
||||
|
||||
// Update config
|
||||
settings[amnezia::protocols::xray::clients] = clients;
|
||||
inbound[amnezia::protocols::xray::settings] = settings;
|
||||
inbounds[0] = inbound;
|
||||
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
|
||||
|
||||
|
||||
// Save updated config to server
|
||||
QString updatedConfig = QJsonDocument(serverConfig).toJson();
|
||||
errorCode = m_sshSession->uploadTextFileToContainer(
|
||||
container,
|
||||
credentials,
|
||||
container,
|
||||
credentials,
|
||||
updatedConfig,
|
||||
amnezia::protocols::xray::serverConfigPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting
|
||||
@@ -116,7 +126,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
// Restart container
|
||||
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
|
||||
errorCode = m_sshSession->runScript(
|
||||
credentials,
|
||||
credentials,
|
||||
m_sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns))
|
||||
);
|
||||
|
||||
@@ -128,75 +138,336 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia
|
||||
return clientId;
|
||||
}
|
||||
|
||||
QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const
|
||||
{
|
||||
QJsonObject streamSettings;
|
||||
const auto &xhttp = srv.xhttp;
|
||||
const auto &mkcp = srv.mkcp;
|
||||
|
||||
// network
|
||||
QString networkValue = "tcp";
|
||||
if (srv.transport == "xhttp")
|
||||
{
|
||||
networkValue = "xhttp";
|
||||
}
|
||||
else if (srv.transport == "mkcp")
|
||||
{
|
||||
networkValue = "kcp";
|
||||
}
|
||||
streamSettings[amnezia::protocols::xray::network] = networkValue;
|
||||
|
||||
// security
|
||||
streamSettings[amnezia::protocols::xray::security] = srv.security;
|
||||
|
||||
// TLS settings
|
||||
if (srv.security == "tls") {
|
||||
QJsonObject tlsSettings;
|
||||
if (!srv.sni.isEmpty()) {
|
||||
tlsSettings[amnezia::protocols::xray::serverName] = srv.sni;
|
||||
}
|
||||
if (!srv.alpn.isEmpty()) {
|
||||
QJsonArray alpnArray;
|
||||
// alpn may be comma-separated: "HTTP/2,HTTP/1.1"
|
||||
for (const QString &a : srv.alpn.split(",")) {
|
||||
alpnArray.append(a.trimmed());
|
||||
}
|
||||
tlsSettings["alpn"] = alpnArray;
|
||||
}
|
||||
if (!srv.fingerprint.isEmpty()) {
|
||||
tlsSettings[amnezia::protocols::xray::fingerprint] = srv.fingerprint;
|
||||
}
|
||||
streamSettings["tlsSettings"] = tlsSettings;
|
||||
}
|
||||
|
||||
// Reality settings
|
||||
if (srv.security == "reality") {
|
||||
QJsonObject realSettings;
|
||||
if (!srv.fingerprint.isEmpty())
|
||||
{
|
||||
realSettings[amnezia::protocols::xray::fingerprint] = srv.fingerprint;
|
||||
}
|
||||
if (!srv.sni.isEmpty())
|
||||
{
|
||||
realSettings[amnezia::protocols::xray::serverName] = srv.sni;
|
||||
}
|
||||
// publicKey and shortId are filled in createConfig after fetching from server
|
||||
streamSettings[amnezia::protocols::xray::realitySettings] = realSettings;
|
||||
}
|
||||
|
||||
// XHTTP transport settings
|
||||
if (srv.transport == "xhttp") {
|
||||
QJsonObject xhttpObj;
|
||||
|
||||
if (!xhttp.host.isEmpty())
|
||||
{
|
||||
xhttpObj["host"] = xhttp.host;
|
||||
}
|
||||
if (!xhttp.path.isEmpty())
|
||||
{
|
||||
xhttpObj["path"] = xhttp.path;
|
||||
}
|
||||
if (!xhttp.mode.isEmpty())
|
||||
{
|
||||
xhttpObj["mode"] = xhttp.mode;
|
||||
}
|
||||
|
||||
// headers
|
||||
if (xhttp.headersTemplate == "HTTP") {
|
||||
QJsonObject headers;
|
||||
headers["Host"] = xhttp.host;
|
||||
xhttpObj["headers"] = headers;
|
||||
}
|
||||
|
||||
if (!xhttp.uplinkMethod.isEmpty())
|
||||
{
|
||||
xhttpObj["method"] = xhttp.uplinkMethod;
|
||||
}
|
||||
if (xhttp.disableGrpc)
|
||||
{
|
||||
xhttpObj["noGRPCHeader"] = true;
|
||||
}
|
||||
if (xhttp.disableSse)
|
||||
{
|
||||
xhttpObj["noSSEHeader"] = true;
|
||||
}
|
||||
|
||||
// Session & Sequence
|
||||
if (!xhttp.sessionPlacement.isEmpty() && xhttp.sessionPlacement != "None")
|
||||
{
|
||||
xhttpObj["scSessionPlacement"] = xhttp.sessionPlacement;
|
||||
}
|
||||
if (!xhttp.seqPlacement.isEmpty() && xhttp.seqPlacement != "None")
|
||||
{
|
||||
xhttpObj["scSeqPlacement"] = xhttp.seqPlacement;
|
||||
}
|
||||
if (!xhttp.uplinkDataPlacement.isEmpty())
|
||||
{
|
||||
xhttpObj["scUplinkDataPlacement"] = xhttp.uplinkDataPlacement;
|
||||
}
|
||||
|
||||
// Traffic shaping
|
||||
if (!xhttp.uplinkChunkSize.isEmpty() && xhttp.uplinkChunkSize != "0")
|
||||
xhttpObj["xhttpUplinkChunkSize"] = xhttp.uplinkChunkSize.toInt();
|
||||
if (!xhttp.scMaxBufferedPosts.isEmpty())
|
||||
xhttpObj["scMaxBufferedPosts"] = xhttp.scMaxBufferedPosts.toInt();
|
||||
|
||||
// scMaxEachPostBytes range
|
||||
if (!xhttp.scMaxEachPostBytesMin.isEmpty() || !xhttp.scMaxEachPostBytesMax.isEmpty()) {
|
||||
QJsonObject range;
|
||||
range["from"] = xhttp.scMaxEachPostBytesMin.toInt();
|
||||
range["to"] = xhttp.scMaxEachPostBytesMax.toInt();
|
||||
xhttpObj["scMaxEachPostBytes"] = range;
|
||||
}
|
||||
|
||||
// scMinPostsIntervalMs range
|
||||
if (!xhttp.scMinPostsIntervalMsMin.isEmpty() || !xhttp.scMinPostsIntervalMsMax.isEmpty()) {
|
||||
QJsonObject range;
|
||||
range["from"] = xhttp.scMinPostsIntervalMsMin.toInt();
|
||||
range["to"] = xhttp.scMinPostsIntervalMsMax.toInt();
|
||||
xhttpObj["scMinPostsIntervalMs"] = range;
|
||||
}
|
||||
|
||||
// scStreamUpServerSecs range
|
||||
if (!xhttp.scStreamUpServerSecsMin.isEmpty() || !xhttp.scStreamUpServerSecsMax.isEmpty()) {
|
||||
QJsonObject range;
|
||||
range["from"] = xhttp.scStreamUpServerSecsMin.toInt();
|
||||
range["to"] = xhttp.scStreamUpServerSecsMax.toInt();
|
||||
xhttpObj["scStreamUpServerSecs"] = range;
|
||||
}
|
||||
|
||||
// xPadding
|
||||
if (xhttp.xPadding.obfsMode) {
|
||||
QJsonObject paddingObj;
|
||||
if (!xhttp.xPadding.bytesMin.isEmpty() || !xhttp.xPadding.bytesMax.isEmpty()) {
|
||||
QJsonObject bytesRange;
|
||||
bytesRange["from"] = xhttp.xPadding.bytesMin.toInt();
|
||||
bytesRange["to"] = xhttp.xPadding.bytesMax.toInt();
|
||||
paddingObj["xPaddingBytes"] = bytesRange;
|
||||
}
|
||||
if (!xhttp.xPadding.key.isEmpty())
|
||||
{
|
||||
paddingObj["xPaddingKey"] = xhttp.xPadding.key;
|
||||
}
|
||||
if (!xhttp.xPadding.header.isEmpty())
|
||||
{
|
||||
paddingObj["xPaddingHeader"] = xhttp.xPadding.header;
|
||||
}
|
||||
if (!xhttp.xPadding.placement.isEmpty())
|
||||
{
|
||||
paddingObj["xPaddingPlacement"] = xhttp.xPadding.placement;
|
||||
}
|
||||
if (!xhttp.xPadding.method.isEmpty())
|
||||
{
|
||||
paddingObj["xPaddingMethod"] = xhttp.xPadding.method;
|
||||
}
|
||||
xhttpObj["xPadding"] = paddingObj;
|
||||
}
|
||||
|
||||
// xmux
|
||||
if (xhttp.xmux.enabled) {
|
||||
QJsonObject muxObj;
|
||||
muxObj["enabled"] = true;
|
||||
|
||||
auto addRange = [&](const char *key, const QString &minV, const QString &maxV) {
|
||||
if (!minV.isEmpty() || !maxV.isEmpty()) {
|
||||
QJsonObject r;
|
||||
r["from"] = minV.toInt();
|
||||
r["to"] = maxV.toInt();
|
||||
muxObj[key] = r;
|
||||
}
|
||||
};
|
||||
|
||||
addRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax);
|
||||
addRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax);
|
||||
addRange("cMaxReuseTimes", xhttp.xmux.cMaxReuseTimesMin, xhttp.xmux.cMaxReuseTimesMax);
|
||||
addRange("hMaxRequestTimes", xhttp.xmux.hMaxRequestTimesMin, xhttp.xmux.hMaxRequestTimesMax);
|
||||
addRange("hMaxReusableSecs", xhttp.xmux.hMaxReusableSecsMin, xhttp.xmux.hMaxReusableSecsMax);
|
||||
|
||||
if (!xhttp.xmux.hKeepAlivePeriod.isEmpty())
|
||||
{
|
||||
muxObj["hKeepAlivePeriod"] = xhttp.xmux.hKeepAlivePeriod.toInt();
|
||||
}
|
||||
|
||||
xhttpObj["xmux"] = muxObj;
|
||||
}
|
||||
|
||||
streamSettings["xhttpSettings"] = xhttpObj;
|
||||
}
|
||||
|
||||
// mKCP transport settings
|
||||
if (srv.transport == "mkcp") {
|
||||
QJsonObject kcpObj;
|
||||
if (!mkcp.tti.isEmpty())
|
||||
{
|
||||
kcpObj["tti"] = mkcp.tti.toInt();
|
||||
}
|
||||
if (!mkcp.uplinkCapacity.isEmpty())
|
||||
{
|
||||
kcpObj["uplinkCapacity"] = mkcp.uplinkCapacity.toInt();
|
||||
}
|
||||
if (!mkcp.downlinkCapacity.isEmpty())
|
||||
{
|
||||
kcpObj["downlinkCapacity"] = mkcp.downlinkCapacity.toInt();
|
||||
}
|
||||
if (!mkcp.readBufferSize.isEmpty())
|
||||
{
|
||||
kcpObj["readBufferSize"] = mkcp.readBufferSize.toInt();
|
||||
}
|
||||
if (!mkcp.writeBufferSize.isEmpty())
|
||||
{
|
||||
kcpObj["writeBufferSize"] = mkcp.writeBufferSize.toInt();
|
||||
}
|
||||
kcpObj["congestion"] = mkcp.congestion;
|
||||
streamSettings["kcpSettings"] = kcpObj;
|
||||
}
|
||||
|
||||
return streamSettings;
|
||||
}
|
||||
|
||||
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const ContainerConfig &containerConfig,
|
||||
const DnsSettings &dnsSettings,
|
||||
ErrorCode &errorCode)
|
||||
{
|
||||
const XrayServerConfig* serverConfig = nullptr;
|
||||
if (auto* xrayConfig = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
serverConfig = &xrayConfig->serverConfig;
|
||||
const XrayServerConfig *serverConfig = nullptr;
|
||||
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
|
||||
serverConfig = &xrayCfg->serverConfig;
|
||||
}
|
||||
|
||||
|
||||
if (!serverConfig) {
|
||||
logger.error() << "No XrayProtocolConfig found";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig {};
|
||||
}
|
||||
|
||||
const XrayServerConfig &srv = *serverConfig;
|
||||
|
||||
QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, dnsSettings, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) {
|
||||
logger.error() << "Failed to prepare server config";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
return XrayProtocolConfig {};
|
||||
}
|
||||
|
||||
amnezia::ScriptVars vars = amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns);
|
||||
vars.append(amnezia::genProtocolVarsForContainer(container, containerConfig));
|
||||
QString config = m_sshSession->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), vars);
|
||||
|
||||
if (config.isEmpty()) {
|
||||
logger.error() << "Failed to get config template";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
// Fetch server keys (Reality only)
|
||||
QString xrayPublicKey;
|
||||
QString xrayShortId;
|
||||
|
||||
if (srv.security == "reality") {
|
||||
xrayPublicKey = m_sshSession->getTextFileFromContainer(container, credentials,
|
||||
amnezia::protocols::xray::PublicKeyPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
|
||||
logger.error() << "Failed to get public key";
|
||||
return XrayProtocolConfig {};
|
||||
}
|
||||
xrayPublicKey.replace("\n", "");
|
||||
|
||||
xrayShortId = m_sshSession->getTextFileFromContainer(container, credentials,
|
||||
amnezia::protocols::xray::shortidPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
|
||||
logger.error() << "Failed to get short ID";
|
||||
return XrayProtocolConfig {};
|
||||
}
|
||||
xrayShortId.replace("\n", "");
|
||||
}
|
||||
|
||||
QString xrayPublicKey =
|
||||
m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
|
||||
logger.error() << "Failed to get public key";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayPublicKey.replace("\n", "");
|
||||
|
||||
QString xrayShortId =
|
||||
m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
|
||||
logger.error() << "Failed to get short ID";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
}
|
||||
xrayShortId.replace("\n", "");
|
||||
|
||||
if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) {
|
||||
logger.error() << "Config template missing required variables:"
|
||||
<< "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID")
|
||||
<< "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY")
|
||||
<< "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID");
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return XrayProtocolConfig{};
|
||||
// Build outbound
|
||||
QJsonObject userObj;
|
||||
userObj[amnezia::protocols::xray::id] = xrayClientId;
|
||||
userObj[amnezia::protocols::xray::encryption] = "none";
|
||||
if (!srv.flow.isEmpty()) {
|
||||
userObj[amnezia::protocols::xray::flow] = srv.flow;
|
||||
}
|
||||
|
||||
config.replace("$XRAY_CLIENT_ID", xrayClientId);
|
||||
config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey);
|
||||
config.replace("$XRAY_SHORT_ID", xrayShortId);
|
||||
QJsonObject vnextEntry;
|
||||
vnextEntry[amnezia::protocols::xray::address] = credentials.hostName;
|
||||
vnextEntry[amnezia::protocols::xray::port] = srv.port.toInt();
|
||||
vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj };
|
||||
|
||||
QJsonObject outboundSettings;
|
||||
outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry };
|
||||
|
||||
QJsonObject outbound;
|
||||
outbound["protocol"] = "vless";
|
||||
outbound[amnezia::protocols::xray::settings] = outboundSettings;
|
||||
|
||||
// Build streamSettings
|
||||
QJsonObject streamObj = buildStreamSettings(srv, xrayClientId);
|
||||
|
||||
// Inject Reality keys
|
||||
if (srv.security == "reality") {
|
||||
QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject();
|
||||
rs[amnezia::protocols::xray::publicKey] = xrayPublicKey;
|
||||
rs[amnezia::protocols::xray::shortId] = xrayShortId;
|
||||
rs[amnezia::protocols::xray::spiderX] = "";
|
||||
streamObj[amnezia::protocols::xray::realitySettings] = rs;
|
||||
}
|
||||
|
||||
outbound[amnezia::protocols::xray::streamSettings] = streamObj;
|
||||
|
||||
// Build full client config
|
||||
QJsonObject inboundObj;
|
||||
inboundObj["listen"] = amnezia::protocols::xray::defaultLocalAddr;
|
||||
inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort;
|
||||
inboundObj["protocol"] = "socks";
|
||||
inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { "udp", true } };
|
||||
|
||||
QJsonObject clientJson;
|
||||
clientJson["log"] = QJsonObject { { "loglevel", "error" } };
|
||||
clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj };
|
||||
clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound };
|
||||
|
||||
QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
|
||||
|
||||
// Return
|
||||
XrayProtocolConfig protocolConfig;
|
||||
if (serverConfig) {
|
||||
protocolConfig.serverConfig = *serverConfig;
|
||||
}
|
||||
|
||||
protocolConfig.serverConfig = srv;
|
||||
|
||||
XrayClientConfig clientConfig;
|
||||
clientConfig.nativeConfig = config;
|
||||
clientConfig.localPort = "";
|
||||
clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort);
|
||||
clientConfig.id = xrayClientId;
|
||||
|
||||
|
||||
protocolConfig.setClientConfig(clientConfig);
|
||||
|
||||
|
||||
return protocolConfig;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
#define XRAY_CONFIGURATOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "configuratorBase.h"
|
||||
#include "core/utils/errorCodes.h"
|
||||
#include "core/utils/routeModes.h"
|
||||
#include "core/utils/commonStructs.h"
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
|
||||
class XrayConfigurator : public ConfiguratorBase
|
||||
{
|
||||
@@ -22,6 +24,10 @@ private:
|
||||
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
|
||||
const amnezia::DnsSettings &dnsSettings,
|
||||
amnezia::ErrorCode &errorCode);
|
||||
|
||||
// Builds the native xray "streamSettings" JSON object from XrayServerConfig
|
||||
QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv,
|
||||
const QString &clientId) const;
|
||||
};
|
||||
|
||||
#endif // XRAY_CONFIGURATOR_H
|
||||
|
||||
@@ -87,6 +87,9 @@ void CoreController::initModels()
|
||||
m_xrayConfigModel = new XrayConfigModel(this);
|
||||
setQmlContextProperty("XrayConfigModel", m_xrayConfigModel);
|
||||
|
||||
m_xrayConfigsModel = new XrayConfigsModel(m_appSettingsRepository, this);
|
||||
setQmlContextProperty("XrayConfigsModel", m_xrayConfigsModel);
|
||||
|
||||
m_torConfigModel = new TorConfigModel(this);
|
||||
setQmlContextProperty("TorConfigModel", m_torConfigModel);
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
#include "ui/models/protocols/openvpnConfigModel.h"
|
||||
#include "ui/models/protocols/wireguardConfigModel.h"
|
||||
#include "ui/models/protocols/xrayConfigModel.h"
|
||||
#include "ui/models/protocols/xrayConfigsModel.h"
|
||||
#include "ui/models/protocolsModel.h"
|
||||
#include "ui/models/services/torConfigModel.h"
|
||||
#include "ui/models/serversModel.h"
|
||||
@@ -202,6 +203,7 @@ private:
|
||||
|
||||
OpenVpnConfigModel* m_openVpnConfigModel;
|
||||
XrayConfigModel* m_xrayConfigModel;
|
||||
XrayConfigsModel* m_xrayConfigsModel;
|
||||
TorConfigModel* m_torConfigModel;
|
||||
WireGuardConfigModel* m_wireGuardConfigModel;
|
||||
AwgConfigModel* m_awgConfigModel;
|
||||
|
||||
@@ -295,6 +295,18 @@ ExportController::ExportResult ExportController::generateXrayConfig(int serverIn
|
||||
vlessServer.shortId = realitySettings.value(amnezia::protocols::xray::shortId).toString();
|
||||
vlessServer.fingerprint = realitySettings.value(amnezia::protocols::xray::fingerprint).toString("chrome");
|
||||
vlessServer.spiderX = realitySettings.value(amnezia::protocols::xray::spiderX).toString("");
|
||||
} else if (vlessServer.security == "tls") {
|
||||
QJsonObject tlsSettings = streamSettings.value("tlsSettings").toObject();
|
||||
vlessServer.serverName = tlsSettings.value(amnezia::protocols::xray::serverName).toString();
|
||||
vlessServer.fingerprint = tlsSettings.value(amnezia::protocols::xray::fingerprint).toString();
|
||||
// alpn: serialize array back to comma-separated for VLESS URI
|
||||
QJsonArray alpnArr = tlsSettings.value("alpn").toArray();
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : alpnArr) {
|
||||
alpnList << v.toString();
|
||||
}
|
||||
// alpn goes into vless URI query param — handled by Serialize via serverName/alpn fields
|
||||
// VlessServerObject doesn't have alpn field, so we embed in serverName if needed
|
||||
}
|
||||
|
||||
result.nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, "AmneziaVPN");
|
||||
|
||||
@@ -63,18 +63,163 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
|
||||
}
|
||||
|
||||
QJsonObject streamSettings = inbound[protocols::xray::streamSettings].toObject();
|
||||
QJsonObject realitySettings = streamSettings[protocols::xray::realitySettings].toObject();
|
||||
if (!realitySettings.contains(protocols::xray::serverNames)) {
|
||||
logger.error() << "Settings missing 'serverNames' field";
|
||||
auto *xrayConfig = config.getXrayProtocolConfig();
|
||||
if (!xrayConfig) {
|
||||
logger.error() << "No XrayProtocolConfig in ContainerConfig";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QString siteName = realitySettings[protocols::xray::serverNames][0].toString();
|
||||
XrayServerConfig &srv = xrayConfig->serverConfig;
|
||||
|
||||
if (auto* xrayConfig = config.getXrayProtocolConfig()) {
|
||||
xrayConfig->serverConfig.site = siteName;
|
||||
// ── Port ─────────────────────────────────────────────────────────
|
||||
if (inbound.contains(protocols::xray::port)) {
|
||||
srv.port = QString::number(inbound[protocols::xray::port].toInt());
|
||||
}
|
||||
|
||||
|
||||
// ── Network (transport) ───────────────────────────────────────────
|
||||
QString networkVal = streamSettings.value(protocols::xray::network).toString("tcp");
|
||||
if (networkVal == "xhttp") {
|
||||
srv.transport = "xhttp";
|
||||
} else if (networkVal == "kcp") {
|
||||
srv.transport = "mkcp";
|
||||
} else {
|
||||
srv.transport = "raw";
|
||||
}
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────
|
||||
srv.security = streamSettings.value(protocols::xray::security).toString("reality");
|
||||
|
||||
// ── Reality settings ──────────────────────────────────────────────
|
||||
if (srv.security == "reality") {
|
||||
QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject();
|
||||
|
||||
// serverNames array → site + sni
|
||||
if (rs.contains(protocols::xray::serverNames)) {
|
||||
QString sniVal = rs[protocols::xray::serverNames].toArray().first().toString();
|
||||
srv.sni = sniVal;
|
||||
srv.site = sniVal;
|
||||
} else if (rs.contains(protocols::xray::serverName)) {
|
||||
srv.sni = rs[protocols::xray::serverName].toString();
|
||||
srv.site = srv.sni;
|
||||
}
|
||||
|
||||
srv.fingerprint = rs.value(protocols::xray::fingerprint).toString("Mozilla/5.0");
|
||||
}
|
||||
|
||||
// ── TLS settings ──────────────────────────────────────────────────
|
||||
if (srv.security == "tls") {
|
||||
QJsonObject tls = streamSettings.value("tlsSettings").toObject();
|
||||
srv.sni = tls.value(protocols::xray::serverName).toString();
|
||||
srv.fingerprint = tls.value(protocols::xray::fingerprint).toString("Mozilla/5.0");
|
||||
|
||||
QJsonArray alpnArr = tls.value("alpn").toArray();
|
||||
QStringList alpnList;
|
||||
for (const QJsonValue &v : alpnArr) {
|
||||
alpnList << v.toString();
|
||||
}
|
||||
srv.alpn = alpnList.join(",");
|
||||
}
|
||||
|
||||
// ── Flow (from users array) ───────────────────────────────────────
|
||||
if (inbound.contains(protocols::xray::settings)) {
|
||||
QJsonObject s = inbound[protocols::xray::settings].toObject();
|
||||
QJsonArray clientsArr = s.value(protocols::xray::clients).toArray();
|
||||
if (!clientsArr.isEmpty()) {
|
||||
srv.flow = clientsArr[0].toObject().value(protocols::xray::flow).toString();
|
||||
}
|
||||
}
|
||||
|
||||
// ── XHTTP settings ────────────────────────────────────────────────
|
||||
if (srv.transport == "xhttp") {
|
||||
QJsonObject xhttpObj = streamSettings.value("xhttpSettings").toObject();
|
||||
srv.xhttp.mode = xhttpObj.value("mode").toString("Auto");
|
||||
srv.xhttp.host = xhttpObj.value("host").toString();
|
||||
srv.xhttp.path = xhttpObj.value("path").toString();
|
||||
srv.xhttp.uplinkMethod = xhttpObj.value("method").toString("POST");
|
||||
srv.xhttp.disableGrpc = xhttpObj.value("noGRPCHeader").toBool(true);
|
||||
srv.xhttp.disableSse = xhttpObj.value("noSSEHeader").toBool(true);
|
||||
|
||||
srv.xhttp.sessionPlacement = xhttpObj.value("scSessionPlacement").toString("Path");
|
||||
srv.xhttp.seqPlacement = xhttpObj.value("scSeqPlacement").toString("Path");
|
||||
srv.xhttp.uplinkDataPlacement = xhttpObj.value("scUplinkDataPlacement").toString("Body");
|
||||
|
||||
if (xhttpObj.contains("xhttpUplinkChunkSize")) {
|
||||
srv.xhttp.uplinkChunkSize = QString::number(xhttpObj["xhttpUplinkChunkSize"].toInt());
|
||||
}
|
||||
if (xhttpObj.contains("scMaxBufferedPosts")) {
|
||||
srv.xhttp.scMaxBufferedPosts = QString::number(xhttpObj["scMaxBufferedPosts"].toInt());
|
||||
}
|
||||
|
||||
auto readRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
QJsonObject r = xhttpObj.value(key).toObject();
|
||||
if (!r.isEmpty()) {
|
||||
minOut = QString::number(r.value("from").toInt());
|
||||
maxOut = QString::number(r.value("to").toInt());
|
||||
}
|
||||
};
|
||||
readRange("scMaxEachPostBytes", srv.xhttp.scMaxEachPostBytesMin, srv.xhttp.scMaxEachPostBytesMax);
|
||||
readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax);
|
||||
readRange("scStreamUpServerSecs", srv.xhttp.scStreamUpServerSecsMin, srv.xhttp.scStreamUpServerSecsMax);
|
||||
|
||||
// xPadding
|
||||
if (xhttpObj.contains("xPadding")) {
|
||||
QJsonObject pad = xhttpObj["xPadding"].toObject();
|
||||
srv.xhttp.xPadding.obfsMode = true;
|
||||
srv.xhttp.xPadding.key = pad.value("xPaddingKey").toString();
|
||||
srv.xhttp.xPadding.header = pad.value("xPaddingHeader").toString();
|
||||
srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString("Cookie");
|
||||
srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString("Repeat-x");
|
||||
QJsonObject bytesRange = pad.value("xPaddingBytes").toObject();
|
||||
if (!bytesRange.isEmpty()) {
|
||||
srv.xhttp.xPadding.bytesMin = QString::number(bytesRange.value("from").toInt());
|
||||
srv.xhttp.xPadding.bytesMax = QString::number(bytesRange.value("to").toInt());
|
||||
}
|
||||
}
|
||||
|
||||
// xmux
|
||||
if (xhttpObj.contains("xmux")) {
|
||||
QJsonObject mux = xhttpObj["xmux"].toObject();
|
||||
srv.xhttp.xmux.enabled = mux.value("enabled").toBool(true);
|
||||
|
||||
auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) {
|
||||
QJsonObject r = mux.value(key).toObject();
|
||||
if (!r.isEmpty()) {
|
||||
minOut = QString::number(r.value("from").toInt());
|
||||
maxOut = QString::number(r.value("to").toInt());
|
||||
}
|
||||
};
|
||||
readMuxRange("maxConcurrency", srv.xhttp.xmux.maxConcurrencyMin, srv.xhttp.xmux.maxConcurrencyMax);
|
||||
readMuxRange("maxConnections", srv.xhttp.xmux.maxConnectionsMin, srv.xhttp.xmux.maxConnectionsMax);
|
||||
readMuxRange("cMaxReuseTimes", srv.xhttp.xmux.cMaxReuseTimesMin, srv.xhttp.xmux.cMaxReuseTimesMax);
|
||||
readMuxRange("hMaxRequestTimes", srv.xhttp.xmux.hMaxRequestTimesMin, srv.xhttp.xmux.hMaxRequestTimesMax);
|
||||
readMuxRange("hMaxReusableSecs", srv.xhttp.xmux.hMaxReusableSecsMin, srv.xhttp.xmux.hMaxReusableSecsMax);
|
||||
|
||||
if (mux.contains("hKeepAlivePeriod"))
|
||||
srv.xhttp.xmux.hKeepAlivePeriod = QString::number(mux["hKeepAlivePeriod"].toInt());
|
||||
}
|
||||
}
|
||||
|
||||
// ── mKCP settings ─────────────────────────────────────────────────
|
||||
if (srv.transport == "mkcp") {
|
||||
QJsonObject kcp = streamSettings.value("kcpSettings").toObject();
|
||||
if (kcp.contains("tti")) {
|
||||
srv.mkcp.tti = QString::number(kcp["tti"].toInt());
|
||||
}
|
||||
if (kcp.contains("uplinkCapacity")) {
|
||||
srv.mkcp.uplinkCapacity = QString::number(kcp["uplinkCapacity"].toInt());
|
||||
}
|
||||
if (kcp.contains("downlinkCapacity")) {
|
||||
srv.mkcp.downlinkCapacity = QString::number(kcp["downlinkCapacity"].toInt());
|
||||
}
|
||||
if (kcp.contains("readBufferSize")) {
|
||||
srv.mkcp.readBufferSize = QString::number(kcp["readBufferSize"].toInt());
|
||||
}
|
||||
if (kcp.contains("writeBufferSize")) {
|
||||
srv.mkcp.writeBufferSize = QString::number(kcp["writeBufferSize"].toInt());
|
||||
}
|
||||
srv.mkcp.congestion = kcp.value("congestion").toBool(true);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
|
||||
@@ -450,4 +450,12 @@ void SecureAppSettingsRepository::setInstallationUuid(const QString &uuid)
|
||||
m_settings->setValue("Conf/installationUuid", uuid);
|
||||
}
|
||||
|
||||
QByteArray SecureAppSettingsRepository::xraySavedConfigs() const
|
||||
{
|
||||
return value("Xray/savedConfigs").toByteArray();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data)
|
||||
{
|
||||
setValue("Xray/savedConfigs", data);
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@ public:
|
||||
|
||||
QString nextAvailableServerName() const;
|
||||
|
||||
QByteArray xraySavedConfigs() const;
|
||||
void setXraySavedConfigs(const QByteArray &data);
|
||||
|
||||
signals:
|
||||
void appLanguageChanged(QLocale locale);
|
||||
void allowedDnsServersChanged(const QStringList &servers);
|
||||
|
||||
@@ -88,6 +88,7 @@ namespace PageLoader
|
||||
PageProtocolXrayXPaddingSettings,
|
||||
PageProtocolXrayFlowSettings,
|
||||
PageProtocolXraySecuritySettings,
|
||||
PageProtocolXrayXPaddingBytesSettings,
|
||||
};
|
||||
Q_ENUM_NS(PageEnum)
|
||||
|
||||
|
||||
3
client/ui/controllers/selfhosted/installUiController.cpp
Executable file → Normal file
3
client/ui/controllers/selfhosted/installUiController.cpp
Executable file → Normal file
@@ -480,7 +480,8 @@ void InstallUiController::updateProtocolConfigModel(int serverIndex, int contain
|
||||
case Proto::Awg: updateIfPresent(m_awgConfigModel, containerConfig.getAwgProtocolConfig()); break;
|
||||
case Proto::WireGuard: updateIfPresent(m_wireGuardConfigModel, containerConfig.getWireGuardProtocolConfig()); break;
|
||||
case Proto::OpenVpn: updateIfPresent(m_openVpnConfigModel, containerConfig.getOpenVpnProtocolConfig()); break;
|
||||
case Proto::Xray: updateIfPresent(m_xrayConfigModel, containerConfig.getXrayProtocolConfig()); break;
|
||||
case Proto::Xray:
|
||||
case Proto::SSXray: updateIfPresent(m_xrayConfigModel, containerConfig.getXrayProtocolConfig()); break;
|
||||
case Proto::TorWebSite: updateIfPresent(m_torConfigModel, containerConfig.getTorProtocolConfig()); break;
|
||||
case Proto::Sftp: updateIfPresent(m_sftpConfigModel, containerConfig.getSftpProtocolConfig()); break;
|
||||
case Proto::Socks5Proxy: updateIfPresent(m_socks5ConfigModel, containerConfig.getSocks5ProxyProtocolConfig()); break;
|
||||
|
||||
@@ -378,3 +378,21 @@ QHash<int, QByteArray> XrayConfigModel::roleNames() const
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
void XrayConfigModel::resetToDefaults()
|
||||
{
|
||||
beginResetModel();
|
||||
m_protocolConfig.serverConfig = amnezia::XrayServerConfig{};
|
||||
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void XrayConfigModel::applyServerConfig(const amnezia::XrayServerConfig &serverConfig)
|
||||
{
|
||||
beginResetModel();
|
||||
m_protocolConfig.serverConfig = serverConfig;
|
||||
// Clear client config since server settings changed
|
||||
m_protocolConfig.clearClientConfig();
|
||||
m_originalProtocolConfig = m_protocolConfig;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
@@ -97,6 +97,8 @@ public slots:
|
||||
void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig);
|
||||
amnezia::XrayProtocolConfig getProtocolConfig();
|
||||
bool isServerSettingsEqual();
|
||||
void resetToDefaults();
|
||||
void applyServerConfig(const amnezia::XrayServerConfig &serverConfig);
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
195
client/ui/models/protocols/xrayConfigsModel.cpp
Normal file
195
client/ui/models/protocols/xrayConfigsModel.cpp
Normal file
@@ -0,0 +1,195 @@
|
||||
#include "xrayConfigsModel.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QUuid>
|
||||
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
|
||||
QJsonObject XrayConfigSnapshot::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj["id"] = id;
|
||||
obj["displayName"] = displayName;
|
||||
obj["createdAt"] = createdAt.toString(Qt::ISODate);
|
||||
obj["serverConfig"] = serverConfig.toJson();
|
||||
return obj;
|
||||
}
|
||||
|
||||
XrayConfigSnapshot XrayConfigSnapshot::fromJson(const QJsonObject &json)
|
||||
{
|
||||
XrayConfigSnapshot s;
|
||||
s.id = json.value("id").toString();
|
||||
s.displayName = json.value("displayName").toString();
|
||||
s.createdAt = QDateTime::fromString(json.value("createdAt").toString(), Qt::ISODate);
|
||||
s.serverConfig = amnezia::XrayServerConfig::fromJson(json.value("serverConfig").toObject());
|
||||
return s;
|
||||
}
|
||||
|
||||
XrayConfigsModel::XrayConfigsModel(SecureAppSettingsRepository *appSettings, QObject *parent)
|
||||
: QAbstractListModel(parent), m_appSettings(appSettings)
|
||||
{
|
||||
loadAll();
|
||||
}
|
||||
|
||||
void XrayConfigsModel::loadAll()
|
||||
{
|
||||
m_configs.clear();
|
||||
QByteArray raw = m_appSettings->xraySavedConfigs();
|
||||
if (raw.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray arr = QJsonDocument::fromJson(raw).array();
|
||||
for (const QJsonValue &v : arr) {
|
||||
m_configs.append(XrayConfigSnapshot::fromJson(v.toObject()));
|
||||
}
|
||||
}
|
||||
|
||||
void XrayConfigsModel::persistAll()
|
||||
{
|
||||
QJsonArray arr;
|
||||
for (const XrayConfigSnapshot &s : m_configs) {
|
||||
arr.append(s.toJson());
|
||||
}
|
||||
m_appSettings->setXraySavedConfigs(QJsonDocument(arr).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
int XrayConfigsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return m_configs.size();
|
||||
}
|
||||
|
||||
QVariant XrayConfigsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= m_configs.size()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
const XrayConfigSnapshot &s = m_configs.at(index.row());
|
||||
|
||||
switch (role) {
|
||||
case IdRole: {
|
||||
return s.id;
|
||||
}
|
||||
case DisplayNameRole: {
|
||||
return s.displayName;
|
||||
}
|
||||
case CreatedAtRole: {
|
||||
return s.createdAt.toString("dd.MM.yyyy HH:mm");
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> XrayConfigsModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[IdRole] = "configId";
|
||||
roles[DisplayNameRole] = "configName";
|
||||
roles[CreatedAtRole] = "configDate";
|
||||
return roles;
|
||||
}
|
||||
|
||||
void XrayConfigsModel::reload()
|
||||
{
|
||||
beginResetModel();
|
||||
loadAll();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void XrayConfigsModel::createFromCurrent(const amnezia::XrayServerConfig &serverConfig)
|
||||
{
|
||||
XrayConfigSnapshot snapshot;
|
||||
snapshot.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
snapshot.displayName = buildDisplayName(serverConfig);
|
||||
snapshot.createdAt = QDateTime::currentDateTime();
|
||||
snapshot.serverConfig = serverConfig;
|
||||
|
||||
beginInsertRows(QModelIndex(), m_configs.size(), m_configs.size());
|
||||
m_configs.append(snapshot);
|
||||
endInsertRows();
|
||||
|
||||
persistAll();
|
||||
}
|
||||
|
||||
amnezia::XrayServerConfig XrayConfigsModel::applyConfig(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_configs.size()) {
|
||||
return amnezia::XrayServerConfig {};
|
||||
}
|
||||
|
||||
return m_configs.at(index).serverConfig;
|
||||
}
|
||||
|
||||
void XrayConfigsModel::removeConfig(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_configs.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
beginRemoveRows(QModelIndex(), index, index);
|
||||
m_configs.removeAt(index);
|
||||
endRemoveRows();
|
||||
|
||||
persistAll();
|
||||
emit configRemoved(index);
|
||||
}
|
||||
|
||||
QString XrayConfigsModel::exportToJson(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_configs.size()) {
|
||||
return {};
|
||||
}
|
||||
return QString::fromUtf8(QJsonDocument(m_configs.at(index).toJson()).toJson(QJsonDocument::Indented));
|
||||
}
|
||||
|
||||
bool XrayConfigsModel::importFromJson(const QString &jsonString)
|
||||
{
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
emit importFailed(tr("Invalid JSON format"));
|
||||
return false;
|
||||
}
|
||||
|
||||
XrayConfigSnapshot snapshot = XrayConfigSnapshot::fromJson(doc.object());
|
||||
if (snapshot.id.isEmpty()) {
|
||||
snapshot.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
}
|
||||
if (snapshot.displayName.isEmpty()) {
|
||||
snapshot.displayName = buildDisplayName(snapshot.serverConfig);
|
||||
}
|
||||
snapshot.createdAt = QDateTime::currentDateTime();
|
||||
|
||||
beginInsertRows(QModelIndex(), m_configs.size(), m_configs.size());
|
||||
m_configs.append(snapshot);
|
||||
endInsertRows();
|
||||
|
||||
persistAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
QString XrayConfigsModel::buildDisplayName(const amnezia::XrayServerConfig &cfg)
|
||||
{
|
||||
// Build a human-readable name: "XHTTP TLS Reality", "RAW Reality", etc.
|
||||
QString transport;
|
||||
if (cfg.transport == "xhttp") {
|
||||
transport = "XHTTP";
|
||||
} else if (cfg.transport == "mkcp") {
|
||||
transport = "mKCP";
|
||||
} else {
|
||||
transport = "RAW (TCP)";
|
||||
}
|
||||
|
||||
QString security;
|
||||
if (cfg.security == "tls") {
|
||||
security = "TLS";
|
||||
} else if (cfg.security == "reality") {
|
||||
security = "Reality";
|
||||
} else {
|
||||
security = "None";
|
||||
}
|
||||
|
||||
return QString("%1 %2").arg(transport, security).trimmed();
|
||||
}
|
||||
69
client/ui/models/protocols/xrayConfigsModel.h
Normal file
69
client/ui/models/protocols/xrayConfigsModel.h
Normal file
@@ -0,0 +1,69 @@
|
||||
#ifndef XRAYCONFIGSMODEL_H
|
||||
#define XRAYCONFIGSMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QDateTime>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
struct XrayConfigSnapshot
|
||||
{
|
||||
QString id;
|
||||
QString displayName; // auto-generated: "XHTTP TLS Reality", "RAW Reality", etc.
|
||||
QDateTime createdAt;
|
||||
amnezia::XrayServerConfig serverConfig;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static XrayConfigSnapshot fromJson(const QJsonObject &json);
|
||||
};
|
||||
|
||||
class XrayConfigsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
IdRole = Qt::UserRole + 1,
|
||||
DisplayNameRole,
|
||||
CreatedAtRole, // "dd.MM.yyyy HH:mm"
|
||||
};
|
||||
|
||||
explicit XrayConfigsModel(SecureAppSettingsRepository *appSettings, QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
public slots:
|
||||
void reload();
|
||||
|
||||
Q_INVOKABLE void createFromCurrent(const amnezia::XrayServerConfig &serverConfig);
|
||||
Q_INVOKABLE amnezia::XrayServerConfig applyConfig(int index) const;
|
||||
Q_INVOKABLE void removeConfig(int index);
|
||||
|
||||
Q_INVOKABLE QString exportToJson(int index) const;
|
||||
Q_INVOKABLE bool importFromJson(const QString &jsonString);
|
||||
|
||||
signals:
|
||||
void configApplied(int index);
|
||||
void configRemoved(int index);
|
||||
void importFailed(const QString &errorMessage);
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
SecureAppSettingsRepository *m_appSettings;
|
||||
QVector<XrayConfigSnapshot> m_configs;
|
||||
|
||||
void persistAll();
|
||||
void loadAll();
|
||||
static QString buildDisplayName(const amnezia::XrayServerConfig &cfg);
|
||||
};
|
||||
|
||||
#endif // XRAYCONFIGSMODEL_H
|
||||
@@ -14,26 +14,11 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
// Temporary model — will be replaced by real XrayConfigsModel
|
||||
ListModel {
|
||||
id: configsModel
|
||||
ListElement {
|
||||
configName: "XHTTP TLS Reality"; configDate: "24.02.2026 11:12"
|
||||
}
|
||||
ListElement {
|
||||
configName: "RAW (TCP) TLS Reality"; configDate: "24.02.2026 11:14"
|
||||
}
|
||||
ListElement {
|
||||
configName: "RAW (TCP) TLS Reality"; configDate: "24.02.2026 11:14"
|
||||
}
|
||||
ListElement {
|
||||
configName: "RAW (TCP) TLS Reality"; configDate: "24.02.2026 11:15"
|
||||
}
|
||||
}
|
||||
property string selectedConfigName: ""
|
||||
property int selectedConfigIndex: -1
|
||||
|
||||
// Currently selected config for the drawer
|
||||
property string selectedConfigName: ""
|
||||
property int selectedConfigIndex: -1
|
||||
// Reload the list every time we open this page
|
||||
Component.onCompleted: XrayConfigsModel.reload()
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
@@ -50,11 +35,12 @@ PageType {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
model: XrayConfigsModel
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
// ── Header ────────────────────────────────────────────────
|
||||
Header2TextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
@@ -65,32 +51,37 @@ PageType {
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
// ── Create config from current settings ───────────────────
|
||||
// ── Create from current settings ──────────────────────────
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Create configuration based on current settings")
|
||||
textMaximumLineCount: 2
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
// XrayConfigModel.createConfigFromCurrent()
|
||||
clickedFunction: function() {
|
||||
XrayConfigsModel.createFromCurrent(XrayConfigModel.getProtocolConfig().serverConfig)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
DividerType {}
|
||||
|
||||
// ── Export ────────────────────────────────────────────────
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Export settings")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
// XrayConfigModel.exportSettings()
|
||||
clickedFunction: function() {
|
||||
if (root.selectedConfigIndex >= 0) {
|
||||
var json = XrayConfigsModel.exportToJson(root.selectedConfigIndex)
|
||||
ExportController.shareText(json, "xray_config.json")
|
||||
} else if (XrayConfigsModel.rowCount() > 0) {
|
||||
// Export the first one if none selected
|
||||
var json = XrayConfigsModel.exportToJson(0)
|
||||
ExportController.shareText(json, "xray_config.json")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
DividerType {}
|
||||
|
||||
// ── Import ────────────────────────────────────────────────
|
||||
LabelWithButtonType {
|
||||
@@ -98,15 +89,14 @@ PageType {
|
||||
text: qsTr("Import settings")
|
||||
descriptionText: qsTr("In JSON format")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
// XrayConfigModel.importSettings()
|
||||
clickedFunction: function() {
|
||||
ImportController.importConfig()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
DividerType {}
|
||||
|
||||
// ── Configurations section label ──────────────────────────
|
||||
// ── Section label ─────────────────────────────────────────
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
@@ -115,42 +105,64 @@ PageType {
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Configurations")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
visible: XrayConfigsModel.rowCount() > 0
|
||||
}
|
||||
}
|
||||
|
||||
model: configsModel
|
||||
// ── Empty state ───────────────────────────────────────────────
|
||||
footer: ColumnLayout {
|
||||
width: listView.width
|
||||
visible: XrayConfigsModel.rowCount() === 0
|
||||
spacing: 0
|
||||
|
||||
Item { Layout.preferredHeight: 32 }
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("No saved configurations yet.\nCreate one from the current settings.")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
// ── Config list items ─────────────────────────────────────────
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: configName
|
||||
descriptionText: configDate
|
||||
|
||||
rightImageSource: "qrc:/images/controls/more-vertical.svg"
|
||||
|
||||
clickedFunction: function () {
|
||||
root.selectedConfigName = configName
|
||||
clickedFunction: function() {
|
||||
root.selectedConfigName = configName
|
||||
root.selectedConfigIndex = index
|
||||
configActionsDrawer.openTriggered()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
DividerType {}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Import result handler ─────────────────────────────────────────
|
||||
Connections {
|
||||
target: XrayConfigsModel
|
||||
function onImportFailed(errorMessage) {
|
||||
PageController.showNotificationMessage(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Per-config actions drawer ─────────────────────────────────────
|
||||
DrawerType2 {
|
||||
id: configActionsDrawer
|
||||
|
||||
parent: root
|
||||
anchors.fill: parent
|
||||
expandedHeight: root.height * 0.4
|
||||
expandedHeight: root.height * 0.35
|
||||
|
||||
expandedStateContent: ColumnLayout {
|
||||
id: drawerContent
|
||||
@@ -166,7 +178,7 @@ PageType {
|
||||
BackButtonType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
backButtonFunction: function () {
|
||||
backButtonFunction: function() {
|
||||
configActionsDrawer.closeTriggered()
|
||||
}
|
||||
}
|
||||
@@ -181,37 +193,57 @@ PageType {
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
// ── Apply config ──────────────────────────────────────────
|
||||
// Apply
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Apply configuration")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
clickedFunction: function() {
|
||||
configActionsDrawer.closeTriggered()
|
||||
// XrayConfigModel.applyConfig(root.selectedConfigIndex)
|
||||
var serverConfig = XrayConfigsModel.applyConfig(root.selectedConfigIndex)
|
||||
XrayConfigModel.applyServerConfig(serverConfig)
|
||||
PageController.closePage()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
DividerType {}
|
||||
|
||||
// Export this config
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Export configuration")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function() {
|
||||
configActionsDrawer.closeTriggered()
|
||||
var json = XrayConfigsModel.exportToJson(root.selectedConfigIndex)
|
||||
ExportController.shareText(json, "xray_config.json")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Delete config ─────────────────────────────────────────
|
||||
DividerType {}
|
||||
|
||||
// Delete
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Delete configuration")
|
||||
textColor: AmneziaStyle.color.vibrantRed
|
||||
clickedFunction: function () {
|
||||
clickedFunction: function() {
|
||||
configActionsDrawer.closeTriggered()
|
||||
// XrayConfigModel.deleteConfig(root.selectedConfigIndex)
|
||||
var yesButtonFunction = function() {
|
||||
XrayConfigsModel.removeConfig(root.selectedConfigIndex)
|
||||
root.selectedConfigIndex = -1
|
||||
root.selectedConfigName = ""
|
||||
}
|
||||
showQuestionDrawer(
|
||||
qsTr("Delete configuration?"),
|
||||
qsTr("This action cannot be undone."),
|
||||
qsTr("Delete"), qsTr("Cancel"),
|
||||
yesButtonFunction, function() {})
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
DividerType {}
|
||||
Item { Layout.preferredHeight: 16 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,6 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
// Temporary local state — will be replaced by model role
|
||||
property int selectedFlow: 1 // 0=Empty, 1=xtls-rprx-vision, 2=xtls-rprx-vision-udp443
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
@@ -25,17 +22,17 @@ PageType {
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flickable
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
contentHeight: mainColumn.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: mainColumn
|
||||
width: flickable.width
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
Header2TextType {
|
||||
@@ -52,8 +49,8 @@ PageType {
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("Empty")
|
||||
checked: root.selectedFlow === 0
|
||||
onClicked: root.selectedFlow = 0
|
||||
checked: flow === ""
|
||||
onClicked: flow = ""
|
||||
}
|
||||
|
||||
DividerType {
|
||||
@@ -63,9 +60,9 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("xtls-rprx-vision")
|
||||
checked: root.selectedFlow === 1
|
||||
onClicked: root.selectedFlow = 1
|
||||
text: "xtls-rprx-vision"
|
||||
checked: flow === "xtls-rprx-vision"
|
||||
onClicked: flow = "xtls-rprx-vision"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
@@ -75,9 +72,9 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("xtls-rprx-vision-udp443")
|
||||
checked: root.selectedFlow === 2
|
||||
onClicked: root.selectedFlow = 2
|
||||
text: "xtls-rprx-vision-udp443"
|
||||
checked: flow === "xtls-rprx-vision-udp443"
|
||||
onClicked: flow = "xtls-rprx-vision-udp443"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
@@ -97,11 +94,10 @@ PageType {
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
text: qsTr("Save")
|
||||
onClicked: {
|
||||
forceActiveFocus()
|
||||
// XrayConfigModel.setFlow(...)
|
||||
PageController.closePage()
|
||||
}
|
||||
Keys.onEnterPressed: clicked()
|
||||
Keys.onReturnPressed: clicked()
|
||||
|
||||
@@ -14,16 +14,6 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
// Temporary local state — will be replaced by model roles
|
||||
property int selectedSecurity: 2 // 0=None, 1=TLS, 2=Reality
|
||||
|
||||
// Shared TLS + Reality fields
|
||||
property string fingerprint: "Mozilla/5.0"
|
||||
property string serverName: "cdn.example.com"
|
||||
|
||||
// TLS-only fields
|
||||
property string alpn: "HTTP/2"
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
@@ -32,17 +22,17 @@ PageType {
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flickable
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
contentHeight: mainColumn.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: mainColumn
|
||||
width: flickable.width
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
Header2TextType {
|
||||
@@ -54,50 +44,45 @@ PageType {
|
||||
text: qsTr("Security")
|
||||
}
|
||||
|
||||
// ── Radio: None ───────────────────────────────────────────
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("None")
|
||||
checked: root.selectedSecurity === 0
|
||||
onClicked: root.selectedSecurity = 0
|
||||
checked: security === "none"
|
||||
onClicked: security = "none"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Radio: TLS ────────────────────────────────────────────
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("TLS")
|
||||
checked: root.selectedSecurity === 1
|
||||
onClicked: root.selectedSecurity = 1
|
||||
checked: security === "tls"
|
||||
onClicked: security = "tls"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Radio: Reality ────────────────────────────────────────
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("Reality")
|
||||
checked: root.selectedSecurity === 2
|
||||
onClicked: root.selectedSecurity = 2
|
||||
checked: security === "reality"
|
||||
onClicked: security = "reality"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// TLS fields (ALPN + Fingerprint + SNI)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// ── TLS fields ────────────────────────────────────────────
|
||||
ColumnLayout {
|
||||
visible: root.selectedSecurity === 1
|
||||
visible: security === "tls"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
@@ -107,7 +92,7 @@ PageType {
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: root.alpn
|
||||
text: alpn
|
||||
descriptionText: qsTr("ALPN")
|
||||
headerText: qsTr("ALPN")
|
||||
drawerParent: root
|
||||
@@ -125,19 +110,26 @@ PageType {
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
root.alpn = selectedText
|
||||
alpn = selectedText
|
||||
tlsAlpnDropDown.text = selectedText
|
||||
tlsAlpnDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === root.alpn) {
|
||||
if (model.get(i).name === alpn) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
tlsAlpnDropDown.text = alpn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
@@ -146,7 +138,7 @@ PageType {
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: root.fingerprint
|
||||
text: fingerprint
|
||||
descriptionText: qsTr("Fingerprint")
|
||||
headerText: qsTr("Fingerprint")
|
||||
drawerParent: root
|
||||
@@ -157,47 +149,54 @@ PageType {
|
||||
name: "Mozilla/5.0"
|
||||
}
|
||||
ListElement {
|
||||
name: "Chrome"
|
||||
name: "chrome"
|
||||
}
|
||||
ListElement {
|
||||
name: "Firefox"
|
||||
name: "firefox"
|
||||
}
|
||||
ListElement {
|
||||
name: "Safari"
|
||||
name: "safari"
|
||||
}
|
||||
ListElement {
|
||||
name: "iOS"
|
||||
name: "ios"
|
||||
}
|
||||
ListElement {
|
||||
name: "Android"
|
||||
name: "android"
|
||||
}
|
||||
ListElement {
|
||||
name: "Edge"
|
||||
name: "edge"
|
||||
}
|
||||
ListElement {
|
||||
name: "360"
|
||||
}
|
||||
ListElement {
|
||||
name: "QQ"
|
||||
name: "qq"
|
||||
}
|
||||
ListElement {
|
||||
name: "Random"
|
||||
name: "random"
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
root.fingerprint = selectedText
|
||||
fingerprint = selectedText
|
||||
tlsFingerprintDropDown.text = selectedText
|
||||
tlsFingerprintDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === root.fingerprint) {
|
||||
if (model.get(i).name === fingerprint) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
tlsFingerprintDropDown.text = fingerprint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -206,16 +205,16 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Server Name (SNI)")
|
||||
textField.text: root.serverName
|
||||
textField.onEditingFinished: root.serverName = textField.text
|
||||
textField.text: sni
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== sni) sni = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// Reality fields (Fingerprint + SNI)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// ── Reality fields ────────────────────────────────────────
|
||||
ColumnLayout {
|
||||
visible: root.selectedSecurity === 2
|
||||
visible: security === "reality"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
@@ -225,7 +224,7 @@ PageType {
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: root.fingerprint
|
||||
text: fingerprint
|
||||
descriptionText: qsTr("Fingerprint")
|
||||
headerText: qsTr("Fingerprint")
|
||||
drawerParent: root
|
||||
@@ -236,47 +235,54 @@ PageType {
|
||||
name: "Mozilla/5.0"
|
||||
}
|
||||
ListElement {
|
||||
name: "Chrome"
|
||||
name: "chrome"
|
||||
}
|
||||
ListElement {
|
||||
name: "Firefox"
|
||||
name: "firefox"
|
||||
}
|
||||
ListElement {
|
||||
name: "Safari"
|
||||
name: "safari"
|
||||
}
|
||||
ListElement {
|
||||
name: "iOS"
|
||||
name: "ios"
|
||||
}
|
||||
ListElement {
|
||||
name: "Android"
|
||||
name: "android"
|
||||
}
|
||||
ListElement {
|
||||
name: "Edge"
|
||||
name: "edge"
|
||||
}
|
||||
ListElement {
|
||||
name: "360"
|
||||
}
|
||||
ListElement {
|
||||
name: "QQ"
|
||||
name: "qq"
|
||||
}
|
||||
ListElement {
|
||||
name: "Random"
|
||||
name: "random"
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
root.fingerprint = selectedText
|
||||
fingerprint = selectedText
|
||||
realityFingerprintDropDown.text = selectedText
|
||||
realityFingerprintDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === root.fingerprint) {
|
||||
if (model.get(i).name === fingerprint) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
realityFingerprintDropDown.text = fingerprint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -285,8 +291,10 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Server Name (SNI)")
|
||||
textField.text: root.serverName
|
||||
textField.onEditingFinished: root.serverName = textField.text
|
||||
textField.text: sni
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== sni) sni = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,11 +312,10 @@ PageType {
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
text: qsTr("Save")
|
||||
onClicked: {
|
||||
forceActiveFocus()
|
||||
// XrayConfigModel.setSecurity(...)
|
||||
PageController.closePage()
|
||||
}
|
||||
Keys.onEnterPressed: clicked()
|
||||
Keys.onReturnPressed: clicked()
|
||||
|
||||
@@ -50,7 +50,6 @@ PageType {
|
||||
|
||||
spacing: 0
|
||||
|
||||
// ── Header ────────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
@@ -69,22 +68,17 @@ PageType {
|
||||
implicitHeight: 40
|
||||
image: "qrc:/images/controls/more-vertical.svg"
|
||||
imageColor: AmneziaStyle.color.mutedGray
|
||||
onClicked: function () {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayConfigsSettings)
|
||||
}
|
||||
onClicked: PageController.goToPage(PageEnum.PageProtocolXrayConfigsSettings)
|
||||
}
|
||||
}
|
||||
|
||||
// ── "More about settings" link ────────────────────────────
|
||||
LabelTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 4
|
||||
|
||||
text: qsTr("More about settings")
|
||||
color: AmneziaStyle.color.burntOrange
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
@@ -92,43 +86,32 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Port field ────────────────────────────────────────────
|
||||
TextFieldWithHeaderType {
|
||||
id: portTextField
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 32
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
enabled: listView.enabled
|
||||
|
||||
headerText: qsTr("Port")
|
||||
textField.text: port
|
||||
textField.maximumLength: 5
|
||||
textField.validator: IntValidator {
|
||||
bottom: 1; top: 65535
|
||||
}
|
||||
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== port) {
|
||||
port = textField.text
|
||||
}
|
||||
if (textField.text !== port) port = textField.text
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
}
|
||||
|
||||
// ── Transport row ─────────────────────────────────────────
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
text: qsTr("Transport")
|
||||
descriptionText: "RAW (TCP)" // TODO: model role
|
||||
descriptionText: transport
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
enabled: listView.enabled
|
||||
|
||||
clickedFunction: function () {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayTransportSettings)
|
||||
}
|
||||
@@ -137,15 +120,12 @@ PageType {
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Security row ──────────────────────────────────────────
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("Security")
|
||||
descriptionText: "TLS" // TODO: model role
|
||||
descriptionText: security
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
enabled: listView.enabled
|
||||
|
||||
clickedFunction: function () {
|
||||
PageController.goToPage(PageEnum.PageProtocolXraySecuritySettings)
|
||||
}
|
||||
@@ -154,15 +134,12 @@ PageType {
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Flow row ──────────────────────────────────────────────
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("Flow")
|
||||
descriptionText: "xtls-rprx-vision" // TODO: model role
|
||||
descriptionText: flow
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
enabled: listView.enabled
|
||||
|
||||
clickedFunction: function () {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayFlowSettings)
|
||||
}
|
||||
@@ -171,83 +148,58 @@ PageType {
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Spacer ────────────────────────────────────────────────
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 24
|
||||
Layout.fillWidth: true; Layout.preferredHeight: 24
|
||||
}
|
||||
|
||||
// ── Save button ───────────────────────────────────────────
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
enabled: portTextField.errorText === ""
|
||||
|
||||
text: qsTr("Save")
|
||||
|
||||
onClicked: function () {
|
||||
forceActiveFocus()
|
||||
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
|
||||
var yesButtonFunction = function () {
|
||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||
return
|
||||
}
|
||||
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
|
||||
}
|
||||
var noButtonFunction = function () {
|
||||
if (!GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
if (!GC.isMobile()) saveButton.forceActiveFocus()
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: saveButton.clicked()
|
||||
Keys.onReturnPressed: saveButton.clicked()
|
||||
}
|
||||
|
||||
// ── Reset settings ────────────────────────────────────────
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("Reset settings")
|
||||
textColor: AmneziaStyle.color.vibrantRed
|
||||
visible: listView.enabled
|
||||
|
||||
clickedFunction: function () {
|
||||
var headerText = qsTr("Reset settings?")
|
||||
var descriptionText = qsTr("All XRay settings will be restored to defaults.")
|
||||
var yesButtonText = qsTr("Reset")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
|
||||
var yesButtonFunction = function () {
|
||||
XrayConfigModel.resetToDefaults()
|
||||
}
|
||||
var noButtonFunction = function () {
|
||||
}
|
||||
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText,
|
||||
yesButtonFunction, noButtonFunction)
|
||||
showQuestionDrawer(qsTr("Reset settings?"), qsTr("All XRay settings will be restored to defaults."),
|
||||
qsTr("Reset"), qsTr("Cancel"), yesButtonFunction, function () {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bottom padding ────────────────────────────────────────
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 32
|
||||
Layout.fillWidth: true; Layout.preferredHeight: 32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,41 +14,6 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
// Temporary local state — will be replaced by model roles
|
||||
property int selectedTransport: 0 // 0=RAW, 1=XHTTP, 2=mKCP
|
||||
|
||||
// XHTTP fields
|
||||
property string xhttpMode: "Auto"
|
||||
property string xhttpHost: "www.googletagmanager.com"
|
||||
property string xhttpPath: ""
|
||||
property string xhttpHeadersTemplate: "HTTP"
|
||||
property string xhttpUplinkMethod: "POST"
|
||||
property bool xhttpDisableGrpc: true
|
||||
property bool xhttpDisableSse: true
|
||||
// Session & Sequence
|
||||
property string sessionPlacement: "Path"
|
||||
property string sessionKey: "Path"
|
||||
property string seqPlacement: "Path"
|
||||
property string seqKey: ""
|
||||
property string uplinkDataPlacement: "Body"
|
||||
property string uplinkDataKey: ""
|
||||
// Traffic Shaping
|
||||
property string uplinkChunkSize: "0"
|
||||
property string scMaxBufferedPosts: ""
|
||||
property string scMaxEachPostBytesMin: "1"
|
||||
property string scMaxEachPostBytesMax: "100"
|
||||
property string scMinPostsIntervalMsMin: "100"
|
||||
property string scMinPostsIntervalMsMax: "800"
|
||||
property string scStreamUpServerSecsMin: "1"
|
||||
property string scStreamUpServerSecsMax: "100"
|
||||
// mKCP fields
|
||||
property string mkcpTti: ""
|
||||
property string mkcpUplinkCapacity: ""
|
||||
property string mkcpDownlinkCapacity: ""
|
||||
property string mkcpReadBufferSize: ""
|
||||
property string mkcpWriteBufferSize: ""
|
||||
property bool mkcpCongestion: true
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
@@ -57,20 +22,19 @@ PageType {
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flickable
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
contentHeight: mainColumn.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: mainColumn
|
||||
width: flickable.width
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
// ── Header ────────────────────────────────────────────────
|
||||
Header2TextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
@@ -80,51 +44,49 @@ PageType {
|
||||
text: qsTr("Transport")
|
||||
}
|
||||
|
||||
// ── Radio: RAW (TCP) ──────────────────────────────────────
|
||||
// ── Radio buttons ─────────────────────────────────────────
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("RAW (TCP)")
|
||||
checked: root.selectedTransport === 0
|
||||
onClicked: root.selectedTransport = 0
|
||||
checked: transport === "raw"
|
||||
onClicked: transport = "raw"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Radio: XHTTP ──────────────────────────────────────────
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("XHTTP")
|
||||
descriptionText: qsTr("Advanced users")
|
||||
checked: root.selectedTransport === 1
|
||||
onClicked: root.selectedTransport = 1
|
||||
checked: transport === "xhttp"
|
||||
onClicked: transport = "xhttp"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Radio: mKCP ───────────────────────────────────────────
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("mKCP")
|
||||
checked: root.selectedTransport === 2
|
||||
onClicked: root.selectedTransport = 2
|
||||
checked: transport === "mkcp"
|
||||
onClicked: transport = "mkcp"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// mKCP Settings (visible when mKCP selected)
|
||||
// mKCP Settings
|
||||
// ══════════════════════════════════════════════════════════
|
||||
ColumnLayout {
|
||||
visible: root.selectedTransport === 2
|
||||
visible: transport === "mkcp"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
@@ -144,8 +106,10 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("TTI")
|
||||
textField.text: root.mkcpTti
|
||||
textField.onEditingFinished: root.mkcpTti = textField.text
|
||||
textField.text: mkcpTti
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpTti) mkcpTti = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -154,8 +118,10 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("uplinkCapacity")
|
||||
textField.text: root.mkcpUplinkCapacity
|
||||
textField.onEditingFinished: root.mkcpUplinkCapacity = textField.text
|
||||
textField.text: mkcpUplinkCapacity
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpUplinkCapacity) mkcpUplinkCapacity = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -164,8 +130,10 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("downlinkCapacity")
|
||||
textField.text: root.mkcpDownlinkCapacity
|
||||
textField.onEditingFinished: root.mkcpDownlinkCapacity = textField.text
|
||||
textField.text: mkcpDownlinkCapacity
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpDownlinkCapacity) mkcpDownlinkCapacity = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -174,8 +142,10 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("readBufferSize")
|
||||
textField.text: root.mkcpReadBufferSize
|
||||
textField.onEditingFinished: root.mkcpReadBufferSize = textField.text
|
||||
textField.text: mkcpReadBufferSize
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpReadBufferSize) mkcpReadBufferSize = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -184,8 +154,10 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("writeBufferSize")
|
||||
textField.text: root.mkcpWriteBufferSize
|
||||
textField.onEditingFinished: root.mkcpWriteBufferSize = textField.text
|
||||
textField.text: mkcpWriteBufferSize
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpWriteBufferSize) mkcpWriteBufferSize = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
@@ -193,32 +165,30 @@ PageType {
|
||||
Layout.margins: 16
|
||||
Layout.topMargin: 8
|
||||
text: qsTr("Congestion")
|
||||
checked: root.mkcpCongestion
|
||||
onToggled: root.mkcpCongestion = checked
|
||||
checked: mkcpCongestion
|
||||
onToggled: mkcpCongestion = checked
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// XHTTP Settings (visible when XHTTP selected)
|
||||
// XHTTP Settings
|
||||
// ══════════════════════════════════════════════════════════
|
||||
ColumnLayout {
|
||||
visible: root.selectedTransport === 1
|
||||
visible: transport === "xhttp"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
// Mode dropdown
|
||||
DropDownType {
|
||||
id: modeDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: root.xhttpMode
|
||||
text: xhttpMode
|
||||
descriptionText: qsTr("Mode")
|
||||
headerText: qsTr("Mode")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
id: modeListView
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
@@ -235,22 +205,28 @@ PageType {
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
root.xhttpMode = selectedText
|
||||
xhttpMode = selectedText
|
||||
modeDropDown.text = selectedText
|
||||
modeDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === root.xhttpMode) {
|
||||
if (model.get(i).name === xhttpMode) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
modeDropDown.text = xhttpMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP Profile label
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
@@ -267,8 +243,10 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Host")
|
||||
textField.text: root.xhttpHost
|
||||
textField.onEditingFinished: root.xhttpHost = textField.text
|
||||
textField.text: xhttpHost
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpHost) xhttpHost = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -277,18 +255,19 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Path")
|
||||
textField.text: root.xhttpPath
|
||||
textField.onEditingFinished: root.xhttpPath = textField.text
|
||||
textField.text: xhttpPath
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpPath) xhttpPath = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
// Headers template dropdown
|
||||
DropDownType {
|
||||
id: headersDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: root.xhttpHeadersTemplate
|
||||
text: xhttpHeadersTemplate
|
||||
descriptionText: qsTr("Headers template")
|
||||
headerText: qsTr("Headers template")
|
||||
drawerParent: root
|
||||
@@ -303,29 +282,35 @@ PageType {
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
root.xhttpHeadersTemplate = selectedText
|
||||
xhttpHeadersTemplate = selectedText
|
||||
headersDropDown.text = selectedText
|
||||
headersDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === root.xhttpHeadersTemplate) {
|
||||
if (model.get(i).name === xhttpHeadersTemplate) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
headersDropDown.text = xhttpHeadersTemplate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UplinkHTTPMethod dropdown
|
||||
DropDownType {
|
||||
id: uplinkMethodDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: root.xhttpUplinkMethod
|
||||
text: xhttpUplinkMethod
|
||||
descriptionText: qsTr("UplinkHTTPMethod")
|
||||
headerText: qsTr("UplinkHTTPMethod")
|
||||
drawerParent: root
|
||||
@@ -343,43 +328,48 @@ PageType {
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
root.xhttpUplinkMethod = selectedText
|
||||
xhttpUplinkMethod = selectedText
|
||||
uplinkMethodDropDown.text = selectedText
|
||||
uplinkMethodDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === root.xhttpUplinkMethod) {
|
||||
if (model.get(i).name === xhttpUplinkMethod) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
uplinkMethodDropDown.text = xhttpUplinkMethod
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disable gRPC Header
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
Layout.topMargin: 16
|
||||
text: qsTr("Disable gRPC Header")
|
||||
descriptionText: qsTr("noGRPCHeader")
|
||||
checked: root.xhttpDisableGrpc
|
||||
onToggled: root.xhttpDisableGrpc = checked
|
||||
checked: xhttpDisableGrpc
|
||||
onToggled: xhttpDisableGrpc = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// Disable SSE Header
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
text: qsTr("Disable SSE Header")
|
||||
descriptionText: qsTr("noSSEHeader")
|
||||
checked: root.xhttpDisableSse
|
||||
onToggled: root.xhttpDisableSse = checked
|
||||
checked: xhttpDisableSse
|
||||
onToggled: xhttpDisableSse = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
@@ -402,7 +392,7 @@ PageType {
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: root.sessionPlacement
|
||||
text: xhttpSessionPlacement
|
||||
descriptionText: qsTr("SessionPlacement")
|
||||
headerText: qsTr("SessionPlacement")
|
||||
drawerParent: root
|
||||
@@ -423,19 +413,26 @@ PageType {
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
root.sessionPlacement = selectedText
|
||||
xhttpSessionPlacement = selectedText
|
||||
sessionPlacementDropDown.text = selectedText
|
||||
sessionPlacementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === root.sessionPlacement) {
|
||||
if (model.get(i).name === xhttpSessionPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
sessionPlacementDropDown.text = xhttpSessionPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
@@ -444,7 +441,7 @@ PageType {
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: root.sessionKey
|
||||
text: xhttpSessionKey
|
||||
descriptionText: qsTr("SessionKey")
|
||||
headerText: qsTr("SessionKey")
|
||||
drawerParent: root
|
||||
@@ -462,19 +459,26 @@ PageType {
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
root.sessionKey = selectedText
|
||||
xhttpSessionKey = selectedText
|
||||
sessionKeyDropDown.text = selectedText
|
||||
sessionKeyDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === root.sessionKey) {
|
||||
if (model.get(i).name === xhttpSessionKey) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
sessionKeyDropDown.text = xhttpSessionKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
@@ -483,7 +487,7 @@ PageType {
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: root.seqPlacement
|
||||
text: xhttpSeqPlacement
|
||||
descriptionText: qsTr("SeqPlacement")
|
||||
headerText: qsTr("SeqPlacement")
|
||||
drawerParent: root
|
||||
@@ -504,19 +508,26 @@ PageType {
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
root.seqPlacement = selectedText
|
||||
xhttpSeqPlacement = selectedText
|
||||
seqPlacementDropDown.text = selectedText
|
||||
seqPlacementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === root.seqPlacement) {
|
||||
if (model.get(i).name === xhttpSeqPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
seqPlacementDropDown.text = xhttpSeqPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -525,8 +536,10 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("SeqKey")
|
||||
textField.text: root.seqKey
|
||||
textField.onEditingFinished: root.seqKey = textField.text
|
||||
textField.text: xhttpSeqKey
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpSeqKey) xhttpSeqKey = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
@@ -535,7 +548,7 @@ PageType {
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: root.uplinkDataPlacement
|
||||
text: xhttpUplinkDataPlacement
|
||||
descriptionText: qsTr("UplinkDataPlacement")
|
||||
headerText: qsTr("UplinkDataPlacement")
|
||||
drawerParent: root
|
||||
@@ -550,19 +563,26 @@ PageType {
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
root.uplinkDataPlacement = selectedText
|
||||
xhttpUplinkDataPlacement = selectedText
|
||||
uplinkDataPlacementDropDown.text = selectedText
|
||||
uplinkDataPlacementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === root.uplinkDataPlacement) {
|
||||
if (model.get(i).name === xhttpUplinkDataPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
uplinkDataPlacementDropDown.text = xhttpUplinkDataPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -571,8 +591,10 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("UplinkDataKey")
|
||||
textField.text: root.uplinkDataKey
|
||||
textField.onEditingFinished: root.uplinkDataKey = textField.text
|
||||
textField.text: xhttpUplinkDataKey
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpUplinkDataKey) xhttpUplinkDataKey = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
// ── Traffic Shaping ───────────────────────────────────
|
||||
@@ -592,11 +614,13 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("UplinkChunkSize")
|
||||
textField.text: root.uplinkChunkSize
|
||||
textField.text: xhttpUplinkChunkSize
|
||||
textField.validator: IntValidator {
|
||||
bottom: 0
|
||||
}
|
||||
textField.onEditingFinished: root.uplinkChunkSize = textField.text
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpUplinkChunkSize) xhttpUplinkChunkSize = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -605,11 +629,12 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("scMaxBufferedPosts")
|
||||
textField.text: root.scMaxBufferedPosts
|
||||
textField.onEditingFinished: root.scMaxBufferedPosts = textField.text
|
||||
textField.text: xhttpScMaxBufferedPosts
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpScMaxBufferedPosts) xhttpScMaxBufferedPosts = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
// scMaxEachPostBytes — min/max range
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
@@ -623,13 +648,12 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: root.scMaxEachPostBytesMin
|
||||
maxValue: root.scMaxEachPostBytesMax
|
||||
onMinChanged: root.scMaxEachPostBytesMin = val
|
||||
onMaxChanged: root.scMaxEachPostBytesMax = val
|
||||
minValue: xhttpScMaxEachPostBytesMin
|
||||
maxValue: xhttpScMaxEachPostBytesMax
|
||||
onMinChanged: xhttpScMaxEachPostBytesMin = val
|
||||
onMaxChanged: xhttpScMaxEachPostBytesMax = val
|
||||
}
|
||||
|
||||
// scMinPostsIntervalMs — min/max range
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
@@ -643,13 +667,12 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: root.scMinPostsIntervalMsMin
|
||||
maxValue: root.scMinPostsIntervalMsMax
|
||||
onMinChanged: root.scMinPostsIntervalMsMin = val
|
||||
onMaxChanged: root.scMinPostsIntervalMsMax = val
|
||||
minValue: xhttpScMinPostsIntervalMsMin
|
||||
maxValue: xhttpScMinPostsIntervalMsMax
|
||||
onMinChanged: xhttpScMinPostsIntervalMsMin = val
|
||||
onMaxChanged: xhttpScMinPostsIntervalMsMax = val
|
||||
}
|
||||
|
||||
// scStreamUpServerSecs — min/max range
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
@@ -663,10 +686,10 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: root.scStreamUpServerSecsMin
|
||||
maxValue: root.scStreamUpServerSecsMax
|
||||
onMinChanged: root.scStreamUpServerSecsMin = val
|
||||
onMaxChanged: root.scStreamUpServerSecsMax = val
|
||||
minValue: xhttpScStreamUpServerSecsMin
|
||||
maxValue: xhttpScStreamUpServerSecsMax
|
||||
onMinChanged: xhttpScStreamUpServerSecsMin = val
|
||||
onMaxChanged: xhttpScStreamUpServerSecsMax = val
|
||||
}
|
||||
|
||||
// ── Padding and multiplexing ──────────────────────────
|
||||
@@ -688,18 +711,20 @@ PageType {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayXPaddingSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("XMux")
|
||||
descriptionText: qsTr("On")
|
||||
descriptionText: xmuxEnabled ? qsTr("On") : qsTr("Off")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayXmuxSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
}
|
||||
@@ -710,7 +735,6 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Save button ───────────────────────────────────────────────────
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
anchors.bottom: parent.bottom
|
||||
@@ -719,11 +743,10 @@ PageType {
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
text: qsTr("Save")
|
||||
onClicked: {
|
||||
forceActiveFocus()
|
||||
// XrayConfigModel.setTransport(...)
|
||||
PageController.closePage()
|
||||
}
|
||||
Keys.onEnterPressed: clicked()
|
||||
Keys.onReturnPressed: clicked()
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
Header2TextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
text: qsTr("xPaddingBytes")
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Range")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xPaddingBytesMin
|
||||
maxValue: xPaddingBytesMax
|
||||
onMinChanged: xPaddingBytesMin = val
|
||||
onMaxChanged: xPaddingBytesMax = val
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 16 }
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
text: qsTr("Save")
|
||||
onClicked: {
|
||||
forceActiveFocus()
|
||||
PageController.closePage()
|
||||
}
|
||||
Keys.onEnterPressed: clicked()
|
||||
Keys.onReturnPressed: clicked()
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,6 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
// Temporary local state
|
||||
property string xPaddingBytes: "0—0"
|
||||
property bool xPaddingObfsMode: true
|
||||
property string xPaddingKey: "www.googletagmanager.com"
|
||||
property string xPaddingHeader: ""
|
||||
property string xPaddingPlacement: "Cookie"
|
||||
property string xPaddingMethod: "Repeat-x"
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
@@ -30,20 +22,19 @@ PageType {
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flickable
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
contentHeight: mainColumn.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: mainColumn
|
||||
width: flickable.width
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
// ── Header ────────────────────────────────────────────────
|
||||
Header2TextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
@@ -53,11 +44,11 @@ PageType {
|
||||
text: qsTr("xPadding")
|
||||
}
|
||||
|
||||
// ── xPaddingBytes nav row ─────────────────────────────────
|
||||
// xPaddingBytes — min/max display row
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("xPaddingBytes")
|
||||
descriptionText: root.xPaddingBytes
|
||||
descriptionText: (xPaddingBytesMin !== "" ? xPaddingBytesMin : "0") + "—" + (xPaddingBytesMax !== "" ? xPaddingBytesMax : "0")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayXPaddingBytesSettings)
|
||||
@@ -67,48 +58,48 @@ PageType {
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── xPaddingObfsMode switcher ─────────────────────────────
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
text: qsTr("xPaddingObfsMode")
|
||||
checked: root.xPaddingObfsMode
|
||||
onToggled: root.xPaddingObfsMode = checked
|
||||
checked: xPaddingObfsMode
|
||||
onToggled: xPaddingObfsMode = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── xPaddingKey ───────────────────────────────────────────
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
headerText: qsTr("xPaddingKey")
|
||||
textField.text: root.xPaddingKey
|
||||
textField.onEditingFinished: root.xPaddingKey = textField.text
|
||||
textField.text: xPaddingKey
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xPaddingKey) xPaddingKey = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
// ── xPaddingHeader ────────────────────────────────────────
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("xPaddingHeader")
|
||||
textField.text: root.xPaddingHeader
|
||||
textField.onEditingFinished: root.xPaddingHeader = textField.text
|
||||
textField.text: xPaddingHeader
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xPaddingHeader) xPaddingHeader = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
// ── xPaddingPlacement dropdown ────────────────────────────
|
||||
DropDownType {
|
||||
id: placementDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: root.xPaddingPlacement
|
||||
text: xPaddingPlacement
|
||||
descriptionText: qsTr("xPaddingPlacement")
|
||||
headerText: qsTr("xPaddingPlacement")
|
||||
drawerParent: root
|
||||
@@ -129,29 +120,35 @@ PageType {
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
root.xPaddingPlacement = selectedText
|
||||
xPaddingPlacement = selectedText
|
||||
placementDropDown.text = selectedText
|
||||
placementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === root.xPaddingPlacement) {
|
||||
if (model.get(i).name === xPaddingPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
placementDropDown.text = xPaddingPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── xPaddingMethod dropdown ───────────────────────────────
|
||||
DropDownType {
|
||||
id: methodDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: root.xPaddingMethod
|
||||
text: xPaddingMethod
|
||||
descriptionText: qsTr("xPaddingMethod")
|
||||
headerText: qsTr("xPaddingMethod")
|
||||
drawerParent: root
|
||||
@@ -169,19 +166,26 @@ PageType {
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
root.xPaddingMethod = selectedText
|
||||
xPaddingMethod = selectedText
|
||||
methodDropDown.text = selectedText
|
||||
methodDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === root.xPaddingMethod) {
|
||||
if (model.get(i).name === xPaddingMethod) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
methodDropDown.text = xPaddingMethod
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -190,7 +194,6 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Save button ───────────────────────────────────────────────────
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
anchors.bottom: parent.bottom
|
||||
@@ -199,11 +202,10 @@ PageType {
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
text: qsTr("Save")
|
||||
onClicked: {
|
||||
forceActiveFocus()
|
||||
// XrayConfigModel.setXPadding(...)
|
||||
PageController.closePage()
|
||||
}
|
||||
Keys.onEnterPressed: clicked()
|
||||
Keys.onReturnPressed: clicked()
|
||||
|
||||
@@ -14,20 +14,6 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
// Temporary local state
|
||||
property bool xmuxEnabled: true
|
||||
property string maxConcurrencyMin: "0"
|
||||
property string maxConcurrencyMax: "0"
|
||||
property string maxConnectionsMin: "0"
|
||||
property string maxConnectionsMax: "0"
|
||||
property string cMaxReuseTimesMin: "0"
|
||||
property string cMaxReuseTimesMax: "0"
|
||||
property string hMaxRequestTimesMin: "0"
|
||||
property string hMaxRequestTimesMax: "0"
|
||||
property string hMaxReusableSecsMin: "0"
|
||||
property string hMaxReusableSecsMax: "0"
|
||||
property string hKeepAlivePeriod: ""
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
@@ -36,20 +22,19 @@ PageType {
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flickable
|
||||
ListViewType {
|
||||
id: listView
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: saveButton.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
contentHeight: mainColumn.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: mainColumn
|
||||
width: flickable.width
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
// ── Header ────────────────────────────────────────────────
|
||||
Header2TextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
@@ -59,23 +44,21 @@ PageType {
|
||||
text: qsTr("xmux")
|
||||
}
|
||||
|
||||
// ── xmux master switcher ──────────────────────────────────
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
text: qsTr("xmux")
|
||||
checked: root.xmuxEnabled
|
||||
onToggled: root.xmuxEnabled = checked
|
||||
checked: xmuxEnabled
|
||||
onToggled: xmuxEnabled = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Min/Max pairs (only when enabled) ─────────────────────
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
enabled: root.xmuxEnabled
|
||||
enabled: xmuxEnabled
|
||||
|
||||
// maxConcurrency
|
||||
CaptionTextType {
|
||||
@@ -91,10 +74,10 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: root.maxConcurrencyMin
|
||||
maxValue: root.maxConcurrencyMax
|
||||
onMinChanged: root.maxConcurrencyMin = val
|
||||
onMaxChanged: root.maxConcurrencyMax = val
|
||||
minValue: xmuxMaxConcurrencyMin
|
||||
maxValue: xmuxMaxConcurrencyMax
|
||||
onMinChanged: xmuxMaxConcurrencyMin = val
|
||||
onMaxChanged: xmuxMaxConcurrencyMax = val
|
||||
}
|
||||
|
||||
// maxConnections
|
||||
@@ -111,10 +94,10 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: root.maxConnectionsMin
|
||||
maxValue: root.maxConnectionsMax
|
||||
onMinChanged: root.maxConnectionsMin = val
|
||||
onMaxChanged: root.maxConnectionsMax = val
|
||||
minValue: xmuxMaxConnectionsMin
|
||||
maxValue: xmuxMaxConnectionsMax
|
||||
onMinChanged: xmuxMaxConnectionsMin = val
|
||||
onMaxChanged: xmuxMaxConnectionsMax = val
|
||||
}
|
||||
|
||||
// cMaxReuseTimes
|
||||
@@ -131,10 +114,10 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: root.cMaxReuseTimesMin
|
||||
maxValue: root.cMaxReuseTimesMax
|
||||
onMinChanged: root.cMaxReuseTimesMin = val
|
||||
onMaxChanged: root.cMaxReuseTimesMax = val
|
||||
minValue: xmuxCMaxReuseTimesMin
|
||||
maxValue: xmuxCMaxReuseTimesMax
|
||||
onMinChanged: xmuxCMaxReuseTimesMin = val
|
||||
onMaxChanged: xmuxCMaxReuseTimesMax = val
|
||||
}
|
||||
|
||||
// hMaxRequestTimes
|
||||
@@ -151,10 +134,10 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: root.hMaxRequestTimesMin
|
||||
maxValue: root.hMaxRequestTimesMax
|
||||
onMinChanged: root.hMaxRequestTimesMin = val
|
||||
onMaxChanged: root.hMaxRequestTimesMax = val
|
||||
minValue: xmuxHMaxRequestTimesMin
|
||||
maxValue: xmuxHMaxRequestTimesMax
|
||||
onMinChanged: xmuxHMaxRequestTimesMin = val
|
||||
onMaxChanged: xmuxHMaxRequestTimesMax = val
|
||||
}
|
||||
|
||||
// hMaxReusableSecs
|
||||
@@ -171,24 +154,25 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: root.hMaxReusableSecsMin
|
||||
maxValue: root.hMaxReusableSecsMax
|
||||
onMinChanged: root.hMaxReusableSecsMin = val
|
||||
onMaxChanged: root.hMaxReusableSecsMax = val
|
||||
minValue: xmuxHMaxReusableSecsMin
|
||||
maxValue: xmuxHMaxReusableSecsMax
|
||||
onMinChanged: xmuxHMaxReusableSecsMin = val
|
||||
onMaxChanged: xmuxHMaxReusableSecsMax = val
|
||||
}
|
||||
|
||||
// hKeepAlivePeriod — single field
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
headerText: qsTr("hKeepAlivePeriod")
|
||||
textField.text: root.hKeepAlivePeriod
|
||||
textField.text: xmuxHKeepAlivePeriod
|
||||
textField.validator: IntValidator {
|
||||
bottom: 0
|
||||
}
|
||||
textField.onEditingFinished: root.hKeepAlivePeriod = textField.text
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xmuxHKeepAlivePeriod) xmuxHKeepAlivePeriod = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +182,6 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Save button ───────────────────────────────────────────────────
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
anchors.bottom: parent.bottom
|
||||
@@ -207,11 +190,10 @@ PageType {
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
text: qsTr("Save")
|
||||
onClicked: {
|
||||
forceActiveFocus()
|
||||
// XrayConfigModel.setXmux(...)
|
||||
PageController.closePage()
|
||||
}
|
||||
Keys.onEnterPressed: clicked()
|
||||
Keys.onReturnPressed: clicked()
|
||||
|
||||
Reference in New Issue
Block a user