Skip to content

Commit 4366eaa

Browse files
committed
feat: immersive pixel-perfect classic Steam-style loading screen with DB auth verification and silent background Steam patching
1 parent 0549c13 commit 4366eaa

4 files changed

Lines changed: 404 additions & 39 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ set(SOURCES
5757
src/chatpage.cpp
5858
src/friendpopover.cpp
5959
src/userprofiledialog.cpp
60+
src/loadingdialog.cpp
6061
)
6162

6263
set(HEADERS
@@ -82,6 +83,7 @@ set(HEADERS
8283
src/chatpage.h
8384
src/friendpopover.h
8485
src/userprofiledialog.h
86+
src/loadingdialog.h
8587
)
8688

8789
# Add static plugin initialization for static builds

src/loadingdialog.cpp

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
#include "loadingdialog.h"
2+
#include "config.h"
3+
#include <QJsonDocument>
4+
#include <QJsonObject>
5+
#include <QKeyEvent>
6+
#include <cmath>
7+
8+
LoadingDialog::LoadingDialog(const QString& username, const QString& password, bool isRegister, bool isGuest, QWidget* parent)
9+
: QDialog(parent)
10+
, m_inputUsername(username)
11+
, m_inputPassword(password)
12+
, m_isRegister(isRegister)
13+
, m_isGuest(isGuest)
14+
, m_success(false)
15+
, m_errorMsg("")
16+
, m_progress(0.0f)
17+
, m_time(0.0f)
18+
, m_stage(0)
19+
, m_textOpacity(1.0f)
20+
, m_statusText("verifying details")
21+
, m_authReply(nullptr)
22+
, m_authCompleted(false)
23+
, m_authSuccess(false)
24+
, m_patchWorker(nullptr)
25+
, m_patchCompleted(false)
26+
, m_patchSuccess(false)
27+
{
28+
setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog);
29+
setAttribute(Qt::WA_TranslucentBackground);
30+
setModal(true);
31+
setFixedSize(860, 520); // Matches onboarding dialog size
32+
33+
m_networkManager = new QNetworkAccessManager(this);
34+
35+
// Setup animation timer (60 FPS -> 16ms interval)
36+
m_animationTimer = new QTimer(this);
37+
connect(m_animationTimer, &QTimer::timeout, this, &LoadingDialog::onUpdate);
38+
m_animationTimer->start(16);
39+
m_elapsedTimer.start();
40+
41+
// Start DB authentication check if not a guest
42+
if (!m_isGuest) {
43+
startAuth();
44+
} else {
45+
m_authCompleted = true;
46+
m_authSuccess = true;
47+
// For guest, immediately start patching Steam
48+
m_stage = 1;
49+
m_statusText = "patching steam";
50+
startPatching();
51+
}
52+
}
53+
54+
LoadingDialog::~LoadingDialog() {
55+
if (m_authReply) {
56+
m_authReply->abort();
57+
m_authReply->deleteLater();
58+
}
59+
}
60+
61+
void LoadingDialog::keyPressEvent(QKeyEvent* event) {
62+
if (event->key() == Qt::Key_Escape) {
63+
// Prevent closing on escape
64+
event->accept();
65+
} else {
66+
QDialog::keyPressEvent(event);
67+
}
68+
}
69+
70+
void LoadingDialog::startAuth() {
71+
QString endpoint = m_isRegister ? "/api/user/register" : "/api/user/login";
72+
QNetworkRequest req(QUrl(Config::WEBSERVER_BASE_URL + endpoint));
73+
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
74+
75+
QJsonObject body;
76+
body["username"] = m_inputUsername;
77+
body["password"] = m_inputPassword;
78+
79+
m_authReply = m_networkManager->post(req, QJsonDocument(body).toJson());
80+
connect(m_authReply, &QNetworkReply::finished, this, &LoadingDialog::onAuthFinished);
81+
}
82+
83+
void LoadingDialog::onAuthFinished() {
84+
if (!m_authReply) return;
85+
m_authReply->deleteLater();
86+
87+
QByteArray responseData = m_authReply->readAll();
88+
QJsonObject obj = QJsonDocument::fromJson(responseData).object();
89+
90+
if (m_authReply->error() == QNetworkReply::NoError && obj["success"].toBool()) {
91+
m_userData = obj["user"].toObject();
92+
m_verifiedUsername = m_userData["username"].toString();
93+
m_authSuccess = true;
94+
m_authCompleted = true;
95+
96+
// Start patching steam as soon as authentication finishes successfully
97+
startPatching();
98+
} else {
99+
m_authSuccess = false;
100+
m_authCompleted = true;
101+
102+
QString errorMsg = "Auth failed";
103+
if (!obj["error"].toString().isEmpty()) {
104+
errorMsg = obj["error"].toString();
105+
} else if (m_authReply->error() != QNetworkReply::NoError) {
106+
errorMsg = m_authReply->errorString();
107+
} else if (m_authReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 401) {
108+
errorMsg = "Invalid username or password";
109+
}
110+
111+
m_errorMsg = errorMsg;
112+
m_success = false;
113+
reject();
114+
}
115+
m_authReply = nullptr;
116+
}
117+
118+
void LoadingDialog::startPatching() {
119+
m_patchWorker = new SteamPatchWorker(this);
120+
connect(m_patchWorker, &SteamPatchWorker::log, this, &LoadingDialog::onPatchLog);
121+
connect(m_patchWorker, &SteamPatchWorker::finished, this, &LoadingDialog::onPatchFinished);
122+
connect(m_patchWorker, &SteamPatchWorker::error, this, &LoadingDialog::onPatchError);
123+
connect(m_patchWorker, &QThread::finished, m_patchWorker, &QObject::deleteLater);
124+
m_patchWorker->start();
125+
}
126+
127+
void LoadingDialog::onPatchLog(const QString& msg, const QString& level) {
128+
// We can print logs to debug console, but loading screen remains clean
129+
qDebug() << "[Loader PatchLog]" << level << ":" << msg;
130+
}
131+
132+
void LoadingDialog::onPatchFinished(const QString&) {
133+
m_patchSuccess = true;
134+
m_patchCompleted = true;
135+
}
136+
137+
void LoadingDialog::onPatchError(const QString& err) {
138+
m_patchSuccess = false;
139+
m_patchCompleted = true;
140+
m_errorMsg = "Steam patch failed: " + err;
141+
m_success = false;
142+
reject();
143+
}
144+
145+
void LoadingDialog::onUpdate() {
146+
float deltaTime = m_elapsedTimer.restart() / 1000.0f;
147+
if (deltaTime > 0.1f) deltaTime = 0.1f; // Cap delta to avoid jumps
148+
149+
m_time += deltaTime;
150+
151+
// Progress sequencing
152+
if (m_stage == 0) {
153+
// Stage 0: Verifying details
154+
if (m_progress < 0.50f) {
155+
m_progress += deltaTime * 0.20f; // Rapid up to 50%
156+
if (m_progress > 0.50f) m_progress = 0.50f;
157+
}
158+
159+
// Transition to Stage 1 when authentication succeeds
160+
if (m_authCompleted && m_authSuccess && m_progress >= 0.50f) {
161+
m_stage = 1;
162+
}
163+
} else {
164+
// Stage 1: Patching steam
165+
// Smoothly fade out text and fade in "patching steam"
166+
if (m_statusText != "patching steam") {
167+
m_textOpacity -= deltaTime * 6.0f;
168+
if (m_textOpacity <= 0.0f) {
169+
m_statusText = "patching steam";
170+
m_textOpacity = 0.0f;
171+
}
172+
} else if (m_textOpacity < 1.0f) {
173+
m_textOpacity += deltaTime * 5.0f;
174+
if (m_textOpacity > 1.0f) m_textOpacity = 1.0f;
175+
}
176+
177+
// Fill remaining progress bar
178+
if (m_progress < 1.0f) {
179+
m_progress += deltaTime * 0.15f; // Takes ~3.3 seconds to finish
180+
if (m_progress > 1.0f) m_progress = 1.0f;
181+
}
182+
183+
// Automatically accept once everything is completed and progress is 100%
184+
if (m_progress >= 1.0f && m_patchCompleted) {
185+
if (m_patchSuccess) {
186+
m_success = true;
187+
accept();
188+
} else {
189+
reject();
190+
}
191+
}
192+
}
193+
194+
update(); // Trigger paintEvent
195+
}
196+
197+
void LoadingDialog::paintEvent(QPaintEvent*) {
198+
QPainter p(this);
199+
p.setRenderHint(QPainter::Antialiasing);
200+
201+
// Apply exact rounded corners window clip matching Onboarding Dialog
202+
int radius = 20;
203+
QPainterPath clipPath;
204+
clipPath.addRoundedRect(rect(), radius, radius);
205+
p.setClipPath(clipPath);
206+
207+
// Background: Absolute solid pure black (#000000)
208+
p.fillRect(rect(), QColor(0, 0, 0));
209+
210+
QPointF center(width() * 0.5, height() * 0.42);
211+
212+
// ==========================================
213+
// 1. DRAW MECHANICAL STEAM LOGO
214+
// ==========================================
215+
float outerRadius = 44.0f;
216+
217+
// Outer crisp ring (slightly transparent white)
218+
QPen ringPen(QColor(255, 255, 255, 240), 2.2f);
219+
p.setPen(ringPen);
220+
p.setBrush(Qt::NoBrush);
221+
p.drawEllipse(center, outerRadius, outerRadius);
222+
223+
// Rotation calculations using sine/cosine time variables
224+
// Left pivot point
225+
QPointF pivotLeft(center.x() - 6.0f + std::cos(m_time * 2.5f) * 0.8f,
226+
center.y() - 1.0f + std::sin(m_time * 2.5f) * 0.8f);
227+
228+
// Right gears center
229+
QPointF gearRight(center.x() + 15.0f, center.y() - 9.0f);
230+
231+
// Angled crankshaft coordinate updates
232+
QPointF stemEnd(pivotLeft.x() - 13.0f + std::sin(m_time * 4.0f) * 1.2f,
233+
pivotLeft.y() - 28.0f + std::cos(m_time * 4.0f) * 1.2f);
234+
235+
// Draw crankshaft stem
236+
QPen stemPen(QColor(255, 255, 255), 5.0f);
237+
stemPen.setCapStyle(Qt::RoundCap);
238+
p.setPen(stemPen);
239+
p.drawLine(pivotLeft, stemEnd);
240+
241+
// Draw main mechanical linkage bar (flywheel linkage)
242+
QPen linkageOuterPen(QColor(255, 255, 255), 7.0f);
243+
linkageOuterPen.setCapStyle(Qt::RoundCap);
244+
p.setPen(linkageOuterPen);
245+
p.drawLine(pivotLeft, gearRight);
246+
247+
// Black inner groove of linkage bar
248+
QPen linkageInnerPen(QColor(0, 0, 0), 1.5f);
249+
linkageInnerPen.setCapStyle(Qt::RoundCap);
250+
p.setPen(linkageInnerPen);
251+
p.drawLine(pivotLeft, gearRight);
252+
253+
// Left pivot disk
254+
p.setPen(Qt::NoPen);
255+
p.setBrush(QColor(0, 0, 0));
256+
p.drawEllipse(pivotLeft, 8.0f, 8.0f);
257+
258+
p.setPen(QPen(QColor(255, 255, 255), 2.5f));
259+
p.setBrush(Qt::NoBrush);
260+
p.drawEllipse(pivotLeft, 8.0f, 8.0f);
261+
262+
p.setPen(Qt::NoPen);
263+
p.setBrush(QColor(255, 255, 255));
264+
p.drawEllipse(pivotLeft, 3.0f, 3.0f);
265+
266+
// Right rotating disk
267+
p.setPen(Qt::NoPen);
268+
p.setBrush(QColor(0, 0, 0));
269+
p.drawEllipse(gearRight, 14.0f, 14.0f);
270+
271+
p.setPen(QPen(QColor(255, 255, 255), 2.5f));
272+
p.setBrush(Qt::NoBrush);
273+
p.drawEllipse(gearRight, 14.0f, 14.0f);
274+
275+
float expandFactor = 1.0f + std::sin(m_time * 5.0f) * 0.04f;
276+
p.setPen(Qt::NoPen);
277+
p.setBrush(QColor(255, 255, 255));
278+
p.drawEllipse(gearRight, 7.0f * expandFactor, 7.0f * expandFactor);
279+
280+
p.setPen(Qt::NoPen);
281+
p.setBrush(QColor(0, 0, 0));
282+
p.drawEllipse(gearRight, 3.2f, 3.2f);
283+
284+
// ==========================================
285+
// 2. DRAW CRISP PROGRESS BAR
286+
// ==========================================
287+
float barWidth = 380.0f;
288+
float barHeight = 3.0f;
289+
QPointF barMin(center.x() - barWidth * 0.5f, center.y() + 85.0f);
290+
291+
// Background track (rgba(255, 255, 255, 0.1))
292+
p.fillRect(QRectF(barMin, QSizeF(barWidth, barHeight)), QColor(255, 255, 255, 25));
293+
294+
// Active progress fill (Solid pure white)
295+
float fillWidth = barWidth * m_progress;
296+
if (fillWidth > 0.0f) {
297+
p.fillRect(QRectF(barMin, QSizeF(fillWidth, barHeight)), QColor(255, 255, 255));
298+
}
299+
300+
// ==========================================
301+
// 3. DRAW SEQUENTIAL TEXT (LOWERCASE SANS-SERIF)
302+
// ==========================================
303+
QFont textFont("Segoe UI");
304+
textFont.setPointSizeF(10.0);
305+
textFont.setLetterSpacing(QFont::AbsoluteSpacing, 0.6);
306+
textFont.setWeight(QFont::Light);
307+
p.setFont(textFont);
308+
309+
// Faded white/gray text color (rgba(255, 255, 255, 0.7 * opacity))
310+
QColor textColor(255, 255, 255, static_cast<int>(178 * m_textOpacity));
311+
p.setPen(textColor);
312+
313+
QRectF textRect(center.x() - 250, barMin.y() + 16.0f, 500, 30);
314+
p.drawText(textRect, Qt::AlignCenter, m_statusText);
315+
}

0 commit comments

Comments
 (0)