mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
feat: add extended vless configuration (#2566)
* update UI XRay, add new page PageProtocolXrayTransportSettings.qml PageProtocolXrayXmuxSettings.qml PageProtocolXrayXPaddingSettings.qml * add UI PageProtocolXrayConfigsSettings, PageProtocolXrayFlowSettings, PageProtocolXraySecuritySettings * add Xray-specific keys * add vars xray model * add new qml padding, update model * update model and export * rename file & update name class & update list xray * fixed ui * add save file in temp * remove debug macros * fixed build windows * fix path Windows * remove save config * fixed changes * fixed conf * fixed UI * fixed size & button save * fixed build iOS * fix: fixed headers base control --------- Co-authored-by: vkamn <vk@amnezia.org>
This commit is contained in:
@@ -201,3 +201,12 @@ bool ImportUiController::decodeQrCode(const QString &code)
|
||||
return mInstance->parseQrCodeChunk(code);
|
||||
}
|
||||
#endif
|
||||
|
||||
QString ImportUiController::readTextFile(const QString &fileName)
|
||||
{
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return {};
|
||||
}
|
||||
return QString::fromUtf8(file.readAll());
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public slots:
|
||||
QString getMaliciousWarningText();
|
||||
bool isNativeWireGuardConfig();
|
||||
void processNativeWireGuardConfig();
|
||||
QString readTextFile(const QString &fileName);
|
||||
|
||||
#if defined Q_OS_ANDROID || defined Q_OS_IOS
|
||||
void startDecodingQr();
|
||||
|
||||
@@ -82,7 +82,15 @@ namespace PageLoader
|
||||
PageSetupWizardApiPremiumInfo,
|
||||
PageSetupWizardApiTrialEmail,
|
||||
|
||||
PageDevMenu
|
||||
PageDevMenu,
|
||||
|
||||
PageProtocolXraySnapshots,
|
||||
PageProtocolXrayTransportSettings,
|
||||
PageProtocolXrayXmuxSettings,
|
||||
PageProtocolXrayXPaddingSettings,
|
||||
PageProtocolXrayFlowSettings,
|
||||
PageProtocolXraySecuritySettings,
|
||||
PageProtocolXrayXPaddingBytesSettings,
|
||||
};
|
||||
Q_ENUM_NS(PageEnum)
|
||||
|
||||
|
||||
@@ -127,3 +127,13 @@ void ExportUiController::applyExportResult(const ExportController::ExportResult
|
||||
|
||||
emit exportConfigChanged();
|
||||
}
|
||||
|
||||
void ExportUiController::setConfigFromString(const QString &config, const QString &fileName)
|
||||
{
|
||||
clearPreviousConfig();
|
||||
m_config = config;
|
||||
emit exportConfigChanged();
|
||||
if (!fileName.isEmpty()) {
|
||||
SystemController::saveFile(fileName, m_config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ public slots:
|
||||
QList<QString> getQrCodes();
|
||||
|
||||
void exportConfig(const QString &fileName);
|
||||
void setConfigFromString(const QString &config, const QString &fileName);
|
||||
|
||||
void updateClientManagementModel(const QString &serverId, int containerIndex);
|
||||
|
||||
|
||||
Executable → Regular
+2
-1
@@ -592,7 +592,8 @@ void InstallUiController::updateProtocolConfigModel(const QString &serverId, int
|
||||
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;
|
||||
|
||||
@@ -8,94 +8,575 @@
|
||||
using namespace amnezia;
|
||||
using namespace ProtocolUtils;
|
||||
|
||||
XrayConfigModel::XrayConfigModel(QObject *parent) : QAbstractListModel(parent)
|
||||
XrayConfigModel::XrayConfigModel(QObject* parent) : QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
int XrayConfigModel::rowCount(const QModelIndex &parent) const
|
||||
int XrayConfigModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool XrayConfigModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
bool XrayConfigModel::setData(const QModelIndex& index, const QVariant& value, int role)
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= ContainerUtils::allContainers().size()) {
|
||||
// This model always has a single row (row 0). Using rowCount() avoids
|
||||
// coupling editing ability to global container list size.
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QString strValue = value.toString();
|
||||
const bool wasUnsavedChanges = hasUnsavedChanges();
|
||||
|
||||
auto& srv = m_protocolConfig.serverConfig;
|
||||
auto& xhttp = srv.xhttp;
|
||||
auto& mkcp = srv.mkcp;
|
||||
auto& pad = xhttp.xPadding;
|
||||
auto& mux = xhttp.xmux;
|
||||
|
||||
QString str = value.toString();
|
||||
|
||||
switch (role)
|
||||
{
|
||||
// ── Main ──────────────────────────────────────────────────────────
|
||||
case Roles::SiteRole: srv.site = str;
|
||||
break;
|
||||
case Roles::PortRole: srv.port = str;
|
||||
break;
|
||||
case Roles::TransportRole: srv.transport = str;
|
||||
break;
|
||||
case Roles::SecurityRole: srv.security = str;
|
||||
break;
|
||||
case Roles::FlowRole: srv.flow = str;
|
||||
break;
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────
|
||||
case Roles::FingerprintRole: srv.fingerprint = str;
|
||||
break;
|
||||
case Roles::SniRole: srv.sni = str;
|
||||
break;
|
||||
case Roles::AlpnRole: srv.alpn = str;
|
||||
break;
|
||||
|
||||
// ── XHTTP ─────────────────────────────────────────────────────────
|
||||
case Roles::XhttpModeRole: xhttp.mode = str;
|
||||
break;
|
||||
case Roles::XhttpHostRole: xhttp.host = str;
|
||||
break;
|
||||
case Roles::XhttpPathRole: xhttp.path = str;
|
||||
break;
|
||||
case Roles::XhttpHeadersTemplateRole: xhttp.headersTemplate = str;
|
||||
break;
|
||||
case Roles::XhttpUplinkMethodRole: xhttp.uplinkMethod = str;
|
||||
break;
|
||||
case Roles::XhttpDisableGrpcRole: xhttp.disableGrpc = value.toBool();
|
||||
break;
|
||||
case Roles::XhttpDisableSseRole: xhttp.disableSse = value.toBool();
|
||||
break;
|
||||
|
||||
case Roles::XhttpSessionPlacementRole: xhttp.sessionPlacement = str;
|
||||
break;
|
||||
case Roles::XhttpSessionKeyRole: xhttp.sessionKey = str;
|
||||
break;
|
||||
case Roles::XhttpSeqPlacementRole: xhttp.seqPlacement = str;
|
||||
break;
|
||||
case Roles::XhttpSeqKeyRole: xhttp.seqKey = str;
|
||||
break;
|
||||
case Roles::XhttpUplinkDataPlacementRole: xhttp.uplinkDataPlacement = str;
|
||||
break;
|
||||
case Roles::XhttpUplinkDataKeyRole: xhttp.uplinkDataKey = str;
|
||||
break;
|
||||
|
||||
case Roles::XhttpUplinkChunkSizeRole: xhttp.uplinkChunkSize = str;
|
||||
break;
|
||||
case Roles::XhttpScMaxBufferedPostsRole: xhttp.scMaxBufferedPosts = str;
|
||||
break;
|
||||
case Roles::XhttpScMaxEachPostBytesMinRole: xhttp.scMaxEachPostBytesMin = str;
|
||||
break;
|
||||
case Roles::XhttpScMaxEachPostBytesMaxRole: xhttp.scMaxEachPostBytesMax = str;
|
||||
break;
|
||||
case Roles::XhttpScMinPostsIntervalMsMinRole: xhttp.scMinPostsIntervalMsMin = str;
|
||||
break;
|
||||
case Roles::XhttpScMinPostsIntervalMsMaxRole: xhttp.scMinPostsIntervalMsMax = str;
|
||||
break;
|
||||
case Roles::XhttpScStreamUpServerSecsMinRole: xhttp.scStreamUpServerSecsMin = str;
|
||||
break;
|
||||
case Roles::XhttpScStreamUpServerSecsMaxRole: xhttp.scStreamUpServerSecsMax = str;
|
||||
break;
|
||||
|
||||
// ── mKCP ──────────────────────────────────────────────────────────
|
||||
case Roles::MkcpTtiRole: mkcp.tti = str;
|
||||
break;
|
||||
case Roles::MkcpUplinkCapacityRole: mkcp.uplinkCapacity = str;
|
||||
break;
|
||||
case Roles::MkcpDownlinkCapacityRole: mkcp.downlinkCapacity = str;
|
||||
break;
|
||||
case Roles::MkcpReadBufferSizeRole: mkcp.readBufferSize = str;
|
||||
break;
|
||||
case Roles::MkcpWriteBufferSizeRole: mkcp.writeBufferSize = str;
|
||||
break;
|
||||
case Roles::MkcpCongestionRole: mkcp.congestion = value.toBool();
|
||||
break;
|
||||
|
||||
// ── xPadding ──────────────────────────────────────────────────────
|
||||
case Roles::XPaddingBytesMinRole: pad.bytesMin = str;
|
||||
break;
|
||||
case Roles::XPaddingBytesMaxRole: pad.bytesMax = str;
|
||||
break;
|
||||
case Roles::XPaddingObfsModeRole: pad.obfsMode = value.toBool();
|
||||
break;
|
||||
case Roles::XPaddingKeyRole: pad.key = str;
|
||||
break;
|
||||
case Roles::XPaddingHeaderRole: pad.header = str;
|
||||
break;
|
||||
case Roles::XPaddingPlacementRole: pad.placement = str;
|
||||
break;
|
||||
case Roles::XPaddingMethodRole: pad.method = str;
|
||||
break;
|
||||
|
||||
// ── xmux ──────────────────────────────────────────────────────────
|
||||
case Roles::XmuxEnabledRole: mux.enabled = value.toBool();
|
||||
break;
|
||||
case Roles::XmuxMaxConcurrencyMinRole: mux.maxConcurrencyMin = str;
|
||||
break;
|
||||
case Roles::XmuxMaxConcurrencyMaxRole: mux.maxConcurrencyMax = str;
|
||||
break;
|
||||
case Roles::XmuxMaxConnectionsMinRole: mux.maxConnectionsMin = str;
|
||||
break;
|
||||
case Roles::XmuxMaxConnectionsMaxRole: mux.maxConnectionsMax = str;
|
||||
break;
|
||||
case Roles::XmuxCMaxReuseTimesMinRole: mux.cMaxReuseTimesMin = str;
|
||||
break;
|
||||
case Roles::XmuxCMaxReuseTimesMaxRole: mux.cMaxReuseTimesMax = str;
|
||||
break;
|
||||
case Roles::XmuxHMaxRequestTimesMinRole: mux.hMaxRequestTimesMin = str;
|
||||
break;
|
||||
case Roles::XmuxHMaxRequestTimesMaxRole: mux.hMaxRequestTimesMax = str;
|
||||
break;
|
||||
case Roles::XmuxHMaxReusableSecsMinRole: mux.hMaxReusableSecsMin = str;
|
||||
break;
|
||||
case Roles::XmuxHMaxReusableSecsMaxRole: mux.hMaxReusableSecsMax = str;
|
||||
break;
|
||||
case Roles::XmuxHKeepAlivePeriodRole: mux.hKeepAlivePeriod = str;
|
||||
break;
|
||||
|
||||
switch (role) {
|
||||
case Roles::SiteRole: m_protocolConfig.serverConfig.site = strValue; break;
|
||||
case Roles::PortRole: m_protocolConfig.serverConfig.port = strValue; break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
emit dataChanged(index, index, QList { role });
|
||||
emit dataChanged(index, index, QList{role});
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QVariant XrayConfigModel::data(const QModelIndex &index, int role) const
|
||||
QVariant XrayConfigModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case Roles::SiteRole: return m_protocolConfig.serverConfig.site;
|
||||
case Roles::PortRole: return m_protocolConfig.serverConfig.port;
|
||||
const auto& srv = m_protocolConfig.serverConfig;
|
||||
const auto& xhttp = srv.xhttp;
|
||||
const auto& mkcp = srv.mkcp;
|
||||
const auto& pad = xhttp.xPadding;
|
||||
const auto& mux = xhttp.xmux;
|
||||
|
||||
switch (role)
|
||||
{
|
||||
// ── Main ──────────────────────────────────────────────────────────
|
||||
case Roles::SiteRole: return srv.site;
|
||||
case Roles::PortRole: return srv.port;
|
||||
case Roles::TransportRole: return srv.transport;
|
||||
case Roles::SecurityRole: return srv.security;
|
||||
case Roles::FlowRole: return srv.flow;
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────
|
||||
case Roles::FingerprintRole: return srv.fingerprint;
|
||||
case Roles::SniRole: return srv.sni;
|
||||
case Roles::AlpnRole: return srv.alpn;
|
||||
|
||||
// ── XHTTP ─────────────────────────────────────────────────────────
|
||||
case Roles::XhttpModeRole: return xhttp.mode;
|
||||
case Roles::XhttpHostRole: return xhttp.host;
|
||||
case Roles::XhttpPathRole: return xhttp.path;
|
||||
case Roles::XhttpHeadersTemplateRole: return xhttp.headersTemplate;
|
||||
case Roles::XhttpUplinkMethodRole: return xhttp.uplinkMethod;
|
||||
case Roles::XhttpDisableGrpcRole: return xhttp.disableGrpc;
|
||||
case Roles::XhttpDisableSseRole: return xhttp.disableSse;
|
||||
|
||||
case Roles::XhttpSessionPlacementRole: return xhttp.sessionPlacement;
|
||||
case Roles::XhttpSessionKeyRole: return xhttp.sessionKey;
|
||||
case Roles::XhttpSeqPlacementRole: return xhttp.seqPlacement;
|
||||
case Roles::XhttpSeqKeyRole: return xhttp.seqKey;
|
||||
case Roles::XhttpUplinkDataPlacementRole: return xhttp.uplinkDataPlacement;
|
||||
case Roles::XhttpUplinkDataKeyRole: return xhttp.uplinkDataKey;
|
||||
|
||||
case Roles::XhttpUplinkChunkSizeRole: return xhttp.uplinkChunkSize;
|
||||
case Roles::XhttpScMaxBufferedPostsRole: return xhttp.scMaxBufferedPosts;
|
||||
case Roles::XhttpScMaxEachPostBytesMinRole: return xhttp.scMaxEachPostBytesMin;
|
||||
case Roles::XhttpScMaxEachPostBytesMaxRole: return xhttp.scMaxEachPostBytesMax;
|
||||
case Roles::XhttpScMinPostsIntervalMsMinRole: return xhttp.scMinPostsIntervalMsMin;
|
||||
case Roles::XhttpScMinPostsIntervalMsMaxRole: return xhttp.scMinPostsIntervalMsMax;
|
||||
case Roles::XhttpScStreamUpServerSecsMinRole: return xhttp.scStreamUpServerSecsMin;
|
||||
case Roles::XhttpScStreamUpServerSecsMaxRole: return xhttp.scStreamUpServerSecsMax;
|
||||
|
||||
// ── mKCP ──────────────────────────────────────────────────────────
|
||||
case Roles::MkcpTtiRole: return mkcp.tti;
|
||||
case Roles::MkcpUplinkCapacityRole: return mkcp.uplinkCapacity;
|
||||
case Roles::MkcpDownlinkCapacityRole: return mkcp.downlinkCapacity;
|
||||
case Roles::MkcpReadBufferSizeRole: return mkcp.readBufferSize;
|
||||
case Roles::MkcpWriteBufferSizeRole: return mkcp.writeBufferSize;
|
||||
case Roles::MkcpCongestionRole: return mkcp.congestion;
|
||||
|
||||
// ── xPadding ──────────────────────────────────────────────────────
|
||||
case Roles::XPaddingBytesMinRole: return pad.bytesMin;
|
||||
case Roles::XPaddingBytesMaxRole: return pad.bytesMax;
|
||||
case Roles::XPaddingObfsModeRole: return pad.obfsMode;
|
||||
case Roles::XPaddingKeyRole: return pad.key;
|
||||
case Roles::XPaddingHeaderRole: return pad.header;
|
||||
case Roles::XPaddingPlacementRole: return pad.placement;
|
||||
case Roles::XPaddingMethodRole: return pad.method;
|
||||
|
||||
// ── xmux ──────────────────────────────────────────────────────────
|
||||
case Roles::XmuxEnabledRole: return mux.enabled;
|
||||
case Roles::XmuxMaxConcurrencyMinRole: return mux.maxConcurrencyMin;
|
||||
case Roles::XmuxMaxConcurrencyMaxRole: return mux.maxConcurrencyMax;
|
||||
case Roles::XmuxMaxConnectionsMinRole: return mux.maxConnectionsMin;
|
||||
case Roles::XmuxMaxConnectionsMaxRole: return mux.maxConnectionsMax;
|
||||
case Roles::XmuxCMaxReuseTimesMinRole: return mux.cMaxReuseTimesMin;
|
||||
case Roles::XmuxCMaxReuseTimesMaxRole: return mux.cMaxReuseTimesMax;
|
||||
case Roles::XmuxHMaxRequestTimesMinRole: return mux.hMaxRequestTimesMin;
|
||||
case Roles::XmuxHMaxRequestTimesMaxRole: return mux.hMaxRequestTimesMax;
|
||||
case Roles::XmuxHMaxReusableSecsMinRole: return mux.hMaxReusableSecsMin;
|
||||
case Roles::XmuxHMaxReusableSecsMaxRole: return mux.hMaxReusableSecsMax;
|
||||
case Roles::XmuxHKeepAlivePeriodRole: return mux.hKeepAlivePeriod;
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig &protocolConfig)
|
||||
void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig)
|
||||
{
|
||||
const bool wasUnsavedChanges = hasUnsavedChanges();
|
||||
|
||||
beginResetModel();
|
||||
|
||||
m_container = container;
|
||||
|
||||
|
||||
m_protocolConfig = protocolConfig;
|
||||
|
||||
|
||||
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
|
||||
|
||||
|
||||
m_originalProtocolConfig = m_protocolConfig;
|
||||
|
||||
|
||||
endResetModel();
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig& config)
|
||||
void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &config)
|
||||
{
|
||||
if (config.port.isEmpty()) {
|
||||
config.port = protocols::xray::defaultPort;
|
||||
}
|
||||
|
||||
if (config.transportProto.isEmpty()) {
|
||||
config.transportProto = ProtocolUtils::transportProtoToString(
|
||||
ProtocolUtils::defaultTransportProto(amnezia::Proto::Xray), amnezia::Proto::Xray);
|
||||
}
|
||||
|
||||
if (config.site.isEmpty()) {
|
||||
config.site = protocols::xray::defaultSite;
|
||||
}
|
||||
|
||||
if (config.transport.isEmpty()) {
|
||||
config.transport = protocols::xray::defaultTransport;
|
||||
}
|
||||
|
||||
if (config.security.isEmpty()) {
|
||||
config.security = protocols::xray::defaultSecurity;
|
||||
}
|
||||
|
||||
if (config.flow.isEmpty()) {
|
||||
config.flow = protocols::xray::defaultFlow;
|
||||
}
|
||||
|
||||
if (config.fingerprint.isEmpty()) {
|
||||
config.fingerprint = protocols::xray::defaultFingerprint;
|
||||
} else if (config.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
|
||||
config.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint);
|
||||
}
|
||||
|
||||
if (config.sni.isEmpty()) {
|
||||
config.sni = protocols::xray::defaultSni;
|
||||
}
|
||||
|
||||
if (config.alpn.isEmpty()) {
|
||||
config.alpn = protocols::xray::defaultAlpn;
|
||||
}
|
||||
|
||||
// XHTTP transport defaults
|
||||
if (config.xhttp.host.isEmpty()) {
|
||||
config.xhttp.host = protocols::xray::defaultXhttpHost;
|
||||
}
|
||||
if (config.xhttp.mode.isEmpty()) {
|
||||
config.xhttp.mode = protocols::xray::defaultXhttpMode;
|
||||
}
|
||||
if (config.xhttp.headersTemplate.isEmpty()) {
|
||||
config.xhttp.headersTemplate = protocols::xray::defaultXhttpHeadersTemplate;
|
||||
}
|
||||
if (config.xhttp.uplinkMethod.isEmpty()) {
|
||||
config.xhttp.uplinkMethod = protocols::xray::defaultXhttpUplinkMethod;
|
||||
}
|
||||
if (config.xhttp.sessionPlacement.isEmpty()) {
|
||||
config.xhttp.sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
|
||||
}
|
||||
if (config.xhttp.sessionKey.isEmpty()) {
|
||||
config.xhttp.sessionKey = protocols::xray::defaultXhttpSessionKey;
|
||||
}
|
||||
if (config.xhttp.seqPlacement.isEmpty()) {
|
||||
config.xhttp.seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
|
||||
}
|
||||
if (config.xhttp.uplinkDataPlacement.isEmpty()) {
|
||||
config.xhttp.uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
|
||||
}
|
||||
|
||||
// xPadding defaults
|
||||
if (config.xhttp.xPadding.placement.isEmpty()) {
|
||||
config.xhttp.xPadding.placement = protocols::xray::defaultXPaddingPlacement;
|
||||
}
|
||||
if (config.xhttp.xPadding.method.isEmpty()) {
|
||||
config.xhttp.xPadding.method = protocols::xray::defaultXPaddingMethod;
|
||||
}
|
||||
}
|
||||
|
||||
amnezia::XrayProtocolConfig XrayConfigModel::getProtocolConfig()
|
||||
{
|
||||
bool serverSettingsChanged = !m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
|
||||
|
||||
const bool serverSettingsChanged =
|
||||
!m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
|
||||
|
||||
if (serverSettingsChanged) {
|
||||
m_protocolConfig.clearClientConfig();
|
||||
}
|
||||
|
||||
return m_protocolConfig;
|
||||
}
|
||||
|
||||
bool XrayConfigModel::isServerSettingsEqual() const
|
||||
{
|
||||
return m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig);
|
||||
}
|
||||
|
||||
bool XrayConfigModel::hasUnsavedChanges() const
|
||||
{
|
||||
return !isServerSettingsEqual();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> XrayConfigModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
|
||||
// Main
|
||||
roles[SiteRole] = "site";
|
||||
roles[PortRole] = "port";
|
||||
roles[TransportRole] = "transport";
|
||||
roles[SecurityRole] = "security";
|
||||
roles[FlowRole] = "flow";
|
||||
|
||||
// Security
|
||||
roles[FingerprintRole] = "fingerprint";
|
||||
roles[SniRole] = "sni";
|
||||
roles[AlpnRole] = "alpn";
|
||||
|
||||
// XHTTP
|
||||
roles[XhttpModeRole] = "xhttpMode";
|
||||
roles[XhttpHostRole] = "xhttpHost";
|
||||
roles[XhttpPathRole] = "xhttpPath";
|
||||
roles[XhttpHeadersTemplateRole] = "xhttpHeadersTemplate";
|
||||
roles[XhttpUplinkMethodRole] = "xhttpUplinkMethod";
|
||||
roles[XhttpDisableGrpcRole] = "xhttpDisableGrpc";
|
||||
roles[XhttpDisableSseRole] = "xhttpDisableSse";
|
||||
|
||||
roles[XhttpSessionPlacementRole] = "xhttpSessionPlacement";
|
||||
roles[XhttpSessionKeyRole] = "xhttpSessionKey";
|
||||
roles[XhttpSeqPlacementRole] = "xhttpSeqPlacement";
|
||||
roles[XhttpSeqKeyRole] = "xhttpSeqKey";
|
||||
roles[XhttpUplinkDataPlacementRole] = "xhttpUplinkDataPlacement";
|
||||
roles[XhttpUplinkDataKeyRole] = "xhttpUplinkDataKey";
|
||||
|
||||
roles[XhttpUplinkChunkSizeRole] = "xhttpUplinkChunkSize";
|
||||
roles[XhttpScMaxBufferedPostsRole] = "xhttpScMaxBufferedPosts";
|
||||
roles[XhttpScMaxEachPostBytesMinRole] = "xhttpScMaxEachPostBytesMin";
|
||||
roles[XhttpScMaxEachPostBytesMaxRole] = "xhttpScMaxEachPostBytesMax";
|
||||
roles[XhttpScMinPostsIntervalMsMinRole] = "xhttpScMinPostsIntervalMsMin";
|
||||
roles[XhttpScMinPostsIntervalMsMaxRole] = "xhttpScMinPostsIntervalMsMax";
|
||||
roles[XhttpScStreamUpServerSecsMinRole] = "xhttpScStreamUpServerSecsMin";
|
||||
roles[XhttpScStreamUpServerSecsMaxRole] = "xhttpScStreamUpServerSecsMax";
|
||||
|
||||
// mKCP
|
||||
roles[MkcpTtiRole] = "mkcpTti";
|
||||
roles[MkcpUplinkCapacityRole] = "mkcpUplinkCapacity";
|
||||
roles[MkcpDownlinkCapacityRole] = "mkcpDownlinkCapacity";
|
||||
roles[MkcpReadBufferSizeRole] = "mkcpReadBufferSize";
|
||||
roles[MkcpWriteBufferSizeRole] = "mkcpWriteBufferSize";
|
||||
roles[MkcpCongestionRole] = "mkcpCongestion";
|
||||
|
||||
// xPadding
|
||||
roles[XPaddingBytesMinRole] = "xPaddingBytesMin";
|
||||
roles[XPaddingBytesMaxRole] = "xPaddingBytesMax";
|
||||
roles[XPaddingObfsModeRole] = "xPaddingObfsMode";
|
||||
roles[XPaddingKeyRole] = "xPaddingKey";
|
||||
roles[XPaddingHeaderRole] = "xPaddingHeader";
|
||||
roles[XPaddingPlacementRole] = "xPaddingPlacement";
|
||||
roles[XPaddingMethodRole] = "xPaddingMethod";
|
||||
|
||||
// xmux
|
||||
roles[XmuxEnabledRole] = "xmuxEnabled";
|
||||
roles[XmuxMaxConcurrencyMinRole] = "xmuxMaxConcurrencyMin";
|
||||
roles[XmuxMaxConcurrencyMaxRole] = "xmuxMaxConcurrencyMax";
|
||||
roles[XmuxMaxConnectionsMinRole] = "xmuxMaxConnectionsMin";
|
||||
roles[XmuxMaxConnectionsMaxRole] = "xmuxMaxConnectionsMax";
|
||||
roles[XmuxCMaxReuseTimesMinRole] = "xmuxCMaxReuseTimesMin";
|
||||
roles[XmuxCMaxReuseTimesMaxRole] = "xmuxCMaxReuseTimesMax";
|
||||
roles[XmuxHMaxRequestTimesMinRole] = "xmuxHMaxRequestTimesMin";
|
||||
roles[XmuxHMaxRequestTimesMaxRole] = "xmuxHMaxRequestTimesMax";
|
||||
roles[XmuxHMaxReusableSecsMinRole] = "xmuxHMaxReusableSecsMin";
|
||||
roles[XmuxHMaxReusableSecsMaxRole] = "xmuxHMaxReusableSecsMax";
|
||||
roles[XmuxHKeepAlivePeriodRole] = "xmuxHKeepAlivePeriod";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
void XrayConfigModel::resetToDefaults()
|
||||
{
|
||||
const bool wasUnsavedChanges = hasUnsavedChanges();
|
||||
|
||||
beginResetModel();
|
||||
m_protocolConfig.serverConfig = amnezia::XrayServerConfig{};
|
||||
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
|
||||
endResetModel();
|
||||
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void XrayConfigModel::applyServerConfig(const amnezia::XrayServerConfig &serverConfig)
|
||||
{
|
||||
const bool wasUnsavedChanges = hasUnsavedChanges();
|
||||
|
||||
beginResetModel();
|
||||
m_protocolConfig.serverConfig = serverConfig;
|
||||
// Clear client config since server settings changed
|
||||
m_protocolConfig.clearClientConfig();
|
||||
m_originalProtocolConfig = m_protocolConfig;
|
||||
endResetModel();
|
||||
|
||||
if (wasUnsavedChanges != hasUnsavedChanges()) {
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::flowOptions()
|
||||
{
|
||||
return {
|
||||
"", // Empty (no flow)
|
||||
"xtls-rprx-vision",
|
||||
"xtls-rprx-vision-udp443"
|
||||
};
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::securityOptions()
|
||||
{
|
||||
return { "none", "tls", "reality" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::transportOptions()
|
||||
{
|
||||
return { "raw", "xhttp", "mkcp" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::fingerprintOptions()
|
||||
{
|
||||
return { "chrome", "firefox", "safari", "ios", "android", "edge", "360", "qq", "random" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::alpnOptions()
|
||||
{
|
||||
return { "HTTP/2", "HTTP/1.1", "HTTP/2,HTTP/1.1" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpModeOptions()
|
||||
{
|
||||
return { "Auto", "Packet-up", "Stream-up", "Stream-one" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpHeadersTemplateOptions()
|
||||
{
|
||||
return { "HTTP", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpUplinkMethodOptions()
|
||||
{
|
||||
return { "POST", "PUT", "PATCH" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpSessionPlacementOptions()
|
||||
{
|
||||
return { "Path", "Header", "Cookie", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpSessionKeyOptions()
|
||||
{
|
||||
return { "Path", "Header", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpSeqPlacementOptions()
|
||||
{
|
||||
return { "Path", "Header", "Cookie", "None" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xhttpUplinkDataPlacementOptions()
|
||||
{
|
||||
// Matches splithttp uplink payload placement (packet-up / advanced)
|
||||
return { "Body", "Auto", "Header", "Cookie" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xPaddingPlacementOptions()
|
||||
{
|
||||
// Xray-core: cookie | header | query | queryInHeader (not "body")
|
||||
return { "Cookie", "Header", "Query", "Query in header" };
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::xPaddingMethodOptions()
|
||||
{
|
||||
return { "Repeat-x", "Tokenish" };
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultTti()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpTti);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultUplinkCapacity()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpUplinkCapacity);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultDownlinkCapacity()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpDownlinkCapacity);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultReadBufferSize()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpReadBufferSize);
|
||||
}
|
||||
|
||||
QString XrayConfigModel::mkcpDefaultWriteBufferSize()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpWriteBufferSize);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define XRAYCONFIGMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QStringList>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -11,23 +12,122 @@
|
||||
class XrayConfigModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool hasUnsavedChanges READ hasUnsavedChanges NOTIFY hasUnsavedChangesChanged)
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
SiteRole,
|
||||
PortRole
|
||||
enum Roles
|
||||
{
|
||||
// ── Main page ─────────────────────────────────────────────────
|
||||
SiteRole = Qt::UserRole + 1,
|
||||
PortRole,
|
||||
TransportRole, // "raw" | "xhttp" | "mkcp" (display in main page row)
|
||||
SecurityRole, // "none" | "tls" | "reality" (display in main page row)
|
||||
FlowRole, // "" | "xtls-rprx-vision" | "xtls-rprx-vision-udp443"
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────
|
||||
FingerprintRole,
|
||||
SniRole,
|
||||
AlpnRole,
|
||||
|
||||
// ── Transport — XHTTP ─────────────────────────────────────────
|
||||
XhttpModeRole,
|
||||
XhttpHostRole,
|
||||
XhttpPathRole,
|
||||
XhttpHeadersTemplateRole,
|
||||
XhttpUplinkMethodRole,
|
||||
XhttpDisableGrpcRole,
|
||||
XhttpDisableSseRole,
|
||||
|
||||
// Session & Sequence
|
||||
XhttpSessionPlacementRole,
|
||||
XhttpSessionKeyRole,
|
||||
XhttpSeqPlacementRole,
|
||||
XhttpSeqKeyRole,
|
||||
XhttpUplinkDataPlacementRole,
|
||||
XhttpUplinkDataKeyRole,
|
||||
|
||||
// Traffic Shaping
|
||||
XhttpUplinkChunkSizeRole,
|
||||
XhttpScMaxBufferedPostsRole,
|
||||
XhttpScMaxEachPostBytesMinRole,
|
||||
XhttpScMaxEachPostBytesMaxRole,
|
||||
XhttpScMinPostsIntervalMsMinRole,
|
||||
XhttpScMinPostsIntervalMsMaxRole,
|
||||
XhttpScStreamUpServerSecsMinRole,
|
||||
XhttpScStreamUpServerSecsMaxRole,
|
||||
|
||||
// ── Transport — mKCP ──────────────────────────────────────────
|
||||
MkcpTtiRole,
|
||||
MkcpUplinkCapacityRole,
|
||||
MkcpDownlinkCapacityRole,
|
||||
MkcpReadBufferSizeRole,
|
||||
MkcpWriteBufferSizeRole,
|
||||
MkcpCongestionRole,
|
||||
|
||||
// ── xPadding ──────────────────────────────────────────────────
|
||||
XPaddingBytesMinRole,
|
||||
XPaddingBytesMaxRole,
|
||||
XPaddingObfsModeRole,
|
||||
XPaddingKeyRole,
|
||||
XPaddingHeaderRole,
|
||||
XPaddingPlacementRole,
|
||||
XPaddingMethodRole,
|
||||
|
||||
// ── xmux ──────────────────────────────────────────────────────
|
||||
XmuxEnabledRole,
|
||||
XmuxMaxConcurrencyMinRole,
|
||||
XmuxMaxConcurrencyMaxRole,
|
||||
XmuxMaxConnectionsMinRole,
|
||||
XmuxMaxConnectionsMaxRole,
|
||||
XmuxCMaxReuseTimesMinRole,
|
||||
XmuxCMaxReuseTimesMaxRole,
|
||||
XmuxHMaxRequestTimesMinRole,
|
||||
XmuxHMaxRequestTimesMaxRole,
|
||||
XmuxHMaxReusableSecsMinRole,
|
||||
XmuxHMaxReusableSecsMaxRole,
|
||||
XmuxHKeepAlivePeriodRole,
|
||||
};
|
||||
|
||||
explicit XrayConfigModel(QObject *parent = nullptr);
|
||||
explicit XrayConfigModel(QObject* parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
bool setData(const QModelIndex& index, const QVariant& value, int role) override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
// ── Static option lists (for QML DropDown models) ─────────────────
|
||||
Q_INVOKABLE static QStringList flowOptions();
|
||||
Q_INVOKABLE static QStringList securityOptions();
|
||||
Q_INVOKABLE static QStringList transportOptions();
|
||||
Q_INVOKABLE static QStringList fingerprintOptions();
|
||||
Q_INVOKABLE static QStringList alpnOptions();
|
||||
Q_INVOKABLE static QStringList xhttpModeOptions();
|
||||
Q_INVOKABLE static QStringList xhttpHeadersTemplateOptions();
|
||||
Q_INVOKABLE static QStringList xhttpUplinkMethodOptions();
|
||||
Q_INVOKABLE static QStringList xhttpSessionPlacementOptions();
|
||||
Q_INVOKABLE static QStringList xhttpSessionKeyOptions();
|
||||
Q_INVOKABLE static QStringList xhttpSeqPlacementOptions();
|
||||
Q_INVOKABLE static QStringList xhttpUplinkDataPlacementOptions();
|
||||
Q_INVOKABLE static QStringList xPaddingPlacementOptions();
|
||||
Q_INVOKABLE static QStringList xPaddingMethodOptions();
|
||||
|
||||
// mKCP display defaults (protocolConstants.h — must match xrayConfigurator empty-field behavior)
|
||||
Q_INVOKABLE static QString mkcpDefaultTti();
|
||||
Q_INVOKABLE static QString mkcpDefaultUplinkCapacity();
|
||||
Q_INVOKABLE static QString mkcpDefaultDownlinkCapacity();
|
||||
Q_INVOKABLE static QString mkcpDefaultReadBufferSize();
|
||||
Q_INVOKABLE static QString mkcpDefaultWriteBufferSize();
|
||||
|
||||
public slots:
|
||||
void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig &protocolConfig);
|
||||
void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig);
|
||||
amnezia::XrayProtocolConfig getProtocolConfig();
|
||||
bool isServerSettingsEqual() const;
|
||||
bool hasUnsavedChanges() const;
|
||||
void resetToDefaults();
|
||||
void applyServerConfig(const amnezia::XrayServerConfig &serverConfig);
|
||||
|
||||
signals:
|
||||
void hasUnsavedChangesChanged();
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
@@ -36,7 +136,7 @@ private:
|
||||
amnezia::DockerContainer m_container;
|
||||
amnezia::XrayProtocolConfig m_protocolConfig;
|
||||
amnezia::XrayProtocolConfig m_originalProtocolConfig;
|
||||
|
||||
|
||||
void applyDefaultsToServerConfig(amnezia::XrayServerConfig& config);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
#include "xrayConfigSnapshotsModel.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;
|
||||
}
|
||||
|
||||
XrayConfigSnapshotsModel::XrayConfigSnapshotsModel(SecureAppSettingsRepository *appSettings,
|
||||
XrayConfigModel *xrayConfigModel, QObject *parent)
|
||||
: QAbstractListModel(parent), m_appSettings(appSettings), m_xrayConfigModel(xrayConfigModel)
|
||||
{
|
||||
loadAll();
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::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 XrayConfigSnapshotsModel::persistAll()
|
||||
{
|
||||
QJsonArray arr;
|
||||
for (const XrayConfigSnapshot &s : m_configs) {
|
||||
arr.append(s.toJson());
|
||||
}
|
||||
m_appSettings->setXraySavedConfigs(QJsonDocument(arr).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
int XrayConfigSnapshotsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return m_configs.size();
|
||||
}
|
||||
|
||||
QVariant XrayConfigSnapshotsModel::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> XrayConfigSnapshotsModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[IdRole] = "configId";
|
||||
roles[DisplayNameRole] = "configName";
|
||||
roles[CreatedAtRole] = "configDate";
|
||||
return roles;
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::reload()
|
||||
{
|
||||
beginResetModel();
|
||||
loadAll();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::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 XrayConfigSnapshotsModel::applyConfig(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_configs.size()) {
|
||||
return amnezia::XrayServerConfig {};
|
||||
}
|
||||
|
||||
return m_configs.at(index).serverConfig;
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::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 XrayConfigSnapshotsModel::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 XrayConfigSnapshotsModel::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 XrayConfigSnapshotsModel::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();
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::createFromCurrentModel()
|
||||
{
|
||||
if (!m_xrayConfigModel) {
|
||||
return;
|
||||
}
|
||||
createFromCurrent(m_xrayConfigModel->getProtocolConfig().serverConfig);
|
||||
}
|
||||
|
||||
void XrayConfigSnapshotsModel::applyConfigToCurrentModel(int index)
|
||||
{
|
||||
if (!m_xrayConfigModel) {
|
||||
return;
|
||||
}
|
||||
amnezia::XrayServerConfig cfg = applyConfig(index);
|
||||
if (cfg.port.isEmpty()) {
|
||||
return; // guard against invalid index
|
||||
}
|
||||
m_xrayConfigModel->applyServerConfig(cfg);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
#ifndef XRAYCONFIGSMODEL_H
|
||||
#define XRAYCONFIGSMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QDateTime>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include "core/models/protocols/xrayProtocolConfig.h"
|
||||
#include "ui/models/protocols/xrayConfigModel.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 XrayConfigSnapshotsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
IdRole = Qt::UserRole + 1,
|
||||
DisplayNameRole,
|
||||
CreatedAtRole, // "dd.MM.yyyy HH:mm"
|
||||
};
|
||||
|
||||
explicit XrayConfigSnapshotsModel(SecureAppSettingsRepository *appSettings, XrayConfigModel *xrayConfigModel,
|
||||
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);
|
||||
|
||||
// Convenience: create snapshot from live model, apply snapshot back to model
|
||||
Q_INVOKABLE void createFromCurrentModel();
|
||||
Q_INVOKABLE void applyConfigToCurrentModel(int index);
|
||||
|
||||
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;
|
||||
XrayConfigModel *m_xrayConfigModel;
|
||||
QVector<XrayConfigSnapshot> m_configs;
|
||||
|
||||
void persistAll();
|
||||
void loadAll();
|
||||
static QString buildDisplayName(const amnezia::XrayServerConfig &cfg);
|
||||
};
|
||||
|
||||
#endif // XRAYCONFIGSMODEL_H
|
||||
@@ -0,0 +1,61 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Style 1.0
|
||||
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
|
||||
// MinMaxRowType — two side-by-side labeled text fields: Min / Max
|
||||
// Usage:
|
||||
// MinMaxRowType {
|
||||
// minValue: "0"
|
||||
// maxValue: "0"
|
||||
// onMinChanged: someProperty = val
|
||||
// onMaxChanged: someProperty = val
|
||||
// }
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string minValue: "0"
|
||||
property string maxValue: "0"
|
||||
|
||||
signal minChanged(string val)
|
||||
signal maxChanged(string val)
|
||||
|
||||
implicitHeight: row.implicitHeight
|
||||
implicitWidth: row.implicitWidth
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
// Min field
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("Min")
|
||||
textField.text: root.minValue
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== root.minValue) {
|
||||
root.minChanged(textField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Max field
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("Max")
|
||||
textField.text: root.maxValue
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== root.maxValue) {
|
||||
root.maxChanged(textField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ Item {
|
||||
id: root
|
||||
|
||||
property string headerText
|
||||
property string subtitleText // optional line under header (e.g. default value hint)
|
||||
property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray
|
||||
property string headerTextColor: AmneziaStyle.color.mutedGray
|
||||
|
||||
@@ -84,6 +85,15 @@ Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SmallTextType {
|
||||
text: root.subtitleText
|
||||
visible: root.subtitleText !== ""
|
||||
color: AmneziaStyle.color.charcoalGray
|
||||
font.pixelSize: 13
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: visible ? 2 : 0
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: textField
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 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.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("Flow")
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("Empty")
|
||||
checked: flow === ""
|
||||
onClicked: flow = ""
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: "xtls-rprx-vision"
|
||||
checked: flow === "xtls-rprx-vision"
|
||||
onClicked: flow = "xtls-rprx-vision"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: "xtls-rprx-vision-udp443"
|
||||
checked: flow === "xtls-rprx-vision-udp443"
|
||||
onClicked: flow = "xtls-rprx-vision-udp443"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
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 (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 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.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("Security")
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("None")
|
||||
checked: security === "none"
|
||||
onClicked: security = "none"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("TLS")
|
||||
checked: security === "tls"
|
||||
onClicked: security = "tls"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("Reality")
|
||||
checked: security === "reality"
|
||||
onClicked: security = "reality"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── TLS fields ────────────────────────────────────────────
|
||||
ColumnLayout {
|
||||
visible: security === "tls"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
DropDownType {
|
||||
id: tlsAlpnDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: alpn
|
||||
descriptionText: qsTr("ALPN")
|
||||
headerText: qsTr("ALPN")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.alpnOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
alpn = selectedText
|
||||
tlsAlpnDropDown.text = selectedText
|
||||
tlsAlpnDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === alpn) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
tlsAlpnDropDown.text = alpn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: tlsFingerprintDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: fingerprint
|
||||
descriptionText: qsTr("Fingerprint")
|
||||
headerText: qsTr("Fingerprint")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.fingerprintOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
fingerprint = selectedText
|
||||
tlsFingerprintDropDown.text = selectedText
|
||||
tlsFingerprintDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === fingerprint) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
tlsFingerprintDropDown.text = fingerprint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Server Name (SNI)")
|
||||
textField.text: sni
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== sni) sni = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reality fields ────────────────────────────────────────
|
||||
ColumnLayout {
|
||||
visible: security === "reality"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
DropDownType {
|
||||
id: realityFingerprintDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: fingerprint
|
||||
descriptionText: qsTr("Fingerprint")
|
||||
headerText: qsTr("Fingerprint")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.fingerprintOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
fingerprint = selectedText
|
||||
realityFingerprintDropDown.text = selectedText
|
||||
realityFingerprintDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === fingerprint) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
realityFingerprintDropDown.text = fingerprint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Server Name (SNI)")
|
||||
textField.text: sni
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== sni) sni = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
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 (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,20 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
function formatTransport(value) {
|
||||
if (value === "raw") return "RAW (TCP)"
|
||||
if (value === "xhttp") return "XHTTP"
|
||||
if (value === "mkcp") return "mKCP"
|
||||
return value
|
||||
}
|
||||
|
||||
function formatSecurity(value) {
|
||||
if (value === "none") return "None"
|
||||
if (value === "tls") return "TLS"
|
||||
if (value === "reality") return "Reality"
|
||||
return value
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
@@ -50,88 +64,125 @@ PageType {
|
||||
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
headerText: qsTr("XRay settings")
|
||||
Layout.topMargin: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("XRay VLESS settings")
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignRight
|
||||
implicitWidth: 40
|
||||
implicitHeight: 40
|
||||
image: "qrc:/images/controls/more-vertical.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
onClicked: PageController.goToPage(PageEnum.PageProtocolXraySnapshots)
|
||||
}
|
||||
}
|
||||
|
||||
LabelTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 4
|
||||
text: qsTr("More about settings")
|
||||
color: AmneziaStyle.color.goldenApricot
|
||||
font.pixelSize: 16
|
||||
lineHeight: 24 + LanguageUiController.getLineHeightAppend()
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: Qt.openUrlExternally("https://docs.amnezia.org")
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: textFieldWithHeaderType
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 32
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
enabled: listView.enabled
|
||||
|
||||
headerText: qsTr("Disguised as traffic from")
|
||||
textField.text: site
|
||||
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== site) {
|
||||
var tmpText = textField.text
|
||||
tmpText = tmpText.toLocaleLowerCase()
|
||||
|
||||
if (tmpText.startsWith("https://")) {
|
||||
tmpText = textField.text.substring(8)
|
||||
site = tmpText
|
||||
} else {
|
||||
site = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: portTextField
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
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
|
||||
}
|
||||
textField.validator: IntValidator {
|
||||
bottom: 1; top: 65535
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== port) port = textField.text
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
text: qsTr("Transport")
|
||||
descriptionText: root.formatTransport(transport)
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
enabled: listView.enabled
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayTransportSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Security")
|
||||
descriptionText: root.formatSecurity(security)
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
enabled: listView.enabled
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageProtocolXraySecuritySettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Flow")
|
||||
descriptionText: flow
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
enabled: listView.enabled
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayFlowSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true; Layout.preferredHeight: 24
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
enabled: portTextField.errorText === ""
|
||||
|
||||
// Show Save immediately while user edits port, even before focus loss.
|
||||
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || textFieldWithHeaderType.textField.text !== port)
|
||||
enabled: visible && textFieldWithHeaderType.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"))
|
||||
@@ -142,16 +193,32 @@ PageType {
|
||||
InstallController.updateContainer(ServersUiController.getServerId(ServersUiController.processedServerIndex), 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()
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Reset settings")
|
||||
textColor: AmneziaStyle.color.vibrantRed
|
||||
visible: listView.enabled
|
||||
clickedFunction: function() {
|
||||
var yesButtonFunction = function() {
|
||||
XrayConfigModel.resetToDefaults()
|
||||
}
|
||||
showQuestionDrawer(qsTr("Reset settings?"), qsTr("All XRay settings will be restored to defaults."),
|
||||
qsTr("Reset"), qsTr("Cancel"), yesButtonFunction, function() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true; Layout.preferredHeight: 32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
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"
|
||||
import Qt.labs.platform 1.1
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property string selectedConfigName: ""
|
||||
property int selectedConfigIndex: -1
|
||||
|
||||
// Reload the list every time we open this page
|
||||
Component.onCompleted: XrayConfigSnapshotsModel.reload()
|
||||
|
||||
// ── Save xray config snapshot to file ────────────────────────────
|
||||
function saveConfigToFile(json) {
|
||||
var fileName = ""
|
||||
if (GC.isMobile()) {
|
||||
fileName = "amnezia_xray_config.json"
|
||||
} else {
|
||||
fileName = SystemController.getFileName(
|
||||
qsTr("Save XRay configuration"),
|
||||
qsTr("JSON files (*.json)"),
|
||||
StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/amnezia_xray_config",
|
||||
true,
|
||||
".json")
|
||||
}
|
||||
if (fileName !== "") {
|
||||
PageController.showBusyIndicator(true)
|
||||
ExportController.setConfigFromString(json, fileName)
|
||||
PageController.showBusyIndicator(false)
|
||||
PageController.showNotificationMessage(qsTr("Configuration saved"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
model: XrayConfigSnapshotsModel
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("XRay Configurations")
|
||||
}
|
||||
|
||||
// ── 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 () {
|
||||
XrayConfigSnapshotsModel.createFromCurrentModel()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Export ────────────────────────────────────────────────
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Export settings")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
var idx = root.selectedConfigIndex >= 0 ? root.selectedConfigIndex : 0
|
||||
if (listView.count > 0) {
|
||||
var json = XrayConfigSnapshotsModel.exportToJson(idx)
|
||||
saveConfigToFile(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Import ────────────────────────────────────────────────
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Import settings")
|
||||
descriptionText: qsTr("In JSON format")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
var filePath = SystemController.getFileName(
|
||||
qsTr("Open XRay configuration"),
|
||||
qsTr("JSON files (*.json)"))
|
||||
if (filePath !== "") {
|
||||
var jsonContent = ImportController.readTextFile(filePath)
|
||||
if (jsonContent !== "") {
|
||||
if (!XrayConfigSnapshotsModel.importFromJson(jsonContent)) {
|
||||
PageController.showNotificationMessage(qsTr("Failed to import configuration"))
|
||||
} else {
|
||||
PageController.showNotificationMessage(qsTr("Configuration imported successfully"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Section label ─────────────────────────────────────────
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Configurations")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
visible: listView.count > 0
|
||||
}
|
||||
}
|
||||
|
||||
// ── Empty state ───────────────────────────────────────────────
|
||||
footer: ColumnLayout {
|
||||
width: listView.width
|
||||
visible: listView.count === 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
|
||||
root.selectedConfigIndex = index
|
||||
configActionsDrawer.openTriggered()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Import result handler ─────────────────────────────────────────
|
||||
Connections {
|
||||
target: XrayConfigSnapshotsModel
|
||||
|
||||
function onImportFailed(errorMessage) {
|
||||
PageController.showNotificationMessage(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Per-config actions drawer ─────────────────────────────────────
|
||||
DrawerType2 {
|
||||
id: configActionsDrawer
|
||||
parent: root
|
||||
anchors.fill: parent
|
||||
expandedHeight: root.height * 0.35
|
||||
|
||||
expandedStateContent: ColumnLayout {
|
||||
id: drawerContent
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: 0
|
||||
|
||||
onImplicitHeightChanged: {
|
||||
configActionsDrawer.expandedHeight = drawerContent.implicitHeight + 32
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
backButtonFunction: function () {
|
||||
configActionsDrawer.closeTriggered()
|
||||
}
|
||||
}
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
Layout.bottomMargin: 16
|
||||
headerText: root.selectedConfigName
|
||||
}
|
||||
|
||||
// Apply
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Apply configuration")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
configActionsDrawer.closeTriggered()
|
||||
XrayConfigSnapshotsModel.applyConfigToCurrentModel(root.selectedConfigIndex)
|
||||
PageController.closePage()
|
||||
}
|
||||
}
|
||||
|
||||
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 = XrayConfigSnapshotsModel.exportToJson(root.selectedConfigIndex)
|
||||
saveConfigToFile(json)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// Delete
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Delete configuration")
|
||||
textColor: AmneziaStyle.color.vibrantRed
|
||||
clickedFunction: function () {
|
||||
configActionsDrawer.closeTriggered()
|
||||
var yesButtonFunction = function () {
|
||||
XrayConfigSnapshotsModel.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,755 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 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.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("Transport")
|
||||
}
|
||||
|
||||
// ── Radio buttons ─────────────────────────────────────────
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("RAW (TCP)")
|
||||
checked: transport === "raw"
|
||||
onToggled: if (checked && transport !== "raw") transport = "raw"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("XHTTP")
|
||||
descriptionText: qsTr("Advanced users")
|
||||
checked: transport === "xhttp"
|
||||
onToggled: if (checked && transport !== "xhttp") transport = "xhttp"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
VerticalRadioButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("mKCP")
|
||||
checked: transport === "mkcp"
|
||||
onToggled: if (checked && transport !== "mkcp") transport = "mkcp"
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// mKCP Settings
|
||||
// ══════════════════════════════════════════════════════════
|
||||
ColumnLayout {
|
||||
visible: transport === "mkcp"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("mKCP Settings")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("TTI")
|
||||
subtitleText: qsTr("Default: %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti())
|
||||
textField.text: mkcpTti
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpTti) mkcpTti = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("uplinkCapacity")
|
||||
subtitleText: qsTr("Default: %1 Mbit/s", "mKCP uplink").arg(XrayConfigModel.mkcpDefaultUplinkCapacity())
|
||||
textField.text: mkcpUplinkCapacity
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpUplinkCapacity) mkcpUplinkCapacity = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("downlinkCapacity")
|
||||
subtitleText: qsTr("Default: %1 Mbit/s", "mKCP downlink").arg(XrayConfigModel.mkcpDefaultDownlinkCapacity())
|
||||
textField.text: mkcpDownlinkCapacity
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpDownlinkCapacity) mkcpDownlinkCapacity = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("readBufferSize")
|
||||
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultReadBufferSize())
|
||||
textField.text: mkcpReadBufferSize
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpReadBufferSize) mkcpReadBufferSize = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("writeBufferSize")
|
||||
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize())
|
||||
textField.text: mkcpWriteBufferSize
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpWriteBufferSize) mkcpWriteBufferSize = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
Layout.topMargin: 8
|
||||
text: qsTr("Congestion")
|
||||
checked: mkcpCongestion
|
||||
onToggled: mkcpCongestion = checked
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// XHTTP Settings
|
||||
// ══════════════════════════════════════════════════════════
|
||||
ColumnLayout {
|
||||
visible: transport === "xhttp"
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
DropDownType {
|
||||
id: modeDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpMode
|
||||
descriptionText: qsTr("Mode")
|
||||
headerText: qsTr("Mode")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpModeOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpMode = selectedText
|
||||
modeDropDown.text = selectedText
|
||||
modeDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpMode) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
modeDropDown.text = xhttpMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("HTTP Profile")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Host")
|
||||
textField.text: xhttpHost
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpHost) xhttpHost = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Path")
|
||||
textField.text: xhttpPath
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpPath) xhttpPath = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: headersDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpHeadersTemplate
|
||||
descriptionText: qsTr("Headers template")
|
||||
headerText: qsTr("Headers template")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpHeadersTemplateOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpHeadersTemplate = selectedText
|
||||
headersDropDown.text = selectedText
|
||||
headersDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpHeadersTemplate) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
headersDropDown.text = xhttpHeadersTemplate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: uplinkMethodDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpUplinkMethod
|
||||
descriptionText: qsTr("UplinkHTTPMethod")
|
||||
headerText: qsTr("UplinkHTTPMethod")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpUplinkMethodOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpUplinkMethod = selectedText
|
||||
uplinkMethodDropDown.text = selectedText
|
||||
uplinkMethodDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpUplinkMethod) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
uplinkMethodDropDown.text = xhttpUplinkMethod
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
Layout.topMargin: 16
|
||||
text: qsTr("Disable gRPC Header")
|
||||
descriptionText: qsTr("noGRPCHeader")
|
||||
checked: xhttpDisableGrpc
|
||||
onToggled: xhttpDisableGrpc = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
text: qsTr("Disable SSE Header")
|
||||
descriptionText: qsTr("noSSEHeader")
|
||||
checked: xhttpDisableSse
|
||||
onToggled: xhttpDisableSse = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
// ── Session & Sequence ────────────────────────────────
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Session & Sequence")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: sessionPlacementDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpSessionPlacement
|
||||
descriptionText: qsTr("SessionPlacement")
|
||||
headerText: qsTr("SessionPlacement")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpSessionPlacementOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpSessionPlacement = selectedText
|
||||
sessionPlacementDropDown.text = selectedText
|
||||
sessionPlacementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpSessionPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
sessionPlacementDropDown.text = xhttpSessionPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: sessionKeyDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpSessionKey
|
||||
descriptionText: qsTr("SessionKey")
|
||||
headerText: qsTr("SessionKey")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpSessionKeyOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpSessionKey = selectedText
|
||||
sessionKeyDropDown.text = selectedText
|
||||
sessionKeyDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpSessionKey) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
sessionKeyDropDown.text = xhttpSessionKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: seqPlacementDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpSeqPlacement
|
||||
descriptionText: qsTr("SeqPlacement")
|
||||
headerText: qsTr("SeqPlacement")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpSeqPlacementOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpSeqPlacement = selectedText
|
||||
seqPlacementDropDown.text = selectedText
|
||||
seqPlacementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpSeqPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
seqPlacementDropDown.text = xhttpSeqPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("SeqKey")
|
||||
textField.text: xhttpSeqKey
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpSeqKey) xhttpSeqKey = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: uplinkDataPlacementDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xhttpUplinkDataPlacement
|
||||
descriptionText: qsTr("UplinkDataPlacement")
|
||||
headerText: qsTr("UplinkDataPlacement")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xhttpUplinkDataPlacementOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xhttpUplinkDataPlacement = selectedText
|
||||
uplinkDataPlacementDropDown.text = selectedText
|
||||
uplinkDataPlacementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xhttpUplinkDataPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
uplinkDataPlacementDropDown.text = xhttpUplinkDataPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("UplinkDataKey")
|
||||
textField.text: xhttpUplinkDataKey
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpUplinkDataKey) xhttpUplinkDataKey = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
// ── Traffic Shaping ───────────────────────────────────
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Traffic Shaping")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("UplinkChunkSize")
|
||||
textField.text: xhttpUplinkChunkSize
|
||||
textField.validator: IntValidator {
|
||||
bottom: 0
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpUplinkChunkSize) xhttpUplinkChunkSize = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("scMaxBufferedPosts")
|
||||
textField.text: xhttpScMaxBufferedPosts
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpScMaxBufferedPosts) xhttpScMaxBufferedPosts = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("scMaxEachPostBytes")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xhttpScMaxEachPostBytesMin
|
||||
maxValue: xhttpScMaxEachPostBytesMax
|
||||
onMinChanged: xhttpScMaxEachPostBytesMin = val
|
||||
onMaxChanged: xhttpScMaxEachPostBytesMax = val
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("scStreamUpServerSecs")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xhttpScStreamUpServerSecsMin
|
||||
maxValue: xhttpScStreamUpServerSecsMax
|
||||
onMinChanged: xhttpScStreamUpServerSecsMin = val
|
||||
onMaxChanged: xhttpScStreamUpServerSecsMax = val
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("scMinPostsIntervalMs")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xhttpScMinPostsIntervalMsMin
|
||||
maxValue: xhttpScMinPostsIntervalMsMax
|
||||
onMinChanged: xhttpScMinPostsIntervalMsMin = val
|
||||
onMaxChanged: xhttpScMinPostsIntervalMsMax = val
|
||||
}
|
||||
|
||||
// ── Padding and multiplexing ──────────────────────────
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Padding and multiplexing")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("xPadding")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayXPaddingSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("XMux")
|
||||
descriptionText: xmuxEnabled ? qsTr("On") : qsTr("Off")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayXmuxSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
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 (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 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.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: 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.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
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 (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 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.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("xPadding")
|
||||
}
|
||||
|
||||
// xPaddingBytes — min/max display row
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("xPaddingBytes")
|
||||
descriptionText: (xPaddingBytesMin !== "" ? xPaddingBytesMin : "0") + "—" + (xPaddingBytesMax !== "" ? xPaddingBytesMax : "0")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
clickedFunction: function () {
|
||||
PageController.goToPage(PageEnum.PageProtocolXrayXPaddingBytesSettings)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
text: qsTr("xPaddingObfsMode")
|
||||
checked: xPaddingObfsMode
|
||||
onToggled: xPaddingObfsMode = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
headerText: qsTr("xPaddingKey")
|
||||
textField.text: xPaddingKey
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xPaddingKey) xPaddingKey = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("xPaddingHeader")
|
||||
textField.text: xPaddingHeader
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xPaddingHeader) xPaddingHeader = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: placementDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xPaddingPlacement
|
||||
descriptionText: qsTr("xPaddingPlacement")
|
||||
headerText: qsTr("xPaddingPlacement")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xPaddingPlacementOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xPaddingPlacement = selectedText
|
||||
placementDropDown.text = selectedText
|
||||
placementDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xPaddingPlacement) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
placementDropDown.text = xPaddingPlacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: methodDropDown
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: xPaddingMethod
|
||||
descriptionText: qsTr("xPaddingMethod")
|
||||
headerText: qsTr("xPaddingMethod")
|
||||
drawerParent: root
|
||||
listView: ListViewWithRadioButtonType {
|
||||
rootWidth: root.width
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
var opts = XrayConfigModel.xPaddingMethodOptions()
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
append({name: opts[i]})
|
||||
}
|
||||
}
|
||||
}
|
||||
clickedFunction: function () {
|
||||
xPaddingMethod = selectedText
|
||||
methodDropDown.text = selectedText
|
||||
methodDropDown.closeTriggered()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
if (model.get(i).name === xPaddingMethod) {
|
||||
selectedIndex = i;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: XrayConfigModel
|
||||
|
||||
function onDataChanged() {
|
||||
methodDropDown.text = xPaddingMethod
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
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 (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 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.visible ? saveButton.top : parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
model: XrayConfigModel
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("xmux")
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
text: qsTr("xmux")
|
||||
checked: xmuxEnabled
|
||||
onToggled: xmuxEnabled = checked
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
enabled: xmuxEnabled
|
||||
|
||||
// maxConcurrency
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("maxConcurrency")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxMaxConcurrencyMin
|
||||
maxValue: xmuxMaxConcurrencyMax
|
||||
onMinChanged: xmuxMaxConcurrencyMin = val
|
||||
onMaxChanged: xmuxMaxConcurrencyMax = val
|
||||
}
|
||||
|
||||
// maxConnections
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("maxConnections")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxMaxConnectionsMin
|
||||
maxValue: xmuxMaxConnectionsMax
|
||||
onMinChanged: xmuxMaxConnectionsMin = val
|
||||
onMaxChanged: xmuxMaxConnectionsMax = val
|
||||
}
|
||||
|
||||
// cMaxReuseTimes
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("cMaxReuseTimes")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxCMaxReuseTimesMin
|
||||
maxValue: xmuxCMaxReuseTimesMax
|
||||
onMinChanged: xmuxCMaxReuseTimesMin = val
|
||||
onMaxChanged: xmuxCMaxReuseTimesMax = val
|
||||
}
|
||||
|
||||
// hMaxRequestTimes
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("hMaxRequestTimes")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxHMaxRequestTimesMin
|
||||
maxValue: xmuxHMaxRequestTimesMax
|
||||
onMinChanged: xmuxHMaxRequestTimesMin = val
|
||||
onMaxChanged: xmuxHMaxRequestTimesMax = val
|
||||
}
|
||||
|
||||
// hMaxReusableSecs
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("hMaxReusableSecs")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
MinMaxRowType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxHMaxReusableSecsMin
|
||||
maxValue: xmuxHMaxReusableSecsMax
|
||||
onMinChanged: xmuxHMaxReusableSecsMin = val
|
||||
onMaxChanged: xmuxHMaxReusableSecsMax = val
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
headerText: qsTr("hKeepAlivePeriod")
|
||||
textField.text: xmuxHKeepAlivePeriod
|
||||
textField.validator: IntValidator {
|
||||
bottom: 0
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xmuxHKeepAlivePeriod) xmuxHKeepAlivePeriod = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
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 (typeof GC !== "undefined" && !GC.isMobile()) {
|
||||
saveButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,16 @@
|
||||
<file>Pages2/PageProtocolRaw.qml</file>
|
||||
<file>Pages2/PageProtocolWireGuardSettings.qml</file>
|
||||
<file>Pages2/PageProtocolXraySettings.qml</file>
|
||||
|
||||
<file>Pages2/PageProtocolXraySnapshots.qml</file>
|
||||
<file>Pages2/PageProtocolXrayFlowSettings.qml</file>
|
||||
<file>Pages2/PageProtocolXraySecuritySettings.qml</file>
|
||||
<file>Pages2/PageProtocolXrayTransportSettings.qml</file>
|
||||
<file>Pages2/PageProtocolXrayXmuxSettings.qml</file>
|
||||
<file>Pages2/PageProtocolXrayXPaddingSettings.qml</file>
|
||||
<file>Pages2/PageProtocolXrayXPaddingBytesSettings.qml</file>
|
||||
<file>Controls2/MinMaxRowType.qml</file>
|
||||
|
||||
<file>Pages2/PageServiceDnsSettings.qml</file>
|
||||
<file>Pages2/PageServiceMtProxySettings.qml</file>
|
||||
<file>Pages2/PageServiceTelemtSettings.qml</file>
|
||||
|
||||
Reference in New Issue
Block a user