wip: added basic gui for compat data

* data is currently pulled directly from github API, awaiting server infra
This commit is contained in:
faith 2024-12-02 15:36:13 +08:00
parent c019b54fec
commit acdd3be52f
8 changed files with 415 additions and 35 deletions

View File

@ -0,0 +1,201 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QProgressDialog>
#include <QMessageBox>
#include <QFileInfo>
#include <iostream>
#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<std::chrono::minutes>(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<QNetworkReply*> replies(remaining_pages);
QFutureWatcher<void> 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<QByteArray>::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<void>::cancel);
dialog.setRange(0, remaining_pages);
connect(&future_watcher, &QFutureWatcher<void>::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<OSType>(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;
}

View File

@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QFuture>
#include <QFutureWatcher>
#include <QtConcurrent>
#include <QtNetwork>
#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<QString, CompatibilityStatus> 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<QString, OSType> LabelToOSType = {
{QStringLiteral("os-linux"), OSType::LinuxOS},
{QStringLiteral("os-macOS"), OSType::macOS},
{QStringLiteral("os-windows"), OSType::Win32OS},
};
inline static const std::unordered_map<CompatibilityStatus, QString> 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<OSType, QString> 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;
};

View File

@ -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();

View File

@ -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<GameInfoClass> game_info_get, QWidget* parent)
: QTableWidget(parent), m_game_info(game_info_get) {
GameListFrame::GameListFrame(std::shared_ptr<GameInfoClass> game_info_get, std::shared_ptr<CompatibilityInfoClass> 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<GameInfoClass> 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);

View File

@ -3,6 +3,10 @@
#pragma once
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QPainter>
#include <QScrollBar>
#include "background_music_player.h"
@ -13,7 +17,7 @@
class GameListFrame : public QTableWidget {
Q_OBJECT
public:
explicit GameListFrame(std::shared_ptr<GameInfoClass> game_info_get, QWidget* parent = nullptr);
explicit GameListFrame(std::shared_ptr<GameInfoClass> game_info_get, std::shared_ptr<CompatibilityInfoClass> 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<QAction*> 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<GameInfoClass> m_game_info;
std::shared_ptr<CompatibilityInfoClass> 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;

View File

@ -3,7 +3,13 @@
#pragma once
#include <unordered_map>
#include <QDir>
#include <QDirIterator>
#include <QImage>
#include <QString>
#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 {

View File

@ -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) {

View File

@ -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<GameInfoClass> m_game_info = std::make_shared<GameInfoClass>();
std::shared_ptr<CompatibilityInfoClass> m_compat_info = std::make_shared<CompatibilityInfoClass>();
QTranslator* translator;