Cockatrice/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp
BruebachL 7d867b9745
Some checks failed
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Build Desktop / Configure (push) Has been cancelled
Build Docker / Servatrice (arm) (push) Has been cancelled
Build Docker / Servatrice (x86) (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 44 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Servatrice_Debian 12 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 26 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
Build Docker / Publish multi-platform Servatrice image (push) Has been cancelled
[Oracle/Client] Report card database download progress (#7253)
* [Oracle/Client] Report card database download progress

Card database updates ran invisibly: MTGJSON parsing spun an indeterminate
bar on the UI thread and the set import blocked the window, while the
onboarding wizard spawned `oracle -b` with no progress to show at all.

- Add byte-level scan progress to `RawJson::scanSetRanges` via an optional
  callback, throttled to ~100 reports per scan.
- Emit `OracleImporter::dataReadProgress` during the scan and import sets on a
  worker thread, driving the wizard's progress bar per set.
- With `-b`, write machine-readable `PROGRESS <stage> <done> <total>` lines to
  stdout for the download/scan/import stages; stderr keeps the log output.
- Parse the oracle stdout in `MainWindow` and forward it to the onboarding
  wizard, giving the card database step a determinate bar with stage-specific
  status text.
- Guard the async workers against the wizard being closed mid-run.
- Add Google Test coverage for scan progress reporting.

* [Oracle/Client] Harden oracle progress workers and quit prompt

Address review feedback on the download-progress change: decompress and read
sets files off the UI thread, cancel the load/import workers before the
wizard can tear down the importer, and show an 'Extracting file...' status
plus a clean 100% tail so the poll never looks stuck. Quitting Cockatrice
while a card database update runs now asks for confirmation.

* Show 100% for 500ms on complete.

* Disable buttons on set import until done.

* Clean up progress bar.

* Drop wrapper around lambda

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-09 22:27:16 +02:00

225 lines
6.5 KiB
C++

#include "first_run_wizard.h"
#include "first_run_wizard_page.h"
#include "pages/account_setup_page.h"
#include "pages/card_database_setup_page.h"
#include "pages/finish_page.h"
#include "pages/preferences_setup_page.h"
#include "pages/theme_setup_page.h"
#include "pages/welcome_page.h"
#include "shader_banner_widget.h"
#include "step_indicator_widget.h"
#include <QCloseEvent>
#include <QEvent>
#include <QFont>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QStackedWidget>
#include <QVBoxLayout>
FirstRunWizard::FirstRunWizard(QWidget *parent) : QDialog(parent)
{
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
setMinimumSize(640, 490);
resize(720, 550);
bannerHost = new BannerHost(this);
titleLabel = new QLabel(this);
QFont titleFont = titleLabel->font();
titleFont.setPointSizeF(titleFont.pointSizeF() * 1.4);
titleFont.setBold(true);
titleLabel->setFont(titleFont);
subtitleLabel = new QLabel(this);
subtitleLabel->setWordWrap(true);
stack = new QStackedWidget(this);
stepIndicator = new StepIndicatorWidget(this);
backButton = new QPushButton(this);
skipButton = new QPushButton(this);
nextButton = new QPushButton(this);
nextButton->setDefault(true);
connect(backButton, &QPushButton::clicked, this, &FirstRunWizard::goBack);
connect(skipButton, &QPushButton::clicked, this, &FirstRunWizard::skip);
connect(nextButton, &QPushButton::clicked, this, &FirstRunWizard::goNext);
auto *headerLayout = new QVBoxLayout;
headerLayout->setContentsMargins(0, 0, 0, 0);
headerLayout->addWidget(bannerHost);
headerLayout->addSpacing(12);
headerLayout->addWidget(titleLabel);
headerLayout->addWidget(subtitleLabel);
auto *navLayout = new QHBoxLayout;
navLayout->addWidget(backButton);
navLayout->addWidget(skipButton);
navLayout->addStretch();
navLayout->addWidget(stepIndicator);
navLayout->addStretch();
navLayout->addWidget(nextButton);
auto *root = new QVBoxLayout(this);
root->addLayout(headerLayout);
root->addSpacing(8);
root->addWidget(stack, 1);
root->addSpacing(8);
root->addLayout(navLayout);
auto *welcome = new WelcomePage(this);
auto *cardDb = new CardDatabaseSetupPage(this);
auto *theme = new ThemeSetupPage(this);
auto *account = new AccountSetupPage(this);
auto *prefs = new PreferencesSetupPage(this);
auto *finishPg = new FinishPage(this);
cardDatabasePage = cardDb;
connect(cardDb, &CardDatabaseSetupPage::updateRequested, this, &FirstRunWizard::cardDatabaseUpdateRequested);
connect(cardDb, &CardDatabaseSetupPage::manualSetupRequested, this,
&FirstRunWizard::manualCardDatabaseSetupRequested);
connect(account, &AccountSetupPage::registerRequested, this, &FirstRunWizard::registerRequested);
connect(account, &AccountSetupPage::connectRequested, this, &FirstRunWizard::connectRequested);
connect(cardDb, &CardDatabaseSetupPage::advanceRequested, this, [this] {
if (stack->currentWidget() == cardDatabasePage) {
showPage(currentIndex + 1);
}
});
addPage(welcome);
addPage(cardDb);
addPage(theme);
addPage(account);
addPage(prefs);
addPage(finishPg);
stepIndicator->setStepCount(pages.count());
retranslateUi();
showPage(0);
}
void FirstRunWizard::addPage(FirstRunWizardPage *page)
{
pages.append(page);
stack->addWidget(page);
connect(page, &FirstRunWizardPage::completeChanged, this, &FirstRunWizard::updateChrome);
}
void FirstRunWizard::showPage(int index)
{
if (index < 0 || index >= pages.count()) {
return;
}
currentIndex = index;
stack->setCurrentIndex(index);
pages[index]->initializePage();
stepIndicator->setCurrentStep(index);
static const QList<BannerHost::Motif> motifs = {
BannerHost::Motif::Welcome, BannerHost::Motif::CardDatabase, BannerHost::Motif::Theming,
BannerHost::Motif::Account, BannerHost::Motif::Preferences, BannerHost::Motif::Finish,
};
if (index < motifs.size()) {
bannerHost->setMotif(motifs[index]);
}
titleLabel->setText(pages[index]->stepTitle());
subtitleLabel->setText(pages[index]->stepSubtitle());
subtitleLabel->setVisible(!pages[index]->stepSubtitle().isEmpty());
updateChrome();
}
void FirstRunWizard::updateChrome()
{
if (currentIndex < 0) {
return;
}
FirstRunWizardPage *page = pages[currentIndex];
const bool isLast = (currentIndex == pages.count() - 1);
backButton->setVisible(currentIndex > 0);
skipButton->setVisible(page->isSkippable());
nextButton->setEnabled(page->isComplete());
QString customText = page->nextButtonText();
if (!customText.isEmpty()) {
nextButton->setText(customText);
} else {
nextButton->setText(isLast ? tr("Finish") : tr("Next"));
}
}
void FirstRunWizard::goNext()
{
FirstRunWizardPage *page = pages[currentIndex];
if (!page->validatePage() || !page->handleNextClick()) {
return;
}
if (currentIndex == pages.count() - 1) {
finish();
return;
}
showPage(currentIndex + 1);
}
void FirstRunWizard::goBack()
{
showPage(currentIndex - 1);
}
void FirstRunWizard::skip()
{
showPage(currentIndex + 1);
}
void FirstRunWizard::onCardDatabaseUpdateFinished(bool success)
{
if (cardDatabasePage) {
cardDatabasePage->onUpdateFinished(success);
}
}
void FirstRunWizard::onCardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total)
{
if (cardDatabasePage) {
cardDatabasePage->onUpdateProgress(stage, done, total);
}
}
void FirstRunWizard::finish()
{
accept();
}
void FirstRunWizard::closeEvent(QCloseEvent *event)
{
// Every step persists its own choice as it's made, so closing early
// isn't destructive -- treat it exactly like reaching the end.
QDialog::closeEvent(event);
}
void FirstRunWizard::changeEvent(QEvent *event)
{
if (event->type() == QEvent::LanguageChange) {
retranslateUi();
}
QDialog::changeEvent(event);
}
void FirstRunWizard::retranslateUi()
{
setWindowTitle(tr("Welcome to Cockatrice"));
backButton->setText(tr("Back"));
skipButton->setText(tr("Skip"));
for (FirstRunWizardPage *page : std::as_const(pages)) {
page->retranslateUi();
}
if (currentIndex >= 0) {
titleLabel->setText(pages[currentIndex]->stepTitle());
subtitleLabel->setText(pages[currentIndex]->stepSubtitle());
}
updateChrome();
}