mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2024-12-28 02:26:07 +00:00
wip: added status column for compat data (#1668)
* wip: added basic gui for compat data * data is currently pulled directly from github API, awaiting server infra * removed unused initalizer * fixes * fix cmake * wip: add some testing date / version * add tooltip * fix nested QJsonObject * fix tooltip color * fix clang-format * Edit style * Add clickable status * formatting * import order * typo * fix clang format 2 --------- Co-authored-by: georgemoralis <giorgosmrls@gmail.com>
This commit is contained in:
parent
b0b74243af
commit
f2a989b9da
|
@ -811,6 +811,8 @@ set(QT_GUI src/qt_gui/about_dialog.cpp
|
||||||
src/qt_gui/background_music_player.h
|
src/qt_gui/background_music_player.h
|
||||||
src/qt_gui/cheats_patches.cpp
|
src/qt_gui/cheats_patches.cpp
|
||||||
src/qt_gui/cheats_patches.h
|
src/qt_gui/cheats_patches.h
|
||||||
|
src/qt_gui/compatibility_info.cpp
|
||||||
|
src/qt_gui/compatibility_info.h
|
||||||
src/qt_gui/main_window_ui.h
|
src/qt_gui/main_window_ui.h
|
||||||
src/qt_gui/main_window.cpp
|
src/qt_gui/main_window.cpp
|
||||||
src/qt_gui/main_window.h
|
src/qt_gui/main_window.h
|
||||||
|
|
227
src/qt_gui/compatibility_info.cpp
Normal file
227
src/qt_gui/compatibility_info.cpp
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QProgressDialog>
|
||||||
|
|
||||||
|
#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) {
|
||||||
|
if (LoadCompatibilityFile())
|
||||||
|
return;
|
||||||
|
|
||||||
|
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! Try again later."));
|
||||||
|
// 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;
|
||||||
|
m_compatibility_database["version"] = COMPAT_DB_VERSION;
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
CompatibilityEntry CompatibilityInfoClass::GetCompatibilityInfo(const std::string& serial) {
|
||||||
|
QString title_id = QString::fromStdString(serial);
|
||||||
|
if (m_compatibility_database.contains(title_id)) {
|
||||||
|
{
|
||||||
|
for (int os_int = 0; os_int != static_cast<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)) {
|
||||||
|
QJsonObject compatibility_entry_obj = compatibility_obj[os_string].toObject();
|
||||||
|
CompatibilityEntry compatibility_entry{
|
||||||
|
LabelToCompatStatus.at(compatibility_entry_obj["status"].toString()),
|
||||||
|
compatibility_entry_obj["version"].toString(),
|
||||||
|
QDateTime::fromString(compatibility_entry_obj["last_tested"].toString(),
|
||||||
|
Qt::ISODate),
|
||||||
|
compatibility_entry_obj["url"].toString(),
|
||||||
|
compatibility_entry_obj["issue_number"].toInt()};
|
||||||
|
return compatibility_entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CompatibilityEntry{CompatibilityStatus::Unknown};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CompatibilityInfoClass::LoadCompatibilityFile() {
|
||||||
|
// Returns true if compatibility is loaded succescfully
|
||||||
|
QFileInfo check_file(m_compatibility_filename);
|
||||||
|
const auto modified_delta = QDateTime::currentDateTime() - check_file.lastModified();
|
||||||
|
if (!check_file.exists() || !check_file.isFile() ||
|
||||||
|
std::chrono::duration_cast<std::chrono::minutes>(modified_delta).count() > 60) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check database version
|
||||||
|
int version_number;
|
||||||
|
if (json_doc.object()["version"].isDouble()) {
|
||||||
|
if (json_doc.object()["version"].toInt() < COMPAT_DB_VERSION)
|
||||||
|
return false;
|
||||||
|
} else
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QJson does not support editing nested objects directly..
|
||||||
|
|
||||||
|
QJsonObject compatibility_obj = m_compatibility_database[title_id].toObject();
|
||||||
|
|
||||||
|
QJsonObject compatibility_data{
|
||||||
|
{{"status", compatibility_status},
|
||||||
|
{"last_tested", issue_obj["updated_at"]},
|
||||||
|
{"version", issue_obj["milestone"].isNull()
|
||||||
|
? "unknown"
|
||||||
|
: issue_obj["milestone"].toObject()["title"].toString()},
|
||||||
|
{"url", issue_obj["html_url"]},
|
||||||
|
{"issue_number", issue_obj["number"]}}};
|
||||||
|
|
||||||
|
compatibility_obj[current_os] = compatibility_data;
|
||||||
|
|
||||||
|
m_compatibility_database[title_id] = compatibility_obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
97
src/qt_gui/compatibility_info.h
Normal file
97
src/qt_gui/compatibility_info.h
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
static constexpr int COMPAT_DB_VERSION = 1;
|
||||||
|
|
||||||
|
enum class CompatibilityStatus {
|
||||||
|
Unknown,
|
||||||
|
Nothing,
|
||||||
|
Boots,
|
||||||
|
Menus,
|
||||||
|
Ingame,
|
||||||
|
Playable,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prioritize different compatibility reports based on user's platform
|
||||||
|
enum class OSType {
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
Win32 = 0,
|
||||||
|
Unknown,
|
||||||
|
Linux,
|
||||||
|
macOS,
|
||||||
|
#elif defined(Q_OS_LINUX)
|
||||||
|
Linux = 0,
|
||||||
|
Unknown,
|
||||||
|
Win32,
|
||||||
|
macOS,
|
||||||
|
#elif defined(Q_OS_MAC)
|
||||||
|
macOS = 0,
|
||||||
|
Unknown,
|
||||||
|
Linux,
|
||||||
|
Win32,
|
||||||
|
#endif
|
||||||
|
// Fake enum to allow for iteration
|
||||||
|
Last
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CompatibilityEntry {
|
||||||
|
CompatibilityStatus status;
|
||||||
|
QString version;
|
||||||
|
QDateTime last_tested;
|
||||||
|
QString url;
|
||||||
|
int issue_number;
|
||||||
|
};
|
||||||
|
|
||||||
|
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::Linux},
|
||||||
|
{QStringLiteral("os-macOS"), OSType::macOS},
|
||||||
|
{QStringLiteral("os-windows"), OSType::Win32},
|
||||||
|
};
|
||||||
|
|
||||||
|
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::Linux, QStringLiteral("os-linux")},
|
||||||
|
{OSType::macOS, QStringLiteral("os-macOS")},
|
||||||
|
{OSType::Win32, QStringLiteral("os-windows")},
|
||||||
|
{OSType::Unknown, QStringLiteral("os-unknown")}};
|
||||||
|
|
||||||
|
CompatibilityInfoClass();
|
||||||
|
~CompatibilityInfoClass();
|
||||||
|
void UpdateCompatibilityDatabase(QWidget* parent = nullptr);
|
||||||
|
bool LoadCompatibilityFile();
|
||||||
|
CompatibilityEntry GetCompatibilityInfo(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;
|
||||||
|
};
|
|
@ -4,6 +4,7 @@
|
||||||
#include <QProgressDialog>
|
#include <QProgressDialog>
|
||||||
|
|
||||||
#include "common/path_util.h"
|
#include "common/path_util.h"
|
||||||
|
#include "compatibility_info.h"
|
||||||
#include "game_info.h"
|
#include "game_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) {
|
m_games = QtConcurrent::mapped(filePaths, [&](const QString& path) {
|
||||||
return readGameInfo(Common::FS::PathFromQString(path));
|
return readGameInfo(Common::FS::PathFromQString(path));
|
||||||
}).results();
|
}).results();
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <QToolTip>
|
||||||
|
#include "common/logging/log.h"
|
||||||
#include "common/path_util.h"
|
#include "common/path_util.h"
|
||||||
#include "common/string_util.h"
|
#include "common/string_util.h"
|
||||||
#include "game_list_frame.h"
|
#include "game_list_frame.h"
|
||||||
|
#include "game_list_utils.h"
|
||||||
|
|
||||||
GameListFrame::GameListFrame(std::shared_ptr<GameInfoClass> game_info_get, QWidget* parent)
|
GameListFrame::GameListFrame(std::shared_ptr<GameInfoClass> game_info_get,
|
||||||
: QTableWidget(parent), m_game_info(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) {
|
||||||
icon_size = Config::getIconSize();
|
icon_size = Config::getIconSize();
|
||||||
this->setShowGrid(false);
|
this->setShowGrid(false);
|
||||||
this->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
this->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||||
|
@ -17,29 +22,30 @@ GameListFrame::GameListFrame(std::shared_ptr<GameInfoClass> game_info_get, QWidg
|
||||||
this->verticalScrollBar()->installEventFilter(this);
|
this->verticalScrollBar()->installEventFilter(this);
|
||||||
this->verticalScrollBar()->setSingleStep(20);
|
this->verticalScrollBar()->setSingleStep(20);
|
||||||
this->horizontalScrollBar()->setSingleStep(20);
|
this->horizontalScrollBar()->setSingleStep(20);
|
||||||
this->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
|
this->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||||
this->verticalHeader()->setVisible(false);
|
this->verticalHeader()->setVisible(false);
|
||||||
this->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
|
this->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
this->horizontalHeader()->setHighlightSections(false);
|
this->horizontalHeader()->setHighlightSections(false);
|
||||||
this->horizontalHeader()->setSortIndicatorShown(true);
|
this->horizontalHeader()->setSortIndicatorShown(true);
|
||||||
this->horizontalHeader()->setStretchLastSection(true);
|
this->horizontalHeader()->setStretchLastSection(true);
|
||||||
this->setContextMenuPolicy(Qt::CustomContextMenu);
|
this->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
this->setColumnCount(9);
|
this->setColumnCount(10);
|
||||||
this->setColumnWidth(1, 300); // Name
|
this->setColumnWidth(1, 300); // Name
|
||||||
this->setColumnWidth(2, 120); // Serial
|
this->setColumnWidth(2, 140); // Compatibility
|
||||||
this->setColumnWidth(3, 90); // Region
|
this->setColumnWidth(3, 120); // Serial
|
||||||
this->setColumnWidth(4, 90); // Firmware
|
this->setColumnWidth(4, 90); // Region
|
||||||
this->setColumnWidth(5, 90); // Size
|
this->setColumnWidth(5, 90); // Firmware
|
||||||
this->setColumnWidth(6, 90); // Version
|
this->setColumnWidth(6, 90); // Size
|
||||||
this->setColumnWidth(7, 120); // Play Time
|
this->setColumnWidth(7, 90); // Version
|
||||||
|
this->setColumnWidth(8, 120); // Play Time
|
||||||
QStringList headers;
|
QStringList headers;
|
||||||
headers << tr("Icon") << tr("Name") << tr("Serial") << tr("Region") << tr("Firmware")
|
headers << tr("Icon") << tr("Name") << tr("Compatibility") << tr("Serial") << tr("Region")
|
||||||
<< tr("Size") << tr("Version") << tr("Play Time") << tr("Path");
|
<< tr("Firmware") << tr("Size") << tr("Version") << tr("Play Time") << tr("Path");
|
||||||
this->setHorizontalHeaderLabels(headers);
|
this->setHorizontalHeaderLabels(headers);
|
||||||
this->horizontalHeader()->setSortIndicatorShown(true);
|
this->horizontalHeader()->setSortIndicatorShown(true);
|
||||||
this->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
this->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||||
this->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Fixed);
|
|
||||||
this->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Fixed);
|
this->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Fixed);
|
||||||
|
this->horizontalHeader()->setSectionResizeMode(4, QHeaderView::Fixed);
|
||||||
PopulateGameList();
|
PopulateGameList();
|
||||||
|
|
||||||
connect(this, &QTableWidget::currentCellChanged, this, &GameListFrame::onCurrentCellChanged);
|
connect(this, &QTableWidget::currentCellChanged, this, &GameListFrame::onCurrentCellChanged);
|
||||||
|
@ -68,6 +74,12 @@ GameListFrame::GameListFrame(std::shared_ptr<GameInfoClass> game_info_get, QWidg
|
||||||
connect(this, &QTableWidget::customContextMenuRequested, this, [=, this](const QPoint& pos) {
|
connect(this, &QTableWidget::customContextMenuRequested, this, [=, this](const QPoint& pos) {
|
||||||
m_gui_context_menus.RequestGameMenu(pos, m_game_info->m_games, this, true);
|
m_gui_context_menus.RequestGameMenu(pos, m_game_info->m_games, this, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connect(this, &QTableWidget::cellClicked, this, [=, this](int row, int column) {
|
||||||
|
if (column == 2 && !m_game_info->m_games[row].compatibility.url.isEmpty()) {
|
||||||
|
QDesktopServices::openUrl(QUrl(m_game_info->m_games[row].compatibility.url));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameListFrame::onCurrentCellChanged(int currentRow, int currentColumn, int previousRow,
|
void GameListFrame::onCurrentCellChanged(int currentRow, int currentColumn, int previousRow,
|
||||||
|
@ -96,16 +108,20 @@ void GameListFrame::PopulateGameList() {
|
||||||
|
|
||||||
for (int i = 0; i < m_game_info->m_games.size(); i++) {
|
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, 1, QString::fromStdString(m_game_info->m_games[i].name));
|
||||||
SetTableItem(i, 2, QString::fromStdString(m_game_info->m_games[i].serial));
|
SetTableItem(i, 3, QString::fromStdString(m_game_info->m_games[i].serial));
|
||||||
SetRegionFlag(i, 3, QString::fromStdString(m_game_info->m_games[i].region));
|
SetRegionFlag(i, 4, 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].fw));
|
||||||
SetTableItem(i, 5, QString::fromStdString(m_game_info->m_games[i].size));
|
SetTableItem(i, 6, QString::fromStdString(m_game_info->m_games[i].size));
|
||||||
SetTableItem(i, 6, QString::fromStdString(m_game_info->m_games[i].version));
|
SetTableItem(i, 7, QString::fromStdString(m_game_info->m_games[i].version));
|
||||||
|
|
||||||
|
m_game_info->m_games[i].compatibility =
|
||||||
|
m_compat_info->GetCompatibilityInfo(m_game_info->m_games[i].serial);
|
||||||
|
SetCompatibilityItem(i, 2, m_game_info->m_games[i].compatibility);
|
||||||
|
|
||||||
QString playTime = GetPlayTime(m_game_info->m_games[i].serial);
|
QString playTime = GetPlayTime(m_game_info->m_games[i].serial);
|
||||||
if (playTime.isEmpty()) {
|
if (playTime.isEmpty()) {
|
||||||
m_game_info->m_games[i].play_time = "0:00:00";
|
m_game_info->m_games[i].play_time = "0:00:00";
|
||||||
SetTableItem(i, 7, tr("Never Played"));
|
SetTableItem(i, 8, tr("Never Played"));
|
||||||
} else {
|
} else {
|
||||||
QStringList timeParts = playTime.split(':');
|
QStringList timeParts = playTime.split(':');
|
||||||
int hours = timeParts[0].toInt();
|
int hours = timeParts[0].toInt();
|
||||||
|
@ -123,15 +139,15 @@ void GameListFrame::PopulateGameList() {
|
||||||
formattedPlayTime = formattedPlayTime.trimmed();
|
formattedPlayTime = formattedPlayTime.trimmed();
|
||||||
m_game_info->m_games[i].play_time = playTime.toStdString();
|
m_game_info->m_games[i].play_time = playTime.toStdString();
|
||||||
if (formattedPlayTime.isEmpty()) {
|
if (formattedPlayTime.isEmpty()) {
|
||||||
SetTableItem(i, 7, QString("%1s").arg(seconds));
|
SetTableItem(i, 8, QString("%1s").arg(seconds));
|
||||||
} else {
|
} else {
|
||||||
SetTableItem(i, 7, formattedPlayTime);
|
SetTableItem(i, 8, formattedPlayTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString path;
|
QString path;
|
||||||
Common::FS::PathToQString(path, m_game_info->m_games[i].path);
|
Common::FS::PathToQString(path, m_game_info->m_games[i].path);
|
||||||
SetTableItem(i, 8, path);
|
SetTableItem(i, 9, path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,6 +219,89 @@ void GameListFrame::ResizeIcons(int iconSize) {
|
||||||
this->horizontalHeader()->setSectionResizeMode(8, QHeaderView::ResizeToContents);
|
this->horizontalHeader()->setSectionResizeMode(8, QHeaderView::ResizeToContents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameListFrame::SetCompatibilityItem(int row, int column, CompatibilityEntry entry) {
|
||||||
|
QTableWidgetItem* item = new QTableWidgetItem();
|
||||||
|
QWidget* widget = new QWidget(this);
|
||||||
|
QGridLayout* layout = new QGridLayout(widget);
|
||||||
|
|
||||||
|
widget->setStyleSheet("QToolTip {background-color: black; color: white;}");
|
||||||
|
|
||||||
|
QColor color;
|
||||||
|
QString status_explanation;
|
||||||
|
|
||||||
|
switch (entry.status) {
|
||||||
|
case CompatibilityStatus::Unknown:
|
||||||
|
color = QStringLiteral("#000000");
|
||||||
|
status_explanation = tr("Compatibility is untested");
|
||||||
|
break;
|
||||||
|
case CompatibilityStatus::Nothing:
|
||||||
|
color = QStringLiteral("#212121");
|
||||||
|
status_explanation = tr("Games does not initialize properly / crashes the emulator");
|
||||||
|
break;
|
||||||
|
case CompatibilityStatus::Boots:
|
||||||
|
color = QStringLiteral("#828282");
|
||||||
|
status_explanation = tr("Game boots, but only displays a blank screen");
|
||||||
|
break;
|
||||||
|
case CompatibilityStatus::Menus:
|
||||||
|
color = QStringLiteral("#FF0000");
|
||||||
|
status_explanation = tr("Game displays an image but does not go past the menu");
|
||||||
|
break;
|
||||||
|
case CompatibilityStatus::Ingame:
|
||||||
|
color = QStringLiteral("#F2D624");
|
||||||
|
status_explanation = tr("Game has game-breaking glitches or unplayable performance");
|
||||||
|
break;
|
||||||
|
case CompatibilityStatus::Playable:
|
||||||
|
color = QStringLiteral("#47D35C");
|
||||||
|
status_explanation =
|
||||||
|
tr("Game can be completed with playable performance and no major glitches");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString tooltip_string;
|
||||||
|
|
||||||
|
if (entry.status == CompatibilityStatus::Unknown) {
|
||||||
|
tooltip_string = status_explanation;
|
||||||
|
} else {
|
||||||
|
tooltip_string =
|
||||||
|
"<p> <i>" + tr("Click to go to issue") + "</i>" + "<br>" + tr("Last updated") +
|
||||||
|
QString(": %1 (%2)").arg(entry.last_tested.toString("yyyy-MM-dd"), entry.version) +
|
||||||
|
"<br>" + status_explanation + "</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
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(entry.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, 1);
|
||||||
|
layout->addWidget(label, 0, 1, 1, 1);
|
||||||
|
layout->setAlignment(Qt::AlignLeft);
|
||||||
|
widget->setLayout(layout);
|
||||||
|
widget->setToolTip(tooltip_string);
|
||||||
|
this->setItem(row, column, item);
|
||||||
|
this->setCellWidget(row, column, widget);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
void GameListFrame::SetTableItem(int row, int column, QString itemStr) {
|
void GameListFrame::SetTableItem(int row, int column, QString itemStr) {
|
||||||
QTableWidgetItem* item = new QTableWidgetItem();
|
QTableWidgetItem* item = new QTableWidgetItem();
|
||||||
QWidget* widget = new QWidget(this);
|
QWidget* widget = new QWidget(this);
|
||||||
|
|
|
@ -3,9 +3,14 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QPainter>
|
||||||
#include <QScrollBar>
|
#include <QScrollBar>
|
||||||
|
|
||||||
#include "background_music_player.h"
|
#include "background_music_player.h"
|
||||||
|
#include "compatibility_info.h"
|
||||||
#include "game_info.h"
|
#include "game_info.h"
|
||||||
#include "game_list_utils.h"
|
#include "game_list_utils.h"
|
||||||
#include "gui_context_menus.h"
|
#include "gui_context_menus.h"
|
||||||
|
@ -13,7 +18,9 @@
|
||||||
class GameListFrame : public QTableWidget {
|
class GameListFrame : public QTableWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
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:
|
Q_SIGNALS:
|
||||||
void GameListFrameClosed();
|
void GameListFrameClosed();
|
||||||
|
|
||||||
|
@ -29,6 +36,7 @@ public Q_SLOTS:
|
||||||
private:
|
private:
|
||||||
void SetTableItem(int row, int column, QString itemStr);
|
void SetTableItem(int row, int column, QString itemStr);
|
||||||
void SetRegionFlag(int row, int column, QString itemStr);
|
void SetRegionFlag(int row, int column, QString itemStr);
|
||||||
|
void SetCompatibilityItem(int row, int column, CompatibilityEntry entry);
|
||||||
QString GetPlayTime(const std::string& serial);
|
QString GetPlayTime(const std::string& serial);
|
||||||
QList<QAction*> m_columnActs;
|
QList<QAction*> m_columnActs;
|
||||||
GameInfoClass* game_inf_get = nullptr;
|
GameInfoClass* game_inf_get = nullptr;
|
||||||
|
@ -42,6 +50,7 @@ public:
|
||||||
GameListUtils m_game_list_utils;
|
GameListUtils m_game_list_utils;
|
||||||
GuiContextMenus m_gui_context_menus;
|
GuiContextMenus m_gui_context_menus;
|
||||||
std::shared_ptr<GameInfoClass> m_game_info;
|
std::shared_ptr<GameInfoClass> m_game_info;
|
||||||
|
std::shared_ptr<CompatibilityInfoClass> m_compat_info;
|
||||||
|
|
||||||
int icon_size;
|
int icon_size;
|
||||||
|
|
||||||
|
@ -59,18 +68,20 @@ public:
|
||||||
case 1:
|
case 1:
|
||||||
return a.name < b.name;
|
return a.name < b.name;
|
||||||
case 2:
|
case 2:
|
||||||
return a.serial.substr(4) < b.serial.substr(4);
|
return a.compatibility.status < b.compatibility.status;
|
||||||
case 3:
|
case 3:
|
||||||
return a.region < b.region;
|
return a.serial.substr(4) < b.serial.substr(4);
|
||||||
case 4:
|
case 4:
|
||||||
return parseAsFloat(a.fw, 0) < parseAsFloat(b.fw, 0);
|
return a.region < b.region;
|
||||||
case 5:
|
case 5:
|
||||||
return parseSizeMB(b.size) < parseSizeMB(a.size);
|
return parseAsFloat(a.fw, 0) < parseAsFloat(b.fw, 0);
|
||||||
case 6:
|
case 6:
|
||||||
return a.version < b.version;
|
return parseSizeMB(b.size) < parseSizeMB(a.size);
|
||||||
case 7:
|
case 7:
|
||||||
return a.play_time < b.play_time;
|
return a.version < b.version;
|
||||||
case 8:
|
case 8:
|
||||||
|
return a.play_time < b.play_time;
|
||||||
|
case 9:
|
||||||
return a.path < b.path;
|
return a.path < b.path;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
@ -82,18 +93,20 @@ public:
|
||||||
case 1:
|
case 1:
|
||||||
return a.name > b.name;
|
return a.name > b.name;
|
||||||
case 2:
|
case 2:
|
||||||
return a.serial.substr(4) > b.serial.substr(4);
|
return a.compatibility.status > b.compatibility.status;
|
||||||
case 3:
|
case 3:
|
||||||
return a.region > b.region;
|
return a.serial.substr(4) > b.serial.substr(4);
|
||||||
case 4:
|
case 4:
|
||||||
return parseAsFloat(a.fw, 0) > parseAsFloat(b.fw, 0);
|
return a.region > b.region;
|
||||||
case 5:
|
case 5:
|
||||||
return parseSizeMB(b.size) > parseSizeMB(a.size);
|
return parseAsFloat(a.fw, 0) > parseAsFloat(b.fw, 0);
|
||||||
case 6:
|
case 6:
|
||||||
return a.version > b.version;
|
return parseSizeMB(b.size) > parseSizeMB(a.size);
|
||||||
case 7:
|
case 7:
|
||||||
return a.play_time > b.play_time;
|
return a.version > b.version;
|
||||||
case 8:
|
case 8:
|
||||||
|
return a.play_time > b.play_time;
|
||||||
|
case 9:
|
||||||
return a.path > b.path;
|
return a.path > b.path;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -3,7 +3,13 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QDirIterator>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QString>
|
||||||
#include "common/path_util.h"
|
#include "common/path_util.h"
|
||||||
|
#include "compatibility_info.h"
|
||||||
|
|
||||||
struct GameInfo {
|
struct GameInfo {
|
||||||
std::filesystem::path path; // root path of game directory
|
std::filesystem::path path; // root path of game directory
|
||||||
|
@ -21,6 +27,7 @@ struct GameInfo {
|
||||||
std::string fw = "Unknown";
|
std::string fw = "Unknown";
|
||||||
|
|
||||||
std::string play_time = "Unknown";
|
std::string play_time = "Unknown";
|
||||||
|
CompatibilityEntry compatibility = CompatibilityEntry{CompatibilityStatus::Unknown};
|
||||||
};
|
};
|
||||||
|
|
||||||
class GameListUtils {
|
class GameListUtils {
|
||||||
|
|
|
@ -138,7 +138,7 @@ void MainWindow::CreateDockWindows() {
|
||||||
setCentralWidget(phCentralWidget);
|
setCentralWidget(phCentralWidget);
|
||||||
|
|
||||||
m_dock_widget.reset(new QDockWidget(tr("Game List"), this));
|
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_list_frame->setObjectName("gamelist");
|
||||||
m_game_grid_frame.reset(new GameGridFrame(m_game_info, this));
|
m_game_grid_frame.reset(new GameGridFrame(m_game_info, this));
|
||||||
m_game_grid_frame->setObjectName("gamegridlist");
|
m_game_grid_frame->setObjectName("gamegridlist");
|
||||||
|
@ -184,6 +184,8 @@ void MainWindow::CreateDockWindows() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::LoadGameLists() {
|
void MainWindow::LoadGameLists() {
|
||||||
|
// Update compatibility database
|
||||||
|
m_compat_info->UpdateCompatibilityDatabase(this);
|
||||||
// Get game info from game folders.
|
// Get game info from game folders.
|
||||||
m_game_info->GetGameInfo(this);
|
m_game_info->GetGameInfo(this);
|
||||||
if (isTableList) {
|
if (isTableList) {
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#include "background_music_player.h"
|
#include "background_music_player.h"
|
||||||
#include "common/config.h"
|
#include "common/config.h"
|
||||||
#include "common/path_util.h"
|
#include "common/path_util.h"
|
||||||
|
#include "compatibility_info.h"
|
||||||
#include "core/file_format/psf.h"
|
#include "core/file_format/psf.h"
|
||||||
#include "core/file_sys/fs.h"
|
#include "core/file_sys/fs.h"
|
||||||
#include "elf_viewer.h"
|
#include "elf_viewer.h"
|
||||||
|
@ -92,6 +93,8 @@ private:
|
||||||
PSF psf;
|
PSF psf;
|
||||||
|
|
||||||
std::shared_ptr<GameInfoClass> m_game_info = std::make_shared<GameInfoClass>();
|
std::shared_ptr<GameInfoClass> m_game_info = std::make_shared<GameInfoClass>();
|
||||||
|
std::shared_ptr<CompatibilityInfoClass> m_compat_info =
|
||||||
|
std::make_shared<CompatibilityInfoClass>();
|
||||||
|
|
||||||
QTranslator* translator;
|
QTranslator* translator;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue