add new qml padding, update model

This commit is contained in:
dranik
2026-03-26 09:37:16 +03:00
parent b91ea95303
commit 57c6f57a12
22 changed files with 1314 additions and 498 deletions

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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");

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -88,6 +88,7 @@ namespace PageLoader
PageProtocolXrayXPaddingSettings,
PageProtocolXrayFlowSettings,
PageProtocolXraySecuritySettings,
PageProtocolXrayXPaddingBytesSettings,
};
Q_ENUM_NS(PageEnum)

View 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;

View File

@@ -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();
}

View File

@@ -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;

View 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();
}

View 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

View File

@@ -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 }
}
}
}

View File

@@ -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()

View File

@@ -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()

View File

@@ -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
}
}
}

View File

@@ -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()

View File

@@ -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()
}
}

View File

@@ -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()

View File

@@ -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()