diff --git a/Build.Counter b/Build.Counter index 629f34ce..c15fb720 100644 --- a/Build.Counter +++ b/Build.Counter @@ -1 +1 @@ -485 +501 diff --git a/src/Qv2rayBase.hpp b/src/Qv2rayBase.hpp index 1e8f20a4..ee94ffa9 100644 --- a/src/Qv2rayBase.hpp +++ b/src/Qv2rayBase.hpp @@ -1,6 +1,7 @@ #ifndef QV2RAYBASE_H #define QV2RAYBASE_H #include +#include #include "QvTinyLog.hpp" #include "QvCoreConfigObjects.hpp" #include "QvNetSpeedPlugin.hpp" diff --git a/src/QvCoreConfigOperations.cpp b/src/QvCoreConfigOperations.cpp index 681694d0..4773c969 100644 --- a/src/QvCoreConfigOperations.cpp +++ b/src/QvCoreConfigOperations.cpp @@ -4,6 +4,7 @@ namespace Qv2ray { namespace ConfigOperations { + QMap GetConnections(list connectionNames) { QMap list; @@ -16,7 +17,6 @@ namespace Qv2ray return list; } - int StartPreparation(QJsonObject fullConfig) { // Writes the final configuration to the disk. @@ -24,7 +24,6 @@ namespace Qv2ray StringToFile(&json, new QFile(QV2RAY_GENERATED_FILE_PATH)); return 0; } - int FindIndexByTag(QJsonArray list, QString *tag) { for (int i = 0; i < list.count(); i++) { diff --git a/src/QvCoreConfigOperations.hpp b/src/QvCoreConfigOperations.hpp index 72916468..20fd3a04 100644 --- a/src/QvCoreConfigOperations.hpp +++ b/src/QvCoreConfigOperations.hpp @@ -34,6 +34,7 @@ namespace Qv2ray // VMess URI Protocol QJsonObject ConvertConfigFromVMessString(QString vmess, QString *alias, QString *errMessage); QJsonObject ConvertConfigFromFile(QString sourceFilePath, bool overrideInbounds); + QString ConvertConfigToVMessString(const StreamSettingsObject &transfer, const VMessServerObject &serverConfig, const QString &alias); } // diff --git a/src/QvCoreConfigOperations_Convertion.cpp b/src/QvCoreConfigOperations_Convertion.cpp index a66f07f2..4b3a9a78 100644 --- a/src/QvCoreConfigOperations_Convertion.cpp +++ b/src/QvCoreConfigOperations_Convertion.cpp @@ -6,6 +6,45 @@ namespace Qv2ray { namespace Conversion { + // From https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) + QString ConvertConfigToVMessString(const StreamSettingsObject &transfer, const VMessServerObject &serverConfig, const QString &alias) + { + QJsonObject vmessUriRoot; + // Constant + vmessUriRoot["v"] = 2; + vmessUriRoot["ps"] = alias; + vmessUriRoot["add"] = QSTRING(serverConfig.address); + vmessUriRoot["port"] = serverConfig.port; + vmessUriRoot["id"] = QSTRING(serverConfig.users.front().id); + vmessUriRoot["aid"] = serverConfig.users.front().alterId; + vmessUriRoot["net"] = QSTRING(transfer.network); + vmessUriRoot["tls"] = QSTRING(transfer.security); + + if (transfer.network == "tcp") { + vmessUriRoot["type"] = QSTRING(transfer.tcpSettings.header.type); + } else if (transfer.network == "kcp") { + vmessUriRoot["type"] = QSTRING(transfer.kcpSettings.header.type); + } else if (transfer.network == "quic") { + vmessUriRoot["type"] = QSTRING(transfer.quicSettings.header.type); + vmessUriRoot["host"] = QSTRING(transfer.quicSettings.security); + vmessUriRoot["path"] = QSTRING(transfer.quicSettings.key); + } else if (transfer.network == "ws") { + auto x = QMap(transfer.wsSettings.headers); + auto host = x.contains("host"); + auto CapHost = x.contains("Host"); + auto realHost = host ? x["host"] : (CapHost ? x["Host"] : ""); + // + vmessUriRoot["host"] = QSTRING(realHost); + vmessUriRoot["path"] = QSTRING(transfer.wsSettings.path); + } else if (transfer.network == "h2" || transfer.network == "http") { + vmessUriRoot["host"] = Stringify(transfer.httpSettings.host, ","); + vmessUriRoot["path"] = QSTRING(transfer.httpSettings.path); + } + + // + auto vmessPart = Base64Encode(JsonToString(vmessUriRoot, QJsonDocument::JsonFormat::Compact)); + return "vmess://" + vmessPart; + } QString DecodeSubscriptionString(QByteArray arr) { // Some subscription providers may use plain vmess:// saperated by lines @@ -150,7 +189,7 @@ namespace Qv2ray if (net == "tcp") { streaming.tcpSettings.header.type = type; - } else if (net == "http") { + } else if (net == "http" || net == "h2") { // Fill hosts for HTTP for (auto _host : QString::fromStdString(host).split(',')) { streaming.httpSettings.host.push_back(_host.toStdString()); diff --git a/src/QvUtils.cpp b/src/QvUtils.cpp index 3d11c8c9..15df8809 100644 --- a/src/QvUtils.cpp +++ b/src/QvUtils.cpp @@ -104,18 +104,18 @@ namespace Qv2ray return JsonFromString(json); } - QString JsonToString(QJsonObject json) + QString JsonToString(QJsonObject json, QJsonDocument::JsonFormat format) { QJsonDocument doc; doc.setObject(json); - return doc.toJson(); + return doc.toJson(format); } - QString JsonToString(QJsonArray array) + QString JsonToString(QJsonArray array, QJsonDocument::JsonFormat format) { QJsonDocument doc; doc.setArray(array); - return doc.toJson(); + return doc.toJson(format); } QString VerifyJsonString(const QString *source) diff --git a/src/QvUtils.hpp b/src/QvUtils.hpp index 3727e174..e95e2664 100644 --- a/src/QvUtils.hpp +++ b/src/QvUtils.hpp @@ -37,8 +37,8 @@ namespace Qv2ray bool StringToFile(const QString *text, QFile *target); // QJsonObject JsonFromString(QString string); - QString JsonToString(QJsonObject json); - QString JsonToString(QJsonArray array); + QString JsonToString(QJsonObject json, QJsonDocument::JsonFormat format = QJsonDocument::JsonFormat::Indented); + QString JsonToString(QJsonArray array, QJsonDocument::JsonFormat format = QJsonDocument::JsonFormat::Indented); // QString VerifyJsonString(const QString *source); // diff --git a/src/ui/w_ExportConfig.cpp b/src/ui/w_ExportConfig.cpp index 464abc10..16b03ddf 100644 --- a/src/ui/w_ExportConfig.cpp +++ b/src/ui/w_ExportConfig.cpp @@ -18,7 +18,7 @@ ConfigExporter::ConfigExporter(const QString &data, QWidget *parent): ConfigExpo { QZXingEncoderConfig conf; conf.border = true; - conf.errorCorrectionLevel = QZXing::EncodeErrorCorrectionLevel_Q; + conf.imageSize = QSize(400, 400); auto img = qzxing.encodeData(data, conf); image = img.copy(); message = data; @@ -40,6 +40,10 @@ void ConfigExporter::changeEvent(QEvent *e) retranslateUi(this); break; + case QEvent::Resize: + imageLabel->setPixmap(QPixmap::fromImage(image)); + break; + default: break; } diff --git a/src/ui/w_ExportConfig.ui b/src/ui/w_ExportConfig.ui index 2c623d34..5eae2daf 100644 --- a/src/ui/w_ExportConfig.ui +++ b/src/ui/w_ExportConfig.ui @@ -16,24 +16,21 @@ + + QFrame::Box + - Image + + + + :/icons/qv2ray.ico + + + false Qt::AlignCenter - - true - - - 0 - - - -1 - - - Qt::NoTextInteraction - @@ -83,6 +80,8 @@ - + + + diff --git a/src/ui/w_ImportConfig.cpp b/src/ui/w_ImportConfig.cpp index 1bab98b4..e8795a84 100644 --- a/src/ui/w_ImportConfig.cpp +++ b/src/ui/w_ImportConfig.cpp @@ -12,6 +12,7 @@ #include "QvCoreConfigOperations.hpp" #include "w_OutboundEditor.hpp" +#include "w_JsonEditor.hpp" #include "w_ImportConfig.hpp" @@ -19,7 +20,7 @@ ImportConfigWindow::ImportConfigWindow(QWidget *parent) : QDialog(parent) { setupUi(this); - nameTxt->setText(QDateTime::currentDateTime().toString("yyyy-MM-dd_hh-MM-ss") + "_.imported"); + nameTxt->setText(QDateTime::currentDateTime().toString("yyyy-MM-dd_hh-mm-ss") + "_imported"); } void ImportConfigWindow::on_importSourceCombo_currentIndexChanged(int index) @@ -35,28 +36,29 @@ void ImportConfigWindow::on_selectFileBtn_clicked() void ImportConfigWindow::on_qrFromScreenBtn_clicked() { - LOG(MODULE_UI, "We currently only support the primary screen.") QThread::msleep(static_cast(doubleSpinBox->value() * 1000)); - QScreen *screen = qApp->primaryScreen(); + bool hasVmessDetected = false; - if (const QWindow *window = windowHandle()) { - screen = window->screen(); + for (auto screen : qApp->screens()) { + if (!screen) { + LOG(MODULE_UI, "Cannot even find a screen. RARE") + QvMessageBox(this, tr("Screenshot failed"), tr("Cannot find a valid screen, it's rare.")); + return; + } + + auto pix = screen->grabWindow(0); + auto str = QZXing().decodeImage(pix.toImage()); + + if (str.isEmpty()) { + continue; + } else { + vmessConnectionStringTxt->appendPlainText(str.trimmed() + NEWLINE); + hasVmessDetected = true; + } } - if (!screen) { - LOG(MODULE_UI, "Cannot even find a screen.") - QvMessageBox(this, tr("Screenshot failed"), tr("Cannot find a valid screen, it's rare.")); - return; - } - - auto pix = screen->grabWindow(0); - auto str = QZXing().decodeImage(pix.toImage()); - - if (str.isEmpty()) { - QvMessageBox(this, tr("QRCode scanning failed"), tr("Cannot find any QRCode from the primary screen.")); - return; - } else { - vmessConnectionStringTxt->appendPlainText(str.trimmed() + NEWLINE); + if (!hasVmessDetected) { + QvMessageBox(this, tr("QRCode scanning failed"), tr("Cannot find any QRCode from any screens.")); } } @@ -154,6 +156,20 @@ void ImportConfigWindow::on_selectImageBtn_clicked() { QString dir = QFileDialog::getOpenFileName(this, tr("Select an image to import")); imageFileEdit->setText(dir); + // + QFile file(dir); + file.open(QFile::OpenModeFlag::ReadOnly); + auto buf = file.readAll(); + file.close(); + // + auto str = QZXing().decodeImage(QImage::fromData(buf)); + + if (str.isEmpty()) { + QvMessageBox(this, tr("QRCode scanning failed"), tr("Cannot find any QRCode from the image.")); + return; + } else { + vmessConnectionStringTxt->appendPlainText(str.trimmed() + NEWLINE); + } } void ImportConfigWindow::on_errorsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) @@ -180,3 +196,42 @@ void ImportConfigWindow::on_errorsList_currentItemChanged(QListWidgetItem *curre c.setPosition(endPos, QTextCursor::KeepAnchor); vmessConnectionStringTxt->setTextCursor(c); } + +void ImportConfigWindow::on_editFileBtn_clicked() +{ + QFile file(fileLineTxt->text()); + + if (!file.exists()) { + QvMessageBox(this, tr("Edit file as JSON"), tr("Provided file not found: ") + fileLineTxt->text()); + return; + } + + auto jsonString = StringFromFile(&file); + auto jsonCheckingError = VerifyJsonString(&jsonString); + + if (!jsonCheckingError.isEmpty()) { + LOG(MODULE_FILE, "Currupted JSON file detected") + + if (QvMessageBoxAsk(this, tr("Edit file as JSON"), tr("The file you selected has json syntax error. Continue editing may make you lose data. Would you like to continue?") + + NEWLINE + jsonCheckingError) != QMessageBox::Yes) { + return; + } else { + LOG(MODULE_FILE, "Continue editing curruped json file, data loss is expected.") + } + } + + auto json = JsonFromString(jsonString); + auto editor = new JsonEditor(json, this); + json = editor->OpenEditor(); + + if (editor->result() == QDialog::Accepted) { + auto str = JsonToString(json); + bool result = StringToFile(&str, &file); + + if (!result) { + QvMessageBox(this, tr("Edit file as JSON"), tr("Failed to save file, please check if you have the required permissions")); + } + } else { + LOG(MODULE_FILE, "Canceled saving a file.") + } +} diff --git a/src/ui/w_ImportConfig.hpp b/src/ui/w_ImportConfig.hpp index b5218206..0be46d33 100644 --- a/src/ui/w_ImportConfig.hpp +++ b/src/ui/w_ImportConfig.hpp @@ -23,6 +23,8 @@ class ImportConfigWindow : public QDialog, private Ui::ImportConfigWindow void on_selectImageBtn_clicked(); void on_errorsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); + void on_editFileBtn_clicked(); + private: QMap vmessErrors; }; diff --git a/src/ui/w_ImportConfig.ui b/src/ui/w_ImportConfig.ui index de015df1..5b35fc08 100644 --- a/src/ui/w_ImportConfig.ui +++ b/src/ui/w_ImportConfig.ui @@ -81,7 +81,7 @@ - 1 + 0 @@ -132,7 +132,7 @@ - + Open in JSON Editor @@ -188,29 +188,6 @@ - - - - Delay in secs - - - - - - - 5.000000000000000 - - - 0.250000000000000 - - - QAbstractSpinBox::AdaptiveDecimalStepType - - - 1.000000000000000 - - - @@ -230,6 +207,36 @@ + + + + After + + + + + + + 5.000000000000000 + + + 0.500000000000000 + + + QAbstractSpinBox::AdaptiveDecimalStepType + + + 1.000000000000000 + + + + + + + second(s). + + + diff --git a/src/ui/w_MainWindow.cpp b/src/ui/w_MainWindow.cpp index 060887f9..821af33a 100644 --- a/src/ui/w_MainWindow.cpp +++ b/src/ui/w_MainWindow.cpp @@ -400,7 +400,8 @@ void MainWindow::ShowAndSetConnection(QString guiConnectionName, bool SetConnect if (guiConnectionName.isEmpty()) return; // --------- BRGIN Show Connection - auto outBoundRoot = (connections[guiConnectionName])["outbounds"].toArray().first().toObject(); + auto root = connections[guiConnectionName]; + auto outBoundRoot = root["outbounds"].toArray().first().toObject(); // auto outboundType = outBoundRoot["protocol"].toString(); _OutBoundTypeLabel->setText(outboundType); @@ -425,7 +426,7 @@ void MainWindow::ShowAndSetConnection(QString guiConnectionName, bool SetConnect _portLabel->setText(QSTRING(to_string(Server.port))); } - routeCountLabel->setText(QString::number((connections[guiConnectionName])["routing"].toArray().count())); + routeCountLabel->setText(QString::number(root["routing"].toObject()["rules"].toArray().count())); // --------- END Show Connection // @@ -683,8 +684,25 @@ void MainWindow::on_pingTestBtn_clicked() void MainWindow::on_shareBtn_clicked() { // Share QR - ConfigExporter v("FUTURE VMESS:// GOES HERE!", this); - v.OpenExport(); + 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 (outboundType == "vmess") { + auto vmessServer = StructFromJsonString(JsonToString(outBoundRoot["settings"].toObject()["vnext"].toArray().first().toObject())); + auto transport = StructFromJsonString(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() diff --git a/src/ui/w_PrefrencesWindow.ui b/src/ui/w_PrefrencesWindow.ui index c6100664..5a12b121 100644 --- a/src/ui/w_PrefrencesWindow.ui +++ b/src/ui/w_PrefrencesWindow.ui @@ -45,7 +45,7 @@ QTabWidget::Rounded - 2 + 0