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
+110
View File
@@ -0,0 +1,110 @@
// The Options dialog. Lane GUI.
//
// docs/03-gui-spec.md §4: every control here maps 1:1 onto a settings.* key from
// contracts/schema/types/Settings.schema.json. The spec's "File Types" and "Site Logins"
// tabs have no backing key (per-category extension lists live on Category via
// category.upsert, not settings.*; login credentials go to the Secret Service) — a real
// tab either binds to a real key or does not exist here, so those two are left out rather
// than shipped as fake affordances.
#pragma once
#include <QDialog>
#include <QJsonObject>
class QCheckBox;
class QComboBox;
class QLabel;
class QLineEdit;
class QSpinBox;
class QTabWidget;
namespace velox::gui {
namespace rpc {
class RpcClient;
} // namespace rpc
class OptionsDialog : public QDialog {
Q_OBJECT
public:
explicit OptionsDialog(rpc::RpcClient *client, QWidget *parent = nullptr);
/// The settings.* keys every tab's controls bind to, in the order settings.get is
/// called with. Exposed so a test can assert nothing here drifts from the schema.
static QStringList allKeys();
/// Pure: keys in `current` whose value differs from `original` (or is altogether
/// absent from `original`). settings.set is documented to write and broadcast exactly
/// the keys present in its params, so sending only what actually changed matters.
static QJsonObject diffChanged(const QJsonObject &original, const QJsonObject &current);
private slots:
void onSettingsLoaded(const QJsonObject &values);
void apply();
private:
void buildGeneralTab();
void buildSaveToTab();
void buildConnectionTab();
void buildDownloadsTab();
void buildProxyTab();
void buildSoundsTab();
void populateFrom(const QJsonObject &values);
QJsonObject currentValues() const;
void browseInto(QLineEdit *target, bool pickFile);
rpc::RpcClient *client_;
QJsonObject original_; // last-known-saved values, keyed by settings.* name
QTabWidget *tabs_;
QLabel *statusLabel_;
// General
QCheckBox *launchOnLogin_;
QCheckBox *minimizeToTray_;
QCheckBox *showDropTarget_;
QCheckBox *confirmOnExit_;
QComboBox *language_;
QCheckBox *checkForUpdates_;
// Save To
QLineEdit *defaultDir_;
QLineEdit *tempDir_;
QComboBox *fileExistsPolicy_;
QCheckBox *createSubfolderPerSite_;
// Connection
QComboBox *connectionPreset_;
QSpinBox *maxSegmentsPerDownload_;
QComboBox *bufferBytes_;
QSpinBox *maxConcurrentDownloads_;
QSpinBox *timeoutSec_;
QSpinBox *maxRetries_;
QSpinBox *retryBackoffSec_;
QSpinBox *maxTotalBufferMiB_;
QSpinBox *maxActiveSegments_;
// Downloads
QCheckBox *speedLimitEnabled_;
QSpinBox *speedLimitKiBps_;
QLineEdit *virusScanCommand_;
QLineEdit *postDownloadCommand_;
QComboBox *duplicatePolicy_;
QCheckBox *verifyChecksums_;
// Proxy
QComboBox *proxyMode_;
QLineEdit *proxyHost_;
QSpinBox *proxyPort_;
QLineEdit *proxyUsername_;
QLineEdit *proxyBypassHosts_;
QLineEdit *proxyPacUrl_;
// Sounds
QCheckBox *soundsEnabled_;
QLineEdit *soundOnComplete_;
QLineEdit *soundOnQueueComplete_;
QLineEdit *soundOnError_;
};
} // namespace velox::gui