gui: Options, Scheduler, Speed Limiter, Batch, Grabber, and the tray icon

Continues the build order past the Add-URL/File-Info/Progress dialog flow.

- OptionsDialog: General/Save To/Connection/Downloads/Proxy/Sounds tabs, every
  control bound to a real settings.* key (contracts/schema/types/Settings.
  schema.json). The spec's File Types and Site Logins tabs have no settings.*
  backing (categories go through category.upsert, credentials through the
  Secret Service) so they don't exist here — a tab either binds to a real key
  or isn't shipped. diffChanged() sends only what actually changed, matching
  settings.set's "changed[] names exactly what took effect" contract.
- SchedulerDialog: per-queue schedule (schedule.get/set) plus maxConcurrent/
  onComplete (queue.upsert), Start Now/Stop. Queue.schema.json already carries
  the schedule so queue.list alone seeds the window.
- SpeedLimiterDialog: the live global limiter (limiter.get/set) — a different
  thing from Options' downloads.speedLimit* default. buildParams() enforces
  the schema's "0 with enabled true must not be offered".
- BatchDialog: clipboard-blob and {start..end}-wildcard tabs sharing one
  category/queue/start-mode footer into download.addBatch.
- GrabberWizard: 4-step QWizard (project label -> start URL/depth/filters ->
  file-type filter -> review), grabber.start feeding a poll+event.grabber.
  progress-driven review page, Finish = grabber.harvest for the checked files.
- TrayIcon: active-count tooltip, Show/Add URL/Pause All/Resume All/Speed
  Limiter submenu/Quit. Quit only closes the GUI — there is no RPC to stop
  veloxd itself, filed as a new gap in daemon-requests-m1.md. MainWindow now
  also hides to tray instead of closing when general.minimizeToTray is set.

Every dialog's non-widget logic (diffChanged, buildSchedule, buildParams,
parseUrlBlob/expandWildcard/buildAddBatchParams, buildFileTypes/
buildStartParams/buildHarvestParams) is a static pure function with its own
test, same shape as FileInfoDialog::buildSpec from the previous round.

Verified end-to-end against a running mockd under ASan+UBSan: all five
surfaces render real data (settings.get values, queue.list's two seeded
queues, limiter.get, a live grabber.start/status crawl returning 3 files) with
no sanitizer reports. gui-check (non-ASan) and dev (ASan+UBSan) presets both
build the whole repo clean; all gui-labeled ctest targets pass.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
This commit is contained in:
2026-09-11 17:17:43 +04:00
co-authored by Claude Sonnet 5
parent 39c69f3871
commit de83ee3cce
22 changed files with 2678 additions and 13 deletions
+518
View File
@@ -0,0 +1,518 @@
#include "dialogs/OptionsDialog.hpp"
#include <QCheckBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QJsonArray>
#include <QLabel>
#include <QLineEdit>
#include <QPointer>
#include <QPushButton>
#include <QSpinBox>
#include <QTabWidget>
#include <QVBoxLayout>
#include <QWidget>
#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 &current) {
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<OptionsDialog> 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<int>(v.value("connection.maxSegmentsPerDownload").toInt(8)));
setBufferCombo(bufferBytes_,
static_cast<qint64>(v.value("connection.bufferBytes").toDouble(1048576)));
maxConcurrentDownloads_->setValue(
static_cast<int>(v.value("connection.maxConcurrentDownloads").toInt(5)));
timeoutSec_->setValue(static_cast<int>(v.value("connection.timeoutSec").toInt(30)));
maxRetries_->setValue(static_cast<int>(v.value("connection.maxRetries").toInt(3)));
retryBackoffSec_->setValue(static_cast<int>(v.value("connection.retryBackoffSec").toInt(5)));
maxTotalBufferMiB_->setValue(static_cast<int>(
v.value("connection.maxTotalBufferBytes").toDouble(134217728) / (1024 * 1024)));
maxActiveSegments_->setValue(
static_cast<int>(v.value("connection.maxActiveSegments").toInt(32)));
speedLimitEnabled_->setChecked(v.value("downloads.speedLimitEnabled").toBool());
speedLimitKiBps_->setValue(
static_cast<int>(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<qint64>(maxTotalBufferMiB_->value()) * 1024 * 1024;
v["connection.maxActiveSegments"] = maxActiveSegments_->value();
v["downloads.speedLimitEnabled"] = speedLimitEnabled_->isChecked();
v["downloads.speedLimitBps"] = static_cast<qint64>(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<OptionsDialog> 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