From acdd3be52f201fc678f1d5189c007dc38461868d Mon Sep 17 00:00:00 2001 From: faith Date: Mon, 2 Dec 2024 15:36:13 +0800 Subject: [PATCH] wip: added basic gui for compat data * data is currently pulled directly from github API, awaiting server infra --- src/qt_gui/compatibility_info.cpp | 201 ++++++++++++++++++++++++++++++ src/qt_gui/compatibility_info.h | 88 +++++++++++++ src/qt_gui/game_info.cpp | 2 + src/qt_gui/game_list_frame.cpp | 110 ++++++++++++---- src/qt_gui/game_list_frame.h | 36 ++++-- src/qt_gui/game_list_utils.h | 7 ++ src/qt_gui/main_window.cpp | 4 +- src/qt_gui/main_window.h | 2 + 8 files changed, 415 insertions(+), 35 deletions(-) create mode 100644 src/qt_gui/compatibility_info.cpp create mode 100644 src/qt_gui/compatibility_info.h diff --git a/src/qt_gui/compatibility_info.cpp b/src/qt_gui/compatibility_info.cpp new file mode 100644 index 000000000..6cd751037 --- /dev/null +++ b/src/qt_gui/compatibility_info.cpp @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include "common/path_util.h" +#include "compatibility_info.h" + +CompatibilityInfoClass::CompatibilityInfoClass() + : m_network_manager(new QNetworkAccessManager(this)) { + QStringList file_paths; + std::filesystem::path compatibility_file_path = + Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / "compatibility_data.json"; + Common::FS::PathToQString(m_compatibility_filename, compatibility_file_path); +}; +CompatibilityInfoClass::~CompatibilityInfoClass() = default; + +void CompatibilityInfoClass::UpdateCompatibilityDatabase(QWidget* parent) { + QFileInfo check_file(m_compatibility_filename); + const auto modified_delta = check_file.lastModified() - QDateTime::currentDateTime(); + if (check_file.exists() && check_file.isFile() && std::chrono::duration_cast(modified_delta).count() < 60) { + if (LoadCompatibilityFile()) + return; + QMessageBox::critical(parent, tr("Error"), + tr("Failure in reading compatibility_data.json.")); + } + + QNetworkReply* reply = FetchPage(1); + WaitForReply(reply); + + QProgressDialog dialog(tr("Fetching compatibility data, please wait"), tr("Cancel"), 0, 0, + parent); + dialog.setWindowTitle(tr("Loading...")); + + int remaining_pages = 0; + if (reply->hasRawHeader("link")) { + QRegularExpression last_page_re("(\\d+)(?=>; rel=\"last\")"); + QRegularExpressionMatch last_page_match = + last_page_re.match(QString(reply->rawHeader("link"))); + if (last_page_match.hasMatch()) { + remaining_pages = last_page_match.captured(0).toInt() - 1; + } + } + + if (reply->error() != QNetworkReply::NoError) { + reply->deleteLater(); + QMessageBox::critical(parent, tr("Error"), + tr("Unable to update compatibility data! Using old compatibility data...")); + //TODO: Try loading compatibility_file.json again + LoadCompatibilityFile(); + return; + } + + ExtractCompatibilityInfo(reply->readAll()); + + QVector replies(remaining_pages); + QFutureWatcher future_watcher; + + for (int i = 0; i < remaining_pages; i++) { + replies[i] = FetchPage(i + 2); + } + + future_watcher.setFuture(QtConcurrent::map(replies, WaitForReply)); + connect(&future_watcher, &QFutureWatcher::finished, [&]() { + for (int i = 0; i < remaining_pages; i++) { + if (replies[i]->error() == QNetworkReply::NoError) { + ExtractCompatibilityInfo(replies[i]->readAll()); + } + replies[i]->deleteLater(); + } + + QFile compatibility_file(m_compatibility_filename); + + if (!compatibility_file.open(QIODevice::WriteOnly | QIODevice::Truncate | + QIODevice::Text)) { + QMessageBox::critical(parent, tr("Error"), + tr("Unable to open compatibility.json for writing.")); + return; + } + + QJsonDocument json_doc; + json_doc.setObject(m_compatibility_database); + compatibility_file.write(json_doc.toJson()); + compatibility_file.close(); + + dialog.reset(); + }); + connect(&dialog, &QProgressDialog::canceled, &future_watcher, + &QFutureWatcher::cancel); + dialog.setRange(0, remaining_pages); + connect(&future_watcher, &QFutureWatcher::progressValueChanged, &dialog, + &QProgressDialog::setValue); + dialog.exec(); +} + +QNetworkReply* CompatibilityInfoClass::FetchPage(int page_num) { + QUrl url = QUrl("https://api.github.com/repos/shadps4-emu/shadps4-game-compatibility/issues"); + QUrlQuery query; + query.addQueryItem("per_page", QString("100")); + query.addQueryItem( + "tags", QString("status-ingame status-playable status-nothing status-boots status-menus")); + query.addQueryItem("page", QString::number(page_num)); + url.setQuery(query); + + QNetworkRequest request(url); + QNetworkReply* reply = m_network_manager->get(request); + + return reply; +} + +void CompatibilityInfoClass::WaitForReply(QNetworkReply* reply) { + QEventLoop loop; + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + return; +}; + +CompatibilityStatus CompatibilityInfoClass::GetCompatibilityStatus(const std::string& serial) { + QString title_id = QString::fromStdString(serial); + if (m_compatibility_database.contains(title_id)) { + { + for (int os_int = 0; os_int != OSType::Last; os_int++) { + QString os_string = OSTypeToString.at(static_cast(os_int)); + QJsonObject compatibility_obj = m_compatibility_database[title_id].toObject(); + if (compatibility_obj.contains(os_string)) { + return LabelToCompatStatus.at( + compatibility_obj[os_string].toString()); + } + } + } + } + return CompatibilityStatus::Unknown; +} + +bool CompatibilityInfoClass::LoadCompatibilityFile() { + QFile compatibility_file(m_compatibility_filename); + if (!compatibility_file.open(QIODevice::ReadOnly)) { + compatibility_file.close(); + return false; + } + QByteArray json_data = compatibility_file.readAll(); + compatibility_file.close(); + + QJsonDocument json_doc = QJsonDocument::fromJson(json_data); + if (json_doc.isEmpty() || json_doc.isNull()) { + return false; + } + + m_compatibility_database = json_doc.object(); + return true; +} + + +void CompatibilityInfoClass::ExtractCompatibilityInfo(QByteArray response) { + QJsonDocument json_doc(QJsonDocument::fromJson(response)); + + if (json_doc.isNull()) { + return; + } + + QJsonArray json_arr; + + json_arr = json_doc.array(); + + for (const auto& issue_ref : std::as_const(json_arr)) { + QJsonObject issue_obj = issue_ref.toObject(); + QString title_id; + QRegularExpression title_id_regex("CUSA[0-9]{5}"); + QRegularExpressionMatch title_id_match = + title_id_regex.match(issue_obj["title"].toString()); + QString current_os = "os-unknown"; + QString compatibility_status = "status-unknown"; + if (issue_obj.contains("labels") && title_id_match.hasMatch()) { + title_id = title_id_match.captured(0); + const QJsonArray& label_array = issue_obj["labels"].toArray(); + for (const auto& elem : label_array) { + QString label = elem.toObject()["name"].toString(); + if (LabelToOSType.contains(label)) { + current_os = label; + continue; + } + if (LabelToCompatStatus.contains(label)) { + compatibility_status = label; + continue; + } + } + + QJsonValueRef compatibility_object_ref = m_compatibility_database[title_id]; + + if (compatibility_object_ref.isNull()) { + compatibility_object_ref = QJsonObject({{current_os, compatibility_status}}); + } else { + compatibility_object_ref.toObject()[current_os] = compatibility_status; + } + } + } + + return; +} diff --git a/src/qt_gui/compatibility_info.h b/src/qt_gui/compatibility_info.h new file mode 100644 index 000000000..ca2e7b328 --- /dev/null +++ b/src/qt_gui/compatibility_info.h @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "common/config.h" +#include "core/file_format/psf.h" + +enum CompatibilityStatus { + Unknown, + Nothing, + Boots, + Menus, + Ingame, + Playable, +}; + +// Prioritize different compatibility reports based on user's platform +enum OSType { +#ifdef Q_OS_WIN + Win32OS = 0, + UnknownOS, + LinuxOS, + macOS, +#elif defined(Q_OS_LINUX) + LinuxOS = 0, + UnknownOS, + Win32OS, + macOS, +#elif defined(Q_OS_MAC) + macOS = 0, + UnknownOS, + LinuxOS, + Win32OS, +#endif + // Fake enum to allow for iteration + Last +}; + +class CompatibilityInfoClass : public QObject { + Q_OBJECT +public: + // Please think of a better alternative + inline static const std::unordered_map LabelToCompatStatus = { + {QStringLiteral("status-nothing"), CompatibilityStatus::Nothing}, + {QStringLiteral("status-boots"), CompatibilityStatus::Boots}, + {QStringLiteral("status-menus"), CompatibilityStatus::Menus}, + {QStringLiteral("status-ingame"), CompatibilityStatus::Ingame}, + {QStringLiteral("status-playable"), CompatibilityStatus::Playable}}; + inline static const std::unordered_map LabelToOSType = { + {QStringLiteral("os-linux"), OSType::LinuxOS}, + {QStringLiteral("os-macOS"), OSType::macOS}, + {QStringLiteral("os-windows"), OSType::Win32OS}, + }; + + inline static const std::unordered_map CompatStatusToString = { + {CompatibilityStatus::Unknown, QStringLiteral("Unknown")}, + {CompatibilityStatus::Nothing, QStringLiteral("Nothing")}, + {CompatibilityStatus::Boots, QStringLiteral("Boots")}, + {CompatibilityStatus::Menus, QStringLiteral("Menus")}, + {CompatibilityStatus::Ingame, QStringLiteral("Ingame")}, + {CompatibilityStatus::Playable, QStringLiteral("Playable")}}; + inline static const std::unordered_map OSTypeToString = { + {OSType::LinuxOS, QStringLiteral("os-linux")}, + {OSType::macOS, QStringLiteral("os-macOS")}, + {OSType::Win32OS, QStringLiteral("os-windows")}, + {OSType::UnknownOS, QStringLiteral("os-unknown")} + }; + + CompatibilityInfoClass(); + ~CompatibilityInfoClass(); + void UpdateCompatibilityDatabase(QWidget* parent = nullptr); + bool LoadCompatibilityFile(); + CompatibilityStatus GetCompatibilityStatus(const std::string& serial); + void ExtractCompatibilityInfo(QByteArray response); + static void WaitForReply(QNetworkReply* reply); + QNetworkReply* FetchPage(int page_num); + +private: + QNetworkAccessManager* m_network_manager; + QString m_compatibility_filename; + QJsonObject m_compatibility_database; +}; \ No newline at end of file diff --git a/src/qt_gui/game_info.cpp b/src/qt_gui/game_info.cpp index 48643f8ed..cf7b0b3a6 100644 --- a/src/qt_gui/game_info.cpp +++ b/src/qt_gui/game_info.cpp @@ -5,6 +5,7 @@ #include "common/path_util.h" #include "game_info.h" +#include "compatibility_info.h" GameInfoClass::GameInfoClass() = default; GameInfoClass::~GameInfoClass() = default; @@ -22,6 +23,7 @@ void GameInfoClass::GetGameInfo(QWidget* parent) { } } } + m_games = QtConcurrent::mapped(filePaths, [&](const QString& path) { return readGameInfo(Common::FS::PathFromQString(path)); }).results(); diff --git a/src/qt_gui/game_list_frame.cpp b/src/qt_gui/game_list_frame.cpp index 99628b083..03acdd5e9 100644 --- a/src/qt_gui/game_list_frame.cpp +++ b/src/qt_gui/game_list_frame.cpp @@ -1,12 +1,15 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "common/logging/log.h" #include "common/path_util.h" #include "common/string_util.h" #include "game_list_frame.h" +#include "game_list_utils.h" -GameListFrame::GameListFrame(std::shared_ptr game_info_get, QWidget* parent) - : QTableWidget(parent), m_game_info(game_info_get) { +GameListFrame::GameListFrame(std::shared_ptr game_info_get, std::shared_ptr compat_info_get, QWidget* parent) + : QTableWidget(parent), m_game_info(game_info_get), m_compat_info(compat_info_get), + networkManager(new QNetworkAccessManager(this)) { icon_size = Config::getIconSize(); this->setShowGrid(false); this->setEditTriggers(QAbstractItemView::NoEditTriggers); @@ -24,17 +27,18 @@ GameListFrame::GameListFrame(std::shared_ptr game_info_get, QWidg this->horizontalHeader()->setSortIndicatorShown(true); this->horizontalHeader()->setStretchLastSection(true); this->setContextMenuPolicy(Qt::CustomContextMenu); - this->setColumnCount(9); + this->setColumnCount(10); this->setColumnWidth(1, 300); // Name - this->setColumnWidth(2, 120); // Serial - this->setColumnWidth(3, 90); // Region - this->setColumnWidth(4, 90); // Firmware - this->setColumnWidth(5, 90); // Size - this->setColumnWidth(6, 90); // Version - this->setColumnWidth(7, 100); // Play Time + this->setColumnWidth(2, 140); // Compatibility + this->setColumnWidth(3, 120); // Serial + this->setColumnWidth(4, 90); // Region + this->setColumnWidth(5, 90); // Firmware + this->setColumnWidth(6, 90); // Size + this->setColumnWidth(7, 90); // Version + this->setColumnWidth(8, 100); // Play Time QStringList headers; - headers << tr("Icon") << tr("Name") << tr("Serial") << tr("Region") << tr("Firmware") - << tr("Size") << tr("Version") << tr("Play Time") << tr("Path"); + headers << tr("Icon") << tr("Name") << tr("Compatibility") << tr("Serial") << tr("Region") + << tr("Firmware") << tr("Size") << tr("Version") << tr("Play Time") << tr("Path"); this->setHorizontalHeaderLabels(headers); this->horizontalHeader()->setSortIndicatorShown(true); this->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); @@ -96,16 +100,19 @@ void GameListFrame::PopulateGameList() { for (int i = 0; i < m_game_info->m_games.size(); i++) { SetTableItem(i, 1, QString::fromStdString(m_game_info->m_games[i].name)); - SetTableItem(i, 2, QString::fromStdString(m_game_info->m_games[i].serial)); - SetRegionFlag(i, 3, QString::fromStdString(m_game_info->m_games[i].region)); - SetTableItem(i, 4, QString::fromStdString(m_game_info->m_games[i].fw)); - SetTableItem(i, 5, QString::fromStdString(m_game_info->m_games[i].size)); - SetTableItem(i, 6, QString::fromStdString(m_game_info->m_games[i].version)); + SetTableItem(i, 3, QString::fromStdString(m_game_info->m_games[i].serial)); + SetRegionFlag(i, 4, QString::fromStdString(m_game_info->m_games[i].region)); + SetTableItem(i, 5, QString::fromStdString(m_game_info->m_games[i].fw)); + SetTableItem(i, 6, QString::fromStdString(m_game_info->m_games[i].size)); + SetTableItem(i, 7, QString::fromStdString(m_game_info->m_games[i].version)); + + m_game_info->m_games[i].compatibility_status = m_compat_info->GetCompatibilityStatus(m_game_info->m_games[i].serial); + SetCompatibilityItem(i, 2, m_game_info->m_games[i].compatibility_status); QString playTime = GetPlayTime(m_game_info->m_games[i].serial); if (playTime.isEmpty()) { m_game_info->m_games[i].play_time = "0:00:00"; - SetTableItem(i, 7, "0"); + SetTableItem(i, 8, "0"); } else { QStringList timeParts = playTime.split(':'); int hours = timeParts[0].toInt(); @@ -123,15 +130,15 @@ void GameListFrame::PopulateGameList() { formattedPlayTime = formattedPlayTime.trimmed(); m_game_info->m_games[i].play_time = playTime.toStdString(); if (formattedPlayTime.isEmpty()) { - SetTableItem(i, 7, "0"); + SetTableItem(i, 8, "0"); } else { - SetTableItem(i, 7, formattedPlayTime); + SetTableItem(i, 8, formattedPlayTime); } } QString path; Common::FS::PathToQString(path, m_game_info->m_games[i].path); - SetTableItem(i, 8, path); + SetTableItem(i, 9, path); } } @@ -203,6 +210,67 @@ void GameListFrame::ResizeIcons(int iconSize) { this->horizontalHeader()->setSectionResizeMode(8, QHeaderView::ResizeToContents); } +void GameListFrame::SetCompatibilityItem(int row, int column, CompatibilityStatus status) { + QTableWidgetItem* item = new QTableWidgetItem(); + QWidget* widget = new QWidget(this); + QGridLayout* layout = new QGridLayout(widget); + + QColor color; + + switch (status) { + case Unknown: + color = QStringLiteral("#000000"); + break; + case Nothing: + color = QStringLiteral("#212121"); + break; + case Boots: + color = QStringLiteral("#828282"); + break; + case Menus: + color = QStringLiteral("#FF0000"); + break; + case Ingame: + color = QStringLiteral("#F2D624"); + break; + case Playable: + color = QStringLiteral("#47D35C"); + break; + } + + QPixmap circle_pixmap(16, 16); + circle_pixmap.fill(Qt::transparent); + QPainter painter(&circle_pixmap); + painter.setRenderHint(QPainter::Antialiasing); + painter.setPen(color); + painter.setBrush(color); + painter.drawEllipse({circle_pixmap.width() / 2.0, circle_pixmap.height() / 2.0}, 6.0, 6.0); + + QLabel* dotLabel = new QLabel("", widget); + dotLabel->setPixmap(circle_pixmap); + + QLabel* label = new QLabel(m_compat_info->CompatStatusToString.at(status), widget); + + label->setStyleSheet("color: white; font-size: 16px; font-weight: bold;"); + + // Create shadow effect + QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect(); + shadowEffect->setBlurRadius(5); // Set the blur radius of the shadow + shadowEffect->setColor(QColor(0, 0, 0, 160)); // Set the color and opacity of the shadow + shadowEffect->setOffset(2, 2); // Set the offset of the shadow + + label->setGraphicsEffect(shadowEffect); // Apply shadow effect to the QLabel + + layout->addWidget(dotLabel, 0, 0, -1, 4); + layout->addWidget(label, 0,4,-1,4); + layout->setAlignment(Qt::AlignLeft); + widget->setLayout(layout); + this->setItem(row, column, item); + this->setCellWidget(row, column, widget); + + return; +} + void GameListFrame::SetTableItem(int row, int column, QString itemStr) { QTableWidgetItem* item = new QTableWidgetItem(); QWidget* widget = new QWidget(this); @@ -282,4 +350,4 @@ QString GameListFrame::GetPlayTime(const std::string& serial) { file.close(); return playTime; -} +} \ No newline at end of file diff --git a/src/qt_gui/game_list_frame.h b/src/qt_gui/game_list_frame.h index 6da2734a8..8d5934234 100644 --- a/src/qt_gui/game_list_frame.h +++ b/src/qt_gui/game_list_frame.h @@ -3,6 +3,10 @@ #pragma once +#include +#include +#include +#include #include #include "background_music_player.h" @@ -13,7 +17,7 @@ class GameListFrame : public QTableWidget { Q_OBJECT public: - explicit GameListFrame(std::shared_ptr game_info_get, QWidget* parent = nullptr); + explicit GameListFrame(std::shared_ptr game_info_get, std::shared_ptr compat_info_get, QWidget* parent = nullptr); Q_SIGNALS: void GameListFrameClosed(); @@ -29,6 +33,7 @@ public Q_SLOTS: private: void SetTableItem(int row, int column, QString itemStr); void SetRegionFlag(int row, int column, QString itemStr); + void SetCompatibilityItem(int row, int column, CompatibilityStatus status); QString GetPlayTime(const std::string& serial); QList m_columnActs; GameInfoClass* game_inf_get = nullptr; @@ -42,6 +47,7 @@ public: GameListUtils m_game_list_utils; GuiContextMenus m_gui_context_menus; std::shared_ptr m_game_info; + std::shared_ptr m_compat_info; int icon_size; @@ -59,18 +65,20 @@ public: case 1: return a.name < b.name; case 2: - return a.serial.substr(4) < b.serial.substr(4); + return a.compatibility_status < b.compatibility_status; case 3: - return a.region < b.region; + return a.serial.substr(4) < b.serial.substr(4); case 4: - return parseAsFloat(a.fw, 0) < parseAsFloat(b.fw, 0); + return a.region < b.region; case 5: - return parseSizeMB(b.size) < parseSizeMB(a.size); + return parseAsFloat(a.fw, 0) < parseAsFloat(b.fw, 0); case 6: - return a.version < b.version; + return parseSizeMB(b.size) < parseSizeMB(a.size); case 7: - return a.play_time < b.play_time; + return a.version < b.version; case 8: + return a.play_time < b.play_time; + case 9: return a.path < b.path; default: return false; @@ -82,18 +90,20 @@ public: case 1: return a.name > b.name; case 2: - return a.serial.substr(4) > b.serial.substr(4); + return a.compatibility_status > b.compatibility_status; case 3: - return a.region > b.region; + return a.serial.substr(4) > b.serial.substr(4); case 4: - return parseAsFloat(a.fw, 0) > parseAsFloat(b.fw, 0); + return a.region > b.region; case 5: - return parseSizeMB(b.size) > parseSizeMB(a.size); + return parseAsFloat(a.fw, 0) > parseAsFloat(b.fw, 0); case 6: - return a.version > b.version; + return parseSizeMB(b.size) > parseSizeMB(a.size); case 7: - return a.play_time > b.play_time; + return a.version > b.version; case 8: + return a.play_time > b.play_time; + case 9: return a.path > b.path; default: return false; diff --git a/src/qt_gui/game_list_utils.h b/src/qt_gui/game_list_utils.h index 3d710c5b7..fa2d900f1 100644 --- a/src/qt_gui/game_list_utils.h +++ b/src/qt_gui/game_list_utils.h @@ -3,7 +3,13 @@ #pragma once +#include +#include +#include +#include +#include #include "common/path_util.h" +#include "compatibility_info.h" struct GameInfo { std::filesystem::path path; // root path of game directory @@ -21,6 +27,7 @@ struct GameInfo { std::string fw = "Unknown"; std::string play_time = "Unknown"; + CompatibilityStatus compatibility_status = CompatibilityStatus::Unknown; }; class GameListUtils { diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index 4c40084d3..b00e14e8b 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -137,7 +137,7 @@ void MainWindow::CreateDockWindows() { setCentralWidget(phCentralWidget); m_dock_widget.reset(new QDockWidget(tr("Game List"), this)); - m_game_list_frame.reset(new GameListFrame(m_game_info, this)); + m_game_list_frame.reset(new GameListFrame(m_game_info, m_compat_info, this)); m_game_list_frame->setObjectName("gamelist"); m_game_grid_frame.reset(new GameGridFrame(m_game_info, this)); m_game_grid_frame->setObjectName("gamegridlist"); @@ -183,6 +183,8 @@ void MainWindow::CreateDockWindows() { } void MainWindow::LoadGameLists() { + // Update compatibility database + m_compat_info->UpdateCompatibilityDatabase(this); // Get game info from game folders. m_game_info->GetGameInfo(this); if (isTableList) { diff --git a/src/qt_gui/main_window.h b/src/qt_gui/main_window.h index 5ae2540ec..21a99670e 100644 --- a/src/qt_gui/main_window.h +++ b/src/qt_gui/main_window.h @@ -16,6 +16,7 @@ #include "emulator.h" #include "game_grid_frame.h" #include "game_info.h" +#include "compatibility_info.h" #include "game_list_frame.h" #include "game_list_utils.h" #include "main_window_themes.h" @@ -92,6 +93,7 @@ private: PSF psf; std::shared_ptr m_game_info = std::make_shared(); + std::shared_ptr m_compat_info = std::make_shared(); QTranslator* translator;