#include "dialogs/BatchDialog.hpp" #include #include #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 { constexpr int kMaxBatchItems = 5000; // download.addBatch's items.maxItems bool looksLikeUrl(const QString &s) { const QUrl u(s); return u.isValid() && !u.host().isEmpty() && (u.scheme() == QLatin1String("http") || u.scheme() == QLatin1String("https")); } } // namespace QStringList BatchDialog::parseUrlBlob(const QString &text) { QStringList out; QSet seen; for (const QString &rawLine : text.split(QLatin1Char('\n'))) { const QString line = rawLine.trimmed(); if (line.isEmpty() || !looksLikeUrl(line) || seen.contains(line)) { continue; } seen.insert(line); out << line; } return out; } QStringList BatchDialog::expandWildcard(const QString &pattern) { static const QRegularExpression kRange(QStringLiteral(R"(\{(\d+)\.\.(\d+)\})")); auto it = kRange.globalMatch(pattern); QRegularExpressionMatch first; int count = 0; while (it.hasNext()) { const QRegularExpressionMatch m = it.next(); if (count == 0) { first = m; } ++count; } if (count == 0) { return looksLikeUrl(pattern) ? QStringList{pattern} : QStringList{}; } if (count > 1) { return {}; // more than one {a..b} token: ambiguous, refuse rather than guess } const QString startStr = first.captured(1); const QString endStr = first.captured(2); const int start = startStr.toInt(); const int end = endStr.toInt(); if (end < start) { return {}; } const bool zeroPad = startStr.startsWith(QLatin1Char('0')) || endStr.startsWith(QLatin1Char('0')); const int width = zeroPad ? std::max(startStr.size(), endStr.size()) : 0; QStringList out; for (int i = start; i <= end && out.size() < kMaxBatchItems; ++i) { const QString num = zeroPad ? QStringLiteral("%1").arg(i, width, 10, QLatin1Char('0')) : QString::number(i); QString url = pattern; url.replace(first.capturedStart(), first.capturedLength(), num); if (looksLikeUrl(url)) { out << url; } } return out; } QJsonObject BatchDialog::buildAddBatchParams(const QStringList &urls, const QString &categoryId, const QString &queueId, const QString &startMode) { QJsonArray items; for (const QString &url : urls) { items.append(QJsonObject{{"url", url}}); } QJsonObject defaults{{"startMode", startMode}}; if (!categoryId.isEmpty()) { defaults["categoryId"] = categoryId; } if (startMode == QLatin1String("queue") && !queueId.isEmpty()) { defaults["queueId"] = queueId; } return QJsonObject{{"items", items}, {"defaults", defaults}}; } BatchDialog::BatchDialog(rpc::RpcClient *client, QJsonArray categories, QJsonArray queues, QWidget *parent) : QDialog(parent), client_(client), tabs_(new QTabWidget(this)), clipboardText_(new QPlainTextEdit(this)), clipboardPreview_(new QListWidget(this)), wildcardPattern_(new QLineEdit(this)), wildcardPreview_(new QListWidget(this)), categoryCombo_(new QComboBox(this)), startModeCombo_(new QComboBox(this)), queueCombo_(new QComboBox(this)), countLabel_(new QLabel(this)), errorLabel_(new QLabel(this)), addButton_(nullptr) { setWindowTitle(tr("Batch Download")); resize(560, 520); auto *clipboardPage = new QWidget(this); clipboardText_->setPlaceholderText(tr("Paste a list of URLs, one per line…")); const QString clipboard = QGuiApplication::clipboard()->text(); if (!parseUrlBlob(clipboard).isEmpty()) { clipboardText_->setPlainText(clipboard); } connect(clipboardText_, &QPlainTextEdit::textChanged, this, &BatchDialog::updateClipboardPreview); auto *clipboardLayout = new QVBoxLayout(clipboardPage); clipboardLayout->addWidget(new QLabel(tr("URLs:"), clipboardPage)); clipboardLayout->addWidget(clipboardText_, 1); clipboardLayout->addWidget(new QLabel(tr("Will add:"), clipboardPage)); clipboardLayout->addWidget(clipboardPreview_, 1); tabs_->addTab(clipboardPage, tr("From Clipboard")); auto *wildcardPage = new QWidget(this); wildcardPattern_->setPlaceholderText( tr("http://host/path/img{1..50}.jpg — one {start..end} range")); connect(wildcardPattern_, &QLineEdit::textChanged, this, &BatchDialog::updateWildcardPreview); auto *wildcardLayout = new QVBoxLayout(wildcardPage); wildcardLayout->addWidget(new QLabel(tr("Pattern:"), wildcardPage)); wildcardLayout->addWidget(wildcardPattern_); wildcardLayout->addWidget(new QLabel(tr("Will add:"), wildcardPage)); wildcardLayout->addWidget(wildcardPreview_, 1); tabs_->addTab(wildcardPage, tr("With Wildcards")); populateCategories(categories); startModeCombo_->addItem(tr("Download Now"), QStringLiteral("now")); startModeCombo_->addItem(tr("Download Later"), QStringLiteral("later")); startModeCombo_->addItem(tr("Add to Queue"), QStringLiteral("queue")); populateQueues(queues); connect(startModeCombo_, &QComboBox::currentIndexChanged, this, [this] { queueCombo_->setEnabled(startModeCombo_->currentData().toString() == QLatin1String("queue")); }); queueCombo_->setEnabled(false); errorLabel_->setStyleSheet(QStringLiteral("color: #c0392b;")); errorLabel_->setWordWrap(true); errorLabel_->hide(); auto *footer = new QFormLayout; footer->addRow(tr("Category:"), categoryCombo_); footer->addRow(tr("Start mode:"), startModeCombo_); footer->addRow(tr("Queue:"), queueCombo_); footer->addRow(countLabel_); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Cancel, this); addButton_ = buttons->addButton(tr("Add"), QDialogButtonBox::AcceptRole); connect(addButton_, &QPushButton::clicked, this, &BatchDialog::submit); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); auto *layout = new QVBoxLayout(this); layout->addWidget(tabs_, 1); layout->addLayout(footer); layout->addWidget(errorLabel_); layout->addWidget(buttons); connect(tabs_, &QTabWidget::currentChanged, this, [this] { updateClipboardPreview(); updateWildcardPreview(); }); updateClipboardPreview(); updateWildcardPreview(); } void BatchDialog::populateCategories(const QJsonArray &categories) { categoryCombo_->addItem(tr("(Automatic)"), QString()); for (const QJsonValue &v : categories) { const QJsonObject c = v.toObject(); categoryCombo_->addItem(c.value("name").toString(), c.value("categoryId").toString()); } } void BatchDialog::populateQueues(const QJsonArray &queues) { for (const QJsonValue &v : queues) { const QJsonObject q = v.toObject(); queueCombo_->addItem(q.value("name").toString(), q.value("queueId").toString()); } if (queues.isEmpty()) { const int queueIdx = startModeCombo_->findData(QStringLiteral("queue")); if (queueIdx >= 0) { startModeCombo_->removeItem(queueIdx); // nothing to add it to } } } QStringList BatchDialog::activeUrls() const { return tabs_->currentIndex() == 0 ? parseUrlBlob(clipboardText_->toPlainText()) : expandWildcard(wildcardPattern_->text().trimmed()); } void BatchDialog::updateClipboardPreview() { const QStringList urls = parseUrlBlob(clipboardText_->toPlainText()); clipboardPreview_->clear(); clipboardPreview_->addItems(urls); if (tabs_->currentIndex() == 0) { countLabel_->setText(tr("%n URL(s)", "", urls.size())); addButton_->setEnabled(!urls.isEmpty()); } } void BatchDialog::updateWildcardPreview() { const QStringList urls = expandWildcard(wildcardPattern_->text().trimmed()); wildcardPreview_->clear(); wildcardPreview_->addItems(urls); if (tabs_->currentIndex() == 1) { countLabel_->setText(tr("%n URL(s)", "", urls.size())); addButton_->setEnabled(!urls.isEmpty()); } } void BatchDialog::submit() { const QStringList urls = activeUrls(); if (urls.isEmpty()) { return; } const QJsonObject params = buildAddBatchParams(urls, categoryCombo_->currentData().toString(), queueCombo_->currentData().toString(), startModeCombo_->currentData().toString()); addButton_->setEnabled(false); QPointer self(this); client_->call(QString::fromLatin1(rpc::method::kDownloadAddBatch), params, [self](const rpc::RpcReply &reply) { if (!self) { return; } self->addButton_->setEnabled(true); if (!reply.ok()) { self->errorLabel_->setText( tr("Could not add the batch: %1").arg(reply.error.message)); self->errorLabel_->show(); return; } const QJsonArray failed = reply.result.toObject().value("failed").toArray(); if (failed.isEmpty()) { self->accept(); return; } self->errorLabel_->setText( tr("%n item(s) could not be added; the rest were.", "", failed.size())); self->errorLabel_->show(); }); } } // namespace velox::gui