Skip to content

Commit 92e6838

Browse files
committed
major ui update
1 parent 1ba50b7 commit 92e6838

6 files changed

Lines changed: 667 additions & 79 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ set(SOURCES
4141
src/mainwindow.cpp
4242
src/glassbutton.cpp
4343
src/gamecard.cpp
44+
src/gamedetailspage.cpp
4445
src/loadingspinner.cpp
4546
src/workers/indexdownloadworker.cpp
4647
src/workers/luadownloadworker.cpp
@@ -56,6 +57,7 @@ set(HEADERS
5657
src/mainwindow.h
5758
src/glassbutton.h
5859
src/gamecard.h
60+
src/gamedetailspage.h
5961
src/loadingspinner.h
6062
src/workers/indexdownloadworker.h
6163
src/workers/luadownloadworker.h

src/gamedetailspage.cpp

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
#include "gamedetailspage.h"
2+
#include "glassbutton.h"
3+
#include "utils/colors.h"
4+
5+
#include <QJsonDocument>
6+
#include <QJsonObject>
7+
#include <QJsonArray>
8+
#include <QKeyEvent>
9+
#include <QPainter>
10+
#include <QPainterPath>
11+
#include <QEventLoop>
12+
#include <QScrollBar>
13+
14+
GameDetailsPage::GameDetailsPage(QNetworkAccessManager* networkManager, QWidget* parent)
15+
: QWidget(parent), m_networkManager(networkManager)
16+
{
17+
setFocusPolicy(Qt::StrongFocus); // For Escape key
18+
buildUI();
19+
}
20+
21+
void GameDetailsPage::buildUI() {
22+
QVBoxLayout* mainLayout = new QVBoxLayout(this);
23+
mainLayout->setContentsMargins(0, 0, 0, 0);
24+
mainLayout->setSpacing(0);
25+
26+
// Top Bar (Back button)
27+
QWidget* topBar = new QWidget();
28+
topBar->setFixedHeight(60);
29+
topBar->setStyleSheet("background: rgba(18, 19, 23, 180); border-bottom: 1px solid rgba(255, 255, 255, 10);");
30+
QHBoxLayout* topBarLayout = new QHBoxLayout(topBar);
31+
topBarLayout->setContentsMargins(20, 0, 20, 0);
32+
33+
GlassButton* backBtn = new GlassButton(MaterialIcons::Refresh, "Back", "", Colors::ON_SURFACE_VARIANT, this);
34+
// Let's just use the button with a smaller size and different style natively,
35+
// but GlassButton is fine. We will configure it simply.
36+
// For now, let's just make it a simple text button to be safe if GlassButton is complex
37+
QPushButton* realBackBtn = new QPushButton("← Back");
38+
realBackBtn->setStyleSheet("QPushButton { font-size: 16px; color: white; background: transparent; border: none; font-weight: bold; }"
39+
"QPushButton:hover { color: #bb86fc; }");
40+
realBackBtn->setCursor(Qt::PointingHandCursor);
41+
connect(realBackBtn, &QPushButton::clicked, this, &GameDetailsPage::backClicked);
42+
43+
topBarLayout->addWidget(realBackBtn);
44+
topBarLayout->addStretch(1);
45+
mainLayout->addWidget(topBar);
46+
47+
// Scroll Area
48+
m_scrollArea = new QScrollArea();
49+
m_scrollArea->setWidgetResizable(true);
50+
m_scrollArea->setStyleSheet("QScrollArea { border: none; background: transparent; } QScrollBar { width: 0px; }");
51+
52+
m_contentWidget = new QWidget();
53+
m_contentWidget->setStyleSheet("background: transparent;");
54+
m_contentLayout = new QVBoxLayout(m_contentWidget);
55+
m_contentLayout->setContentsMargins(0, 0, 0, 40);
56+
m_contentLayout->setSpacing(30);
57+
58+
// Hero Banner
59+
m_heroBanner = new QLabel();
60+
m_heroBanner->setFixedHeight(250);
61+
m_heroBanner->setStyleSheet("background: #1e1e1e; border-radius: 12px; margin: 20px 20px 0 20px;");
62+
m_heroBanner->setScaledContents(true);
63+
m_heroBanner->setAlignment(Qt::AlignCenter);
64+
m_contentLayout->addWidget(m_heroBanner);
65+
66+
// Info Row (Description + Install Context)
67+
QWidget* infoRow = new QWidget();
68+
infoRow->setStyleSheet("margin: 0 20px;");
69+
QHBoxLayout* infoLayout = new QHBoxLayout(infoRow);
70+
infoLayout->setContentsMargins(0, 0, 0, 0);
71+
infoLayout->setSpacing(40);
72+
73+
m_descriptionLabel = new QLabel("Loading description...");
74+
m_descriptionLabel->setWordWrap(true);
75+
m_descriptionLabel->setStyleSheet("font-size: 14px; color: #dddddd; line-height: 1.5;");
76+
m_descriptionLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
77+
infoLayout->addWidget(m_descriptionLabel, 3); // Takes 3/4 space
78+
79+
// Install Button Widget
80+
QWidget* actionWidget = new QWidget();
81+
QVBoxLayout* actionLayout = new QVBoxLayout(actionWidget);
82+
actionLayout->setContentsMargins(0, 0, 0, 0);
83+
actionLayout->setAlignment(Qt::AlignTop | Qt::AlignRight);
84+
85+
m_installButton = new GlassButton(MaterialIcons::Download, "Add to Library", "Download patch", Colors::PRIMARY);
86+
// GlassButton has a fixed width in its class usually, but we'll add it
87+
connect(m_installButton, &QPushButton::clicked, this, [this]() {
88+
emit addToLibraryClicked(m_appId, m_gameName, m_hasFix);
89+
});
90+
actionLayout->addWidget(m_installButton);
91+
infoLayout->addWidget(actionWidget, 1); // Takes 1/4 space
92+
93+
m_contentLayout->addWidget(infoRow);
94+
95+
// Screenshots Section
96+
QLabel* ssTitle = new QLabel("Game Previews");
97+
ssTitle->setStyleSheet("font-size: 18px; font-weight: bold; color: white; margin: 10px 20px 0 20px;");
98+
m_contentLayout->addWidget(ssTitle);
99+
100+
QScrollArea* ssScroll = new QScrollArea();
101+
ssScroll->setWidgetResizable(true);
102+
ssScroll->setFixedHeight(200);
103+
ssScroll->setStyleSheet("QScrollArea { border: none; background: transparent; } QScrollBar { height: 0px; }");
104+
105+
QWidget* ssContainer = new QWidget();
106+
ssContainer->setStyleSheet("background: transparent; margin: 0 20px;");
107+
m_screenshotLayout = new QHBoxLayout(ssContainer);
108+
m_screenshotLayout->setContentsMargins(0, 0, 0, 0);
109+
m_screenshotLayout->setSpacing(15);
110+
m_screenshotLayout->setAlignment(Qt::AlignLeft);
111+
112+
ssScroll->setWidget(ssContainer);
113+
m_contentLayout->addWidget(ssScroll);
114+
115+
// Features and Security Row
116+
QWidget* detailsRow = new QWidget();
117+
detailsRow->setStyleSheet("margin: 20px 20px;");
118+
QHBoxLayout* detailsLayout = new QHBoxLayout(detailsRow);
119+
detailsLayout->setContentsMargins(0, 0, 0, 0);
120+
detailsLayout->setSpacing(40);
121+
detailsLayout->setAlignment(Qt::AlignTop);
122+
123+
// Features Section
124+
QWidget* featuresWidget = new QWidget();
125+
m_featuresLayout = new QVBoxLayout(featuresWidget);
126+
m_featuresLayout->setAlignment(Qt::AlignTop);
127+
m_featuresLayout->setContentsMargins(0, 0, 0, 0);
128+
129+
QLabel* fTitle = new QLabel("Game Features");
130+
fTitle->setStyleSheet("font-size: 16px; font-weight: bold; color: white;");
131+
m_featuresLayout->addWidget(fTitle);
132+
133+
detailsLayout->addWidget(featuresWidget, 1);
134+
135+
// Security Section
136+
QWidget* securityWidget = new QWidget();
137+
m_securityLayout = new QVBoxLayout(securityWidget);
138+
m_securityLayout->setAlignment(Qt::AlignTop);
139+
m_securityLayout->setContentsMargins(0, 0, 0, 0);
140+
141+
QLabel* secTitle = new QLabel("# Security / DRM");
142+
secTitle->setStyleSheet("font-size: 16px; font-weight: bold; color: #FFB74D;");
143+
m_securityLayout->addWidget(secTitle);
144+
145+
detailsLayout->addWidget(securityWidget, 1);
146+
147+
m_contentLayout->addWidget(detailsRow);
148+
m_contentLayout->addStretch(1);
149+
150+
m_scrollArea->setWidget(m_contentWidget);
151+
mainLayout->addWidget(m_scrollArea);
152+
}
153+
154+
void GameDetailsPage::keyPressEvent(QKeyEvent* event) {
155+
if (event->key() == Qt::Key_Escape) {
156+
emit backClicked();
157+
}
158+
QWidget::keyPressEvent(event);
159+
}
160+
161+
void GameDetailsPage::clear() {
162+
m_heroBanner->clear();
163+
m_heroBanner->setText("");
164+
m_descriptionLabel->setText("");
165+
166+
// Clear screenshots
167+
QLayoutItem* child;
168+
while ((child = m_screenshotLayout->takeAt(0)) != nullptr) {
169+
if (child->widget()) delete child->widget();
170+
delete child;
171+
}
172+
173+
// Clear features (keep title)
174+
while (m_featuresLayout->count() > 1) {
175+
child = m_featuresLayout->takeAt(1);
176+
if (child->widget()) delete child->widget();
177+
delete child;
178+
}
179+
180+
// Clear security (keep title)
181+
while (m_securityLayout->count() > 1) {
182+
child = m_securityLayout->takeAt(1);
183+
if (child->widget()) delete child->widget();
184+
delete child;
185+
}
186+
}
187+
188+
void GameDetailsPage::showSkeleton() {
189+
clear();
190+
m_descriptionLabel->setText("Loading game details...");
191+
m_heroBanner->setStyleSheet("background: #2a2a2a; border-radius: 12px; margin: 20px 20px 0 20px;");
192+
193+
// Add empty screenshot skeletons
194+
for (int i = 0; i < 3; ++i) {
195+
QLabel* emptySs = new QLabel();
196+
emptySs->setFixedSize(300, 169);
197+
emptySs->setStyleSheet("background: #2a2a2a; border-radius: 8px;");
198+
m_screenshotLayout->addWidget(emptySs);
199+
}
200+
}
201+
202+
void GameDetailsPage::loadGame(const QString& appId, const QString& name, bool supported, bool hasFix) {
203+
m_appId = appId;
204+
m_gameName = name;
205+
m_supported = supported;
206+
m_hasFix = hasFix;
207+
208+
// Reset UI to skeleton state
209+
showSkeleton();
210+
211+
// Update Action Button
212+
m_installButton->setEnabled(supported);
213+
if (hasFix) {
214+
m_installButton->setDescription(QString("Download patch for %1").arg(name));
215+
m_installButton->setAccentColor(Colors::ACCENT_GREEN);
216+
} else {
217+
m_installButton->setDescription(QString("Generate patch for %1").arg(name));
218+
m_installButton->setAccentColor(Colors::PRIMARY);
219+
}
220+
221+
// Set fallback hero image right away
222+
QString heroUrl = QString("https://cdn.akamai.steamstatic.com/steam/apps/%1/library_hero.jpg").arg(appId);
223+
QNetworkRequest hReq{QUrl(heroUrl)};
224+
hReq.setHeader(QNetworkRequest::UserAgentHeader, "SteamLuaPatcher/2.0");
225+
QNetworkReply* heroReply = m_networkManager->get(hReq);
226+
connect(heroReply, &QNetworkReply::finished, this, [this, heroReply]() {
227+
heroReply->deleteLater();
228+
if (heroReply->error() == QNetworkReply::NoError) {
229+
QPixmap rawPix;
230+
if (rawPix.loadFromData(heroReply->readAll())) {
231+
QPixmap rounded(rawPix.size());
232+
rounded.fill(Qt::transparent);
233+
QPainter p(&rounded);
234+
p.setRenderHint(QPainter::Antialiasing);
235+
QPainterPath path;
236+
path.addRoundedRect(rounded.rect(), 12, 12);
237+
p.setClipPath(path);
238+
p.drawPixmap(0, 0, rawPix);
239+
m_heroBanner->setStyleSheet("margin: 20px 20px 0 20px;");
240+
m_heroBanner->setPixmap(rounded);
241+
}
242+
}
243+
});
244+
245+
// Fetch details from Steam
246+
QUrl url(QString("https://store.steampowered.com/api/appdetails?appids=%1").arg(appId));
247+
QNetworkRequest req(url);
248+
req.setHeader(QNetworkRequest::UserAgentHeader, "SteamLuaPatcher/2.0");
249+
QNetworkReply* reply = m_networkManager->get(req);
250+
connect(reply, &QNetworkReply::finished, this, [this, reply](){ onDetailsReceived(reply); });
251+
252+
// Give focus so Escape key works
253+
setFocus();
254+
255+
// Reset scroll to top
256+
m_scrollArea->verticalScrollBar()->setValue(0);
257+
}
258+
259+
void GameDetailsPage::onDetailsReceived(QNetworkReply* reply) {
260+
reply->deleteLater();
261+
if (reply->error() != QNetworkReply::NoError) {
262+
m_descriptionLabel->setText("Failed to load details from Steam.");
263+
return;
264+
}
265+
266+
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
267+
QJsonObject root = doc.object();
268+
269+
if (root.contains(m_appId)) {
270+
QJsonObject appInfo = root[m_appId].toObject();
271+
if (appInfo["success"].toBool() && appInfo.contains("data")) {
272+
populate(appInfo["data"].toObject());
273+
return;
274+
}
275+
}
276+
m_descriptionLabel->setText("Game details not found on Steam.");
277+
}
278+
279+
void GameDetailsPage::populate(const QJsonObject& data) {
280+
// Description
281+
QString desc = data["short_description"].toString();
282+
// Steam sometimes returns HTML in short_description (e.g., &quot;)
283+
// We can just set it as rich text, Qt handles basic HTML
284+
m_descriptionLabel->setText(desc);
285+
m_descriptionLabel->setTextFormat(Qt::RichText);
286+
287+
// Screenshots
288+
QJsonArray screenshots = data["screenshots"].toArray();
289+
290+
// Clear skeletons first
291+
QLayoutItem* child;
292+
while ((child = m_screenshotLayout->takeAt(0)) != nullptr) {
293+
if (child->widget()) delete child->widget();
294+
delete child;
295+
}
296+
297+
int count = qMin(screenshots.size(), 6); // Max 6 screenshots
298+
for (int i = 0; i < count; ++i) {
299+
QJsonObject ss = screenshots[i].toObject();
300+
QString url = ss["path_thumbnail"].toString();
301+
302+
QLabel* imgLbl = new QLabel();
303+
imgLbl->setFixedSize(300, 169);
304+
imgLbl->setStyleSheet("background: #2a2a2a; border-radius: 8px;");
305+
imgLbl->setScaledContents(true);
306+
m_screenshotLayout->addWidget(imgLbl);
307+
308+
// Fetch Image
309+
QNetworkRequest req{QUrl(url)};
310+
req.setHeader(QNetworkRequest::UserAgentHeader, "SteamLuaPatcher/2.0");
311+
QNetworkReply* imgReply = m_networkManager->get(req);
312+
connect(imgReply, &QNetworkReply::finished, this, [imgReply, imgLbl]() {
313+
imgReply->deleteLater();
314+
if (imgReply->error() == QNetworkReply::NoError && imgLbl) {
315+
QPixmap rawPix;
316+
if (rawPix.loadFromData(imgReply->readAll())) {
317+
QPixmap rounded(300, 169);
318+
rounded.fill(Qt::transparent);
319+
QPainter p(&rounded);
320+
p.setRenderHint(QPainter::Antialiasing);
321+
QPainterPath path;
322+
path.addRoundedRect(rounded.rect(), 8, 8);
323+
p.setClipPath(path);
324+
p.drawPixmap(rounded.rect(), rawPix.scaled(300, 169, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
325+
if (imgLbl) imgLbl->setPixmap(rounded);
326+
}
327+
}
328+
});
329+
}
330+
331+
// Features (Genres + Categories)
332+
QJsonArray genres = data["genres"].toArray();
333+
for (const QJsonValue& val : genres) {
334+
QString genre = val.toObject()["description"].toString();
335+
QLabel* l = new QLabel("" + genre);
336+
l->setStyleSheet("color: #aaaaaa; font-size: 14px;");
337+
m_featuresLayout->addWidget(l);
338+
}
339+
340+
QJsonArray cats = data["categories"].toArray();
341+
for (const QJsonValue& val : cats) {
342+
QString cat = val.toObject()["description"].toString();
343+
QLabel* l = new QLabel("" + cat);
344+
l->setStyleSheet("color: #aaaaaa; font-size: 14px;");
345+
m_featuresLayout->addWidget(l);
346+
}
347+
348+
// Security / DRM (Denuvo, etc.)
349+
bool hasDrm = false;
350+
QString drmNotice = data["drm_notice"].toString();
351+
QString legalNotice = data["legal_notice"].toString();
352+
353+
if (!drmNotice.isEmpty()) {
354+
QLabel* l = new QLabel(drmNotice);
355+
l->setWordWrap(true);
356+
l->setStyleSheet("color: #FF5252; font-size: 14px;");
357+
m_securityLayout->addWidget(l);
358+
hasDrm = true;
359+
}
360+
if (legalNotice.contains("Denuvo", Qt::CaseInsensitive) || legalNotice.contains("Anti-cheat", Qt::CaseInsensitive)) {
361+
QLabel* l = new QLabel("Warning: Contains Third-party DRM/Anti-Cheat");
362+
l->setWordWrap(true);
363+
l->setStyleSheet("color: #FF5252; font-size: 14px; font-weight: bold;");
364+
m_securityLayout->addWidget(l);
365+
hasDrm = true;
366+
}
367+
368+
if (!hasDrm) {
369+
QLabel* l = new QLabel("✓ No third-party DRM known");
370+
l->setStyleSheet("color: #4CAF50; font-size: 14px;");
371+
m_securityLayout->addWidget(l);
372+
}
373+
}

0 commit comments

Comments
 (0)