[App] Add onboarding wizard (#7064)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions

* [App] Add onboarding wizard

Took 10 minutes

Took 3 minutes

Took 7 minutes

Took 9 minutes

Took 2 minutes

Took 1 minute


Took 7 minutes

* Adjust CI

Took 14 minutes

Took 56 seconds

Took 2 seconds

Took 3 seconds

* Adjust CI again

Took 14 minutes

Took 2 seconds

* Comments and fixes

Took 9 seconds


Took 1 minute

* Rebase.

Took 5 minutes

Took 50 seconds

Took 15 seconds

* Comments.

Took 7 minutes

* CI lol

Took 3 minutes

* CI again lol

Took 4 minutes

* Drop some settings, add some new ones.

Took 19 minutes

* Resize when expanding section

Took 4 minutes

Took 3 minutes

Took 7 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-08-24 23:05:58 +02:00 committed by GitHub
parent 815c5987b4
commit 2b7b4e8168
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 3207 additions and 336 deletions

View file

@ -0,0 +1,195 @@
#include "shader_banner_widget.h"
#include "banner_shader_config.h"
#include <QPainter>
#include <QQmlContext>
#include <QQmlEngine>
#include <QQuickWidget>
#include <QResizeEvent>
#include <QStackedLayout>
namespace
{
// Near-black base palette -- the background is dark and quiet so the green
// accent stands out.
constexpr QRgb kColorA = 0x1A1A20;
constexpr QRgb kColorB = 0x0E0E12;
constexpr QRgb kAccent = 0x8BDD6B;
} // namespace
class GradientFallbackWidget : public QWidget
{
public:
using QWidget::QWidget;
protected:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
QLinearGradient gradient(0, 0, width(), height());
gradient.setColorAt(0.0, QColor(kColorA));
gradient.setColorAt(1.0, QColor(kColorB));
painter.fillRect(rect(), gradient);
}
};
BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
{
setFixedHeight(150);
stack = new QStackedLayout(this);
stack->setContentsMargins(0, 0, 0, 0);
fallback = new GradientFallbackWidget(this);
stack->addWidget(fallback);
quickWidget = new QQuickWidget(this);
quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
config = new BannerShaderConfig(quickWidget->engine());
quickWidget->rootContext()->setContextProperty("bannerConfig", config);
quickWidget->setSource(QUrl("qrc:/onboarding/qml/BrandBanner.qml"));
if (quickWidget->status() == QQuickWidget::Error) {
activateFallback();
} else {
connect(quickWidget, &QQuickWidget::sceneGraphError, this, &BannerHost::onSceneGraphFailed);
stack->addWidget(quickWidget);
stack->setCurrentWidget(quickWidget);
}
connect(&clock, &QTimer::timeout, this, &BannerHost::tick);
clock.setInterval(16); // ~60fps; the shader itself is cheap, this is just a wall clock
applyMotifPreset(currentMotif);
updateAspect();
}
void BannerHost::activateFallback()
{
if (usingFallback) {
return;
}
usingFallback = true;
clock.stop();
stack->setCurrentWidget(fallback);
if (quickWidget) {
quickWidget->deleteLater(); // takes BannerShaderConfig (parented to its engine) with it
quickWidget = nullptr;
config = nullptr;
}
}
void BannerHost::onSceneGraphFailed()
{
activateFallback();
}
void BannerHost::setMotif(Motif motif)
{
currentMotif = motif;
applyMotifPreset(motif);
}
BannerHost::Preset BannerHost::presetFor(Motif motif)
{
// speed/seed tuned per motif so e.g. the network "pulse" (Account) reads
// at a deliberately calmer cadence than the data "scan" lines
// (Preferences), even though both come from the same shader.
switch (motif) {
case Motif::Welcome:
return {0.0, 0.6, 0.15};
case Motif::CardDatabase:
return {1.0, 1.3, 0.42};
case Motif::Theming:
return {2.0, 1.2, 0.73};
case Motif::Account:
return {3.0, 0.8, 0.28};
case Motif::Preferences:
return {4.0, 1.0, 0.61};
case Motif::Finish:
return {5.0, 1.0, 0.91};
}
return {0.0, 0.6, 0.15};
}
void BannerHost::applyMotifPreset(Motif motif)
{
if (usingFallback || !config) {
return;
}
const Preset p = presetFor(motif);
config->setColorA(QColor(kColorA));
config->setColorB(QColor(kColorB));
config->setAccent(QColor(kAccent));
config->setLogoVisible(motif == Motif::Welcome);
if (isFirstApply) {
// Nothing on screen yet -- write straight into the front bank, no
// crossfade needed for the very first paint.
config->setModeA(p.mode);
config->setSpeedA(p.speed);
config->setSeedA(p.seed);
config->setFrontIsA(true);
isFirstApply = false;
return;
}
// Write the new preset into whichever bank is currently hidden, then
// flip which one is front. QML's opacity Behavior does the actual
// crossfade -- BannerHost never animates anything itself.
if (config->frontIsA()) {
config->setModeB(p.mode);
config->setSpeedB(p.speed);
config->setSeedB(p.seed);
config->setFrontIsA(false);
} else {
config->setModeA(p.mode);
config->setSpeedA(p.speed);
config->setSeedA(p.seed);
config->setFrontIsA(true);
}
}
void BannerHost::updateAspect()
{
if (config && height() > 0) {
config->setAspect(qreal(width()) / qreal(height()));
}
}
void BannerHost::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
updateAspect();
}
void BannerHost::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
if (!usingFallback) {
elapsed.restart();
clock.start();
}
}
void BannerHost::hideEvent(QHideEvent *event)
{
QWidget::hideEvent(event);
clock.stop();
}
void BannerHost::tick()
{
if (config) {
qreal t = elapsed.elapsed() / 1000.0;
config->setTime(t);
// Visible breathing for the logo: oscillates between 0.0 and 1.0
qreal glow = 0.5 + 0.5 * qSin(t * 0.4);
config->setLogoGlow(glow);
}
}