Qv2ray/src/ui/w_MainWindow.cpp

886 lines
34 KiB
C++

#include <QAction>
#include <QCloseEvent>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QHeaderView>
#include <QInputDialog>
#include <QMenu>
#include <QStandardItemModel>
#include <QDesktopServices>
#include <QUrl>
#include <QVersionNumber>
#include <QKeyEvent>
#include "w_OutboundEditor.hpp"
#include "w_ImportConfig.hpp"
#include "w_MainWindow.hpp"
#include "w_RoutesEditor.hpp"
#include "w_PrefrencesWindow.hpp"
#include "w_SubscriptionEditor.hpp"
#include "w_JsonEditor.hpp"
#include "w_ExportConfig.hpp"
#include "QvPingModel.hpp"
#include "QvNetSpeedPlugin.hpp"
#include "QvPACHandler.hpp"
#include "QvSystemProxyConfigurator.hpp"
#define TRAY_TOOLTIP_PREFIX "Qv2ray " QV2RAY_VERSION_STRING
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent),
vinstance(),
uploadList(),
downloadList(),
HTTPRequestHelper(),
hTray(new QSystemTrayIcon(this))
{
auto conf = GetGlobalConfig();
vinstance = new ConnectionInstance(this);
setupUi(this);
//
pacServer = new PACHandler();
//
this->setWindowIcon(QIcon(":/icons/qv2ray.png"));
hTray->setIcon(QIcon(conf.uiConfig.useDarkTrayIcon ? ":/icons/ui_dark/tray.png" : ":/icons/ui_light/tray.png"));
importConfigButton->setIcon(QICON_R("import.png"));
duplicateBtn->setIcon(QICON_R("duplicate.png"));
removeConfigButton->setIcon(QICON_R("delete.png"));
editConfigButton->setIcon(QICON_R("edit.png"));
editJsonBtn->setIcon(QICON_R("json.png"));
//
pingTestBtn->setIcon(QICON_R("ping_gauge.png"));
shareBtn->setIcon(QICON_R("share.png"));
updownImageBox->setStyleSheet("image: url(" + QV2RAY_UI_RESOURCES_ROOT + "netspeed_arrow.png)");
updownImageBox_2->setStyleSheet("image: url(" + QV2RAY_UI_RESOURCES_ROOT + "netspeed_arrow.png)");
//
hTray->setToolTip(TRAY_TOOLTIP_PREFIX);
//
QAction *action_Tray_ShowHide = new QAction(this->windowIcon(), tr("Hide"), this);
QAction *action_Tray_Quit = new QAction(tr("Quit"), this);
QAction *action_Tray_Start = new QAction(tr("Connect"), this);
QAction *action_Tray_Reconnect = new QAction(tr("Reconnect"), this);
QAction *action_Tray_Stop = new QAction(tr("Disconnect"), this);
//
QAction *action_RCM_RenameConnection = new QAction(tr("Rename"), this);
QAction *action_RCM_StartThis = new QAction(tr("Connect to this"), this);
QAction *action_RCM_ConvToComplex = new QAction(tr("Edit as Complex Config"), this);
QAction *action_RCM_EditJson = new QAction(QICON_R("json.png"), tr("Edit as Json"), this);
QAction *action_RCM_ShareQR = new QAction(QICON_R("share.png"), tr("Share as QRCode/VMess URL"), this);
//
action_Tray_Start->setEnabled(true);
action_Tray_Stop->setEnabled(false);
action_Tray_Reconnect->setEnabled(false);
trayMenu->addAction(action_Tray_ShowHide);
trayMenu->addSeparator();
trayMenu->addAction(action_Tray_Start);
trayMenu->addAction(action_Tray_Stop);
trayMenu->addAction(action_Tray_Reconnect);
trayMenu->addSeparator();
trayMenu->addAction(action_Tray_Quit);
connect(action_Tray_ShowHide, &QAction::triggered, this, &MainWindow::ToggleVisibility);
connect(action_Tray_Start, &QAction::triggered, this, &MainWindow::on_startButton_clicked);
connect(action_Tray_Stop, &QAction::triggered, this, &MainWindow::on_stopButton_clicked);
connect(action_Tray_Reconnect, &QAction::triggered, this, &MainWindow::on_reconnectButton_clicked);
connect(action_Tray_Quit, &QAction::triggered, this, &MainWindow::quit);
connect(hTray, &QSystemTrayIcon::activated, this, &MainWindow::on_activatedTray);
connect(logText, &QTextBrowser::textChanged, this, &MainWindow::QTextScrollToBottom);
connect(action_RCM_RenameConnection, &QAction::triggered, this, &MainWindow::on_action_RenameConnection_triggered);
connect(action_RCM_StartThis, &QAction::triggered, this, &MainWindow::on_action_StartThis_triggered);
connect(action_RCM_EditJson, &QAction::triggered, this, &MainWindow::on_action_RCM_EditJson_triggered);
connect(action_RCM_ConvToComplex, &QAction::triggered, this, &MainWindow::on_action_RCM_ConvToComplex_triggered);
//
// Share optionss
connect(action_RCM_ShareQR, &QAction::triggered, this, &MainWindow::on_action_RCM_ShareQR_triggered);
//
connect(this, &MainWindow::Connect, this, &MainWindow::on_startButton_clicked);
connect(this, &MainWindow::DisConnect, this, &MainWindow::on_stopButton_clicked);
connect(this, &MainWindow::ReConnect, this, &MainWindow::on_reconnectButton_clicked);
//
hTray->setContextMenu(trayMenu);
hTray->show();
//
listMenu = new QMenu(this);
listMenu->addAction(action_RCM_RenameConnection);
listMenu->addAction(action_RCM_StartThis);
listMenu->addAction(action_RCM_ConvToComplex);
listMenu->addAction(action_RCM_EditJson);
listMenu->addAction(action_RCM_ShareQR);
//
LoadConnections();
QObject::connect(&HTTPRequestHelper, &QvHttpRequestHelper::httpRequestFinished, this, &MainWindow::VersionUpdate);
HTTPRequestHelper.get("https://api.github.com/repos/lhy0403/Qv2ray/releases/latest");
//
// For charts
uploadSerie = new QSplineSeries(this);
downloadSerie = new QSplineSeries(this);
uploadSerie->setName("Upload");
downloadSerie->setName("Download");
for (int i = 0; i < 30 ; i++) {
uploadList.append(0);
downloadList.append(0);
uploadSerie->append(i, 0);
downloadSerie->append(i, 0);
}
speedChartObj = new QChart();
speedChartObj->setTheme(conf.uiConfig.useDarkTheme ? QChart::ChartThemeDark : QChart::ChartThemeLight);
speedChartObj->setTitle("Qv2ray Speed Chart");
speedChartObj->legend()->hide();
speedChartObj->createDefaultAxes();
speedChartObj->addSeries(uploadSerie);
speedChartObj->addSeries(downloadSerie);
speedChartObj->createDefaultAxes();
speedChartObj->axes(Qt::Vertical).first()->setRange(0, 512);
static_cast<QValueAxis>(speedChartObj->axes(Qt::Horizontal).first()).setLabelFormat("dd.dd");
speedChartObj->axes(Qt::Horizontal).first()->setRange(0, 30);
speedChartObj->setContentsMargins(-20, -50, -20, -25);
speedChartView = new QChartView(speedChartObj, this);
speedChartView->setRenderHint(QPainter::RenderHint::HighQualityAntialiasing, true);
auto layout = new QHBoxLayout(speedChart);
layout->addWidget(speedChartView);
speedChart->setLayout(layout);
//
bool hasAutoStart = vinstance->ValidateKernal();
if (hasAutoStart) {
// At least kernal is ready.
if (conf.autoStartConfig != "" && QList<string>::fromStdList(conf.configs).contains(conf.autoStartConfig)) {
// Has auto start.
CurrentConnectionName = QSTRING(conf.autoStartConfig);
auto item = connectionListWidget->findItems(QSTRING(conf.autoStartConfig), Qt::MatchExactly).front();
item->setSelected(true);
connectionListWidget->setCurrentItem(item);
on_connectionListWidget_itemClicked(item);
trayMenu->actions()[0]->setText(tr("Show"));
this->hide();
on_startButton_clicked();
} else if (connectionListWidget->count() != 0) {
// The first one is default.
connectionListWidget->setCurrentRow(0);
ShowAndSetConnection(connectionListWidget->item(0)->text(), true, false);
this->show();
}
} else {
this->show();
}
StartProcessingPlugins(this);
}
void MainWindow::on_action_StartThis_triggered()
{
if (connectionListWidget->selectedItems().empty()) {
QvMessageBox(this, tr("No connection selected!"), tr("Please select a config from the list."));
return;
}
CurrentConnectionName = connectionListWidget->currentItem()->text();
on_reconnectButton_clicked();
}
void MainWindow::VersionUpdate(QByteArray &data)
{
auto conf = GetGlobalConfig();
QString jsonString(data);
QJsonObject root = JsonFromString(jsonString);
//
QVersionNumber newversion = QVersionNumber::fromString(root["tag_name"].toString("").remove(0, 1));
QVersionNumber current = QVersionNumber::fromString(QSTRING(QV2RAY_VERSION_STRING).remove(0, 1));
QVersionNumber ignored = QVersionNumber::fromString(QSTRING(conf.ignoredVersion));
LOG(MODULE_UPDATE, "Received update info, Latest: " + newversion.toString().toStdString() + " Current: " + current.toString().toStdString() + " Ignored: " + ignored.toString().toStdString())
// If the version is newer than us.
// And new version is newer than the ignored version.
if (newversion > current && newversion > ignored) {
LOG(MODULE_UPDATE, "New version detected.")
auto link = root["html_url"].toString("");
auto result = QvMessageBoxAsk(this, tr("Update"),
tr("Found a new version: ") + root["tag_name"].toString("") +
"\r\n" +
root["name"].toString("") +
"\r\n------------\r\n" +
root["body"].toString("") +
"\r\n------------\r\n" +
tr("Download Link: ") + link, QMessageBox::Ignore);
if (result == QMessageBox::Yes) {
QDesktopServices::openUrl(QUrl::fromUserInput(link));
} else if (result == QMessageBox::Ignore) {
conf.ignoredVersion = newversion.toString().toStdString();
SetGlobalConfig(conf);
OnConfigListChanged(false);
}
}
}
void MainWindow::LoadConnections()
{
auto conf = GetGlobalConfig();
connections = GetConnections(conf.configs);
connectionListWidget->clear();
for (int i = 0; i < connections.count(); i++) {
connectionListWidget->addItem(connections.keys()[i]);
}
connectionListWidget->sortItems();
removeConfigButton->setEnabled(false);
editConfigButton->setEnabled(false);
editJsonBtn->setEnabled(false);
duplicateBtn->setEnabled(false);
// We set the current item back...
if (vinstance->ConnectionStatus == STARTED && !CurrentConnectionName.isEmpty()) {
auto items = connectionListWidget->findItems(CurrentConnectionName, Qt::MatchFlag::MatchExactly);
if (items.count() > 0) {
connectionListWidget->setCurrentItem(items.first());
}
ShowAndSetConnection(CurrentConnectionName, false, false);
}
}
void MainWindow::OnConfigListChanged(bool need_restart)
{
auto statusText = statusLabel->text();
//
// A strange bug prevents us to change the UI language `live`ly
// https://github.com/lhy0403/Qv2ray/issues/34
//
//retranslateUi(this);
statusLabel->setText(statusText);
bool isRunning = vinstance->ConnectionStatus == STARTED;
if (isRunning && need_restart) on_stopButton_clicked();
LoadConnections();
if (isRunning && need_restart) on_startButton_clicked();
}
MainWindow::~MainWindow()
{
hTray->hide();
delete this->hTray;
delete this->vinstance;
}
void MainWindow::UpdateLog()
{
logText->append(vinstance->ReadProcessOutput().trimmed());
}
void MainWindow::on_startButton_clicked()
{
if (vinstance->ConnectionStatus != STARTED) {
// Reset the graph
for (int i = 0; i < 30 ; i++) {
uploadList[i] = 0;
downloadList[i] = 0;
uploadSerie->replace(i, 0, 0);
downloadSerie->replace(i, 0, 0);
}
// Check Selection
if (CurrentConnectionName.isEmpty()) {
QvMessageBox(this, tr("No connection selected!"), tr("Please select a config from the list."));
return;
}
LOG(MODULE_VCORE, ("Connecting to: " + CurrentConnectionName).toStdString())
logText->clear();
//
auto connectionRoot = connections[CurrentConnectionName];
//
CurrentFullConfig = GenerateRuntimeConfig(connectionRoot);
StartPreparation(CurrentFullConfig);
bool startFlag = this->vinstance->StartV2rayCore();
if (startFlag) {
this->hTray->showMessage("Qv2ray", tr("Connected To Server: ") + CurrentConnectionName);
hTray->setToolTip(TRAY_TOOLTIP_PREFIX "\r\n" + tr("Connected To Server: ") + CurrentConnectionName);
statusLabel->setText(tr("Connected") + ": " + CurrentConnectionName);
//
auto conf = GetGlobalConfig();
if (conf.connectionConfig.enableStats) {
vinstance->SetAPIPort(conf.connectionConfig.statsPort);
speedTimerId = startTimer(1000);
}
bool usePAC = conf.inboundConfig.pacConfig.usePAC;
bool pacUseSocks = conf.inboundConfig.pacConfig.useSocksProxy;
bool httpEnabled = conf.inboundConfig.useHTTP;
bool socksEnabled = conf.inboundConfig.useSocks;
if (usePAC) {
bool canStartPAC = true;
QString pacProxyString; // Something like this --> SOCKS5 127.0.0.1:1080; SOCKS 127.0.0.1:1080; DIRECT; http://proxy:8080
if (pacUseSocks) {
if (socksEnabled) {
pacProxyString = "SOCKS5 " + QSTRING(conf.inboundConfig.pacConfig.proxyIP) + ":" + QString::number(conf.inboundConfig.socks_port);
} else {
LOG(MODULE_UI, "PAC is using SOCKS, but it is not enabled")
QvMessageBox(this, tr("Configuring PAC"), tr("Could not start PAC server as it is configured to use SOCKS, but it is not enabled"));
canStartPAC = false;
}
} else {
if (httpEnabled) {
pacProxyString = "http://" + QSTRING(conf.inboundConfig.pacConfig.proxyIP) + ":" + QString::number(conf.inboundConfig.http_port);
} else {
LOG(MODULE_UI, "PAC is using HTTP, but it is not enabled")
QvMessageBox(this, tr("Configuring PAC"), tr("Could not start PAC server as it is configured to use HTTP, but it is not enabled"));
canStartPAC = false;
}
}
if (canStartPAC) {
pacServer->SetProxyString(pacProxyString);
pacServer->StartListen();
} else {
LOG(MODULE_PROXY, "Not starting PAC due to previous error.")
}
}
//
// Set system proxy if necessary
bool isComplex = CheckIsComplexConfig(connectionRoot);
if (conf.inboundConfig.setSystemProxy && !isComplex) {
// Is simple config and we will try to set system proxy.
LOG(MODULE_UI, "Preparing to set system proxy")
//
QString proxyAddress;
bool canSetSystemProxy = true;
if (usePAC) {
if ((httpEnabled && !pacUseSocks) || (socksEnabled && pacUseSocks)) {
// If we use PAC and socks/http are properly configured for PAC
LOG(MODULE_PROXY, "System proxy uses PAC")
proxyAddress = "http://" + QSTRING(conf.inboundConfig.listenip) + ":" + QString::number(conf.inboundConfig.pacConfig.port) + "/pac";
} else {
// Not properly configured
LOG(MODULE_PROXY, "Failed to process pac due to following reasons:")
LOG(MODULE_PROXY, " --> PAC is configured to use socks but socks is not enabled.")
LOG(MODULE_PROXY, " --> PAC is configuted to use http but http is not enabled.")
QvMessageBox(this, tr("PAC Processing Failed"), tr("HTTP or SOCKS inbound is not properly configured for PAC") +
NEWLINE + tr("Qv2ray will continue, but will not set system proxy."));
canSetSystemProxy = false;
}
} else {
// Not using PAC
if (httpEnabled) {
// Not use PAC, System proxy should use HTTP
LOG(MODULE_PROXY, "Using system proxy with HTTP")
proxyAddress = "localhost";
} else {
LOG(MODULE_PROXY, "HTTP is not enabled, cannot set system proxy.")
QvMessageBox(this, tr("Cannot set system proxy"), tr("HTTP inbound is not enabled"));
canSetSystemProxy = false;
}
}
if (canSetSystemProxy) {
LOG(MODULE_UI, "Setting system proxy for simple config")
// --------------------We only use HTTP here->>|=========|
SetSystemProxy(proxyAddress, conf.inboundConfig.http_port, usePAC);
}
}
} else {
// If failed, show mainwindow
this->show();
}
trayMenu->actions()[2]->setEnabled(!startFlag);
trayMenu->actions()[3]->setEnabled(startFlag);
trayMenu->actions()[4]->setEnabled(startFlag);
//
startButton->setEnabled(!startFlag);
stopButton->setEnabled(startFlag);
}
}
void MainWindow::on_stopButton_clicked()
{
if (vinstance->ConnectionStatus != STOPPED) {
// Is running or starting
this->vinstance->StopV2rayCore();
killTimer(speedTimerId);
hTray->setToolTip(TRAY_TOOLTIP_PREFIX);
QFile(QV2RAY_GENERATED_FILE_PATH).remove();
statusLabel->setText(tr("Disconnected"));
logText->setText("");
trayMenu->actions()[2]->setEnabled(true);
trayMenu->actions()[3]->setEnabled(false);
trayMenu->actions()[4]->setEnabled(false);
//
startButton->setEnabled(true);
stopButton->setEnabled(false);
//
netspeedLabel->setText("0.00 B/s\r\n0.00 B/s");
dataamountLabel->setText("0.00 B\r\n0.00 B");
//
// Who cares the check... (inboundConfig.pacConfig.usePAC)
pacServer->StopServer();
ClearSystemProxy();
LOG(MODULE_UI, "Stopped successfully.")
}
}
void MainWindow::closeEvent(QCloseEvent *event)
{
this->hide();
trayMenu->actions()[0]->setText(tr("Show"));
event->ignore();
}
void MainWindow::on_activatedTray(QSystemTrayIcon::ActivationReason reason)
{
switch (reason) {
case QSystemTrayIcon::Trigger:
// Toggle Show/Hide
#ifndef __APPLE__
// Every single click will trigger the Show/Hide toggling.
// So, as what common macOS Apps do, we don't toggle visibility here.
ToggleVisibility();
#endif
break;
case QSystemTrayIcon::MiddleClick:
if (this->vinstance->ConnectionStatus == STARTED) {
on_stopButton_clicked();
} else {
on_startButton_clicked();
}
break;
case QSystemTrayIcon::DoubleClick:
#ifdef __APPLE__
ToggleVisibility();
#endif
break;
default:
break;
}
}
void MainWindow::ToggleVisibility()
{
if (this->isHidden()) {
this->show();
#ifdef Q_OS_WIN
setWindowState(Qt::WindowNoState);
SetWindowPos(HWND(this->winId()), HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
QThread::msleep(20);
SetWindowPos(HWND(this->winId()), HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
#endif
trayMenu->actions()[0]->setText(tr("Hide"));
} else {
this->hide();
trayMenu->actions()[0]->setText(tr("Show"));
}
}
void MainWindow::quit()
{
StopProcessingPlugins();
on_stopButton_clicked();
QApplication::quit();
}
void MainWindow::on_actionExit_triggered()
{
quit();
}
void MainWindow::QTextScrollToBottom()
{
auto bar = logText->verticalScrollBar();
if (bar->value() >= bar->maximum() - 10) bar->setValue(bar->maximum());
}
void MainWindow::ShowAndSetConnection(QString guiConnectionName, bool SetConnection, bool ApplyConnection)
{
// Check empty again...
if (guiConnectionName.isEmpty()) return;
//
removeConfigButton->setEnabled(true);
editConfigButton->setEnabled(true);
editJsonBtn->setEnabled(true);
duplicateBtn->setEnabled(true);
//
// --------- BRGIN Show Connection
auto root = connections[guiConnectionName];
//
auto isComplexConfig = root["routing"].toObject()["rules"].toArray().count() > 0;
routeCountLabel->setText(isComplexConfig > 0 ? tr("Complex") : tr("Simple"));
if (isComplexConfig) {
_OutBoundTypeLabel->setText(tr("N/A"));
_hostLabel->setText(tr("N/A"));
_portLabel->setText(tr("N/A"));
} else {
auto outBoundRoot = root["outbounds"].toArray().first().toObject();
auto outboundType = outBoundRoot["protocol"].toString();
_OutBoundTypeLabel->setText(outboundType);
if (outboundType == "vmess") {
auto Server = StructFromJsonString<VMessServerObject>(JsonToString(outBoundRoot["settings"].toObject()["vnext"].toArray().first().toObject()));
_hostLabel->setText(QSTRING(Server.address));
_portLabel->setText(QSTRING(to_string(Server.port)));
} else if (outboundType == "shadowsocks") {
auto x = JsonToString(outBoundRoot["settings"].toObject()["servers"].toArray().first().toObject());
auto Server = StructFromJsonString<ShadowSocksServerObject>(x);
_hostLabel->setText(QSTRING(Server.address));
_portLabel->setText(QSTRING(to_string(Server.port)));
} else if (outboundType == "socks") {
auto x = JsonToString(outBoundRoot["settings"].toObject()["servers"].toArray().first().toObject());
auto Server = StructFromJsonString<SocksServerObject>(x);
_hostLabel->setText(QSTRING(Server.address));
_portLabel->setText(QSTRING(to_string(Server.port)));
}
}
// --------- END Show Connection
//
// Set Connection
if (SetConnection) {
CurrentConnectionName = guiConnectionName;
}
// Restart Connection
if (ApplyConnection) {
on_reconnectButton_clicked();
}
}
void MainWindow::on_connectionListWidget_itemClicked(QListWidgetItem *item)
{
Q_UNUSED(item)
int currentRow = connectionListWidget->currentRow();
if (currentRow < 0) return;
QString currentText = connectionListWidget->currentItem()->text();
bool canSetConnection = !isRenamingInProgress && vinstance->ConnectionStatus != STARTED;
ShowAndSetConnection(currentText, canSetConnection, false);
}
void MainWindow::on_prefrencesBtn_clicked()
{
PrefrencesWindow *w = new PrefrencesWindow(this);
connect(w, &PrefrencesWindow::s_reload_config, this, &MainWindow::OnConfigListChanged);
w->show();
}
void MainWindow::on_connectionListWidget_doubleClicked(const QModelIndex &index)
{
Q_UNUSED(index)
int currentRow = connectionListWidget->currentRow();
if (currentRow < 0) return;
QString currentText = connectionListWidget->currentItem()->text();
ShowAndSetConnection(currentText, true, true);
}
void MainWindow::on_clearlogButton_clicked()
{
logText->clear();
}
void MainWindow::on_connectionListWidget_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous)
{
Q_UNUSED(previous)
isRenamingInProgress = false;
on_connectionListWidget_itemClicked(current);
}
void MainWindow::on_connectionListWidget_customContextMenuRequested(const QPoint &pos)
{
Q_UNUSED(pos)
listMenu->popup(QCursor::pos());
}
void MainWindow::on_action_RenameConnection_triggered()
{
auto item = connectionListWidget->currentItem();
item->setFlags(item->flags() | Qt::ItemIsEditable);
connectionListWidget->editItem(item);
originalName = item->text();
isRenamingInProgress = true;
}
void MainWindow::on_connectionListWidget_itemChanged(QListWidgetItem *item)
{
DEBUG(MODULE_UI, "A connection ListViewItem is changed.")
if (isRenamingInProgress) {
// In this case it's after we entered the name.
LOG(MODULE_CONNECTION, "RENAME: " + originalName.toStdString() + " -> " + item->text().toStdString())
auto newName = item->text();
auto config = GetGlobalConfig();
auto configList = QList<string>::fromStdList(config.configs);
if (newName.trimmed().isEmpty()) {
QvMessageBox(this, tr("Rename a Connection"), tr("The name cannot be empty"));
return;
}
// If I really did some changes.
if (originalName != newName) {
if (configList.contains(newName.toStdString())) {
QvMessageBox(this, tr("Rename a Connection"), tr("The name has been used already, Please choose another."));
return;
}
//
// Change auto start config.
if (originalName.toStdString() == config.autoStartConfig) config.autoStartConfig = newName.toStdString();
configList[configList.indexOf(originalName.toStdString())] = newName.toStdString();
config.configs = configList.toStdList();
//
RenameConnection(originalName, newName);
//
SetGlobalConfig(config);
bool running = CurrentConnectionName == originalName;
if (running) CurrentConnectionName = newName;
OnConfigListChanged(running);
auto newItem = connectionListWidget->findItems(newName, Qt::MatchExactly).front();
connectionListWidget->setCurrentItem(newItem);
}
}
}
void MainWindow::on_removeConfigButton_clicked()
{
if (connectionListWidget->currentIndex().row() < 0) return;
if (QvMessageBoxAsk(this, tr("Removing this Connection"), tr("Are you sure to remove this connection?")) == QMessageBox::Yes) {
auto connectionName = connectionListWidget->currentItem()->text();
if (connectionName == CurrentConnectionName) {
on_stopButton_clicked();
CurrentConnectionName = "";
}
auto conf = GetGlobalConfig();
QList<string> list = QList<string>::fromStdList(conf.configs);
list.removeOne(connectionName.toStdString());
conf.configs = list.toStdList();
if (!RemoveConnection(connectionName)) {
QvMessageBox(this, tr("Removing this Connection"), tr("Failed to delete connection file, please delete manually."));
}
SetGlobalConfig(conf);
OnConfigListChanged(false);
ShowAndSetConnection(CurrentConnectionName, false, false);
}
}
void MainWindow::on_importConfigButton_clicked()
{
// BETA
ImportConfigWindow *w = new ImportConfigWindow(this);
auto configs = w->OpenImport();
auto gConf = GetGlobalConfig();
for (auto conf : configs) {
auto name = configs.key(conf, "");
if (name.isEmpty())
continue;
SaveConnectionConfig(conf, &name, false);
gConf.configs.push_back(name.toStdString());
}
SetGlobalConfig(gConf);
OnConfigListChanged(false);
}
void MainWindow::on_editConfigButton_clicked()
{
// Check if we have a connection selected...
if (connectionListWidget->currentIndex().row() < 0) {
QvMessageBox(this, tr("No Config Selected"), tr("Please Select a Config"));
return;
}
auto alias = connectionListWidget->currentItem()->text();
auto outBoundRoot = connections[alias];
CONFIGROOT root;
bool isChanged = false;
if (CheckIsComplexConfig(outBoundRoot)) {
LOG(MODULE_UI, "INFO: Opening route editor.")
RouteEditor *routeWindow = new RouteEditor(outBoundRoot, this);
root = routeWindow->OpenEditor();
isChanged = routeWindow->result() == QDialog::Accepted;
} else {
LOG(MODULE_UI, "INFO: Opening single connection edit window.")
OutboundEditor *w = new OutboundEditor(OUTBOUND(outBoundRoot["outbounds"].toArray().first().toObject()), this);
auto outboundEntry = w->OpenEditor();
isChanged = w->result() == QDialog::Accepted;
QJsonArray outboundsList;
outboundsList.push_back(outboundEntry);
root.insert("outbounds", outboundsList);
}
if (isChanged) {
connections[alias] = root;
// true indicates the alias will NOT change
SaveConnectionConfig(root, &alias, true);
OnConfigListChanged(alias == CurrentConnectionName);
ShowAndSetConnection(CurrentConnectionName, false, false);
}
}
void MainWindow::on_reconnectButton_clicked()
{
on_stopButton_clicked();
on_startButton_clicked();
}
void MainWindow::on_action_RCM_ConvToComplex_triggered()
{
// Check if we have a connection selected...
if (connectionListWidget->currentIndex().row() < 0) {
QvMessageBox(this, tr("No Config Selected"), tr("Please Select a Config"));
return;
}
auto alias = connectionListWidget->currentItem()->text();
auto outBoundRoot = connections[alias];
CONFIGROOT root;
bool isChanged = false;
//
LOG(MODULE_UI, "INFO: Opening route editor.")
RouteEditor *routeWindow = new RouteEditor(outBoundRoot, this);
root = routeWindow->OpenEditor();
isChanged = routeWindow->result() == QDialog::Accepted;
if (isChanged) {
connections[alias] = root;
// true indicates the alias will NOT change
SaveConnectionConfig(root, &alias, true);
OnConfigListChanged(alias == CurrentConnectionName);
ShowAndSetConnection(CurrentConnectionName, false, false);
}
}
void MainWindow::on_action_RCM_EditJson_triggered()
{
// Check if we have a connection selected...
if (connectionListWidget->currentIndex().row() < 0) {
QvMessageBox(this, tr("No Config Selected"), tr("Please Select a Config"));
return;
}
auto alias = connectionListWidget->currentItem()->text();
JsonEditor *w = new JsonEditor(connections[alias], this);
auto root = CONFIGROOT(w->OpenEditor());
bool isChanged = w->result() == QDialog::Accepted;
delete w;
if (isChanged) {
connections[alias] = root;
// Alias here will not change.
SaveConnectionConfig(root, &alias, true);
ShowAndSetConnection(CurrentConnectionName, false, false);
}
}
void MainWindow::on_editJsonBtn_clicked()
{
// See above.
on_action_RCM_EditJson_triggered();
}
void MainWindow::on_pingTestBtn_clicked()
{
// Ping
}
void MainWindow::on_shareBtn_clicked()
{
// Share QR
if (connectionListWidget->currentRow() < 0) {
return;
}
auto alias = connectionListWidget->currentItem()->text();
auto root = connections[alias];
auto outBoundRoot = root["outbounds"].toArray().first().toObject();
auto outboundType = outBoundRoot["protocol"].toString();
if (!CheckIsComplexConfig(root) && outboundType == "vmess") {
auto vmessServer = StructFromJsonString<VMessServerObject>(JsonToString(outBoundRoot["settings"].toObject()["vnext"].toArray().first().toObject()));
auto transport = StructFromJsonString<StreamSettingsObject>(JsonToString(outBoundRoot["streamSettings"].toObject()));
auto vmess = ConvertConfigToVMessString(transport, vmessServer, alias);
ConfigExporter v(vmess, this);
v.OpenExport();
} else {
QvMessageBox(this, tr("Share Connection"), tr("There're no support of sharing configs other than vmess"));
}
}
void MainWindow::on_action_RCM_ShareQR_triggered(bool checked)
{
Q_UNUSED(checked)
on_shareBtn_clicked();
}
void MainWindow::timerEvent(QTimerEvent *event)
{
Q_UNUSED(event)
auto inbounds = CurrentFullConfig["inbounds"].toArray();
long _totalSpeedUp = 0, _totalSpeedDown = 0, _totalDataUp = 0, _totalDataDown = 0;
foreach (auto inbound, inbounds) {
auto tag = inbound.toObject()["tag"].toString();
if (tag.isEmpty()) continue;
// TODO: A proper scheme...
if (tag == QV2RAY_API_TAG_INBOUND) {
continue;
}
_totalSpeedUp += vinstance->getTagLastUplink(tag);
_totalSpeedDown += vinstance->getTagLastDownlink(tag);
_totalDataUp += vinstance->getTagTotalUplink(tag);
_totalDataDown += vinstance->getTagTotalDownlink(tag);
}
double max = 0;
double historyMax = 0;
auto graphVUp = _totalSpeedUp / 1024;
auto graphVDown = _totalSpeedDown / 1024;
for (auto i = 0; i < 29; i++) {
historyMax = MAX(historyMax, MAX(uploadList[i + 1], downloadList[i + 1]));
uploadList[i] = uploadList[i + 1];
downloadList[i] = downloadList[i + 1];
uploadSerie->replace(i, i, uploadList[i + 1]);
downloadSerie->replace(i, i, downloadList[i + 1]);
}
uploadList[uploadList.count() - 1] = graphVUp;
downloadList[uploadList.count() - 1] = graphVDown;
uploadSerie->replace(29, 29, graphVUp);
downloadSerie->replace(29, 29, graphVDown);
//
max = MAX(MAX(graphVUp, graphVDown), historyMax);
speedChartObj->axes(Qt::Vertical).first()->setRange(0, max * 1.2);
//
//
totalSpeedUp = FormatBytes(_totalSpeedUp);
totalSpeedDown = FormatBytes(_totalSpeedDown);
totalDataUp = FormatBytes(_totalDataUp);
totalDataDown = FormatBytes(_totalDataDown);
//
netspeedLabel->setText(totalSpeedUp + "/s\r\n" + totalSpeedDown + "/s");
dataamountLabel->setText(totalDataUp + "\r\n" + totalDataDown);
//
hTray->setToolTip(TRAY_TOOLTIP_PREFIX "\r\n" + tr("Connected To Server: ") + CurrentConnectionName + "\r\nUp: " + totalSpeedUp + "/s Down: " + totalSpeedDown + "/s");
}
void MainWindow::on_duplicateBtn_clicked()
{
if (connectionListWidget->currentRow() < 0) {
return;
}
auto alias = connectionListWidget->currentItem()->text();
auto conf = ConvertConfigFromFile(QV2RAY_CONFIG_DIR + alias + QV2RAY_CONFIG_FILE_EXTENSION, false);
// Alias may change.
SaveConnectionConfig(conf, &alias, false);
auto config = GetGlobalConfig();
config.configs.push_back(alias.toStdString());
SetGlobalConfig(config);
this->OnConfigListChanged(false);
}