#include "dialogs/OptionsDialog.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "rpc/Protocol.hpp" #include "rpc/RpcClient.hpp" namespace velox::gui { namespace { struct BufferChoice { const char *label; qint64 bytes; }; // 64 KiB - 16 MiB, matching Settings.schema.json's connection.bufferBytes range. constexpr BufferChoice kBufferChoices[] = { {"64 KiB", 64 * 1024}, {"128 KiB", 128 * 1024}, {"256 KiB", 256 * 1024}, {"512 KiB", 512 * 1024}, {"1 MiB", 1024 * 1024}, {"2 MiB", 2 * 1024 * 1024}, {"4 MiB", 4 * 1024 * 1024}, {"8 MiB", 8 * 1024 * 1024}, {"16 MiB", 16 * 1024 * 1024}, }; void setBufferCombo(QComboBox *combo, qint64 bytes) { for (int i = 0; i < combo->count(); ++i) { if (combo->itemData(i).toLongLong() == bytes) { combo->setCurrentIndex(i); return; } } combo->setCurrentIndex(combo->count() / 2); // an unrecognized value: land near the middle } QStringList splitHosts(const QString &text) { QStringList out; for (const QString &h : text.split(QLatin1Char(','), Qt::SkipEmptyParts)) { out << h.trimmed(); } return out; } } // namespace QStringList OptionsDialog::allKeys() { return { "general.launchOnLogin", "general.minimizeToTray", "general.showDropTarget", "general.confirmOnExit", "general.language", "general.checkForUpdates", "saveTo.defaultDir", "saveTo.tempDir", "saveTo.fileExistsPolicy", "saveTo.createSubfolderPerSite", "connection.preset", "connection.maxSegmentsPerDownload", "connection.bufferBytes", "connection.maxConcurrentDownloads", "connection.timeoutSec", "connection.maxRetries", "connection.retryBackoffSec", "connection.maxTotalBufferBytes", "connection.maxActiveSegments", "downloads.speedLimitEnabled", "downloads.speedLimitBps", "downloads.virusScanCommand", "downloads.postDownloadCommand", "downloads.duplicatePolicy", "downloads.verifyChecksums", "proxy.mode", "proxy.host", "proxy.port", "proxy.username", "proxy.bypassHosts", "proxy.pacUrl", "sounds.enabled", "sounds.onComplete", "sounds.onQueueComplete", "sounds.onError", }; } QJsonObject OptionsDialog::diffChanged(const QJsonObject &original, const QJsonObject ¤t) { QJsonObject changed; for (auto it = current.constBegin(); it != current.constEnd(); ++it) { if (!original.contains(it.key()) || original.value(it.key()) != it.value()) { changed.insert(it.key(), it.value()); } } return changed; } OptionsDialog::OptionsDialog(rpc::RpcClient *client, QWidget *parent) : QDialog(parent), client_(client), tabs_(new QTabWidget(this)), statusLabel_(new QLabel(this)) { setWindowTitle(tr("Options")); resize(560, 480); buildGeneralTab(); buildSaveToTab(); buildConnectionTab(); buildDownloadsTab(); buildProxyTab(); buildSoundsTab(); statusLabel_->setStyleSheet(QStringLiteral("color: #c0392b;")); statusLabel_->hide(); auto *buttons = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel, this); connect(buttons, &QDialogButtonBox::accepted, this, [this] { apply(); accept(); }); connect(buttons->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &OptionsDialog::apply); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); auto *layout = new QVBoxLayout(this); layout->addWidget(tabs_); layout->addWidget(statusLabel_); layout->addWidget(buttons); tabs_->setEnabled(false); // stays disabled until settings.get answers QPointer self(this); client_->call(QString::fromLatin1(rpc::method::kSettingsGet), QJsonObject{{"keys", QJsonArray::fromStringList(allKeys())}}, [self](const rpc::RpcReply &reply) { if (!self) { return; } if (!reply.ok()) { self->statusLabel_->setText( tr("Could not load settings: %1").arg(reply.error.message)); self->statusLabel_->show(); return; } self->onSettingsLoaded(reply.result.toObject().value("values").toObject()); }); } void OptionsDialog::onSettingsLoaded(const QJsonObject &values) { original_ = values; populateFrom(values); tabs_->setEnabled(true); } void OptionsDialog::buildGeneralTab() { auto *page = new QWidget(this); launchOnLogin_ = new QCheckBox(tr("Launch on login"), page); minimizeToTray_ = new QCheckBox(tr("Minimize to tray"), page); showDropTarget_ = new QCheckBox(tr("Show floating drop target"), page); confirmOnExit_ = new QCheckBox(tr("Confirm on exit"), page); language_ = new QComboBox(page); language_->addItem(tr("System default"), QStringLiteral("system")); language_->addItem(tr("English"), QStringLiteral("en")); language_->addItem(tr("العربية"), QStringLiteral("ar")); checkForUpdates_ = new QCheckBox(tr("Check for updates"), page); auto *form = new QFormLayout(page); form->addRow(launchOnLogin_); form->addRow(minimizeToTray_); form->addRow(showDropTarget_); form->addRow(confirmOnExit_); form->addRow(tr("Language:"), language_); form->addRow(checkForUpdates_); tabs_->addTab(page, tr("General")); } void OptionsDialog::buildSaveToTab() { auto *page = new QWidget(this); defaultDir_ = new QLineEdit(page); tempDir_ = new QLineEdit(page); fileExistsPolicy_ = new QComboBox(page); fileExistsPolicy_->addItem(tr("Ask"), QStringLiteral("ask")); fileExistsPolicy_->addItem(tr("Rename"), QStringLiteral("rename")); fileExistsPolicy_->addItem(tr("Overwrite"), QStringLiteral("overwrite")); fileExistsPolicy_->addItem(tr("Resume"), QStringLiteral("resume")); createSubfolderPerSite_ = new QCheckBox(tr("Create a subfolder per site"), page); auto *defaultDirRow = new QWidget(page); auto *defaultDirLayout = new QHBoxLayout(defaultDirRow); defaultDirLayout->setContentsMargins(0, 0, 0, 0); auto *browseDefault = new QPushButton(tr("Browse…"), defaultDirRow); connect(browseDefault, &QPushButton::clicked, this, [this] { browseInto(defaultDir_, false); }); defaultDirLayout->addWidget(defaultDir_, 1); defaultDirLayout->addWidget(browseDefault); auto *tempDirRow = new QWidget(page); auto *tempDirLayout = new QHBoxLayout(tempDirRow); tempDirLayout->setContentsMargins(0, 0, 0, 0); auto *browseTemp = new QPushButton(tr("Browse…"), tempDirRow); connect(browseTemp, &QPushButton::clicked, this, [this] { browseInto(tempDir_, false); }); tempDirLayout->addWidget(tempDir_, 1); tempDirLayout->addWidget(browseTemp); auto *form = new QFormLayout(page); form->addRow(tr("Default download directory:"), defaultDirRow); form->addRow(tr("Temp folder:"), tempDirRow); form->addRow(tr("If a file already exists:"), fileExistsPolicy_); form->addRow(createSubfolderPerSite_); tabs_->addTab(page, tr("Save To")); } void OptionsDialog::buildConnectionTab() { auto *page = new QWidget(this); connectionPreset_ = new QComboBox(page); connectionPreset_->addItem(tr("Auto"), QStringLiteral("auto")); connectionPreset_->addItem(tr("LAN"), QStringLiteral("lan")); connectionPreset_->addItem(tr("Broadband"), QStringLiteral("broadband")); connectionPreset_->addItem(tr("Slow"), QStringLiteral("slow")); maxSegmentsPerDownload_ = new QSpinBox(page); maxSegmentsPerDownload_->setRange(1, 32); bufferBytes_ = new QComboBox(page); for (const auto &choice : kBufferChoices) { bufferBytes_->addItem(QString::fromLatin1(choice.label), QVariant::fromValue(choice.bytes)); } maxConcurrentDownloads_ = new QSpinBox(page); maxConcurrentDownloads_->setRange(1, 64); timeoutSec_ = new QSpinBox(page); timeoutSec_->setRange(1, 3600); timeoutSec_->setSuffix(tr(" s")); maxRetries_ = new QSpinBox(page); maxRetries_->setRange(0, 100); retryBackoffSec_ = new QSpinBox(page); retryBackoffSec_->setRange(0, 3600); retryBackoffSec_->setSuffix(tr(" s")); maxTotalBufferMiB_ = new QSpinBox(page); maxTotalBufferMiB_->setRange(16, 2048); maxTotalBufferMiB_->setSuffix(tr(" MiB")); maxActiveSegments_ = new QSpinBox(page); maxActiveSegments_->setRange(1, 256); auto *form = new QFormLayout(page); form->addRow(tr("Connection type:"), connectionPreset_); form->addRow(tr("Max connections per download:"), maxSegmentsPerDownload_); form->addRow(tr("Write buffer per connection:"), bufferBytes_); form->addRow(tr("Max concurrent downloads:"), maxConcurrentDownloads_); form->addRow(tr("Timeout:"), timeoutSec_); form->addRow(tr("Max retries:"), maxRetries_); form->addRow(tr("Retry backoff:"), retryBackoffSec_); form->addRow(tr("Max total buffer memory:"), maxTotalBufferMiB_); form->addRow(tr("Max active segments (global):"), maxActiveSegments_); tabs_->addTab(page, tr("Connection")); } void OptionsDialog::buildDownloadsTab() { auto *page = new QWidget(this); speedLimitEnabled_ = new QCheckBox(tr("Limit download speed by default"), page); speedLimitKiBps_ = new QSpinBox(page); speedLimitKiBps_->setRange(0, 1000000); speedLimitKiBps_->setSuffix(tr(" KiB/s")); connect(speedLimitEnabled_, &QCheckBox::toggled, speedLimitKiBps_, &QWidget::setEnabled); virusScanCommand_ = new QLineEdit(page); virusScanCommand_->setPlaceholderText(tr("e.g. clamscan %f")); postDownloadCommand_ = new QLineEdit(page); postDownloadCommand_->setPlaceholderText(tr("e.g. notify-send Downloaded %f")); duplicatePolicy_ = new QComboBox(page); duplicatePolicy_->addItem(tr("Ask"), QStringLiteral("ask")); duplicatePolicy_->addItem(tr("Skip"), QStringLiteral("skip")); duplicatePolicy_->addItem(tr("Rename"), QStringLiteral("rename")); duplicatePolicy_->addItem(tr("Redownload"), QStringLiteral("redownload")); verifyChecksums_ = new QCheckBox(tr("Verify checksums when available"), page); auto *form = new QFormLayout(page); form->addRow(speedLimitEnabled_); form->addRow(tr("Speed limit:"), speedLimitKiBps_); form->addRow(tr("Virus-scan command:"), virusScanCommand_); form->addRow(tr("Post-download command:"), postDownloadCommand_); form->addRow(tr("Duplicate URL:"), duplicatePolicy_); form->addRow(verifyChecksums_); tabs_->addTab(page, tr("Downloads")); } void OptionsDialog::buildProxyTab() { auto *page = new QWidget(this); proxyMode_ = new QComboBox(page); proxyMode_->addItem(tr("System"), QStringLiteral("system")); proxyMode_->addItem(tr("None"), QStringLiteral("none")); proxyMode_->addItem(tr("HTTP"), QStringLiteral("http")); proxyMode_->addItem(tr("HTTPS"), QStringLiteral("https")); proxyMode_->addItem(tr("SOCKS5"), QStringLiteral("socks5")); proxyMode_->addItem(tr("Automatic (PAC)"), QStringLiteral("pac")); proxyHost_ = new QLineEdit(page); proxyPort_ = new QSpinBox(page); proxyPort_->setRange(1, 65535); proxyUsername_ = new QLineEdit(page); proxyBypassHosts_ = new QLineEdit(page); proxyBypassHosts_->setPlaceholderText(tr("comma-separated, e.g. localhost, *.internal")); proxyPacUrl_ = new QLineEdit(page); const auto updateEnabled = [this] { const QString mode = proxyMode_->currentData().toString(); const bool manual = mode == QLatin1String("http") || mode == QLatin1String("https") || mode == QLatin1String("socks5"); proxyHost_->setEnabled(manual); proxyPort_->setEnabled(manual); proxyUsername_->setEnabled(manual); proxyPacUrl_->setEnabled(mode == QLatin1String("pac")); }; connect(proxyMode_, &QComboBox::currentIndexChanged, this, updateEnabled); updateEnabled(); auto *form = new QFormLayout(page); form->addRow(tr("Mode:"), proxyMode_); form->addRow(tr("Host:"), proxyHost_); form->addRow(tr("Port:"), proxyPort_); form->addRow(tr("Username:"), proxyUsername_); form->addRow( new QLabel(tr("(password is stored in the Secret Service, set separately)"), page)); form->addRow(tr("Bypass for:"), proxyBypassHosts_); form->addRow(tr("PAC URL:"), proxyPacUrl_); tabs_->addTab(page, tr("Proxy")); } void OptionsDialog::buildSoundsTab() { auto *page = new QWidget(this); soundsEnabled_ = new QCheckBox(tr("Play sounds"), page); const auto soundRow = [this, page](QLineEdit *&edit) { auto *row = new QWidget(page); auto *layout = new QHBoxLayout(row); layout->setContentsMargins(0, 0, 0, 0); edit = new QLineEdit(row); auto *browse = new QPushButton(tr("Browse…"), row); connect(browse, &QPushButton::clicked, this, [this, edit] { browseInto(edit, true); }); layout->addWidget(edit, 1); layout->addWidget(browse); return row; }; auto *completeRow = soundRow(soundOnComplete_); auto *queueRow = soundRow(soundOnQueueComplete_); auto *errorRow = soundRow(soundOnError_); connect(soundsEnabled_, &QCheckBox::toggled, completeRow, &QWidget::setEnabled); connect(soundsEnabled_, &QCheckBox::toggled, queueRow, &QWidget::setEnabled); connect(soundsEnabled_, &QCheckBox::toggled, errorRow, &QWidget::setEnabled); auto *form = new QFormLayout(page); form->addRow(soundsEnabled_); form->addRow(tr("Download complete:"), completeRow); form->addRow(tr("Queue complete:"), queueRow); form->addRow(tr("Error:"), errorRow); tabs_->addTab(page, tr("Sounds")); } void OptionsDialog::browseInto(QLineEdit *target, bool pickFile) { const QString picked = pickFile ? QFileDialog::getOpenFileName(this, tr("Choose a sound file"), target->text()) : QFileDialog::getExistingDirectory(this, tr("Choose a folder"), target->text()); if (!picked.isEmpty()) { target->setText(picked); } } void OptionsDialog::populateFrom(const QJsonObject &v) { launchOnLogin_->setChecked(v.value("general.launchOnLogin").toBool()); minimizeToTray_->setChecked(v.value("general.minimizeToTray").toBool()); showDropTarget_->setChecked(v.value("general.showDropTarget").toBool(true)); confirmOnExit_->setChecked(v.value("general.confirmOnExit").toBool()); const int langIdx = language_->findData(v.value("general.language").toString("system")); language_->setCurrentIndex(langIdx >= 0 ? langIdx : 0); checkForUpdates_->setChecked(v.value("general.checkForUpdates").toBool(true)); defaultDir_->setText(v.value("saveTo.defaultDir").toString()); tempDir_->setText(v.value("saveTo.tempDir").toString()); const int policyIdx = fileExistsPolicy_->findData(v.value("saveTo.fileExistsPolicy").toString("ask")); fileExistsPolicy_->setCurrentIndex(policyIdx >= 0 ? policyIdx : 0); createSubfolderPerSite_->setChecked(v.value("saveTo.createSubfolderPerSite").toBool()); const int presetIdx = connectionPreset_->findData(v.value("connection.preset").toString("auto")); connectionPreset_->setCurrentIndex(presetIdx >= 0 ? presetIdx : 0); maxSegmentsPerDownload_->setValue( static_cast(v.value("connection.maxSegmentsPerDownload").toInt(8))); setBufferCombo(bufferBytes_, static_cast(v.value("connection.bufferBytes").toDouble(1048576))); maxConcurrentDownloads_->setValue( static_cast(v.value("connection.maxConcurrentDownloads").toInt(5))); timeoutSec_->setValue(static_cast(v.value("connection.timeoutSec").toInt(30))); maxRetries_->setValue(static_cast(v.value("connection.maxRetries").toInt(3))); retryBackoffSec_->setValue(static_cast(v.value("connection.retryBackoffSec").toInt(5))); maxTotalBufferMiB_->setValue(static_cast( v.value("connection.maxTotalBufferBytes").toDouble(134217728) / (1024 * 1024))); maxActiveSegments_->setValue( static_cast(v.value("connection.maxActiveSegments").toInt(32))); speedLimitEnabled_->setChecked(v.value("downloads.speedLimitEnabled").toBool()); speedLimitKiBps_->setValue( static_cast(v.value("downloads.speedLimitBps").toDouble() / 1024)); speedLimitKiBps_->setEnabled(speedLimitEnabled_->isChecked()); virusScanCommand_->setText(v.value("downloads.virusScanCommand").toString()); postDownloadCommand_->setText(v.value("downloads.postDownloadCommand").toString()); const int dupIdx = duplicatePolicy_->findData(v.value("downloads.duplicatePolicy").toString("ask")); duplicatePolicy_->setCurrentIndex(dupIdx >= 0 ? dupIdx : 0); verifyChecksums_->setChecked(v.value("downloads.verifyChecksums").toBool()); const int modeIdx = proxyMode_->findData(v.value("proxy.mode").toString("system")); proxyMode_->setCurrentIndex(modeIdx >= 0 ? modeIdx : 0); proxyHost_->setText(v.value("proxy.host").toString()); proxyPort_->setValue(v.value("proxy.port").toInt(1080)); proxyUsername_->setText(v.value("proxy.username").toString()); QStringList bypass; for (const QJsonValue &h : v.value("proxy.bypassHosts").toArray()) { bypass << h.toString(); } proxyBypassHosts_->setText(bypass.join(QStringLiteral(", "))); proxyPacUrl_->setText(v.value("proxy.pacUrl").toString()); soundsEnabled_->setChecked(v.value("sounds.enabled").toBool()); soundOnComplete_->setText(v.value("sounds.onComplete").toString()); soundOnQueueComplete_->setText(v.value("sounds.onQueueComplete").toString()); soundOnError_->setText(v.value("sounds.onError").toString()); } QJsonObject OptionsDialog::currentValues() const { QJsonObject v; v["general.launchOnLogin"] = launchOnLogin_->isChecked(); v["general.minimizeToTray"] = minimizeToTray_->isChecked(); v["general.showDropTarget"] = showDropTarget_->isChecked(); v["general.confirmOnExit"] = confirmOnExit_->isChecked(); v["general.language"] = language_->currentData().toString(); v["general.checkForUpdates"] = checkForUpdates_->isChecked(); v["saveTo.defaultDir"] = defaultDir_->text(); v["saveTo.tempDir"] = tempDir_->text(); v["saveTo.fileExistsPolicy"] = fileExistsPolicy_->currentData().toString(); v["saveTo.createSubfolderPerSite"] = createSubfolderPerSite_->isChecked(); v["connection.preset"] = connectionPreset_->currentData().toString(); v["connection.maxSegmentsPerDownload"] = maxSegmentsPerDownload_->value(); v["connection.bufferBytes"] = bufferBytes_->currentData().toLongLong(); v["connection.maxConcurrentDownloads"] = maxConcurrentDownloads_->value(); v["connection.timeoutSec"] = timeoutSec_->value(); v["connection.maxRetries"] = maxRetries_->value(); v["connection.retryBackoffSec"] = retryBackoffSec_->value(); v["connection.maxTotalBufferBytes"] = static_cast(maxTotalBufferMiB_->value()) * 1024 * 1024; v["connection.maxActiveSegments"] = maxActiveSegments_->value(); v["downloads.speedLimitEnabled"] = speedLimitEnabled_->isChecked(); v["downloads.speedLimitBps"] = static_cast(speedLimitKiBps_->value()) * 1024; v["downloads.virusScanCommand"] = virusScanCommand_->text(); v["downloads.postDownloadCommand"] = postDownloadCommand_->text(); v["downloads.duplicatePolicy"] = duplicatePolicy_->currentData().toString(); v["downloads.verifyChecksums"] = verifyChecksums_->isChecked(); v["proxy.mode"] = proxyMode_->currentData().toString(); v["proxy.host"] = proxyHost_->text(); v["proxy.port"] = proxyPort_->value(); v["proxy.username"] = proxyUsername_->text(); v["proxy.bypassHosts"] = QJsonArray::fromStringList(splitHosts(proxyBypassHosts_->text())); v["proxy.pacUrl"] = proxyPacUrl_->text(); v["sounds.enabled"] = soundsEnabled_->isChecked(); v["sounds.onComplete"] = soundOnComplete_->text(); v["sounds.onQueueComplete"] = soundOnQueueComplete_->text(); v["sounds.onError"] = soundOnError_->text(); return v; } void OptionsDialog::apply() { const QJsonObject changed = diffChanged(original_, currentValues()); if (changed.isEmpty()) { return; } QPointer self(this); client_->call(QString::fromLatin1(rpc::method::kSettingsSet), QJsonObject{{"values", changed}}, [self](const rpc::RpcReply &reply) { if (!self) { return; } if (!reply.ok()) { self->statusLabel_->setText( tr("Could not save settings: %1").arg(reply.error.message)); self->statusLabel_->show(); return; } self->statusLabel_->hide(); const QJsonObject written = reply.result.toObject().value("values").toObject(); for (auto it = written.constBegin(); it != written.constEnd(); ++it) { self->original_.insert(it.key(), it.value()); } }); } } // namespace velox::gui